Repository: codecentric/spring-boot-admin Branch: master Commit: 9baececf03a2 Files: 1285 Total size: 4.2 MB Directory structure: gitextract_6_xkhu95/ ├── .editorconfig ├── .gitattributes ├── .github/ │ ├── CODEOWNERS │ ├── ISSUE_TEMPLATE/ │ │ ├── spring-boot-admin-bug.md │ │ └── spring-boot-admin-enhancement.md │ ├── copilot-instructions.md │ ├── milestones.sh │ ├── release.yml │ └── workflows/ │ ├── build-feature.yml │ ├── build-main.yml │ ├── deploy-documentation.yml │ ├── issue-metrics.yml │ ├── release-to-maven-central.yml │ └── vulnerability-scan.yml ├── .gitignore ├── .gnupg.tar.enc ├── .mvn/ │ └── wrapper/ │ └── maven-wrapper.properties ├── CONTRIBUTING.md ├── LICENSE.txt ├── README.md ├── lombok.config ├── mvnw ├── mvnw.cmd ├── pom.xml ├── renovate.json ├── spring-boot-admin-build/ │ └── pom.xml ├── spring-boot-admin-client/ │ ├── pom.xml │ └── src/ │ ├── main/ │ │ ├── java/ │ │ │ └── de/ │ │ │ └── codecentric/ │ │ │ └── boot/ │ │ │ └── admin/ │ │ │ └── client/ │ │ │ ├── config/ │ │ │ │ ├── ClientProperties.java │ │ │ │ ├── ClientRuntimeHints.java │ │ │ │ ├── CloudFoundryApplicationProperties.java │ │ │ │ ├── InstanceProperties.java │ │ │ │ ├── ServiceHostType.java │ │ │ │ ├── SpringBootAdminClientAutoConfiguration.java │ │ │ │ ├── SpringBootAdminClientCloudFoundryAutoConfiguration.java │ │ │ │ ├── SpringBootAdminClientEnabledCondition.java │ │ │ │ ├── SpringNativeClientAutoConfiguration.java │ │ │ │ └── package-info.java │ │ │ └── registration/ │ │ │ ├── Application.java │ │ │ ├── ApplicationFactory.java │ │ │ ├── ApplicationRegistrator.java │ │ │ ├── CloudFoundryApplicationFactory.java │ │ │ ├── DefaultApplicationFactory.java │ │ │ ├── DefaultApplicationRegistrator.java │ │ │ ├── ReactiveApplicationFactory.java │ │ │ ├── RegistrationApplicationListener.java │ │ │ ├── RegistrationClient.java │ │ │ ├── RestClientRegistrationClient.java │ │ │ ├── ServletApplicationFactory.java │ │ │ ├── metadata/ │ │ │ │ ├── CloudFoundryMetadataContributor.java │ │ │ │ ├── CompositeMetadataContributor.java │ │ │ │ ├── MetadataContributor.java │ │ │ │ ├── StartupDateMetadataContributor.java │ │ │ │ └── package-info.java │ │ │ └── package-info.java │ │ └── resources/ │ │ └── META-INF/ │ │ ├── additional-spring-configuration-metadata.json │ │ └── spring/ │ │ └── org.springframework.boot.autoconfigure.AutoConfiguration.imports │ └── test/ │ ├── java/ │ │ └── de/ │ │ └── codecentric/ │ │ └── boot/ │ │ └── admin/ │ │ └── client/ │ │ ├── AbstractClientApplicationTest.java │ │ ├── ClientReactiveApplicationTest.java │ │ ├── ClientServletApplicationTest.java │ │ ├── config/ │ │ │ ├── ClientPropertiesTest.java │ │ │ ├── CloudFoundryApplicationPropertiesTest.java │ │ │ ├── SpringBootAdminClientAutoConfigurationTest.java │ │ │ ├── SpringBootAdminClientCloudFoundryAutoConfigurationTest.java │ │ │ ├── SpringBootAdminClientEnabledConditionTest.java │ │ │ └── SpringBootAdminClientRegistrationClientAutoConfigurationTest.java │ │ └── registration/ │ │ ├── AbstractRegistrationClientTest.java │ │ ├── ApplicationTest.java │ │ ├── CloudFoundryApplicationFactoryTest.java │ │ ├── DefaultApplicationFactoryTest.java │ │ ├── DefaultApplicationRegistratorTest.java │ │ ├── ReactiveApplicationFactoryTest.java │ │ ├── RegistrationApplicationListenerTest.java │ │ ├── RestClientRegistrationClientTest.java │ │ ├── ServletApplicationFactoryTest.java │ │ └── metadata/ │ │ ├── CloudFoundryMetadataContributorTest.java │ │ ├── CompositeMetadataContributorTest.java │ │ └── StartupDateMetadataContributorTest.java │ └── resources/ │ ├── application.yml │ ├── junit-platform.properties │ └── logback-test.xml ├── spring-boot-admin-dependencies/ │ └── pom.xml ├── spring-boot-admin-docs/ │ ├── pom.xml │ └── src/ │ └── site/ │ ├── .gitignore │ ├── README.md │ ├── babel.config.js │ ├── current/ │ │ ├── 404.template.html │ │ └── index.template.html │ ├── docs/ │ │ ├── 01-getting-started/ │ │ │ ├── 10-server-setup.md │ │ │ ├── 20-client-registration.md │ │ │ ├── 50-snapshots.md │ │ │ ├── _category_.json │ │ │ └── index.md │ │ ├── 02-server/ │ │ │ ├── 01-server.mdx │ │ │ ├── 02-security.md │ │ │ ├── 10-Events.mdx │ │ │ ├── 20-Clustering.mdx │ │ │ ├── 30-persistence.md │ │ │ ├── 40-instance-registry.md │ │ │ ├── 99-server-properties.mdx │ │ │ ├── _category_.json │ │ │ ├── index.md │ │ │ └── notifications/ │ │ │ ├── 90-custom-notifiers.md │ │ │ ├── _category_.json │ │ │ ├── index.mdx │ │ │ ├── notifier-dingtalk.mdx │ │ │ ├── notifier-discord.mdx │ │ │ ├── notifier-hipchat.mdx │ │ │ ├── notifier-lets-chat.mdx │ │ │ ├── notifier-mail.mdx │ │ │ ├── notifier-mattermost.mdx │ │ │ ├── notifier-msteams.mdx │ │ │ ├── notifier-rocketchat.mdx │ │ │ ├── notifier-slack.mdx │ │ │ ├── notifier-telegram.mdx │ │ │ └── notifier-webex.mdx │ │ ├── 03-client/ │ │ │ ├── 10-client-features.md │ │ │ ├── 20-registration.md │ │ │ ├── 30-metadata.md │ │ │ ├── 40-service-discovery.md │ │ │ ├── 80-configuration.md │ │ │ ├── 99-properties.mdx │ │ │ ├── _category_.json │ │ │ └── index.md │ │ ├── 04-integration/ │ │ │ ├── 10-eureka.md │ │ │ ├── 20-consul.md │ │ │ ├── 30-zookeeper.md │ │ │ ├── 40-hazelcast.md │ │ │ ├── _category_.json │ │ │ └── index.md │ │ ├── 05-security/ │ │ │ ├── 10-server-authentication.md │ │ │ ├── 20-actuator-security.md │ │ │ ├── 30-csrf-protection.md │ │ │ ├── _category_.json │ │ │ └── index.md │ │ ├── 06-customization/ │ │ │ ├── _category_.json │ │ │ ├── index.md │ │ │ ├── monitoring/ │ │ │ │ ├── 01-instance-filters.md │ │ │ │ ├── 02-custom-health-status.md │ │ │ │ └── _category_.json │ │ │ └── server/ │ │ │ ├── 04-endpoint-detection.md │ │ │ └── _category_.json │ │ ├── 08-third-party/ │ │ │ ├── _category_.json │ │ │ ├── index.md │ │ │ └── pyctuator.md │ │ ├── 09-samples/ │ │ │ ├── 10-sample-servlet.md │ │ │ ├── 20-sample-reactive.md │ │ │ ├── 30-sample-eureka.md │ │ │ ├── 40-sample-consul.md │ │ │ ├── 50-sample-zookeeper.md │ │ │ ├── 60-sample-hazelcast.md │ │ │ ├── 70-sample-custom-ui.md │ │ │ ├── _category_.json │ │ │ └── index.md │ │ ├── 10-reference/ │ │ │ ├── 10-event-types.md │ │ │ ├── 20-rest-api.md │ │ │ ├── 60-actuator-endpoints.mdx │ │ │ ├── _category_.json │ │ │ └── index.md │ │ ├── 11-upgrading/ │ │ │ ├── 01-spring-boot-admin-4.md │ │ │ ├── _category_.json │ │ │ └── index.md │ │ ├── index.mdx │ │ └── index.module.css │ ├── docusaurus.config.ts │ ├── package.json │ ├── sidebars.ts │ ├── src/ │ │ ├── components/ │ │ │ ├── CopyButton.module.css │ │ │ ├── CopyButton.tsx │ │ │ ├── HexMesh.module.css │ │ │ ├── HexMesh.tsx │ │ │ ├── PropertyTable.module.css │ │ │ ├── PropertyTable.tsx │ │ │ ├── Screenshot.module.css │ │ │ └── Screenshot.tsx │ │ ├── css/ │ │ │ └── custom.css │ │ ├── global.d.ts │ │ ├── pages/ │ │ │ ├── faq.md │ │ │ ├── impressum.md │ │ │ ├── index.tsx │ │ │ ├── markdown-page.md │ │ │ └── privacy.md │ │ ├── propertiesUtil.ts │ │ └── theme/ │ │ ├── DocCard/ │ │ │ ├── index.js │ │ │ └── styles.module.css │ │ └── MDXComponents.ts │ ├── static/ │ │ └── .nojekyll │ └── tsconfig.json ├── spring-boot-admin-samples/ │ ├── pom.xml │ ├── spring-boot-admin-sample-consul/ │ │ ├── docker-compose.yml │ │ ├── pom.xml │ │ └── src/ │ │ ├── main/ │ │ │ ├── java/ │ │ │ │ └── de/ │ │ │ │ └── codecentric/ │ │ │ │ └── boot/ │ │ │ │ └── admin/ │ │ │ │ └── sample/ │ │ │ │ └── SpringBootAdminConsulApplication.java │ │ │ └── resources/ │ │ │ ├── application-dev.yml │ │ │ ├── application-insecure.yml │ │ │ ├── application-secure.yml │ │ │ └── application.yml │ │ └── test/ │ │ └── java/ │ │ └── de/ │ │ └── codecentric/ │ │ └── boot/ │ │ └── admin/ │ │ └── sample/ │ │ └── SpringBootAdminConsulApplicationTest.java │ ├── spring-boot-admin-sample-custom-ui/ │ │ ├── .gitignore │ │ ├── README.md │ │ ├── babel.config.js │ │ ├── package.json │ │ ├── pom.xml │ │ ├── src/ │ │ │ ├── custom-endpoint.vue │ │ │ ├── custom-subitem.vue │ │ │ ├── custom.css │ │ │ ├── custom.vue │ │ │ ├── handle.vue │ │ │ ├── index.js │ │ │ └── routes.txt │ │ └── vite.config.js │ ├── spring-boot-admin-sample-eureka/ │ │ ├── docker-compose.yml │ │ ├── pom.xml │ │ └── src/ │ │ ├── main/ │ │ │ ├── docker/ │ │ │ │ └── Dockerfile │ │ │ ├── java/ │ │ │ │ └── de/ │ │ │ │ └── codecentric/ │ │ │ │ └── boot/ │ │ │ │ └── admin/ │ │ │ │ └── SpringBootAdminEurekaApplication.java │ │ │ └── resources/ │ │ │ ├── application-dev.yml │ │ │ ├── application-insecure.yml │ │ │ ├── application-secure.yml │ │ │ └── application.yml │ │ └── test/ │ │ └── java/ │ │ └── de/ │ │ └── codecentric/ │ │ └── boot/ │ │ └── admin/ │ │ └── SpringBootAdminEurekaApplicationTest.java │ ├── spring-boot-admin-sample-hazelcast/ │ │ ├── pom.xml │ │ └── src/ │ │ ├── main/ │ │ │ ├── java/ │ │ │ │ └── de/ │ │ │ │ └── codecentric/ │ │ │ │ └── boot/ │ │ │ │ └── admin/ │ │ │ │ └── sample/ │ │ │ │ └── SpringBootAdminHazelcastApplication.java │ │ │ └── resources/ │ │ │ ├── application-dev.yml │ │ │ ├── application-insecure.yml │ │ │ ├── application-secure.yml │ │ │ └── application.yml │ │ └── test/ │ │ └── java/ │ │ └── de/ │ │ └── codecentric/ │ │ └── boot/ │ │ └── admin/ │ │ └── sample/ │ │ └── SpringBootAdminHazelcastApplicationTest.java │ ├── spring-boot-admin-sample-reactive/ │ │ ├── pom.xml │ │ └── src/ │ │ ├── main/ │ │ │ ├── java/ │ │ │ │ └── de/ │ │ │ │ └── codecentric/ │ │ │ │ └── boot/ │ │ │ │ └── admin/ │ │ │ │ └── sample/ │ │ │ │ └── SpringBootAdminReactiveApplication.java │ │ │ └── resources/ │ │ │ ├── application-dev.yml │ │ │ ├── application-insecure.yml │ │ │ ├── application-secure.yml │ │ │ └── application.yml │ │ └── test/ │ │ └── java/ │ │ └── de/ │ │ └── codecentric/ │ │ └── boot/ │ │ └── admin/ │ │ └── sample/ │ │ └── SpringBootAdminReactiveApplicationTest.java │ ├── spring-boot-admin-sample-servlet/ │ │ ├── pom.xml │ │ └── src/ │ │ ├── main/ │ │ │ ├── java/ │ │ │ │ └── de/ │ │ │ │ └── codecentric/ │ │ │ │ └── boot/ │ │ │ │ └── admin/ │ │ │ │ └── sample/ │ │ │ │ ├── CustomCsrfFilter.java │ │ │ │ ├── CustomEndpoint.java │ │ │ │ ├── CustomNotifier.java │ │ │ │ ├── NotifierConfig.java │ │ │ │ ├── SecurityPermitAllConfig.java │ │ │ │ ├── SecuritySecureConfig.java │ │ │ │ └── SpringBootAdminServletApplication.java │ │ │ └── resources/ │ │ │ ├── application-dev.yml │ │ │ ├── application-insecure.yml │ │ │ ├── application-secure.yml │ │ │ ├── application-themed.yml │ │ │ └── application.yml │ │ └── test/ │ │ └── java/ │ │ └── de/ │ │ └── codecentric/ │ │ └── boot/ │ │ └── admin/ │ │ └── sample/ │ │ └── SpringBootAdminServletApplicationTest.java │ ├── spring-boot-admin-sample-servlet-graalvm/ │ │ ├── Readme.md │ │ ├── pom.xml │ │ └── src/ │ │ └── main/ │ │ ├── java/ │ │ │ └── de/ │ │ │ └── codecentric/ │ │ │ └── boot/ │ │ │ └── admin/ │ │ │ └── sample/ │ │ │ └── SpringBootAdminServletApplication.java │ │ └── resources/ │ │ └── application.yml │ ├── spring-boot-admin-sample-war/ │ │ ├── .gitignore │ │ ├── pom.xml │ │ └── src/ │ │ └── main/ │ │ ├── java/ │ │ │ └── de/ │ │ │ └── codecentric/ │ │ │ └── boot/ │ │ │ └── admin/ │ │ │ └── sample/ │ │ │ └── SpringBootAdminWarApplication.java │ │ └── resources/ │ │ ├── application-dev.yml │ │ ├── application-insecure.yml │ │ ├── application-secure.yml │ │ └── application.yml │ └── spring-boot-admin-sample-zookeeper/ │ ├── docker-compose.yml │ ├── pom.xml │ └── src/ │ ├── main/ │ │ ├── java/ │ │ │ └── de/ │ │ │ └── codecentric/ │ │ │ └── boot/ │ │ │ └── admin/ │ │ │ └── sample/ │ │ │ └── SpringBootAdminZookeeperApplication.java │ │ └── resources/ │ │ └── application.yml │ └── test/ │ └── java/ │ └── de/ │ └── codecentric/ │ └── boot/ │ └── admin/ │ └── sample/ │ └── SpringBootAdminZookeeperApplicationTest.java ├── spring-boot-admin-server/ │ ├── pom.xml │ └── src/ │ ├── main/ │ │ ├── java/ │ │ │ └── de/ │ │ │ └── codecentric/ │ │ │ └── boot/ │ │ │ └── admin/ │ │ │ └── server/ │ │ │ ├── config/ │ │ │ │ ├── AdminServerAutoConfiguration.java │ │ │ │ ├── AdminServerCloudFoundryAutoConfiguration.java │ │ │ │ ├── AdminServerHazelcastAutoConfiguration.java │ │ │ │ ├── AdminServerInstanceWebClientConfiguration.java │ │ │ │ ├── AdminServerMarkerConfiguration.java │ │ │ │ ├── AdminServerNotifierAutoConfiguration.java │ │ │ │ ├── AdminServerProperties.java │ │ │ │ ├── AdminServerWebConfiguration.java │ │ │ │ ├── EnableAdminServer.java │ │ │ │ ├── SpringBootAdminServerEnabledCondition.java │ │ │ │ └── package-info.java │ │ │ ├── domain/ │ │ │ │ ├── entities/ │ │ │ │ │ ├── Application.java │ │ │ │ │ ├── EventsourcingInstanceRepository.java │ │ │ │ │ ├── Instance.java │ │ │ │ │ ├── InstanceRepository.java │ │ │ │ │ ├── SnapshottingInstanceRepository.java │ │ │ │ │ └── package-info.java │ │ │ │ ├── events/ │ │ │ │ │ ├── InstanceDeregisteredEvent.java │ │ │ │ │ ├── InstanceEndpointsDetectedEvent.java │ │ │ │ │ ├── InstanceEvent.java │ │ │ │ │ ├── InstanceInfoChangedEvent.java │ │ │ │ │ ├── InstanceRegisteredEvent.java │ │ │ │ │ ├── InstanceRegistrationUpdatedEvent.java │ │ │ │ │ ├── InstanceStatusChangedEvent.java │ │ │ │ │ └── package-info.java │ │ │ │ └── values/ │ │ │ │ ├── BuildVersion.java │ │ │ │ ├── Endpoint.java │ │ │ │ ├── Endpoints.java │ │ │ │ ├── Info.java │ │ │ │ ├── InstanceId.java │ │ │ │ ├── Registration.java │ │ │ │ ├── StatusInfo.java │ │ │ │ ├── Tags.java │ │ │ │ └── package-info.java │ │ │ ├── eventstore/ │ │ │ │ ├── ConcurrentMapEventStore.java │ │ │ │ ├── HazelcastEventStore.java │ │ │ │ ├── InMemoryEventStore.java │ │ │ │ ├── InstanceEventPublisher.java │ │ │ │ ├── InstanceEventStore.java │ │ │ │ ├── OptimisticLockingException.java │ │ │ │ └── package-info.java │ │ │ ├── notify/ │ │ │ │ ├── AbstractEventNotifier.java │ │ │ │ ├── AbstractStatusChangeNotifier.java │ │ │ │ ├── CompositeNotifier.java │ │ │ │ ├── DingTalkNotifier.java │ │ │ │ ├── DiscordNotifier.java │ │ │ │ ├── FeiShuNotifier.java │ │ │ │ ├── HazelcastNotificationTrigger.java │ │ │ │ ├── HipchatNotifier.java │ │ │ │ ├── LetsChatNotifier.java │ │ │ │ ├── LoggingNotifier.java │ │ │ │ ├── MailNotifier.java │ │ │ │ ├── MattermostNotifier.java │ │ │ │ ├── MicrosoftTeamsNotifier.java │ │ │ │ ├── NotificationTrigger.java │ │ │ │ ├── Notifier.java │ │ │ │ ├── NotifierProxyProperties.java │ │ │ │ ├── OpsGenieNotifier.java │ │ │ │ ├── PagerdutyNotifier.java │ │ │ │ ├── RemindingNotifier.java │ │ │ │ ├── RocketChatNotifier.java │ │ │ │ ├── SlackNotifier.java │ │ │ │ ├── TelegramNotifier.java │ │ │ │ ├── WebexNotifier.java │ │ │ │ ├── filter/ │ │ │ │ │ ├── AbstractContentNotifier.java │ │ │ │ │ ├── AbstractNotificationFilter.java │ │ │ │ │ ├── ApplicationNameNotificationFilter.java │ │ │ │ │ ├── ExpiringNotificationFilter.java │ │ │ │ │ ├── FilteringNotifier.java │ │ │ │ │ ├── InstanceIdNotificationFilter.java │ │ │ │ │ ├── NotificationFilter.java │ │ │ │ │ ├── package-info.java │ │ │ │ │ └── web/ │ │ │ │ │ ├── NotificationFilterController.java │ │ │ │ │ └── package-info.java │ │ │ │ └── package-info.java │ │ │ ├── services/ │ │ │ │ ├── AbstractEventHandler.java │ │ │ │ ├── ApiMediaTypeHandler.java │ │ │ │ ├── ApplicationRegistry.java │ │ │ │ ├── CloudFoundryInstanceIdGenerator.java │ │ │ │ ├── EndpointDetectionTrigger.java │ │ │ │ ├── EndpointDetector.java │ │ │ │ ├── HashingInstanceUrlIdGenerator.java │ │ │ │ ├── InfoUpdateTrigger.java │ │ │ │ ├── InfoUpdater.java │ │ │ │ ├── InstanceFilter.java │ │ │ │ ├── InstanceIdGenerator.java │ │ │ │ ├── InstanceRegistry.java │ │ │ │ ├── IntervalCheck.java │ │ │ │ ├── StatusUpdateTrigger.java │ │ │ │ ├── StatusUpdater.java │ │ │ │ ├── endpoints/ │ │ │ │ │ ├── ChainingStrategy.java │ │ │ │ │ ├── EndpointDetectionStrategy.java │ │ │ │ │ ├── ProbeEndpointsStrategy.java │ │ │ │ │ ├── QueryIndexEndpointStrategy.java │ │ │ │ │ └── package-info.java │ │ │ │ └── package-info.java │ │ │ ├── utils/ │ │ │ │ └── jackson/ │ │ │ │ ├── AdminServerModule.java │ │ │ │ ├── BuildVersionMixin.java │ │ │ │ ├── EndpointMixin.java │ │ │ │ ├── EndpointsMixin.java │ │ │ │ ├── InfoMixin.java │ │ │ │ ├── InstanceDeregisteredEventMixin.java │ │ │ │ ├── InstanceEndpointsDetectedEventMixin.java │ │ │ │ ├── InstanceEventMixin.java │ │ │ │ ├── InstanceIdMixin.java │ │ │ │ ├── InstanceInfoChangedEventMixin.java │ │ │ │ ├── InstanceRegisteredEventMixin.java │ │ │ │ ├── InstanceRegistrationUpdatedEventMixin.java │ │ │ │ ├── InstanceStatusChangedEventMixin.java │ │ │ │ ├── RegistrationBeanSerializerModifier.java │ │ │ │ ├── RegistrationDeserializer.java │ │ │ │ ├── SanitizingMapSerializer.java │ │ │ │ ├── StatusInfoMixin.java │ │ │ │ ├── TagsMixin.java │ │ │ │ └── package-info.java │ │ │ └── web/ │ │ │ ├── AdminController.java │ │ │ ├── ApplicationsController.java │ │ │ ├── HttpHeaderFilter.java │ │ │ ├── InstanceWebProxy.java │ │ │ ├── InstancesController.java │ │ │ ├── PathUtils.java │ │ │ ├── client/ │ │ │ │ ├── BasicAuthHttpHeaderProvider.java │ │ │ │ ├── CloudFoundryHttpHeaderProvider.java │ │ │ │ ├── CompositeHttpHeadersProvider.java │ │ │ │ ├── HttpHeadersProvider.java │ │ │ │ ├── InstanceExchangeFilterFunction.java │ │ │ │ ├── InstanceExchangeFilterFunctions.java │ │ │ │ ├── InstanceWebClient.java │ │ │ │ ├── InstanceWebClientCustomizer.java │ │ │ │ ├── LegacyEndpointConverter.java │ │ │ │ ├── LegacyEndpointConverters.java │ │ │ │ ├── RefreshInstancesEvent.java │ │ │ │ ├── cookies/ │ │ │ │ │ ├── CookieStoreCleanupTrigger.java │ │ │ │ │ ├── JdkPerInstanceCookieStore.java │ │ │ │ │ ├── PerInstanceCookieStore.java │ │ │ │ │ └── package-info.java │ │ │ │ ├── exception/ │ │ │ │ │ ├── InstanceWebClientException.java │ │ │ │ │ ├── ResolveEndpointException.java │ │ │ │ │ ├── ResolveInstanceException.java │ │ │ │ │ └── package-info.java │ │ │ │ ├── package-info.java │ │ │ │ └── reactive/ │ │ │ │ ├── CompositeReactiveHttpHeadersProvider.java │ │ │ │ └── ReactiveHttpHeadersProvider.java │ │ │ ├── package-info.java │ │ │ ├── reactive/ │ │ │ │ ├── AdminControllerHandlerMapping.java │ │ │ │ ├── InstancesProxyController.java │ │ │ │ └── package-info.java │ │ │ └── servlet/ │ │ │ ├── AdminControllerHandlerMapping.java │ │ │ ├── InstancesProxyController.java │ │ │ └── package-info.java │ │ └── resources/ │ │ └── META-INF/ │ │ ├── additional-spring-configuration-metadata.json │ │ ├── spring/ │ │ │ └── org.springframework.boot.autoconfigure.AutoConfiguration.imports │ │ └── spring-boot-admin-server/ │ │ └── mail/ │ │ └── status-changed.html │ └── test/ │ ├── java/ │ │ └── de/ │ │ └── codecentric/ │ │ └── boot/ │ │ └── admin/ │ │ └── server/ │ │ ├── AbstractAdminApplicationTest.java │ │ ├── AdminApplicationHazelcastTest.java │ │ ├── AdminReactiveApplicationTest.java │ │ ├── AdminServletApplicationTest.java │ │ ├── config/ │ │ │ ├── AdminServerAutoConfigurationTest.java │ │ │ ├── AdminServerCloudFoundryAutoConfigurationTest.java │ │ │ ├── AdminServerInstanceWebClientConfigurationTest.java │ │ │ ├── AdminServerNotifierAutoConfigurationTest.java │ │ │ ├── AdminServerPropertiesTest.java │ │ │ └── SpringBootAdminServerEnabledConditionTest.java │ │ ├── domain/ │ │ │ ├── entities/ │ │ │ │ ├── AbstractInstanceRepositoryTest.java │ │ │ │ ├── EventsourcingInstanceRepositoryTest.java │ │ │ │ ├── InstanceTest.java │ │ │ │ └── SnapshottingInstanceRepositoryTest.java │ │ │ └── values/ │ │ │ ├── BuildVersionTest.java │ │ │ ├── EndpointTest.java │ │ │ ├── EndpointsTest.java │ │ │ ├── InfoTest.java │ │ │ ├── InstanceIdTest.java │ │ │ ├── RegistrationTest.java │ │ │ ├── StatusInfoTest.java │ │ │ └── TagsTest.java │ │ ├── eventstore/ │ │ │ ├── AbstractEventStoreTest.java │ │ │ ├── HazelcastEventStoreTest.java │ │ │ ├── HazelcastEventStoreWithClientConfigTest.java │ │ │ ├── HazelcastEventStoreWithServerConfigTest.java │ │ │ └── InMemoryEventStoreTest.java │ │ ├── notify/ │ │ │ ├── CompositeNotifierTest.java │ │ │ ├── DingTalkNotifierTest.java │ │ │ ├── DiscordNotifierTest.java │ │ │ ├── FeiShuNotifierTest.java │ │ │ ├── HazelcastNotificationTriggerTest.java │ │ │ ├── HipchatNotifierTest.java │ │ │ ├── LetsChatNotifierTest.java │ │ │ ├── MailNotifierIntegrationTest.java │ │ │ ├── MailNotifierTest.java │ │ │ ├── MattermostNotifierTest.java │ │ │ ├── MicrosoftTeamsNotifierTest.java │ │ │ ├── NotificationTriggerTest.java │ │ │ ├── OpsGenieNotifierTest.java │ │ │ ├── PagerdutyNotifierTest.java │ │ │ ├── RemindingNotifierTest.java │ │ │ ├── RocketChatNotifierTest.java │ │ │ ├── SlackNotifierTest.java │ │ │ ├── TelegramNotifierTest.java │ │ │ ├── TestNotifier.java │ │ │ ├── WebexNotifierTest.java │ │ │ └── filter/ │ │ │ ├── FilteringNotifierTest.java │ │ │ ├── InstanceIdNotificationFilterTest.java │ │ │ ├── InstanceNameNotificationFilterTest.java │ │ │ └── web/ │ │ │ └── NotificationFilterControllerTest.java │ │ ├── services/ │ │ │ ├── AbstractEventHandlerTest.java │ │ │ ├── ApplicationRegistryTest.java │ │ │ ├── CloudFoundryInstanceIdGeneratorTest.java │ │ │ ├── EndpointDetectionTriggerTest.java │ │ │ ├── EndpointDetectorTest.java │ │ │ ├── InfoUpdateTriggerTest.java │ │ │ ├── InfoUpdaterTest.java │ │ │ ├── InstanceRegistryTest.java │ │ │ ├── IntervalCheckTest.java │ │ │ ├── StatusUpdateTriggerTest.java │ │ │ ├── StatusUpdaterTest.java │ │ │ └── endpoints/ │ │ │ ├── ChainingStrategyTest.java │ │ │ ├── ProbeEndpointsStrategyTest.java │ │ │ └── QueryIndexEndpointStrategyTest.java │ │ ├── utils/ │ │ │ └── jackson/ │ │ │ ├── BuildVersionMixinTest.java │ │ │ ├── EndpointMixinTest.java │ │ │ ├── EndpointsMixinTest.java │ │ │ ├── InfoMixinTest.java │ │ │ ├── InstanceDeregisteredEventMixinTest.java │ │ │ ├── InstanceEndpointsDetectedEventMixinTest.java │ │ │ ├── InstanceEventMixinTest.java │ │ │ ├── InstanceIdMixinTest.java │ │ │ ├── InstanceInfoChangedEventMixinTest.java │ │ │ ├── InstanceRegisteredEventMixinTest.java │ │ │ ├── InstanceRegistrationUpdatedEventMixinTest.java │ │ │ ├── InstanceStatusChangedEventMixinTest.java │ │ │ ├── RegistrationDeserializerTest.java │ │ │ ├── StatusInfoMixinTest.java │ │ │ └── TagsMixinTest.java │ │ └── web/ │ │ ├── AbstractInstancesProxyControllerIntegrationTest.java │ │ ├── ConnectionCloseExtension.java │ │ ├── InstancesControllerIntegrationTest.java │ │ ├── PathUtilsTest.java │ │ ├── client/ │ │ │ ├── BasicAuthHttpHeaderProviderTest.java │ │ │ ├── CloudFoundryHttpHeaderProviderTest.java │ │ │ ├── CompositeHttpHeadersProviderTest.java │ │ │ ├── InstanceExchangeFilterFunctionsTest.java │ │ │ ├── InstanceWebClientTest.java │ │ │ ├── LegacyEndpointConvertersTest.java │ │ │ ├── cookies/ │ │ │ │ ├── CookieStoreCleanupTriggerTest.java │ │ │ │ └── JdkPerInstanceCookieStoreTest.java │ │ │ └── reactive/ │ │ │ └── CompositeReactiveHttpHeadersProviderTest.java │ │ ├── reactive/ │ │ │ └── InstancesProxyControllerIntegrationTest.java │ │ └── servlet/ │ │ └── InstancesProxyControllerIntegrationTest.java │ └── resources/ │ ├── application.yml │ ├── de/ │ │ └── codecentric/ │ │ └── boot/ │ │ └── admin/ │ │ └── server/ │ │ ├── junit-platform.properties │ │ ├── notify/ │ │ │ ├── allowed-file.html │ │ │ ├── custom-mail.html │ │ │ ├── expected-custom-mail │ │ │ ├── expected-default-mail │ │ │ └── vulnerable-file.html │ │ └── web/ │ │ └── client/ │ │ ├── beans-expected.json │ │ ├── beans-legacy.json │ │ ├── configprops-expected.json │ │ ├── configprops-legacy.json │ │ ├── env-expected.json │ │ ├── env-legacy.json │ │ ├── flyway-expected.json │ │ ├── flyway-legacy.json │ │ ├── health-expected.json │ │ ├── health-legacy.json │ │ ├── httptrace-expected.json │ │ ├── httptrace-legacy.json │ │ ├── liquibase-expected.json │ │ ├── liquibase-legacy.json │ │ ├── mappings-expected.json │ │ ├── mappings-legacy.json │ │ ├── threaddump-expected.json │ │ └── threaddump-legacy.json │ ├── logback-test.xml │ └── server-config-test.properties ├── spring-boot-admin-server-cloud/ │ ├── pom.xml │ └── src/ │ ├── main/ │ │ ├── java/ │ │ │ └── de/ │ │ │ └── codecentric/ │ │ │ └── boot/ │ │ │ └── admin/ │ │ │ └── server/ │ │ │ └── cloud/ │ │ │ ├── config/ │ │ │ │ ├── AdminServerDiscoveryAutoConfiguration.java │ │ │ │ └── package-info.java │ │ │ └── discovery/ │ │ │ ├── DefaultServiceInstanceConverter.java │ │ │ ├── EurekaServiceInstanceConverter.java │ │ │ ├── InstanceDiscoveryListener.java │ │ │ ├── KubernetesServiceInstanceConverter.java │ │ │ ├── ServiceInstanceConverter.java │ │ │ └── package-info.java │ │ └── resources/ │ │ └── META-INF/ │ │ ├── additional-spring-configuration-metadata.json │ │ └── spring/ │ │ └── org.springframework.boot.autoconfigure.AutoConfiguration.imports │ └── test/ │ ├── java/ │ │ └── de/ │ │ └── codecentric/ │ │ └── boot/ │ │ └── admin/ │ │ └── server/ │ │ └── cloud/ │ │ ├── AdminApplicationDiscoveryTest.java │ │ ├── config/ │ │ │ └── AdminServerDiscoveryAutoConfigurationTest.java │ │ └── discovery/ │ │ ├── DefaultServiceInstanceConverterTest.java │ │ ├── EurekaServiceInstanceConverterTest.java │ │ ├── InstanceDiscoveryListenerTest.java │ │ └── KubernetesServiceInstanceConverterTest.java │ └── resources/ │ ├── application.yml │ └── logback-test.xml ├── spring-boot-admin-server-ui/ │ ├── .gitignore │ ├── .npmrc │ ├── .nvmrc │ ├── .prettierrc.json │ ├── .storybook/ │ │ ├── main.js │ │ ├── preview-head.html │ │ ├── preview.js │ │ └── storybook.css │ ├── README.md │ ├── components.d.ts │ ├── eslint.config.mjs │ ├── package.json │ ├── pom.xml │ ├── postcss.config.js │ ├── src/ │ │ ├── main/ │ │ │ ├── frontend/ │ │ │ │ ├── HealthStatus.ts │ │ │ │ ├── components/ │ │ │ │ │ ├── ActionScope.ts │ │ │ │ │ ├── __snapshots__/ │ │ │ │ │ │ └── sba-formatted-obj.spec.ts.snap │ │ │ │ │ ├── font-awesome-icon.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── sba-accordion.spec.ts │ │ │ │ │ ├── sba-accordion.stories.ts │ │ │ │ │ ├── sba-accordion.vue │ │ │ │ │ ├── sba-action-button-scoped.spec.tsx │ │ │ │ │ ├── sba-action-button-scoped.stories.ts │ │ │ │ │ ├── sba-action-button-scoped.vue │ │ │ │ │ ├── sba-alert.stories.ts │ │ │ │ │ ├── sba-alert.vue │ │ │ │ │ ├── sba-button-group.stories.ts │ │ │ │ │ ├── sba-button-group.vue │ │ │ │ │ ├── sba-button.stories.ts │ │ │ │ │ ├── sba-button.vue │ │ │ │ │ ├── sba-checkbox.stories.ts │ │ │ │ │ ├── sba-checkbox.vue │ │ │ │ │ ├── sba-confirm-button.spec.ts │ │ │ │ │ ├── sba-confirm-button.stories.ts │ │ │ │ │ ├── sba-confirm-button.vue │ │ │ │ │ ├── sba-dropdown/ │ │ │ │ │ │ ├── sba-dropdown-divider.vue │ │ │ │ │ │ ├── sba-dropdown-item.vue │ │ │ │ │ │ ├── sba-dropdown.stories.ts │ │ │ │ │ │ └── sba-dropdown.vue │ │ │ │ │ ├── sba-formatted-obj.spec.ts │ │ │ │ │ ├── sba-formatted-obj.vue │ │ │ │ │ ├── sba-icon-button.stories.ts │ │ │ │ │ ├── sba-icon-button.vue │ │ │ │ │ ├── sba-input.stories.ts │ │ │ │ │ ├── sba-input.vue │ │ │ │ │ ├── sba-key-value-table.stories.ts │ │ │ │ │ ├── sba-key-value-table.vue │ │ │ │ │ ├── sba-loading-spinner.vue │ │ │ │ │ ├── sba-modal.spec.ts │ │ │ │ │ ├── sba-modal.stories.ts │ │ │ │ │ ├── sba-modal.vue │ │ │ │ │ ├── sba-nav/ │ │ │ │ │ │ ├── sba-nav-dropdown.stories.ts │ │ │ │ │ │ ├── sba-nav-dropdown.vue │ │ │ │ │ │ ├── sba-nav-item.stories.ts │ │ │ │ │ │ └── sba-nav-item.vue │ │ │ │ │ ├── sba-navbar/ │ │ │ │ │ │ ├── sba-navbar-brand.vue │ │ │ │ │ │ ├── sba-navbar-nav.vue │ │ │ │ │ │ ├── sba-navbar-toggle.stories.ts │ │ │ │ │ │ ├── sba-navbar-toggle.vue │ │ │ │ │ │ ├── sba-navbar.stories.ts │ │ │ │ │ │ └── sba-navbar.vue │ │ │ │ │ ├── sba-pagination-nav.spec.ts │ │ │ │ │ ├── sba-pagination-nav.stories.ts │ │ │ │ │ ├── sba-pagination-nav.vue │ │ │ │ │ ├── sba-panel.stories.ts │ │ │ │ │ ├── sba-panel.vue │ │ │ │ │ ├── sba-select.stories.ts │ │ │ │ │ ├── sba-select.vue │ │ │ │ │ ├── sba-status-badge.spec.ts │ │ │ │ │ ├── sba-status-badge.vue │ │ │ │ │ ├── sba-status.spec.ts │ │ │ │ │ ├── sba-status.stories.ts │ │ │ │ │ ├── sba-status.vue │ │ │ │ │ ├── sba-sticky-subnav.vue │ │ │ │ │ ├── sba-tag.stories.ts │ │ │ │ │ ├── sba-tag.vue │ │ │ │ │ ├── sba-tags.stories.ts │ │ │ │ │ ├── sba-tags.vue │ │ │ │ │ ├── sba-time-ago.spec.ts │ │ │ │ │ ├── sba-time-ago.vue │ │ │ │ │ ├── sba-toggle-scope-button.spec.ts │ │ │ │ │ ├── sba-toggle-scope-button.stories.ts │ │ │ │ │ ├── sba-toggle-scope-button.vue │ │ │ │ │ ├── sba-wave.vue │ │ │ │ │ └── table.stories.ts │ │ │ │ ├── components.d.ts │ │ │ │ ├── composables/ │ │ │ │ │ ├── ViewRegistry.ts │ │ │ │ │ ├── useApplicationStore.ts │ │ │ │ │ ├── useClassnameShortener.spec.ts │ │ │ │ │ ├── useClassnameShortener.ts │ │ │ │ │ └── useDateTimeFormatter.ts │ │ │ │ ├── directives/ │ │ │ │ │ ├── on-resize.ts │ │ │ │ │ ├── popper.ts │ │ │ │ │ └── sticks-below.ts │ │ │ │ ├── global.d.ts │ │ │ │ ├── i18n/ │ │ │ │ │ ├── PrimeLocale.ts │ │ │ │ │ ├── i18n.de.json │ │ │ │ │ ├── i18n.en.json │ │ │ │ │ ├── i18n.es.json │ │ │ │ │ ├── i18n.fr.json │ │ │ │ │ ├── i18n.is.json │ │ │ │ │ ├── i18n.ko.json │ │ │ │ │ ├── i18n.pt-BR.json │ │ │ │ │ ├── i18n.ru.json │ │ │ │ │ ├── i18n.zh-CN.json │ │ │ │ │ ├── i18n.zh-TW.json │ │ │ │ │ └── index.ts │ │ │ │ ├── index.css │ │ │ │ ├── index.html │ │ │ │ ├── index.stories.jsx │ │ │ │ ├── index.ts │ │ │ │ ├── login/ │ │ │ │ │ ├── login.i18n.de.json │ │ │ │ │ ├── login.i18n.en.json │ │ │ │ │ ├── login.i18n.es.json │ │ │ │ │ ├── login.i18n.fr.json │ │ │ │ │ ├── login.i18n.is.json │ │ │ │ │ ├── login.i18n.ko.json │ │ │ │ │ ├── login.i18n.pt-BR.json │ │ │ │ │ ├── login.i18n.ru.json │ │ │ │ │ ├── login.i18n.zh-CN.json │ │ │ │ │ ├── login.i18n.zh-TW.json │ │ │ │ │ ├── login.stories.ts │ │ │ │ │ └── login.vue │ │ │ │ ├── login.css │ │ │ │ ├── login.html │ │ │ │ ├── login.ts │ │ │ │ ├── mixins/ │ │ │ │ │ └── subscribing.ts │ │ │ │ ├── mocks/ │ │ │ │ │ ├── applications/ │ │ │ │ │ │ ├── data.ts │ │ │ │ │ │ └── index.ts │ │ │ │ │ ├── browser.ts │ │ │ │ │ ├── fixtures/ │ │ │ │ │ │ └── eventStream/ │ │ │ │ │ │ ├── registerWithOneInstance.ts │ │ │ │ │ │ ├── registerWithTwoInstances.ts │ │ │ │ │ │ └── removeInstance.ts │ │ │ │ │ ├── instance/ │ │ │ │ │ │ ├── auditevents/ │ │ │ │ │ │ │ ├── data.ts │ │ │ │ │ │ │ └── index.ts │ │ │ │ │ │ ├── dependencies/ │ │ │ │ │ │ │ ├── data.ts │ │ │ │ │ │ │ └── index.ts │ │ │ │ │ │ ├── flyway/ │ │ │ │ │ │ │ ├── data.ts │ │ │ │ │ │ │ └── index.ts │ │ │ │ │ │ ├── health/ │ │ │ │ │ │ │ └── index.ts │ │ │ │ │ │ ├── httptrace/ │ │ │ │ │ │ │ ├── data.ts │ │ │ │ │ │ │ └── index.ts │ │ │ │ │ │ ├── info/ │ │ │ │ │ │ │ └── index.ts │ │ │ │ │ │ ├── jolokia/ │ │ │ │ │ │ │ ├── data.read.ts │ │ │ │ │ │ │ ├── data.ts │ │ │ │ │ │ │ └── index.ts │ │ │ │ │ │ ├── liquibase/ │ │ │ │ │ │ │ ├── data.ts │ │ │ │ │ │ │ └── index.ts │ │ │ │ │ │ ├── mappings/ │ │ │ │ │ │ │ ├── data.ts │ │ │ │ │ │ │ └── index.ts │ │ │ │ │ │ ├── metrics/ │ │ │ │ │ │ │ ├── data.ts │ │ │ │ │ │ │ └── index.ts │ │ │ │ │ │ ├── scheduledtasks/ │ │ │ │ │ │ │ ├── data.ts │ │ │ │ │ │ │ └── index.ts │ │ │ │ │ │ └── sessions/ │ │ │ │ │ │ ├── data.ts │ │ │ │ │ │ └── index.ts │ │ │ │ │ └── server.ts │ │ │ │ ├── notificationcenter.d.ts │ │ │ │ ├── notifications.ts │ │ │ │ ├── plugins/ │ │ │ │ │ └── modal/ │ │ │ │ │ ├── ConfirmButtons.vue │ │ │ │ │ ├── Modal.vue │ │ │ │ │ ├── api.ts │ │ │ │ │ ├── helpers.ts │ │ │ │ │ └── index.ts │ │ │ │ ├── public/ │ │ │ │ │ └── variables.css │ │ │ │ ├── sba-config.ts │ │ │ │ ├── sba-settings.js │ │ │ │ ├── services/ │ │ │ │ │ ├── application.spec.ts │ │ │ │ │ ├── application.ts │ │ │ │ │ ├── bus.ts │ │ │ │ │ ├── instance.spec.ts │ │ │ │ │ ├── instance.ts │ │ │ │ │ ├── notification-filter.ts │ │ │ │ │ ├── spring-mime-types.ts │ │ │ │ │ ├── startup-activator-tree.ts │ │ │ │ │ ├── startup-actuator.fixture.spec.json │ │ │ │ │ ├── startup-actuator.spec.ts │ │ │ │ │ └── startup-actuator.ts │ │ │ │ ├── shell/ │ │ │ │ │ ├── i18n.de.json │ │ │ │ │ ├── i18n.en.json │ │ │ │ │ ├── i18n.es.json │ │ │ │ │ ├── i18n.fr.json │ │ │ │ │ ├── i18n.is.json │ │ │ │ │ ├── i18n.ko.json │ │ │ │ │ ├── i18n.ru.json │ │ │ │ │ ├── i18n.zh-CN.json │ │ │ │ │ ├── i18n.zh-TW.json │ │ │ │ │ ├── index.vue │ │ │ │ │ ├── navbar.spec.ts │ │ │ │ │ ├── navbar.vue │ │ │ │ │ ├── sba-dropdown-logout-item.vue │ │ │ │ │ ├── sba-nav-language-selector.spec.ts │ │ │ │ │ ├── sba-nav-language-selector.vue │ │ │ │ │ └── sba-nav-usermenu.vue │ │ │ │ ├── store.spec.ts │ │ │ │ ├── store.ts │ │ │ │ ├── test-utils.ts │ │ │ │ ├── tests/ │ │ │ │ │ └── setup.ts │ │ │ │ ├── toast-theme.css │ │ │ │ ├── utils/ │ │ │ │ │ ├── array.ts │ │ │ │ │ ├── autolink.spec.ts │ │ │ │ │ ├── autolink.ts │ │ │ │ │ ├── axios.spec.ts │ │ │ │ │ ├── axios.ts │ │ │ │ │ ├── collections.spec.ts │ │ │ │ │ ├── collections.ts │ │ │ │ │ ├── d3.ts │ │ │ │ │ ├── eventsource-polyfill.ts │ │ │ │ │ ├── formatWithDataTypes.spec.ts │ │ │ │ │ ├── formatWithDataTypes.ts │ │ │ │ │ ├── http-status.ts │ │ │ │ │ ├── iso8601-duration.spec.ts │ │ │ │ │ ├── iso8601-duration.ts │ │ │ │ │ ├── logtail.ts │ │ │ │ │ ├── objToYaml.ts │ │ │ │ │ ├── prettyTime.ts │ │ │ │ │ ├── rxjs.spec.ts │ │ │ │ │ ├── rxjs.ts │ │ │ │ │ ├── sanitizeHtml.spec.ts │ │ │ │ │ ├── sanitizeHtml.ts │ │ │ │ │ ├── shortenClassname.spec.ts │ │ │ │ │ ├── shortenClassname.ts │ │ │ │ │ ├── sortObject.spec.ts │ │ │ │ │ ├── sortObject.ts │ │ │ │ │ ├── toast.ts │ │ │ │ │ ├── transformToJSON.spec.ts │ │ │ │ │ ├── transformToJSON.ts │ │ │ │ │ ├── uri.spec.ts │ │ │ │ │ ├── uri.ts │ │ │ │ │ ├── useRouterState.spec.ts │ │ │ │ │ ├── useRouterState.ts │ │ │ │ │ └── useSubscription.ts │ │ │ │ ├── viewRegistry.spec.ts │ │ │ │ ├── viewRegistry.ts │ │ │ │ ├── views/ │ │ │ │ │ ├── ViewGroup.ts │ │ │ │ │ ├── about/ │ │ │ │ │ │ ├── handle.vue │ │ │ │ │ │ ├── i18n.de.json │ │ │ │ │ │ ├── i18n.en.json │ │ │ │ │ │ ├── i18n.es.json │ │ │ │ │ │ ├── i18n.fr.json │ │ │ │ │ │ ├── i18n.is.json │ │ │ │ │ │ ├── i18n.ko.json │ │ │ │ │ │ ├── i18n.pt-BR.json │ │ │ │ │ │ ├── i18n.ru.json │ │ │ │ │ │ ├── i18n.zh-CN.json │ │ │ │ │ │ ├── i18n.zh-TW.json │ │ │ │ │ │ └── index.vue │ │ │ │ │ ├── applications/ │ │ │ │ │ │ ├── ActionHandler.ts │ │ │ │ │ │ ├── ApplicationListItemAction.spec.ts │ │ │ │ │ │ ├── ApplicationListItemAction.vue │ │ │ │ │ │ ├── ApplicationNotificationCenter.vue │ │ │ │ │ │ ├── ApplicationStats.vue │ │ │ │ │ │ ├── ApplicationStatusHero.spec.ts │ │ │ │ │ │ ├── ApplicationStatusHero.vue │ │ │ │ │ │ ├── InstancesList.spec.ts │ │ │ │ │ │ ├── InstancesList.vue │ │ │ │ │ │ ├── NotificationFilterSettings.vue │ │ │ │ │ │ ├── applications.spec.ts │ │ │ │ │ │ ├── handle.vue │ │ │ │ │ │ ├── i18n.de.json │ │ │ │ │ │ ├── i18n.en.json │ │ │ │ │ │ ├── i18n.es.json │ │ │ │ │ │ ├── i18n.fr.json │ │ │ │ │ │ ├── i18n.is.json │ │ │ │ │ │ ├── i18n.ko.json │ │ │ │ │ │ ├── i18n.pt-BR.json │ │ │ │ │ │ ├── i18n.ru.json │ │ │ │ │ │ ├── i18n.zh-CN.json │ │ │ │ │ │ ├── i18n.zh-TW.json │ │ │ │ │ │ ├── index.vue │ │ │ │ │ │ └── listItem/ │ │ │ │ │ │ ├── ItemInformation.vue │ │ │ │ │ │ └── ItemTags.vue │ │ │ │ │ ├── external/ │ │ │ │ │ │ ├── index.spec.ts │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ └── style.css │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── instances/ │ │ │ │ │ │ ├── auditevents/ │ │ │ │ │ │ │ ├── auditevents-list.stories.ts │ │ │ │ │ │ │ ├── auditevents-list.vue │ │ │ │ │ │ │ ├── i18n.de.json │ │ │ │ │ │ │ ├── i18n.en.json │ │ │ │ │ │ │ ├── i18n.es.json │ │ │ │ │ │ │ ├── i18n.fr.json │ │ │ │ │ │ │ ├── i18n.is.json │ │ │ │ │ │ │ ├── i18n.ko.json │ │ │ │ │ │ │ ├── i18n.pt-BR.json │ │ │ │ │ │ │ ├── i18n.ru.json │ │ │ │ │ │ │ ├── i18n.zh-CN.json │ │ │ │ │ │ │ ├── i18n.zh-TW.json │ │ │ │ │ │ │ ├── index.spec.ts │ │ │ │ │ │ │ └── index.vue │ │ │ │ │ │ ├── beans/ │ │ │ │ │ │ │ ├── beans-list-details.vue │ │ │ │ │ │ │ ├── beans-list.vue │ │ │ │ │ │ │ ├── i18n.de.json │ │ │ │ │ │ │ ├── i18n.en.json │ │ │ │ │ │ │ ├── i18n.es.json │ │ │ │ │ │ │ ├── i18n.fr.json │ │ │ │ │ │ │ ├── i18n.is.json │ │ │ │ │ │ │ ├── i18n.ko.json │ │ │ │ │ │ │ ├── i18n.pt-BR.json │ │ │ │ │ │ │ ├── i18n.ru.json │ │ │ │ │ │ │ ├── i18n.zh-CN.json │ │ │ │ │ │ │ ├── i18n.zh-TW.json │ │ │ │ │ │ │ └── index.vue │ │ │ │ │ │ ├── caches/ │ │ │ │ │ │ │ ├── caches-list.vue │ │ │ │ │ │ │ ├── i18n.de.json │ │ │ │ │ │ │ ├── i18n.en.json │ │ │ │ │ │ │ ├── i18n.es.json │ │ │ │ │ │ │ ├── i18n.fr.json │ │ │ │ │ │ │ ├── i18n.is.json │ │ │ │ │ │ │ ├── i18n.ko.json │ │ │ │ │ │ │ ├── i18n.pt-BR.json │ │ │ │ │ │ │ ├── i18n.ru.json │ │ │ │ │ │ │ ├── i18n.zh-CN.json │ │ │ │ │ │ │ ├── i18n.zh-TW.json │ │ │ │ │ │ │ └── index.vue │ │ │ │ │ │ ├── conditions/ │ │ │ │ │ │ │ ├── conditions-list-details.spec.ts │ │ │ │ │ │ │ ├── conditions-list-details.vue │ │ │ │ │ │ │ ├── conditions-list.vue │ │ │ │ │ │ │ ├── i18n.de.json │ │ │ │ │ │ │ ├── i18n.en.json │ │ │ │ │ │ │ ├── i18n.zh-TW.json │ │ │ │ │ │ │ └── index.vue │ │ │ │ │ │ ├── configprops/ │ │ │ │ │ │ │ ├── i18n.de.json │ │ │ │ │ │ │ ├── i18n.en.json │ │ │ │ │ │ │ ├── i18n.es.json │ │ │ │ │ │ │ ├── i18n.fr.json │ │ │ │ │ │ │ ├── i18n.is.json │ │ │ │ │ │ │ ├── i18n.ko.json │ │ │ │ │ │ │ ├── i18n.pt-BR.json │ │ │ │ │ │ │ ├── i18n.ru.json │ │ │ │ │ │ │ ├── i18n.zh-CN.json │ │ │ │ │ │ │ ├── i18n.zh-TW.json │ │ │ │ │ │ │ └── index.vue │ │ │ │ │ │ ├── dependencies/ │ │ │ │ │ │ │ ├── SbomList.vue │ │ │ │ │ │ │ ├── i18n.de.json │ │ │ │ │ │ │ ├── i18n.en.json │ │ │ │ │ │ │ ├── i18n.zh-TW.json │ │ │ │ │ │ │ ├── index.spec.ts │ │ │ │ │ │ │ └── index.vue │ │ │ │ │ │ ├── details/ │ │ │ │ │ │ │ ├── LineChart.vue │ │ │ │ │ │ │ ├── __snapshots__/ │ │ │ │ │ │ │ │ └── health-details.spec.ts.snap │ │ │ │ │ │ │ ├── cache-chart.vue │ │ │ │ │ │ │ ├── datasource-chart.vue │ │ │ │ │ │ │ ├── details-cache.spec.ts │ │ │ │ │ │ │ ├── details-cache.vue │ │ │ │ │ │ │ ├── details-caches.vue │ │ │ │ │ │ │ ├── details-datasource.spec.ts │ │ │ │ │ │ │ ├── details-datasource.vue │ │ │ │ │ │ │ ├── details-datasources.vue │ │ │ │ │ │ │ ├── details-gc.vue │ │ │ │ │ │ │ ├── details-health.spec.ts │ │ │ │ │ │ │ ├── details-health.vue │ │ │ │ │ │ │ ├── details-hero.vue │ │ │ │ │ │ │ ├── details-info.spec.ts │ │ │ │ │ │ │ ├── details-info.vue │ │ │ │ │ │ │ ├── details-memory.spec.ts │ │ │ │ │ │ │ ├── details-memory.vue │ │ │ │ │ │ │ ├── details-metadata.spec.ts │ │ │ │ │ │ │ ├── details-metadata.vue │ │ │ │ │ │ │ ├── details-nav.vue │ │ │ │ │ │ │ ├── details-process.vue │ │ │ │ │ │ │ ├── details-threads.spec.ts │ │ │ │ │ │ │ ├── details-threads.vue │ │ │ │ │ │ │ ├── health-details.spec.ts │ │ │ │ │ │ │ ├── health-details.vue │ │ │ │ │ │ │ ├── i18n.de.json │ │ │ │ │ │ │ ├── i18n.en.json │ │ │ │ │ │ │ ├── i18n.es.json │ │ │ │ │ │ │ ├── i18n.fr.json │ │ │ │ │ │ │ ├── i18n.is.json │ │ │ │ │ │ │ ├── i18n.ko.json │ │ │ │ │ │ │ ├── i18n.pt-BR.json │ │ │ │ │ │ │ ├── i18n.ru.json │ │ │ │ │ │ │ ├── i18n.zh-CN.json │ │ │ │ │ │ │ ├── i18n.zh-TW.json │ │ │ │ │ │ │ ├── index.spec.ts │ │ │ │ │ │ │ ├── index.vue │ │ │ │ │ │ │ ├── instance-switcher.vue │ │ │ │ │ │ │ ├── mem-chart.vue │ │ │ │ │ │ │ ├── process-uptime.ts │ │ │ │ │ │ │ └── threads-chart.vue │ │ │ │ │ │ ├── env/ │ │ │ │ │ │ │ ├── busrefresh.spec.ts │ │ │ │ │ │ │ ├── busrefresh.vue │ │ │ │ │ │ │ ├── env-manager.vue │ │ │ │ │ │ │ ├── i18n.de.json │ │ │ │ │ │ │ ├── i18n.en.json │ │ │ │ │ │ │ ├── i18n.es.json │ │ │ │ │ │ │ ├── i18n.fr.json │ │ │ │ │ │ │ ├── i18n.is.json │ │ │ │ │ │ │ ├── i18n.ko.json │ │ │ │ │ │ │ ├── i18n.pt-BR.json │ │ │ │ │ │ │ ├── i18n.ru.json │ │ │ │ │ │ │ ├── i18n.zh-CN.json │ │ │ │ │ │ │ ├── i18n.zh-TW.json │ │ │ │ │ │ │ ├── index.vue │ │ │ │ │ │ │ ├── refresh.spec.ts │ │ │ │ │ │ │ └── refresh.vue │ │ │ │ │ │ ├── flyway/ │ │ │ │ │ │ │ ├── i18n.de.json │ │ │ │ │ │ │ ├── i18n.en.json │ │ │ │ │ │ │ ├── i18n.es.json │ │ │ │ │ │ │ ├── i18n.fr.json │ │ │ │ │ │ │ ├── i18n.is.json │ │ │ │ │ │ │ ├── i18n.ko.json │ │ │ │ │ │ │ ├── i18n.pt-BR.json │ │ │ │ │ │ │ ├── i18n.ru.json │ │ │ │ │ │ │ ├── i18n.zh-CN.json │ │ │ │ │ │ │ ├── i18n.zh-TW.json │ │ │ │ │ │ │ └── index.vue │ │ │ │ │ │ ├── gateway/ │ │ │ │ │ │ │ ├── add-route.vue │ │ │ │ │ │ │ ├── global-filters.vue │ │ │ │ │ │ │ ├── i18n.de.json │ │ │ │ │ │ │ ├── i18n.en.json │ │ │ │ │ │ │ ├── i18n.es.json │ │ │ │ │ │ │ ├── i18n.fr.json │ │ │ │ │ │ │ ├── i18n.is.json │ │ │ │ │ │ │ ├── i18n.ko.json │ │ │ │ │ │ │ ├── i18n.pt-BR.json │ │ │ │ │ │ │ ├── i18n.ru.json │ │ │ │ │ │ │ ├── i18n.zh-CN.json │ │ │ │ │ │ │ ├── i18n.zh-TW.json │ │ │ │ │ │ │ ├── index.vue │ │ │ │ │ │ │ ├── refresh-route-cache.vue │ │ │ │ │ │ │ ├── route-definition.vue │ │ │ │ │ │ │ ├── route.vue │ │ │ │ │ │ │ ├── routes-list.vue │ │ │ │ │ │ │ └── routes.vue │ │ │ │ │ │ ├── heapdump/ │ │ │ │ │ │ │ ├── i18n.de.json │ │ │ │ │ │ │ ├── i18n.en.json │ │ │ │ │ │ │ ├── i18n.es.json │ │ │ │ │ │ │ ├── i18n.fr.json │ │ │ │ │ │ │ ├── i18n.is.json │ │ │ │ │ │ │ ├── i18n.ko.json │ │ │ │ │ │ │ ├── i18n.pt-BR.json │ │ │ │ │ │ │ ├── i18n.ru.json │ │ │ │ │ │ │ ├── i18n.zh-CN.json │ │ │ │ │ │ │ ├── i18n.zh-TW.json │ │ │ │ │ │ │ └── index.vue │ │ │ │ │ │ ├── httpexchanges/ │ │ │ │ │ │ │ ├── Exchange.ts │ │ │ │ │ │ │ ├── content-column.vue │ │ │ │ │ │ │ ├── exchanges-chart.vue │ │ │ │ │ │ │ ├── exchanges-list.vue │ │ │ │ │ │ │ ├── i18n.de.json │ │ │ │ │ │ │ ├── i18n.en.json │ │ │ │ │ │ │ ├── i18n.es.json │ │ │ │ │ │ │ ├── i18n.fr.json │ │ │ │ │ │ │ ├── i18n.is.json │ │ │ │ │ │ │ ├── i18n.ko.json │ │ │ │ │ │ │ ├── i18n.pt-BR.json │ │ │ │ │ │ │ ├── i18n.ru.json │ │ │ │ │ │ │ ├── i18n.zh-CN.json │ │ │ │ │ │ │ ├── i18n.zh-TW.json │ │ │ │ │ │ │ ├── index.spec.ts │ │ │ │ │ │ │ └── index.vue │ │ │ │ │ │ ├── httptrace/ │ │ │ │ │ │ │ ├── i18n.de.json │ │ │ │ │ │ │ ├── i18n.en.json │ │ │ │ │ │ │ ├── i18n.es.json │ │ │ │ │ │ │ ├── i18n.fr.json │ │ │ │ │ │ │ ├── i18n.is.json │ │ │ │ │ │ │ ├── i18n.ko.json │ │ │ │ │ │ │ ├── i18n.pt-BR.json │ │ │ │ │ │ │ ├── i18n.ru.json │ │ │ │ │ │ │ ├── i18n.zh-CN.json │ │ │ │ │ │ │ ├── i18n.zh-TW.json │ │ │ │ │ │ │ ├── index.vue │ │ │ │ │ │ │ ├── traces-chart.vue │ │ │ │ │ │ │ └── traces-list.vue │ │ │ │ │ │ ├── iframe/ │ │ │ │ │ │ │ ├── IframeView.vue │ │ │ │ │ │ │ └── index.ts │ │ │ │ │ │ ├── jolokia/ │ │ │ │ │ │ │ ├── MBean.ts │ │ │ │ │ │ │ ├── MBeanDescriptor.ts │ │ │ │ │ │ │ ├── i18n.de.json │ │ │ │ │ │ │ ├── i18n.en.json │ │ │ │ │ │ │ ├── i18n.es.json │ │ │ │ │ │ │ ├── i18n.fr.json │ │ │ │ │ │ │ ├── i18n.is.json │ │ │ │ │ │ │ ├── i18n.ko.json │ │ │ │ │ │ │ ├── i18n.pt-BR.json │ │ │ │ │ │ │ ├── i18n.ru.json │ │ │ │ │ │ │ ├── i18n.zh-CN.json │ │ │ │ │ │ │ ├── i18n.zh-TW.json │ │ │ │ │ │ │ ├── index.spec.ts │ │ │ │ │ │ │ ├── index.vue │ │ │ │ │ │ │ ├── m-bean-attribute.spec.ts │ │ │ │ │ │ │ ├── m-bean-attribute.vue │ │ │ │ │ │ │ ├── m-bean-attributes.vue │ │ │ │ │ │ │ ├── m-bean-operation-invocation.vue │ │ │ │ │ │ │ ├── m-bean-operation.spec.ts │ │ │ │ │ │ │ ├── m-bean-operation.vue │ │ │ │ │ │ │ ├── m-bean-operations.vue │ │ │ │ │ │ │ ├── responseHandler.spec.ts │ │ │ │ │ │ │ ├── responseHandler.ts │ │ │ │ │ │ │ ├── utils.spec.ts │ │ │ │ │ │ │ └── utils.ts │ │ │ │ │ │ ├── liquibase/ │ │ │ │ │ │ │ ├── i18n.de.json │ │ │ │ │ │ │ ├── i18n.en.json │ │ │ │ │ │ │ ├── i18n.es.json │ │ │ │ │ │ │ ├── i18n.fr.json │ │ │ │ │ │ │ ├── i18n.is.json │ │ │ │ │ │ │ ├── i18n.ko.json │ │ │ │ │ │ │ ├── i18n.pt-BR.json │ │ │ │ │ │ │ ├── i18n.ru.json │ │ │ │ │ │ │ ├── i18n.zh-CN.json │ │ │ │ │ │ │ ├── i18n.zh-TW.json │ │ │ │ │ │ │ └── index.vue │ │ │ │ │ │ ├── logfile/ │ │ │ │ │ │ │ ├── i18n.de.json │ │ │ │ │ │ │ ├── i18n.en.json │ │ │ │ │ │ │ ├── i18n.es.json │ │ │ │ │ │ │ ├── i18n.fr.json │ │ │ │ │ │ │ ├── i18n.is.json │ │ │ │ │ │ │ ├── i18n.ko.json │ │ │ │ │ │ │ ├── i18n.pt-BR.json │ │ │ │ │ │ │ ├── i18n.ru.json │ │ │ │ │ │ │ ├── i18n.zh-CN.json │ │ │ │ │ │ │ ├── i18n.zh-TW.json │ │ │ │ │ │ │ └── index.vue │ │ │ │ │ │ ├── loggers/ │ │ │ │ │ │ │ ├── i18n.de.json │ │ │ │ │ │ │ ├── i18n.en.json │ │ │ │ │ │ │ ├── i18n.es.json │ │ │ │ │ │ │ ├── i18n.fr.json │ │ │ │ │ │ │ ├── i18n.is.json │ │ │ │ │ │ │ ├── i18n.ko.json │ │ │ │ │ │ │ ├── i18n.pt-BR.json │ │ │ │ │ │ │ ├── i18n.ru.json │ │ │ │ │ │ │ ├── i18n.zh-CN.json │ │ │ │ │ │ │ ├── i18n.zh-TW.json │ │ │ │ │ │ │ ├── index.vue │ │ │ │ │ │ │ ├── logger-control.vue │ │ │ │ │ │ │ ├── loggers-list.spec.ts │ │ │ │ │ │ │ ├── loggers-list.vue │ │ │ │ │ │ │ ├── loggers.vue │ │ │ │ │ │ │ ├── service.spec.ts │ │ │ │ │ │ │ └── service.ts │ │ │ │ │ │ ├── mappings/ │ │ │ │ │ │ │ ├── DispatcherMappings.spec.ts │ │ │ │ │ │ │ ├── DispatcherMappings.vue │ │ │ │ │ │ │ ├── ServletFilterMappings.vue │ │ │ │ │ │ │ ├── ServletMappings.vue │ │ │ │ │ │ │ ├── i18n.de.json │ │ │ │ │ │ │ ├── i18n.en.json │ │ │ │ │ │ │ ├── i18n.es.json │ │ │ │ │ │ │ ├── i18n.fr.json │ │ │ │ │ │ │ ├── i18n.is.json │ │ │ │ │ │ │ ├── i18n.ko.json │ │ │ │ │ │ │ ├── i18n.pt-BR.json │ │ │ │ │ │ │ ├── i18n.ru.json │ │ │ │ │ │ │ ├── i18n.zh-CN.json │ │ │ │ │ │ │ ├── i18n.zh-TW.json │ │ │ │ │ │ │ ├── index.spec.ts │ │ │ │ │ │ │ └── index.vue │ │ │ │ │ │ ├── metrics/ │ │ │ │ │ │ │ ├── i18n.de.json │ │ │ │ │ │ │ ├── i18n.en.json │ │ │ │ │ │ │ ├── i18n.es.json │ │ │ │ │ │ │ ├── i18n.fr.json │ │ │ │ │ │ │ ├── i18n.is.json │ │ │ │ │ │ │ ├── i18n.ko.json │ │ │ │ │ │ │ ├── i18n.pt-BR.json │ │ │ │ │ │ │ ├── i18n.ru.json │ │ │ │ │ │ │ ├── i18n.zh-CN.json │ │ │ │ │ │ │ ├── i18n.zh-TW.json │ │ │ │ │ │ │ ├── index.vue │ │ │ │ │ │ │ └── metric.vue │ │ │ │ │ │ ├── quartz/ │ │ │ │ │ │ │ ├── i18n.de.json │ │ │ │ │ │ │ ├── i18n.en.json │ │ │ │ │ │ │ ├── i18n.es.json │ │ │ │ │ │ │ ├── i18n.fr.json │ │ │ │ │ │ │ ├── i18n.is.json │ │ │ │ │ │ │ ├── i18n.ko.json │ │ │ │ │ │ │ ├── i18n.pt-BR.json │ │ │ │ │ │ │ ├── i18n.ru.json │ │ │ │ │ │ │ ├── i18n.zh-CN.json │ │ │ │ │ │ │ ├── i18n.zh-TW.json │ │ │ │ │ │ │ ├── index.vue │ │ │ │ │ │ │ ├── trigger-row.spec.ts │ │ │ │ │ │ │ └── trigger-row.vue │ │ │ │ │ │ ├── sbomdependencytrees/ │ │ │ │ │ │ │ ├── dependencyTree.ts │ │ │ │ │ │ │ ├── i18n.en.json │ │ │ │ │ │ │ ├── i18n.zh-TW.json │ │ │ │ │ │ │ ├── index.spec.ts │ │ │ │ │ │ │ ├── index.vue │ │ │ │ │ │ │ ├── sbomUtils.spec.ts │ │ │ │ │ │ │ ├── sbomUtils.ts │ │ │ │ │ │ │ ├── tree.spec.ts │ │ │ │ │ │ │ └── tree.vue │ │ │ │ │ │ ├── scheduledtasks/ │ │ │ │ │ │ │ ├── i18n.de.json │ │ │ │ │ │ │ ├── i18n.en.json │ │ │ │ │ │ │ ├── i18n.es.json │ │ │ │ │ │ │ ├── i18n.fr.json │ │ │ │ │ │ │ ├── i18n.is.json │ │ │ │ │ │ │ ├── i18n.ko.json │ │ │ │ │ │ │ ├── i18n.pt-BR.json │ │ │ │ │ │ │ ├── i18n.ru.json │ │ │ │ │ │ │ ├── i18n.zh-CN.json │ │ │ │ │ │ │ ├── i18n.zh-TW.json │ │ │ │ │ │ │ ├── index.vue │ │ │ │ │ │ │ ├── scheduled-task-executions.spec.ts │ │ │ │ │ │ │ └── scheduled-task-executions.vue │ │ │ │ │ │ ├── sessions/ │ │ │ │ │ │ │ ├── i18n.de.json │ │ │ │ │ │ │ ├── i18n.en.json │ │ │ │ │ │ │ ├── i18n.es.json │ │ │ │ │ │ │ ├── i18n.fr.json │ │ │ │ │ │ │ ├── i18n.is.json │ │ │ │ │ │ │ ├── i18n.ko.json │ │ │ │ │ │ │ ├── i18n.pt-BR.json │ │ │ │ │ │ │ ├── i18n.ru.json │ │ │ │ │ │ │ ├── i18n.zh-CN.json │ │ │ │ │ │ │ ├── i18n.zh-TW.json │ │ │ │ │ │ │ ├── index.vue │ │ │ │ │ │ │ └── sessions-list.vue │ │ │ │ │ │ ├── shell/ │ │ │ │ │ │ │ ├── InstanceShell.vue │ │ │ │ │ │ │ ├── i18n.de.json │ │ │ │ │ │ │ ├── i18n.en.json │ │ │ │ │ │ │ ├── i18n.es.json │ │ │ │ │ │ │ ├── i18n.fr.json │ │ │ │ │ │ │ ├── i18n.is.json │ │ │ │ │ │ │ ├── i18n.ko.json │ │ │ │ │ │ │ ├── i18n.pt-BR.json │ │ │ │ │ │ │ ├── i18n.ru.json │ │ │ │ │ │ │ ├── i18n.zh-CN.json │ │ │ │ │ │ │ ├── i18n.zh-TW.json │ │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ │ ├── sba-instance-section.vue │ │ │ │ │ │ │ ├── sidebar.stories.ts │ │ │ │ │ │ │ └── sidebar.vue │ │ │ │ │ │ ├── startup/ │ │ │ │ │ │ │ ├── i18n.de.json │ │ │ │ │ │ │ ├── i18n.en.json │ │ │ │ │ │ │ ├── i18n.es.json │ │ │ │ │ │ │ ├── i18n.is.json │ │ │ │ │ │ │ ├── i18n.zh-TW.json │ │ │ │ │ │ │ ├── index.vue │ │ │ │ │ │ │ ├── tree-item.vue │ │ │ │ │ │ │ └── tree-table.vue │ │ │ │ │ │ └── threaddump/ │ │ │ │ │ │ ├── i18n.de.json │ │ │ │ │ │ ├── i18n.en.json │ │ │ │ │ │ ├── i18n.es.json │ │ │ │ │ │ ├── i18n.fr.json │ │ │ │ │ │ ├── i18n.is.json │ │ │ │ │ │ ├── i18n.ko.json │ │ │ │ │ │ ├── i18n.pt-BR.json │ │ │ │ │ │ ├── i18n.ru.json │ │ │ │ │ │ ├── i18n.zh-CN.json │ │ │ │ │ │ ├── i18n.zh-TW.json │ │ │ │ │ │ ├── index.vue │ │ │ │ │ │ ├── thread-list-item.vue │ │ │ │ │ │ ├── thread-tag.vue │ │ │ │ │ │ └── threads-list.vue │ │ │ │ │ ├── journal/ │ │ │ │ │ │ ├── InstanceEvent.ts │ │ │ │ │ │ ├── JournalTable.spec.ts │ │ │ │ │ │ ├── JournalTable.vue │ │ │ │ │ │ ├── deduplicate-events.spec.ts │ │ │ │ │ │ ├── deduplicate-events.ts │ │ │ │ │ │ ├── i18n.de.json │ │ │ │ │ │ ├── i18n.en.json │ │ │ │ │ │ ├── i18n.es.json │ │ │ │ │ │ ├── i18n.fr.json │ │ │ │ │ │ ├── i18n.is.json │ │ │ │ │ │ ├── i18n.ko.json │ │ │ │ │ │ ├── i18n.pt-BR.json │ │ │ │ │ │ ├── i18n.ru.json │ │ │ │ │ │ ├── i18n.zh-CN.json │ │ │ │ │ │ ├── i18n.zh-TW.json │ │ │ │ │ │ ├── index.spec.ts │ │ │ │ │ │ ├── index.vue │ │ │ │ │ │ └── utils.ts │ │ │ │ │ └── wallboard/ │ │ │ │ │ ├── hex-mesh.vue │ │ │ │ │ ├── i18n.de.json │ │ │ │ │ ├── i18n.en.json │ │ │ │ │ ├── i18n.es.json │ │ │ │ │ ├── i18n.fr.json │ │ │ │ │ ├── i18n.is.json │ │ │ │ │ ├── i18n.ko.json │ │ │ │ │ ├── i18n.pt-BR.json │ │ │ │ │ ├── i18n.ru.json │ │ │ │ │ ├── i18n.zh-CN.json │ │ │ │ │ ├── i18n.zh-TW.json │ │ │ │ │ ├── index.vue │ │ │ │ │ ├── utils.spec.ts │ │ │ │ │ ├── utils.ts │ │ │ │ │ ├── wallboard.spec.ts │ │ │ │ │ └── wallboard.stories.ts │ │ │ │ └── vite-env.d.ts │ │ │ ├── java/ │ │ │ │ └── de/ │ │ │ │ └── codecentric/ │ │ │ │ └── boot/ │ │ │ │ └── admin/ │ │ │ │ └── server/ │ │ │ │ └── ui/ │ │ │ │ ├── config/ │ │ │ │ │ ├── AdminServerUiAutoConfiguration.java │ │ │ │ │ ├── AdminServerUiProperties.java │ │ │ │ │ ├── CssColorUtils.java │ │ │ │ │ ├── ServerRuntimeHints.java │ │ │ │ │ ├── SpringNativeServerAutoConfiguration.java │ │ │ │ │ └── package-info.java │ │ │ │ ├── extensions/ │ │ │ │ │ ├── UiExtension.java │ │ │ │ │ ├── UiExtensions.java │ │ │ │ │ ├── UiExtensionsScanner.java │ │ │ │ │ ├── UiRoutesScanner.java │ │ │ │ │ └── package-info.java │ │ │ │ └── web/ │ │ │ │ ├── HomepageForwardingFilterConfig.java │ │ │ │ ├── HomepageForwardingMatcher.java │ │ │ │ ├── UiController.java │ │ │ │ ├── package-info.java │ │ │ │ ├── reactive/ │ │ │ │ │ ├── HomepageForwardingFilter.java │ │ │ │ │ └── package-info.java │ │ │ │ └── servlet/ │ │ │ │ ├── HomepageForwardingFilter.java │ │ │ │ └── package-info.java │ │ │ └── resources/ │ │ │ └── META-INF/ │ │ │ └── spring/ │ │ │ └── org.springframework.boot.autoconfigure.AutoConfiguration.imports │ │ └── test/ │ │ ├── java/ │ │ │ └── de/ │ │ │ └── codecentric/ │ │ │ └── boot/ │ │ │ └── admin/ │ │ │ └── server/ │ │ │ └── ui/ │ │ │ ├── AbstractAdminUiApplicationTest.java │ │ │ ├── AdminUiReactiveApplicationTest.java │ │ │ ├── AdminUiServletApplicationTest.java │ │ │ ├── config/ │ │ │ │ ├── AdminServerUiAutoConfigurationTest.java │ │ │ │ ├── AdminServerUiPropertiesTest.java │ │ │ │ ├── CssColorUtilsTest.java │ │ │ │ ├── ReactiveAdminServerUiAutoConfigurationAdminContextPathTest.java │ │ │ │ ├── ReactiveAdminServerUiAutoConfigurationBothPathsTest.java │ │ │ │ ├── ReactiveAdminServerUiAutoConfigurationTest.java │ │ │ │ └── ReactiveAdminServerUiAutoConfigurationWebfluxBasePathTest.java │ │ │ ├── extensions/ │ │ │ │ ├── UiExtensionsScannerTest.java │ │ │ │ └── UiRoutesScannerTest.java │ │ │ └── web/ │ │ │ ├── HomepageForwardingMatcherTest.java │ │ │ └── UiControllerTest.java │ │ └── resources/ │ │ ├── META-INF/ │ │ │ └── test-extensions/ │ │ │ └── custom/ │ │ │ ├── custom.abcdef.css │ │ │ ├── custom.abcdef.js │ │ │ ├── custom.txt │ │ │ └── routes.txt │ │ ├── application.yml │ │ └── mockito-extensions/ │ │ └── org.mockito.plugins.MockMaker │ ├── tailwind.config.js │ ├── tsconfig.json │ ├── tsconfig.node.json │ └── vite.config.mts ├── spring-boot-admin-starter-client/ │ └── pom.xml ├── spring-boot-admin-starter-server/ │ └── pom.xml └── src/ └── checkstyle/ ├── checkstyle-header.txt └── checkstyle.xml ================================================ FILE CONTENTS ================================================ ================================================ FILE: .editorconfig ================================================ root = true [*] insert_final_newline = true 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 indent_size = 4 ij_java_space_before_for_left_brace = true ij_java_keep_multiple_expressions_in_one_line = true ij_java_keep_simple_blocks_in_one_line = true ij_java_keep_simple_classes_in_one_line = true ij_java_else_on_new_line = true ij_java_catch_on_new_line = true ij_java_finally_on_new_line = true ij_java_align_multiline_method_parentheses = false ij_java_keep_blank_lines_before_right_brace = 1 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.**, *, tools.**, |, de.codecentric.boot.admin.**, |, $* ij_java_layout_static_imports_separately = true [*.xml] indent_style = space indent_size = 4 [*.{js,ts,vue}] indent_style = space indent_size = 2 ================================================ FILE: .gitattributes ================================================ # All text files should have the "lf" (Unix) line endings * text eol=lf # windows cmd shoud have the "crlf" (Win32) line endings *.cmd eol=crlf # Explicitly declare text files you want to always be normalized and converted # to native line endings on checkout. *.java text *.js text *.css text *.html text *.properties text *.xml text *.yml text # Denote all files that are truly binary and should not be modified. *.png binary *.jpg binary *.jar binary *.ttf binary ================================================ FILE: .github/CODEOWNERS ================================================ * @codecentric/spring-boot-admins ================================================ FILE: .github/ISSUE_TEMPLATE/spring-boot-admin-bug.md ================================================ --- name: Bug about: Spring Boot Admin issue template for reporting bugs title: '' labels: bug, waiting-for-triage assignees: '' --- ## Spring Boot Admin Server information - **Version**: - **Spring Boot version**: - **Configured Security**: - **Webflux or Servlet application**: ## Client information - **Spring Boot versions**: - **Used discovery mechanism**: - **Webflux or Servlet application**: ## Description ================================================ FILE: .github/ISSUE_TEMPLATE/spring-boot-admin-enhancement.md ================================================ --- name: Enhancement / Feature Request about: Spring Boot Admin issue template for proposing Enhancements or making feature requests title: '' labels: enhancement, waiting-for-triage assignees: '' --- ================================================ FILE: .github/copilot-instructions.md ================================================ # Spring Boot Admin Spring Boot Admin is a multi-module Maven project providing an admin interface for Spring Boot applications that expose actuator endpoints. It consists of a Vue.js frontend and Java backend components with 19 Maven modules. Always reference these instructions first and fallback to search or bash commands only when you encounter unexpected information that does not match the info here. ## Working Effectively ### Prerequisites and Environment Setup - Install Java 17 (OpenJDK Temurin 17.0.16+ recommended) - project requires exactly Java 17 - Install Node.js 22.18.0 exactly (project specifies this in .nvmrc and package.json) - Download Node.js 22.18.0: `curl -fsSL https://nodejs.org/dist/v22.18.0/node-v22.18.0-linux-x64.tar.xz -o /tmp/node.tar.xz` - Extract and configure PATH: `cd /tmp && tar -xf node.tar.xz && export PATH="/tmp/node-v22.18.0-linux-x64/bin:$PATH"` - Verify versions: `java -version` (should show 17.x) and `node --version` (should show v22.18.0) ### Building the Project - **NEVER CANCEL builds - they take time but will complete successfully** - **CRITICAL**: Set timeout to 20+ minutes for builds, 60+ minutes for tests - Clean compile: `export PATH="/tmp/node-v22.18.0-linux-x64/bin:$PATH" && ./mvnw clean compile -B --no-transfer-progress -DskipTests` -- takes ~10.5 minutes - Full package: `export PATH="/tmp/node-v22.18.0-linux-x64/bin:$PATH" && ./mvnw package -B --no-transfer-progress -DskipTests` -- takes ~4 minutes - Install to local repository: `export PATH="/tmp/node-v22.18.0-linux-x64/bin:$PATH" && ./mvnw install -B --no-transfer-progress -DskipTests` -- takes ~1.5 minutes ### Testing - **NEVER CANCEL test runs - set 60+ minute timeouts** - Full test suite: `export PATH="/tmp/node-v22.18.0-linux-x64/bin:$PATH" && ./mvnw test -B --no-transfer-progress` -- takes 20-40 minutes - UI tests only: `cd spring-boot-admin-server-ui && export PATH="/tmp/node-v22.18.0-linux-x64/bin:$PATH" && npm run test` -- takes ~44 seconds - UI build only: `cd spring-boot-admin-server-ui && export PATH="/tmp/node-v22.18.0-linux-x64/bin:$PATH" && npm run build` -- takes ~16 seconds ### Code Quality and Formatting - Format Java code: `./mvnw spring-javaformat:apply` - Check Java formatting: `./mvnw spring-javaformat:validate` - Lint UI code: `cd spring-boot-admin-server-ui && npm run lint` - Format UI code: `cd spring-boot-admin-server-ui && npm run format:fix` - **ALWAYS** run `./mvnw spring-javaformat:apply` and UI formatting before committing - **ALWAYS** run `./mvnw checkstyle:check` to verify compliance ### Running Sample Applications - Build and install all modules first: `export PATH="/tmp/node-v22.18.0-linux-x64/bin:$PATH" && ./mvnw install -B --no-transfer-progress -DskipTests` - Run servlet sample: `cd spring-boot-admin-samples/spring-boot-admin-sample-servlet && export PATH="/tmp/node-v22.18.0-linux-x64/bin:$PATH" && ../../mvnw spring-boot:run -Dspring-boot.run.profiles=dev,insecure` - Access UI at: `http://localhost:8080` (username: user, password shown in logs or use insecure profile) - Application starts in ~3 seconds and shows "all up" status with registered instance ### UI Development Mode - For UI development: `cd spring-boot-admin-server-ui && npm run build:watch` (builds on file changes) - Configure Spring Boot Admin Server with: ``` spring.boot.admin.ui.cache.no-cache: true spring.boot.admin.ui.resource-locations: file:../../spring-boot-admin-server-ui/target/dist/ spring.boot.admin.ui.template-location: file:../../spring-boot-admin-server-ui/target/dist/ spring.boot.admin.ui.cache-templates: false ``` ## Validation Scenarios - **MANUAL VALIDATION REQUIRED**: After building and running, always test actual functionality - Launch sample application and verify Spring Boot Admin UI loads at `http://localhost:8080` - Verify application registration: Should show "spring-boot-admin-sample-servlet" with UP status - Test navigation: Click on Applications, Wallboard, and instance details - Check endpoints: Verify actuator endpoints are detected and accessible - Test monitoring features: Instance details should show health, metrics, loggers, etc. ## Key Modules and Locations ### Primary Components - **Root**: `/` - Main Maven reactor project (pom.xml) - **Server Backend**: `spring-boot-admin-server/` - Core Spring Boot Admin server functionality - **UI Frontend**: `spring-boot-admin-server-ui/` - Vue.js 3 web interface with Vite build - **Client**: `spring-boot-admin-client/` - Client library for connecting applications - **Documentation**: `spring-boot-admin-docs/` - Asciidoc documentation ### Starter Modules - **Server Starter**: `spring-boot-admin-starter-server/` - Auto-configuration for servers - **Client Starter**: `spring-boot-admin-starter-client/` - Auto-configuration for clients ### Sample Applications (in spring-boot-admin-samples/) - **servlet**: Standard servlet-based sample (recommended for testing) - **reactive**: WebFlux reactive sample - **eureka**: Eureka discovery integration - **consul**: Consul discovery integration - **hazelcast**: Hazelcast session management - **war**: WAR deployment sample - **zookeeper**: Zookeeper discovery integration ### Infrastructure - **Dependencies**: `spring-boot-admin-dependencies/` - Dependency management BOM - **Build**: `spring-boot-admin-build/` - Build configuration and tooling - **Server Cloud**: `spring-boot-admin-server-cloud/` - Spring Cloud integrations ## Common Issues and Solutions ### Build Issues - Node.js version mismatch: Always use exact PATH override with v22.18.0 - Missing dependencies: Run `./mvnw install` to install all modules to local Maven repository - Checkstyle violations: Run `./mvnw spring-javaformat:apply` to auto-fix formatting - UI build failures: Verify Node.js version and run `npm install` in spring-boot-admin-server-ui/ - "Command timed out": Increase timeout - builds legitimately take 10+ minutes ### Runtime Issues - Sample app dependency resolution: Must run `./mvnw install` first to populate local repository - Config server connection refused: Normal warning, config server is optional in dev mode - Security warnings: Use `insecure` profile for development to bypass authentication ## Development Workflow 1. **Setup**: Configure Node.js 22.18.0 in PATH: `export PATH="/tmp/node-v22.18.0-linux-x64/bin:$PATH"` 2. **Initial Build**: Run `./mvnw clean compile` to verify everything builds (~10.5 minutes - be patient) 3. **Install Dependencies**: Run `./mvnw install -DskipTests` for sample app dependencies (~1.5 minutes) 4. **Make Changes**: Edit code in appropriate modules 5. **Format Code**: Run `./mvnw spring-javaformat:apply` and UI `npm run format:fix` 6. **Test Build**: Run build commands to verify changes don't break anything 7. **Manual Validation**: Start sample application and test UI functionality 8. **Final Check**: Ensure formatting and linting pass before committing ## Critical Timing Information - **NEVER CANCEL**: Build commands take significant time but complete successfully - Clean compile: ~10.5 minutes (normal, expected) - Package build: ~4 minutes - Install build: ~1.5 minutes - Full test suite: 20-40 minutes (set 60+ minute timeout) - UI tests only: ~44 seconds - UI build only: ~16 seconds - Application startup: ~3 seconds ## Technology Stack - **Backend**: Spring Boot 3.5, Java 17, Maven multi-module - **Frontend**: Vue.js 3, Vite, TypeScript, Tailwind CSS - **Testing**: JUnit 5 (Java), Vitest (UI), Playwright integration - **Build**: Maven 3.9+, Node.js 22.18.0, npm - **Code Quality**: Spring Java Format, Checkstyle, ESLint, Prettier ## Repository Structure Overview ``` spring-boot-admin/ # Root project (19 modules) ├── .github/ # GitHub configuration ├── spring-boot-admin-build/ # Build configuration ├── spring-boot-admin-client/ # Client library ├── spring-boot-admin-dependencies/ # Dependency management ├── spring-boot-admin-docs/ # Documentation (Asciidoc) ├── spring-boot-admin-samples/ # Sample applications │ ├── spring-boot-admin-sample-servlet/ # Main sample (recommended) │ ├── spring-boot-admin-sample-reactive/ # WebFlux sample │ └── [other samples]/ # Various integration samples ├── spring-boot-admin-server/ # Core server implementation ├── spring-boot-admin-server-cloud/ # Spring Cloud integrations ├── spring-boot-admin-server-ui/ # Vue.js frontend │ ├── src/main/frontend/ # Vue.js source code │ ├── package.json # Node.js dependencies │ └── .nvmrc # Node.js version specification ├── spring-boot-admin-starter-*/ # Spring Boot auto-configuration ├── mvnw # Maven wrapper └── pom.xml # Root Maven configuration ``` ================================================ FILE: .github/milestones.sh ================================================ #!/usr/bin/env bash set -euo pipefail show_help() { cat </dev/null 2>&1; then echo "ERROR: jq is required. Install jq and try again." exit 1 fi if ! command -v git >/dev/null 2>&1; then echo "ERROR: git is required. Install git and try again." exit 1 fi # If base not provided, attempt to find previous tag ordered by creation date (descending) if [[ -z "$BASE" ]]; then if git rev-parse --verify "$TAG" >/dev/null 2>&1; then mapfile -t TAGS < <(git tag --sort=-creatordate) PREV_TAG="" found=0 for i in "${!TAGS[@]}"; do if [[ "${TAGS[$i]}" == "$TAG" ]]; then found=1 if [[ $((i+1)) -lt ${#TAGS[@]} ]]; then PREV_TAG="${TAGS[$((i+1))]}" fi break fi done if [[ $found -eq 0 ]]; then echo "Warning: tag '$TAG' not found locally. You may need to 'git fetch --tags'." BASE="" else if [[ -n "$PREV_TAG" ]]; then echo "Detected previous tag: $PREV_TAG" BASE="$PREV_TAG" else echo "No previous tag found (this looks like the oldest tag). Continuing with single tag commit." BASE="" fi fi else echo "Warning: tag '$TAG' not found locally. Continuing; will attempt API-based fallback if needed." BASE="" fi fi # Build commit list: if BASE is empty, use the single tag's commit; otherwise range base..tag COMMITS=() if [[ -n "$BASE" ]]; then # verify both reachable locally; if not present, try to fetch tags/branches from remote if ! git rev-parse --verify "$BASE" >/dev/null 2>&1 || ! git rev-parse --verify "$TAG" >/dev/null 2>&1; then echo "One of base or tag is not present locally. Attempting to fetch tags/heads from origin..." git fetch --tags --prune --no-recurse-submodules --quiet || true fi # Re-check git rev-parse --verify "$BASE" >/dev/null 2>&1 || { echo "ERROR: base '$BASE' not found locally after fetch"; exit 1; } git rev-parse --verify "$TAG" >/dev/null 2>&1 || { echo "ERROR: tag '$TAG' not found locally after fetch"; exit 1; } # list commits from base (exclusive) to tag (inclusive) while IFS= read -r sha; do COMMITS+=("$sha") done < <(git rev-list --reverse "${BASE}..${TAG}") else # try to get tag commit locally; if not present, fall back to GitHub compare API to get commits if git rev-parse --verify "$TAG" >/dev/null 2>&1; then TAG_SHA="$(git rev-list -n 1 "$TAG")" COMMITS+=("$TAG_SHA") else echo "Tag not present locally. Falling back to GitHub compare API to get commits for tag ${TAG}..." # We attempt to compare default branch...tag to get commits — best-effort default_branch="$(curl -sSL -H "$AUTH" "${API}/repos/${OWNER}/${REPO}" | jq -r '.default_branch')" if [[ -z "$default_branch" || "$default_branch" == "null" ]]; then echo "ERROR: could not determine default branch via API." exit 1 fi # Use compare endpoint: default_branch...tag cmp=$(curl -sSL -H "$AUTH" "${API}/repos/${OWNER}/${REPO}/compare/${default_branch}...${TAG}") if echo "$cmp" | jq -e 'has("commits")' >/dev/null 2>&1; then mapfile -t commits_from_api < <(echo "$cmp" | jq -r '.commits[]?.sha') COMMITS+=("${commits_from_api[@]}") else echo "API compare did not return commits. Response:" echo "$cmp" | jq -C . exit 1 fi fi fi echo "Found ${#COMMITS[@]} commit(s)." # For each commit, call GitHub API to get associated PRs declare -A PRS_MAP=() for sha in "${COMMITS[@]}"; do echo "Querying PRs for commit $sha..." resp="$(curl -sSL -H "$AUTH" -H "$ACCEPT" \ "${API}/repos/${OWNER}/${REPO}/commits/${sha}/pulls")" if echo "$resp" | jq -e 'has("message")' >/dev/null 2>&1; then msg=$(echo "$resp" | jq -r '.message // empty') if [[ -n "$msg" ]]; then echo "GitHub API error for commit $sha: $msg" echo "Full response:" echo "$resp" | jq -C . exit 1 fi fi pr_numbers=$(echo "$resp" | jq -r '.[]?.number' || true) if [[ -n "$pr_numbers" ]]; then while IFS= read -r pr; do if [[ -n "$pr" ]]; then PRS_MAP["$pr"]=1 echo " -> PR #$pr" fi done <<<"$pr_numbers" fi done if [[ ${#PRS_MAP[@]} -eq 0 ]]; then echo "No PRs associated with commits found. Exiting." exit 0 fi PR_LIST=() for k in "${!PRS_MAP[@]}"; do PR_LIST+=("$k"); done echo "Total unique PRs to update: ${#PR_LIST[@]}" # Check existing milestones for a matching title echo "Checking for existing milestone named '$TAG'..." MILESTONES_RESP="$(curl -sSL -H "$AUTH" "${API}/repos/${OWNER}/${REPO}/milestones?state=all&per_page=100")" MILESTONE_NUMBER="$(echo "$MILESTONES_RESP" | jq -r --arg TITLE "$TAG" '.[] | select(.title==$TITLE) | .number' | head -n1 || true)" if [[ -n "$MILESTONE_NUMBER" && "$MILESTONE_NUMBER" != "null" ]]; then echo "Found existing milestone '$TAG' (number: $MILESTONE_NUMBER)." else if [[ $DRYRUN -eq 1 ]]; then echo "Would create milestone '$TAG' (dry-run)." MILESTONE_NUMBER="DUMMY_ID" else echo "Creating milestone '$TAG'..." create_resp="$(curl -sSL -H "$AUTH" -H "Content-Type: application/json" \ -d "{\"title\": \"${TAG}\", \"description\": \"Auto-created milestone for tag ${TAG}\"}" \ "${API}/repos/${OWNER}/${REPO}/milestones")" if echo "$create_resp" | jq -e 'has("message")' >/dev/null 2>&1; then err=$(echo "$create_resp" | jq -r '.message // empty') echo "Error creating milestone: $err" echo "Response: $create_resp" | jq -C . exit 1 fi MILESTONE_NUMBER="$(echo "$create_resp" | jq -r '.number')" if [[ -z "$MILESTONE_NUMBER" || "$MILESTONE_NUMBER" == "null" ]]; then echo "Failed to create milestone. Response: $create_resp" exit 1 fi echo "Created milestone '${TAG}' (number: $MILESTONE_NUMBER)." fi fi # Patch each PR (issues endpoint) to set milestone for prnum in "${PR_LIST[@]}"; do if [[ $DRYRUN -eq 1 ]]; then echo "Would update PR #${prnum} -> milestone ${MILESTONE_NUMBER}" continue fi echo "Updating PR #${prnum} -> milestone ${MILESTONE_NUMBER}..." patch_resp="$(curl -sSL -X PATCH -H "$AUTH" -H "Content-Type: application/json" \ -d "{\"milestone\": ${MILESTONE_NUMBER}}" \ "${API}/repos/${OWNER}/${REPO}/issues/${prnum}")" if echo "$patch_resp" | jq -e 'has("message")' >/dev/null 2>&1; then err=$(echo "$patch_resp" | jq -r '.message // empty') echo "Warning: failed to update PR #${prnum}: $err" echo "Response: $patch_resp" | jq -C . # continue so we attempt remaining PRs continue fi echo "PR #${prnum} updated." done echo "Done." exit 0 ================================================ FILE: .github/release.yml ================================================ changelog: categories: - title: Features labels: - '*' exclude: labels: - fix - breaking-change - dependencies - bot - title: Bug Fixes labels: - fix - title: Breaking Changes labels: - breaking-change - title: Dependencies labels: - dependencies ================================================ FILE: .github/workflows/build-feature.yml ================================================ name: Build Pull Request on: push: branches-ignore: - master - 1.* - 2.* pull_request: jobs: build: strategy: matrix: os: [ubuntu-latest] runs-on: ${{ matrix.os }} steps: - name: free disk space continue-on-error: true run: | df -h sudo swapoff -a sudo rm -f /swapfile sudo apt clean if [ -n "$(docker image ls -q)" ]; then docker rmi -f $(docker image ls -aq) || true fi df -h - uses: actions/checkout@v6 - name: Set up JDK uses: actions/setup-java@v5 with: distribution: 'temurin' java-version: '17' cache: 'maven' cache-dependency-path: '**/pom.xml' - name: Set up Node uses: actions/setup-node@v6 with: node-version: 'lts/*' cache: 'npm' cache-dependency-path: '**/package-lock.json' - name: Build with Maven run: ./mvnw -B --no-transfer-progress install -P coverage - name: Upload surefire reports if: always() uses: actions/upload-artifact@v7 with: name: surefire-reports path: | **/target/surefire-reports/*.xml **/target/surefire-reports/*.txt **/target/surefire-reports/*.dump* **/target/surefire-reports/*.out **/target/surefire-reports/*.err if-no-files-found: warn retention-days: 14 ================================================ FILE: .github/workflows/build-main.yml ================================================ name: Build Main / Version Branch on: push: branches: - master - 1.* - 2.* - 3.* jobs: build: strategy: matrix: os: [ ubuntu-latest ] runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v6 - name: Set up JDK uses: actions/setup-java@v5 with: distribution: 'temurin' java-version: '17' cache: 'maven' cache-dependency-path: '**/pom.xml' - name: Set up Node uses: actions/setup-node@v6 with: node-version: 'lts/*' cache: 'npm' cache-dependency-path: '**/package-lock.json' - name: Build with Maven run: ./mvnw -B --no-transfer-progress install -P coverage - name: Upload surefire reports if: always() uses: actions/upload-artifact@v7 with: name: surefire-reports path: | **/target/surefire-reports/*.xml **/target/surefire-reports/*.txt **/target/surefire-reports/*.dump* **/target/surefire-reports/*.out **/target/surefire-reports/*.err if-no-files-found: warn retention-days: 14 - name: Upload coverage to Codecov uses: codecov/codecov-action@v5 publish-snapshot: needs: build runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 - name: Set up JDK uses: actions/setup-java@v5 with: distribution: 'temurin' java-version: '17' cache: 'maven' cache-dependency-path: '**/pom.xml' - name: Set up Node uses: actions/setup-node@v6 with: node-version: 'lts/*' cache: 'npm' cache-dependency-path: '**/package-lock.json' - name: Publish SNAPSHOT version to GitHub Packages (we can skip tests, since we only deploy, if the build workflow succeeded) run: ./mvnw -B deploy --no-transfer-progress -DskipTests env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Extract Maven project version for Asciidoc GitHub Pages directory naming run: echo ::set-output name=version::$(./mvnw -q -Dexec.executable=echo -Dexec.args='${project.version}' --non-recursive exec:exec) id: project - name: Show extracted Maven project version run: echo ${{ steps.project.outputs.version }} - name: Build documentation with Maven run: ./mvnw -B --no-transfer-progress site - name: Deploy documentation to GitHub Pages uses: JamesIves/github-pages-deploy-action@v4.8.0 with: branch: gh-pages folder: spring-boot-admin-docs/target/generated-docs/build target-folder: ${{ steps.project.outputs.version }} clean: true # Automatically remove deleted files from the deploy branch ================================================ FILE: .github/workflows/deploy-documentation.yml ================================================ name: Deploy Documentation on: workflow_dispatch: inputs: releaseversion: description: 'Version to publish the documentation for. This should be a tag that exists in the repository.' type: string required: true copyDocsToCurrent: 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 env: VERSION: ${{ github.event.inputs.releaseversion }} jobs: deploy-documentation: runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 with: ref: refs/tags/${{ github.event.inputs.releaseversion }} - name: Set up JDK uses: actions/setup-java@v5 with: distribution: 'temurin' java-version: '17' cache: 'maven' cache-dependency-path: '**/pom.xml' - name: Set up Node uses: actions/setup-node@v6 with: node-version: 'lts/*' cache: 'npm' cache-dependency-path: '**/package-lock.json' - name: Set projects Maven version to GitHub Action GUI set version run: ./mvnw versions:set "-DnewVersion=${{ github.event.inputs.releaseversion }}" --no-transfer-progress - name: Build with Maven run: ./mvnw -B --no-transfer-progress package -DskipTests - name: Build documentation with Maven run: ./mvnw -B --no-transfer-progress -pl spring-boot-admin-docs site - name: Deploy documentation to GitHub Pages uses: JamesIves/github-pages-deploy-action@v4.8.0 with: branch: gh-pages folder: spring-boot-admin-docs/target/generated-docs/build target-folder: ${{ github.event.inputs.releaseversion }} clean: true - name: Deploy redirect for /current to /${{ github.event.inputs.releaseversion }} uses: JamesIves/github-pages-deploy-action@v4.8.0 if: github.event.inputs.copyDocsToCurrent == 'true' with: branch: gh-pages folder: spring-boot-admin-docs/target/generated-docs/build/current target-folder: /current clean: true - name: Deploy deeplink redirect for /current/* to /${{ github.event.inputs.releaseversion }}/* uses: JamesIves/github-pages-deploy-action@v4.8.0 if: github.event.inputs.copyDocsToCurrent == 'true' with: branch: gh-pages folder: spring-boot-admin-docs/target/generated-docs/build/current target-folder: / clean: false ================================================ FILE: .github/workflows/issue-metrics.yml ================================================ name: Monthly issue metrics on: workflow_dispatch: inputs: start_date: description: 'Start date (YYYY-MM-DD)' required: false type: string end_date: description: 'End date (YYYY-MM-DD)' required: false type: string schedule: - cron: "3 2 1 * *" permissions: contents: read jobs: build: name: issue metrics runs-on: ubuntu-latest permissions: issues: write pull-requests: read steps: - name: Get dates for last month shell: bash run: | # Use input dates if provided, otherwise calculate the previous month if [ -n "${{ inputs.start_date }}" ] && [ -n "${{ inputs.end_date }}" ]; then first_day="${{ inputs.start_date }}" last_day="${{ inputs.end_date }}" else # Calculate the first day of the previous month first_day=$(date -d "last month" +%Y-%m-01) # Calculate the last day of the previous month last_day=$(date -d "$first_day +1 month -1 day" +%Y-%m-%d) fi #Set an environment variable with the date range echo "$first_day..$last_day" echo "last_month=$first_day..$last_day" >> "$GITHUB_ENV" - name: Run issue-metrics tool uses: github/issue-metrics@v3 env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} SEARCH_QUERY: 'spring-boot-admin:codecentric/spring-boot-admin is:issue created:${{ env.last_month }} -reason:"not planned"' ================================================ FILE: .github/workflows/release-to-maven-central.yml ================================================ name: Release To Maven Central on: workflow_dispatch: inputs: releaseversion: description: "Version to release and publish to maven central." required: true copyDocsToCurrent: 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 env: VERSION: ${{ github.event.inputs.releaseversion }} jobs: publish-central-and-pages: runs-on: ubuntu-latest steps: - run: echo "Will start a Maven Central upload with version ${{ github.event.inputs.releaseversion }}" - name: free disk space continue-on-error: true run: | df -h sudo swapoff -a sudo rm -f /swapfile sudo apt clean if [ -n "$(docker image ls -q)" ]; then docker rmi -f $(docker image ls -aq) || true fi df -h - uses: actions/checkout@v6 - name: Set up settings.xml for Maven Central Repository uses: actions/setup-java@v5 with: distribution: 'temurin' java-version: '17' server-id: central server-username: MAVEN_USERNAME server-password: MAVEN_PASSWORD gpg-private-key: ${{ secrets.MAVEN_GPG_PRIVATE_KEY }} gpg-passphrase: MAVEN_GPG_PASSPHRASE cache: 'maven' env: MAVEN_USERNAME: ${{ secrets.OSS_SONATYPE_USERNAME }} MAVEN_PASSWORD: ${{ secrets.OSS_SONATYPE_PASSWORD }} MAVEN_GPG_PASSPHRASE: ${{ secrets.MAVEN_GPG_PASSPHRASE }} - name: Cache node modules uses: actions/cache@v5 env: cache-name: cache-node-modules with: path: ~/.npm key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }} restore-keys: | ${{ runner.os }}-build-${{ env.cache-name }}- - name: Set projects Maven version to GitHub Action GUI set version run: ./mvnw versions:set "-DnewVersion=${{ github.event.inputs.releaseversion }}" --no-transfer-progress - name: Publish package run: ./mvnw -B deploy --no-transfer-progress -P central-deploy -DskipTests env: #TODO: This is a workaround for NEXUS-27902 which uses XStream that fails on JVM >16 JDK_JAVA_OPTIONS: "--add-opens java.base/java.util=ALL-UNNAMED --add-opens java.base/java.lang.reflect=ALL-UNNAMED --add-opens java.base/java.text=ALL-UNNAMED --add-opens java.desktop/java.awt.font=ALL-UNNAMED" MAVEN_USERNAME: ${{ secrets.OSS_SONATYPE_USERNAME }} MAVEN_PASSWORD: ${{ secrets.OSS_SONATYPE_PASSWORD }} MAVEN_GPG_PASSPHRASE: ${{ secrets.MAVEN_GPG_PASSPHRASE }} - name: Build documentation with Maven run: ./mvnw -B --no-transfer-progress -pl spring-boot-admin-docs site - name: Deploy documentation to GitHub Pages for version ${{ github.event.inputs.releaseversion }} uses: JamesIves/github-pages-deploy-action@v4.8.0 with: branch: gh-pages folder: spring-boot-admin-docs/target/generated-docs/build target-folder: ${{ github.event.inputs.releaseversion }} clean: true - name: Deploy redirect for /current to /${{ github.event.inputs.releaseversion }} uses: JamesIves/github-pages-deploy-action@v4.8.0 if: github.event.inputs.copyDocsToCurrent == 'true' with: branch: gh-pages folder: spring-boot-admin-docs/target/generated-docs/build/current target-folder: /current clean: true - name: Deploy deeplink redirect for /current/* to /${{ github.event.inputs.releaseversion }}/* uses: JamesIves/github-pages-deploy-action@v4.8.0 if: github.event.inputs.copyDocsToCurrent == 'true' with: branch: gh-pages folder: spring-boot-admin-docs/target/generated-docs/build/current target-folder: / clean: false publish-github-release: needs: publish-central-and-pages runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 - name: free disk space continue-on-error: true run: | df -h sudo swapoff -a sudo rm -f /swapfile sudo apt clean if [ -n "$(docker image ls -q)" ]; then docker rmi -f $(docker image ls -aq) || true fi df -h - name: Generate changelog id: changelog uses: metcalfc/changelog-generator@v4.6.2 with: myToken: ${{ secrets.GITHUB_TOKEN }} - name: Create GitHub Release id: create_release uses: softprops/action-gh-release@v2 with: tag_name: ${{ github.event.inputs.releaseversion }} token: ${{ secrets.GITHUB_TOKEN }} draft: false prerelease: ${{ contains(github.event.inputs.releaseversion, '-') }} ================================================ FILE: .github/workflows/vulnerability-scan.yml ================================================ name: Vulnerability Scan on: workflow_dispatch: {} jobs: semgrep: name: semgrep/ci runs-on: ubuntu-latest container: image: returntocorp/semgrep if: (github.actor != 'renovate') steps: - uses: actions/checkout@v6 - run: semgrep ci env: SEMGREP_RULES: p/default ================================================ FILE: .gitignore ================================================ # Maven target/ # Eclipse .settings/ .classpath .project .factorypath .apt_generated/ # Intellij .idea/ *.iml *.iws #vscode .vscode/ # gnupg keyring /.gnupg #nodejs node_modules/ node/ #flattened POMs .flattened-pom.xml .DS_Store mockServiceWorker.js /.github/agents/ ================================================ FILE: .mvn/wrapper/maven-wrapper.properties ================================================ distributionType=only-script distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.14/apache-maven-3.9.14-bin.zip ================================================ FILE: CONTRIBUTING.md ================================================ # Contributing to Spring Boot Admin Contributions are highly welcome. Feel free to submit Pull Requests. Maybe watch out for tickets tagged with `ideal-for-contribution`, these tickets should always be a good starting point for contributing. You can find some hints for starting development in the [README](spring-boot-admin-server-ui/README.md) of `spring-boot-admin-server-ui`. ## Coding Conventions ### Java / Server We try to satisfy the [Code Style of Spring Framework](https://github.com/spring-projects/spring-framework/wiki/Code-Style). The [Spring Java Format Plugin](https://github.com/spring-io/spring-javaformat) is added to the build. Checkstyle will enforce the consistency of the code. Nevertheless, there are some disabled rules, due to backward compatibility. You can find these disabled rules in a comment in `src/checkstyle/checkstyle.xml`. However, you can always run `./mvnw spring-javaformat:apply` to fix some basic errors, like indentation. ### JavaScript / Client The Vue frontend implements basic prettier rules as well as Vue3 recommended rules. To check if there are any violations run `npm run lint` and `npm run format`. Append `:fix` to let eslint or prettier auto-solve most issues. ## Working with the code ### IntelliJ The IntelliJ settings are based on the IntelliJ-IDEA-Editor-Settings from spring, but have been adapted slightly, you can find the original settings [here](https://github.com/spring-projects/spring-framework/wiki/IntelliJ-IDEA-Editor-Settings). The custom settings are stored in `.editorconfig` and are imported automatically by IntelliJ. If you are using IntelliJ, there is also a [formatter-plugin provided by Spring](https://github.com/spring-io/spring-javaformat#intellij-idea). (i) Plugin version x did not work in IntelliJ IDEA Ultimate 2020.3. #### Checkstyle Plugin This plugin scans Java files with the project's custom CheckStyle rules from within IDEA. Install and configure the Checkstyle Plugin, and enable the configuration file. ##### Configuration Before configuration, add the `spring-javaformat-checkstyle` JAR to the Third-Party checks. 1. Preferences > Tools > Checkstyle > Third-Party Checks 2. Add `~/.m2/repository/io/spring/javaformat/spring-javaformat-checkstyle/0.0.26/spring-javaformat-checkstyle-0.0.26.jar` ##### Configuration File Add the configuration file and enabled it: 1. Preferences > Tools > Checkstyle > Configuration File > + 2. Add a Name, ex. Spring Boot Admin 3. Use a local Checkstyle File, Browse to `src/checkstyle/checkstyle.xml` and click Next 4. Enter the full path to the checkstyle header file: `/src/checkstyle/checkstyle-header.txt`, click Finish 5. Select the new configuration file to enable it #### Prettier Plugin This plugin is able to run Prettier from within IntelliJ. It can even be configured to run on "Reformat Code" action. It comes bundles with the IDE but needs to be enabled. ##### Configuration 1. Preferences > Languages & Frameworks > JavaScript > Prettier 2. Manual Prettier configuration 1. Prettier package: `/spring-boot-admin-server-ui/node_modules/prettier` 2. Enable "Run on 'Reformat Code' action" ================================================ FILE: LICENSE.txt ================================================ Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "{}" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright {yyyy} {name of copyright owner} 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. ================================================ FILE: README.md ================================================ # Spring Boot Admin by [codecentric](https://codecentric.de) [![Apache License 2](https://img.shields.io/badge/license-ASF2-blue.svg)](https://www.apache.org/licenses/LICENSE-2.0.txt) ![Build Status](https://github.com/codecentric/spring-boot-admin/actions/workflows/build-main.yml/badge.svg?branch=master) [![codecov](https://codecov.io/gh/codecentric/spring-boot-admin/branch/master/graph/badge.svg?token=u5SWsZpj5S)](https://codecov.io/gh/codecentric/spring-boot-admin) ![Maven Central Version](https://img.shields.io/maven-central/v/de.codecentric/spring-boot-admin) This community project provides an admin interface for [Spring Boot ®](http://projects.spring.io/spring-boot/ "Official Spring-Boot website") web applications that expose actuator endpoints. Monitoring Python applications is available using [Pyctuator](https://github.com/SolarEdgeTech/pyctuator). ## Compatibility Matrix In the Spring Boot Admin Server App, the Spring Boot Admin's version matches the major and minor versions of Spring Boot. | Spring Boot Version | Spring Boot Admin | |---------------------|-------------------| | 2.7 | 2.7.Y | | 3.0 | 3.0.Y | | 4.0 | 4.0.Y | Nevertheless, it is possible to monitor any version of a Spring Boot service independently of the underlying Spring Boot version in the service. Hence, it is possible to run Spring Boot Admin Server version 2.6 and monitor a service that is running on Spring Boot 2.3 using Spring Boot Admin Client version 2.3. ## Getting Started [A quick guide](https://docs.spring-boot-admin.com/current) to get started can be found in our docs. There are introductory talks available on YouTube: Cloud Native Spring Boot® Admin by Johannes Edmeier @ Spring I/O 2019
**Cloud Native Spring Boot® Admin by Johannes Edmeier @ Spring I/O 2019** Monitoring Spring Boot® Applications with Spring Boot Admin @ Spring I/O 2018
**Monitoring Spring Boot® Applications with Spring Boot Admin @ Spring I/O 2018** Spring Boot® Admin - Monitoring and Configuring Spring Boot Applications at Runtime
**Spring Boot® Admin - Monitoring and Configuring Spring Boot Applications at Runtime** ## Getting Help Having trouble with codecentric's Spring Boot Admin? We’d like to help! * Check the [reference documentation](http://codecentric.github.io/spring-boot-admin/current/). * Ask a question on [stackoverflow.com](http://stackoverflow.com/questions/tagged/spring-boot-admin) - we monitor questions tagged with `spring-boot-admin`. * Ask for help in our [spring-boot-admin Gitter chat](https://gitter.im/codecentric/spring-boot-admin) * Report bugs at http://github.com/codecentric/spring-boot-admin/issues. ## Reference Guide ### Translated versions The following reference guides have been translated by users of Spring Boot Admin and are not part of the official bundle. The maintainers of Spring Boot Admin will not update and maintain the guides mentioned below. [Version 2.6.6 (Chinese translated by @qq253498229)](https://consolelog.gitee.io/docs-spring-boot-admin-docs-chinese/) ## Trademarks and licenses The source code of codecentric's Spring Boot Admin is licensed under [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0) Spring, Spring Boot and Spring Cloud are trademarks of [Pivotal Software, Inc.](https://pivotal.io/) in the U.S. and other countries. ## Snapshot builds You can access snapshot builds from the github snapshot repository by adding the following to your `repositories`: ```xml sba-snapshot Spring Boot Admin Snapshots https://maven.pkg.github.com/codecentric/spring-boot-admin true false ``` ## Contributing See [CONTRIBUTING.md](CONTRIBUTING.md) file. ================================================ FILE: lombok.config ================================================ lombok.noArgsConstructor.extraPrivate = false ================================================ FILE: mvnw ================================================ #!/bin/sh # ---------------------------------------------------------------------------- # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you 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. # ---------------------------------------------------------------------------- # ---------------------------------------------------------------------------- # Apache Maven Wrapper startup batch script, version 3.3.4 # # Optional ENV vars # ----------------- # JAVA_HOME - location of a JDK home dir, required when download maven via java source # MVNW_REPOURL - repo url base for downloading maven distribution # MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven # MVNW_VERBOSE - true: enable verbose log; debug: trace the mvnw script; others: silence the output # ---------------------------------------------------------------------------- set -euf [ "${MVNW_VERBOSE-}" != debug ] || set -x # OS specific support. native_path() { printf %s\\n "$1"; } case "$(uname)" in CYGWIN* | MINGW*) [ -z "${JAVA_HOME-}" ] || JAVA_HOME="$(cygpath --unix "$JAVA_HOME")" native_path() { cygpath --path --windows "$1"; } ;; esac # set JAVACMD and JAVACCMD set_java_home() { # For Cygwin and MinGW, ensure paths are in Unix format before anything is touched if [ -n "${JAVA_HOME-}" ]; then if [ -x "$JAVA_HOME/jre/sh/java" ]; then # IBM's JDK on AIX uses strange locations for the executables JAVACMD="$JAVA_HOME/jre/sh/java" JAVACCMD="$JAVA_HOME/jre/sh/javac" else JAVACMD="$JAVA_HOME/bin/java" JAVACCMD="$JAVA_HOME/bin/javac" if [ ! -x "$JAVACMD" ] || [ ! -x "$JAVACCMD" ]; then echo "The JAVA_HOME environment variable is not defined correctly, so mvnw cannot run." >&2 echo "JAVA_HOME is set to \"$JAVA_HOME\", but \"\$JAVA_HOME/bin/java\" or \"\$JAVA_HOME/bin/javac\" does not exist." >&2 return 1 fi fi else JAVACMD="$( 'set' +e 'unset' -f command 2>/dev/null 'command' -v java )" || : JAVACCMD="$( 'set' +e 'unset' -f command 2>/dev/null 'command' -v javac )" || : if [ ! -x "${JAVACMD-}" ] || [ ! -x "${JAVACCMD-}" ]; then echo "The java/javac command does not exist in PATH nor is JAVA_HOME set, so mvnw cannot run." >&2 return 1 fi fi } # hash string like Java String::hashCode hash_string() { str="${1:-}" h=0 while [ -n "$str" ]; do char="${str%"${str#?}"}" h=$(((h * 31 + $(LC_CTYPE=C printf %d "'$char")) % 4294967296)) str="${str#?}" done printf %x\\n $h } verbose() { :; } [ "${MVNW_VERBOSE-}" != true ] || verbose() { printf %s\\n "${1-}"; } die() { printf %s\\n "$1" >&2 exit 1 } trim() { # MWRAPPER-139: # Trims trailing and leading whitespace, carriage returns, tabs, and linefeeds. # Needed for removing poorly interpreted newline sequences when running in more # exotic environments such as mingw bash on Windows. printf "%s" "${1}" | tr -d '[:space:]' } scriptDir="$(dirname "$0")" scriptName="$(basename "$0")" # parse distributionUrl and optional distributionSha256Sum, requires .mvn/wrapper/maven-wrapper.properties while IFS="=" read -r key value; do case "${key-}" in distributionUrl) distributionUrl=$(trim "${value-}") ;; distributionSha256Sum) distributionSha256Sum=$(trim "${value-}") ;; esac done <"$scriptDir/.mvn/wrapper/maven-wrapper.properties" [ -n "${distributionUrl-}" ] || die "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties" case "${distributionUrl##*/}" in maven-mvnd-*bin.*) MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ case "${PROCESSOR_ARCHITECTURE-}${PROCESSOR_ARCHITEW6432-}:$(uname -a)" in *AMD64:CYGWIN* | *AMD64:MINGW*) distributionPlatform=windows-amd64 ;; :Darwin*x86_64) distributionPlatform=darwin-amd64 ;; :Darwin*arm64) distributionPlatform=darwin-aarch64 ;; :Linux*x86_64*) distributionPlatform=linux-amd64 ;; *) echo "Cannot detect native platform for mvnd on $(uname)-$(uname -m), use pure java version" >&2 distributionPlatform=linux-amd64 ;; esac distributionUrl="${distributionUrl%-bin.*}-$distributionPlatform.zip" ;; maven-mvnd-*) MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ ;; *) MVN_CMD="mvn${scriptName#mvnw}" _MVNW_REPO_PATTERN=/org/apache/maven/ ;; esac # apply MVNW_REPOURL and calculate MAVEN_HOME # maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/ [ -z "${MVNW_REPOURL-}" ] || distributionUrl="$MVNW_REPOURL$_MVNW_REPO_PATTERN${distributionUrl#*"$_MVNW_REPO_PATTERN"}" distributionUrlName="${distributionUrl##*/}" distributionUrlNameMain="${distributionUrlName%.*}" distributionUrlNameMain="${distributionUrlNameMain%-bin}" MAVEN_USER_HOME="${MAVEN_USER_HOME:-${HOME}/.m2}" MAVEN_HOME="${MAVEN_USER_HOME}/wrapper/dists/${distributionUrlNameMain-}/$(hash_string "$distributionUrl")" exec_maven() { unset MVNW_VERBOSE MVNW_USERNAME MVNW_PASSWORD MVNW_REPOURL || : exec "$MAVEN_HOME/bin/$MVN_CMD" "$@" || die "cannot exec $MAVEN_HOME/bin/$MVN_CMD" } if [ -d "$MAVEN_HOME" ]; then verbose "found existing MAVEN_HOME at $MAVEN_HOME" exec_maven "$@" fi case "${distributionUrl-}" in *?-bin.zip | *?maven-mvnd-?*-?*.zip) ;; *) die "distributionUrl is not valid, must match *-bin.zip or maven-mvnd-*.zip, but found '${distributionUrl-}'" ;; esac # prepare tmp dir if TMP_DOWNLOAD_DIR="$(mktemp -d)" && [ -d "$TMP_DOWNLOAD_DIR" ]; then clean() { rm -rf -- "$TMP_DOWNLOAD_DIR"; } trap clean HUP INT TERM EXIT else die "cannot create temp dir" fi mkdir -p -- "${MAVEN_HOME%/*}" # Download and Install Apache Maven verbose "Couldn't find MAVEN_HOME, downloading and installing it ..." verbose "Downloading from: $distributionUrl" verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName" # select .zip or .tar.gz if ! command -v unzip >/dev/null; then distributionUrl="${distributionUrl%.zip}.tar.gz" distributionUrlName="${distributionUrl##*/}" fi # verbose opt __MVNW_QUIET_WGET=--quiet __MVNW_QUIET_CURL=--silent __MVNW_QUIET_UNZIP=-q __MVNW_QUIET_TAR='' [ "${MVNW_VERBOSE-}" != true ] || __MVNW_QUIET_WGET='' __MVNW_QUIET_CURL='' __MVNW_QUIET_UNZIP='' __MVNW_QUIET_TAR=v # normalize http auth case "${MVNW_PASSWORD:+has-password}" in '') MVNW_USERNAME='' MVNW_PASSWORD='' ;; has-password) [ -n "${MVNW_USERNAME-}" ] || MVNW_USERNAME='' MVNW_PASSWORD='' ;; esac if [ -z "${MVNW_USERNAME-}" ] && command -v wget >/dev/null; then verbose "Found wget ... using wget" wget ${__MVNW_QUIET_WGET:+"$__MVNW_QUIET_WGET"} "$distributionUrl" -O "$TMP_DOWNLOAD_DIR/$distributionUrlName" || die "wget: Failed to fetch $distributionUrl" elif [ -z "${MVNW_USERNAME-}" ] && command -v curl >/dev/null; then verbose "Found curl ... using curl" curl ${__MVNW_QUIET_CURL:+"$__MVNW_QUIET_CURL"} -f -L -o "$TMP_DOWNLOAD_DIR/$distributionUrlName" "$distributionUrl" || die "curl: Failed to fetch $distributionUrl" elif set_java_home; then verbose "Falling back to use Java to download" javaSource="$TMP_DOWNLOAD_DIR/Downloader.java" targetZip="$TMP_DOWNLOAD_DIR/$distributionUrlName" cat >"$javaSource" <<-END public class Downloader extends java.net.Authenticator { protected java.net.PasswordAuthentication getPasswordAuthentication() { return new java.net.PasswordAuthentication( System.getenv( "MVNW_USERNAME" ), System.getenv( "MVNW_PASSWORD" ).toCharArray() ); } public static void main( String[] args ) throws Exception { setDefault( new Downloader() ); java.nio.file.Files.copy( java.net.URI.create( args[0] ).toURL().openStream(), java.nio.file.Paths.get( args[1] ).toAbsolutePath().normalize() ); } } END # For Cygwin/MinGW, switch paths to Windows format before running javac and java verbose " - Compiling Downloader.java ..." "$(native_path "$JAVACCMD")" "$(native_path "$javaSource")" || die "Failed to compile Downloader.java" verbose " - Running Downloader.java ..." "$(native_path "$JAVACMD")" -cp "$(native_path "$TMP_DOWNLOAD_DIR")" Downloader "$distributionUrl" "$(native_path "$targetZip")" fi # If specified, validate the SHA-256 sum of the Maven distribution zip file if [ -n "${distributionSha256Sum-}" ]; then distributionSha256Result=false if [ "$MVN_CMD" = mvnd.sh ]; then echo "Checksum validation is not supported for maven-mvnd." >&2 echo "Please disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2 exit 1 elif command -v sha256sum >/dev/null; then if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | sha256sum -c - >/dev/null 2>&1; then distributionSha256Result=true fi elif command -v shasum >/dev/null; then if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | shasum -a 256 -c >/dev/null 2>&1; then distributionSha256Result=true fi else echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." >&2 echo "Please install either command, or disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2 exit 1 fi if [ $distributionSha256Result = false ]; then echo "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised." >&2 echo "If you updated your Maven version, you need to update the specified distributionSha256Sum property." >&2 exit 1 fi fi # unzip and move if command -v unzip >/dev/null; then unzip ${__MVNW_QUIET_UNZIP:+"$__MVNW_QUIET_UNZIP"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -d "$TMP_DOWNLOAD_DIR" || die "failed to unzip" else tar xzf${__MVNW_QUIET_TAR:+"$__MVNW_QUIET_TAR"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -C "$TMP_DOWNLOAD_DIR" || die "failed to untar" fi # Find the actual extracted directory name (handles snapshots where filename != directory name) actualDistributionDir="" # First try the expected directory name (for regular distributions) if [ -d "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" ]; then if [ -f "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain/bin/$MVN_CMD" ]; then actualDistributionDir="$distributionUrlNameMain" fi fi # If not found, search for any directory with the Maven executable (for snapshots) if [ -z "$actualDistributionDir" ]; then # enable globbing to iterate over items set +f for dir in "$TMP_DOWNLOAD_DIR"/*; do if [ -d "$dir" ]; then if [ -f "$dir/bin/$MVN_CMD" ]; then actualDistributionDir="$(basename "$dir")" break fi fi done set -f fi if [ -z "$actualDistributionDir" ]; then verbose "Contents of $TMP_DOWNLOAD_DIR:" verbose "$(ls -la "$TMP_DOWNLOAD_DIR")" die "Could not find Maven distribution directory in extracted archive" fi verbose "Found extracted Maven distribution directory: $actualDistributionDir" printf %s\\n "$distributionUrl" >"$TMP_DOWNLOAD_DIR/$actualDistributionDir/mvnw.url" mv -- "$TMP_DOWNLOAD_DIR/$actualDistributionDir" "$MAVEN_HOME" || [ -d "$MAVEN_HOME" ] || die "fail to move MAVEN_HOME" clean || : exec_maven "$@" ================================================ FILE: mvnw.cmd ================================================ <# : batch portion @REM ---------------------------------------------------------------------------- @REM Licensed to the Apache Software Foundation (ASF) under one @REM or more contributor license agreements. See the NOTICE file @REM distributed with this work for additional information @REM regarding copyright ownership. The ASF licenses this file @REM to you under the Apache License, Version 2.0 (the @REM "License"); you may not use this file except in compliance @REM with the License. You may obtain a copy of the License at @REM @REM http://www.apache.org/licenses/LICENSE-2.0 @REM @REM Unless required by applicable law or agreed to in writing, @REM software distributed under the License is distributed on an @REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY @REM KIND, either express or implied. See the License for the @REM specific language governing permissions and limitations @REM under the License. @REM ---------------------------------------------------------------------------- @REM ---------------------------------------------------------------------------- @REM Apache Maven Wrapper startup batch script, version 3.3.4 @REM @REM Optional ENV vars @REM MVNW_REPOURL - repo url base for downloading maven distribution @REM MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven @REM MVNW_VERBOSE - true: enable verbose log; others: silence the output @REM ---------------------------------------------------------------------------- @IF "%__MVNW_ARG0_NAME__%"=="" (SET __MVNW_ARG0_NAME__=%~nx0) @SET __MVNW_CMD__= @SET __MVNW_ERROR__= @SET __MVNW_PSMODULEP_SAVE=%PSModulePath% @SET PSModulePath= @FOR /F "usebackq tokens=1* delims==" %%A IN (`powershell -noprofile "& {$scriptDir='%~dp0'; $script='%__MVNW_ARG0_NAME__%'; icm -ScriptBlock ([Scriptblock]::Create((Get-Content -Raw '%~f0'))) -NoNewScope}"`) DO @( IF "%%A"=="MVN_CMD" (set __MVNW_CMD__=%%B) ELSE IF "%%B"=="" (echo %%A) ELSE (echo %%A=%%B) ) @SET PSModulePath=%__MVNW_PSMODULEP_SAVE% @SET __MVNW_PSMODULEP_SAVE= @SET __MVNW_ARG0_NAME__= @SET MVNW_USERNAME= @SET MVNW_PASSWORD= @IF NOT "%__MVNW_CMD__%"=="" ("%__MVNW_CMD__%" %*) @echo Cannot start maven from wrapper >&2 && exit /b 1 @GOTO :EOF : end batch / begin powershell #> $ErrorActionPreference = "Stop" if ($env:MVNW_VERBOSE -eq "true") { $VerbosePreference = "Continue" } # calculate distributionUrl, requires .mvn/wrapper/maven-wrapper.properties $distributionUrl = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionUrl if (!$distributionUrl) { Write-Error "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties" } switch -wildcard -casesensitive ( $($distributionUrl -replace '^.*/','') ) { "maven-mvnd-*" { $USE_MVND = $true $distributionUrl = $distributionUrl -replace '-bin\.[^.]*$',"-windows-amd64.zip" $MVN_CMD = "mvnd.cmd" break } default { $USE_MVND = $false $MVN_CMD = $script -replace '^mvnw','mvn' break } } # apply MVNW_REPOURL and calculate MAVEN_HOME # maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/ if ($env:MVNW_REPOURL) { $MVNW_REPO_PATTERN = if ($USE_MVND -eq $False) { "/org/apache/maven/" } else { "/maven/mvnd/" } $distributionUrl = "$env:MVNW_REPOURL$MVNW_REPO_PATTERN$($distributionUrl -replace "^.*$MVNW_REPO_PATTERN",'')" } $distributionUrlName = $distributionUrl -replace '^.*/','' $distributionUrlNameMain = $distributionUrlName -replace '\.[^.]*$','' -replace '-bin$','' $MAVEN_M2_PATH = "$HOME/.m2" if ($env:MAVEN_USER_HOME) { $MAVEN_M2_PATH = "$env:MAVEN_USER_HOME" } if (-not (Test-Path -Path $MAVEN_M2_PATH)) { New-Item -Path $MAVEN_M2_PATH -ItemType Directory | Out-Null } $MAVEN_WRAPPER_DISTS = $null if ((Get-Item $MAVEN_M2_PATH).Target[0] -eq $null) { $MAVEN_WRAPPER_DISTS = "$MAVEN_M2_PATH/wrapper/dists" } else { $MAVEN_WRAPPER_DISTS = (Get-Item $MAVEN_M2_PATH).Target[0] + "/wrapper/dists" } $MAVEN_HOME_PARENT = "$MAVEN_WRAPPER_DISTS/$distributionUrlNameMain" $MAVEN_HOME_NAME = ([System.Security.Cryptography.SHA256]::Create().ComputeHash([byte[]][char[]]$distributionUrl) | ForEach-Object {$_.ToString("x2")}) -join '' $MAVEN_HOME = "$MAVEN_HOME_PARENT/$MAVEN_HOME_NAME" if (Test-Path -Path "$MAVEN_HOME" -PathType Container) { Write-Verbose "found existing MAVEN_HOME at $MAVEN_HOME" Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD" exit $? } if (! $distributionUrlNameMain -or ($distributionUrlName -eq $distributionUrlNameMain)) { Write-Error "distributionUrl is not valid, must end with *-bin.zip, but found $distributionUrl" } # prepare tmp dir $TMP_DOWNLOAD_DIR_HOLDER = New-TemporaryFile $TMP_DOWNLOAD_DIR = New-Item -Itemtype Directory -Path "$TMP_DOWNLOAD_DIR_HOLDER.dir" $TMP_DOWNLOAD_DIR_HOLDER.Delete() | Out-Null trap { if ($TMP_DOWNLOAD_DIR.Exists) { try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null } catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" } } } New-Item -Itemtype Directory -Path "$MAVEN_HOME_PARENT" -Force | Out-Null # Download and Install Apache Maven Write-Verbose "Couldn't find MAVEN_HOME, downloading and installing it ..." Write-Verbose "Downloading from: $distributionUrl" Write-Verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName" $webclient = New-Object System.Net.WebClient if ($env:MVNW_USERNAME -and $env:MVNW_PASSWORD) { $webclient.Credentials = New-Object System.Net.NetworkCredential($env:MVNW_USERNAME, $env:MVNW_PASSWORD) } [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 $webclient.DownloadFile($distributionUrl, "$TMP_DOWNLOAD_DIR/$distributionUrlName") | Out-Null # If specified, validate the SHA-256 sum of the Maven distribution zip file $distributionSha256Sum = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionSha256Sum if ($distributionSha256Sum) { if ($USE_MVND) { Write-Error "Checksum validation is not supported for maven-mvnd. `nPlease disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." } Import-Module $PSHOME\Modules\Microsoft.PowerShell.Utility -Function Get-FileHash if ((Get-FileHash "$TMP_DOWNLOAD_DIR/$distributionUrlName" -Algorithm SHA256).Hash.ToLower() -ne $distributionSha256Sum) { Write-Error "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised. If you updated your Maven version, you need to update the specified distributionSha256Sum property." } } # unzip and move Expand-Archive "$TMP_DOWNLOAD_DIR/$distributionUrlName" -DestinationPath "$TMP_DOWNLOAD_DIR" | Out-Null # Find the actual extracted directory name (handles snapshots where filename != directory name) $actualDistributionDir = "" # First try the expected directory name (for regular distributions) $expectedPath = Join-Path "$TMP_DOWNLOAD_DIR" "$distributionUrlNameMain" $expectedMvnPath = Join-Path "$expectedPath" "bin/$MVN_CMD" if ((Test-Path -Path $expectedPath -PathType Container) -and (Test-Path -Path $expectedMvnPath -PathType Leaf)) { $actualDistributionDir = $distributionUrlNameMain } # If not found, search for any directory with the Maven executable (for snapshots) if (!$actualDistributionDir) { Get-ChildItem -Path "$TMP_DOWNLOAD_DIR" -Directory | ForEach-Object { $testPath = Join-Path $_.FullName "bin/$MVN_CMD" if (Test-Path -Path $testPath -PathType Leaf) { $actualDistributionDir = $_.Name } } } if (!$actualDistributionDir) { Write-Error "Could not find Maven distribution directory in extracted archive" } Write-Verbose "Found extracted Maven distribution directory: $actualDistributionDir" Rename-Item -Path "$TMP_DOWNLOAD_DIR/$actualDistributionDir" -NewName $MAVEN_HOME_NAME | Out-Null try { Move-Item -Path "$TMP_DOWNLOAD_DIR/$MAVEN_HOME_NAME" -Destination $MAVEN_HOME_PARENT | Out-Null } catch { if (! (Test-Path -Path "$MAVEN_HOME" -PathType Container)) { Write-Error "fail to move MAVEN_HOME" } } finally { try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null } catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" } } Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD" ================================================ FILE: pom.xml ================================================ 4.0.0 de.codecentric spring-boot-admin ${revision} pom Spring Boot Admin Spring Boot Admin https://github.com/codecentric/spring-boot-admin/ 4.1.0-SNAPSHOT 17 v22.12.0 3.9 ${java.version} ${java.version} @ UTF-8 UTF-8 true 4.0.3 2025.1.1 2.5.1 12.3.1 3.0.2 3.13.2 5.6.0 4.3.0 12.1.7 3.6.1 3.15.0 2.21.0 3.5.0 3.10.0 3.1.4 3.6.2 3.5.5 3.5.5 3.1.4 3.5.0 3.12.0 3.5.0 3.4.0 3.5.1 3.2.8 2.0.0 0.8.14 4.9.10 1.7.3 2.9.1 3.6.0 0.0.47 0.10.0 0.7.2 spring-boot-admin-dependencies spring-boot-admin-build spring-boot-admin-server spring-boot-admin-server-ui spring-boot-admin-client spring-boot-admin-docs spring-boot-admin-starter-server spring-boot-admin-starter-client spring-boot-admin-samples codecentric AG https://www.codecentric.de Apache License, Version 2.0 https://www.apache.org/licenses/LICENSE-2.0 scm:git:git://github.com/codecentric/spring-boot-admin.git scm:git:ssh://git@github.com/codecentric/spring-boot-admin.git https://github.com/codecentric/spring-boot-admin codecentric AG spring-boot-admin@codecentric.de https://www.codecentric.de/ org.apache.maven.plugins maven-source-plugin attach-sources jar-no-fork org.apache.maven.plugins maven-javadoc-plugin ${java.version} ${project.build.sourceEncoding} ${project.reporting.outputEncoding} ${project.reporting.outputEncoding} true attach-javadocs jar true org.apache.maven.plugins maven-checkstyle-plugin src/checkstyle/checkstyle.xml src/checkstyle/checkstyle-header.txt true true true checkstyle-validation validate check org.apache.maven.plugins maven-compiler-plugin true ${maven.compiler.source} ${maven.compiler.target} org.projectlombok lombok org.springframework.boot spring-boot-configuration-processor org.codehaus.mojo flatten-maven-plugin true oss true remove remove flatten process-resources flatten flatten-clean clean clean org.apache.maven.plugins maven-enforcer-plugin enforce-maven enforce ${require.maven.version} io.spring.javaformat spring-javaformat-maven-plugin ${spring-javaformat-maven-plugin.version} validate true validate org.codehaus.mojo versions-maven-plugin ${versions-maven-plugin.version} org.apache.maven.plugins maven-compiler-plugin ${maven-compiler-plugin.version} org.apache.maven.plugins maven-clean-plugin ${maven-clean-plugin.version} org.apache.maven.plugins maven-source-plugin ${maven-source-plugin.version} org.apache.maven.plugins maven-jar-plugin ${maven-jar-plugin.version} org.apache.maven.plugins maven-javadoc-plugin ${maven-javadoc-plugin.version} org.apache.maven.plugins maven-deploy-plugin ${maven-deploy-plugin.version} org.apache.maven.plugins maven-dependency-plugin ${maven-dependency-plugin.version} org.apache.maven.plugins maven-enforcer-plugin ${maven-enforcer-plugin.version} org.apache.maven.plugins maven-install-plugin ${maven-install-plugin.version} org.apache.maven.plugins maven-war-plugin ${maven-war-plugin.version} org.springframework.boot spring-boot-maven-plugin ${spring-boot.version} org.codehaus.mojo build-helper-maven-plugin ${build-helper-maven-plugin.version} org.codehaus.mojo flatten-maven-plugin ${flatten-maven-plugin.version} org.cyclonedx cyclonedx-maven-plugin ${cyclonedx-maven-plugin.version} org.apache.maven.plugins maven-failsafe-plugin ${maven-failsafe-plugin.version} integration-test verify org.apache.maven.plugins maven-surefire-plugin ${maven-surefire-plugin.version} classesAndMethods false true 2 **/*Tests.java **/*Test.java **/Abstract*.java org.apache.maven.plugins maven-resources-plugin ${maven-resources-plugin.version} ${resource.delimiter} false org.apache.maven.plugins maven-gpg-plugin ${maven-gpg-plugin.version} org.apache.maven.plugins maven-checkstyle-plugin ${maven-checkstyle-plugin.version} com.puppycrawl.tools checkstyle ${checkstyle.version} io.spring.javaformat spring-javaformat-checkstyle ${spring-javaformat-maven-plugin.version} com.github.eirslett frontend-maven-plugin ${frontend-maven-plugin.version} pl.project13.maven git-commit-id-plugin ${git-commit-id-maven-plugin.version} org.jacoco jacoco-maven-plugin ${jacoco-maven-plugin.version} org.sonatype.central central-publishing-maven-plugin ${central-publishing-maven-plugin.version} org.rodnansol spring-configuration-property-documenter-maven-plugin ${spring-conf-prop-documenter-maven-plugin.version} include-cloud !excludeSpringCloud spring-boot-admin-server-cloud coverage org.jacoco jacoco-maven-plugin pre-unit-test prepare-agent post-unit-test report central-deploy org.apache.maven.plugins maven-gpg-plugin sign-artifacts verify sign --pinentry-mode loopback org.sonatype.central central-publishing-maven-plugin true central true spring-repo !disableSpringSnapshots spring-milestone false https://repo.spring.io/milestone spring-snapshot true https://repo.spring.io/snapshot netflix-candidates Netflix Candidates https://artifactory-oss.prod.netflix.net/artifactory/maven-oss-candidates false spring-milestone false https://repo.spring.io/milestone spring-snapshot true https://repo.spring.io/snapshot noNpm com.github.eirslett frontend-maven-plugin true github GitHub Packages https://maven.pkg.github.com/codecentric/spring-boot-admin ================================================ FILE: renovate.json ================================================ { "extends": [ "config:recommended" ], "labels": [ "bot", "dependencies" ], "minimumReleaseAge": "5 days", "internalChecksFilter": "strict", "packageRules": [ { "description": "Automatically merge minor and patch-level updates when checks pass, creates a PR otherwise", "matchUpdateTypes": [ "minor", "patch", "digest" ], "automerge": true, "automergeType": "pr", "platformAutomerge": true }, { "matchPackageNames": [ "org.apache.maven.plugins:maven-site-plugin", "spring-boot-admin" ], "enabled": false }, { "description": "Checkstyle 13+ requires Java 21, stay on 12.x", "matchPackageNames": [ "com.puppycrawl.tools:checkstyle" ], "allowedVersions": "<13" } ] } ================================================ FILE: spring-boot-admin-build/pom.xml ================================================ 4.0.0 spring-boot-admin-build pom Spring Boot Admin Build Spring Boot Admin Build de.codecentric spring-boot-admin-dependencies ${revision} ../spring-boot-admin-dependencies org.springframework.boot spring-boot-dependencies ${spring-boot.version} pom import org.springframework.cloud spring-cloud-dependencies ${spring-cloud.version} pom import org.jolokia jolokia-support-springboot ${jolokia-support-springboot.version} org.wiremock wiremock-standalone ${wiremock.version} test com.hazelcast hazelcast ${hazelcast.version} tests test com.google.code.findbugs jsr305 ${findbugs-jsr305.version} provided org.eclipse.jetty jetty-alpn-server ${jetty.version} test org.awaitility awaitility ${awaitility.version} test org.codehaus.mojo flatten-maven-plugin true flatten process-resources flatten true oss true expand remove remove org.codehaus.mojo build-helper-maven-plugin generate-automatic-module-name regex-property automatic-module-name ${project.groupId}.${project.artifactId} [^a-zA-Z0-9]+ . org.apache.maven.plugins maven-jar-plugin true ${automatic-module-name} ================================================ FILE: spring-boot-admin-client/pom.xml ================================================ 4.0.0 spring-boot-admin-client Spring Boot Admin Client Spring Boot Admin Client de.codecentric spring-boot-admin-build ${revision} ../spring-boot-admin-build org.springframework.boot spring-boot-starter org.springframework.boot spring-boot-starter-actuator org.springframework.boot spring-boot-starter-restclient org.springframework.boot spring-boot-starter-webflux true org.springframework.boot spring-boot-starter-webmvc true org.springframework.boot spring-boot-autoconfigure-processor true org.springframework.boot spring-boot-configuration-processor true org.projectlombok lombok true org.springframework.boot spring-boot-starter-test test org.wiremock wiremock-standalone test org.awaitility awaitility test ================================================ FILE: spring-boot-admin-client/src/main/java/de/codecentric/boot/admin/client/config/ClientProperties.java ================================================ /* * 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.config; 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; @lombok.Data @ConfigurationProperties(prefix = "spring.boot.admin.client") public class ClientProperties { /** * The admin server urls to register at */ private String[] url = new String[] {}; /** * The admin rest-apis path. */ private String apiPath = "instances"; /** * Time interval the registration is repeated */ @DurationUnit(ChronoUnit.MILLIS) private Duration period = Duration.ofMillis(10_000L); /** * Connect timeout for the registration. */ @DurationUnit(ChronoUnit.MILLIS) private Duration connectTimeout = Duration.ofMillis(5_000L); /** * Read timeout (in ms) for the registration. */ @DurationUnit(ChronoUnit.MILLIS) private Duration readTimeout = Duration.ofMillis(5_000L); /** * Username for basic authentication on admin server */ @Nullable private String username; /** * Password for basic authentication on admin server */ @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; /** * Enable automatic registration when the application is ready. */ private boolean autoRegistration = true; /** * Enable registration against one or all admin servers */ private boolean registerOnce = true; /** * Enable Spring Boot Admin Client. */ private boolean enabled = true; public String[] getAdminUrl() { String[] adminUrls = this.url.clone(); for (int i = 0; i < adminUrls.length; i++) { adminUrls[i] += "/" + this.apiPath; } return adminUrls; } public boolean isAutoDeregistration(Environment environment) { return (this.autoDeregistration != null) ? this.autoDeregistration : (CloudPlatform.getActive(environment) != null); } } ================================================ FILE: spring-boot-admin-client/src/main/java/de/codecentric/boot/admin/client/config/ClientRuntimeHints.java ================================================ /* * 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.config; import lombok.SneakyThrows; import org.springframework.aot.hint.ExecutableMode; import org.springframework.aot.hint.MemberCategory; import org.springframework.aot.hint.RuntimeHints; import org.springframework.aot.hint.RuntimeHintsRegistrar; import org.springframework.boot.web.server.context.WebServerInitializedEvent; import org.springframework.context.annotation.Configuration; import de.codecentric.boot.admin.client.registration.Application; import de.codecentric.boot.admin.client.registration.DefaultApplicationFactory; @Configuration public class ClientRuntimeHints implements RuntimeHintsRegistrar { @Override public void registerHints(RuntimeHints hints, ClassLoader classLoader) { registerReflectionHints(hints); } @SneakyThrows private static void registerReflectionHints(RuntimeHints hints) { hints.reflection() .registerType(Application.Builder.class, MemberCategory.INVOKE_PUBLIC_METHODS, MemberCategory.INVOKE_PUBLIC_CONSTRUCTORS) .registerType(Application.class, MemberCategory.INVOKE_PUBLIC_METHODS, MemberCategory.INVOKE_PUBLIC_CONSTRUCTORS) .registerConstructor(Application.Builder.class.getDeclaredConstructor(), ExecutableMode.INVOKE) .registerMethod(Application.Builder.class.getMethod("build"), ExecutableMode.INVOKE) .registerMethod(Application.class.getMethod("builder"), ExecutableMode.INVOKE) .registerMethod(DefaultApplicationFactory.class.getMethod("onWebServerInitialized", WebServerInitializedEvent.class), ExecutableMode.INVOKE); } } ================================================ FILE: spring-boot-admin-client/src/main/java/de/codecentric/boot/admin/client/config/CloudFoundryApplicationProperties.java ================================================ /* * 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.config; import java.util.ArrayList; import java.util.List; import org.jspecify.annotations.Nullable; import org.springframework.boot.context.properties.ConfigurationProperties; @lombok.Data @ConfigurationProperties("vcap.application") public class CloudFoundryApplicationProperties { @Nullable private String applicationId; @Nullable private String instanceIndex; private List uris = new ArrayList<>(); } ================================================ FILE: spring-boot-admin-client/src/main/java/de/codecentric/boot/admin/client/config/InstanceProperties.java ================================================ /* * 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.config; 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; @lombok.Data @ConfigurationProperties(prefix = "spring.boot.admin.client.instance") 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; /** * 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; /** * 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; /** * 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; /** * Path for computing the service-url to register with. If not specified, defaults to * "/" */ @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; /** * Name to register with. Defaults to ${spring.application.name} */ @Value("${spring.application.name:spring-boot-application}") private String name = "spring-boot-application"; /** * Should the registered urls be built with server.address or with hostname. * @deprecated Use serviceHostType instead. */ @Deprecated private boolean preferIp = false; /** * Should the registered urls be built with server.address or with hostname. */ private ServiceHostType serviceHostType = ServiceHostType.CANONICAL_HOST_NAME; /** * Metadata that should be associated with this application */ private Map metadata = new LinkedHashMap<>(); } ================================================ FILE: spring-boot-admin-client/src/main/java/de/codecentric/boot/admin/client/config/ServiceHostType.java ================================================ /* * 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.config; public enum ServiceHostType { IP, HOST_NAME, CANONICAL_HOST_NAME, } ================================================ FILE: spring-boot-admin-client/src/main/java/de/codecentric/boot/admin/client/config/SpringBootAdminClientAutoConfiguration.java ================================================ /* * 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.config; import java.util.Collections; import java.util.List; import jakarta.servlet.ServletContext; import org.springframework.beans.factory.ObjectProvider; import org.springframework.boot.actuate.autoconfigure.endpoint.web.WebEndpointAutoConfiguration; 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.AutoConfigureAfter; 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.condition.ConditionalOnWebApplication.Type; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.boot.http.client.ClientHttpRequestFactoryBuilder; 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 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.DefaultApplicationRegistrator; import de.codecentric.boot.admin.client.registration.ReactiveApplicationFactory; import de.codecentric.boot.admin.client.registration.RegistrationApplicationListener; import de.codecentric.boot.admin.client.registration.RegistrationClient; import de.codecentric.boot.admin.client.registration.RestClientRegistrationClient; import de.codecentric.boot.admin.client.registration.ServletApplicationFactory; import de.codecentric.boot.admin.client.registration.metadata.CompositeMetadataContributor; import de.codecentric.boot.admin.client.registration.metadata.MetadataContributor; import de.codecentric.boot.admin.client.registration.metadata.StartupDateMetadataContributor; @Configuration(proxyBeanMethods = false) @ConditionalOnWebApplication @Conditional(SpringBootAdminClientEnabledCondition.class) @AutoConfigureAfter({ WebEndpointAutoConfiguration.class, RestClientAutoConfiguration.class }) @EnableConfigurationProperties({ ClientProperties.class, InstanceProperties.class, ServerProperties.class, ManagementServerProperties.class }) public class SpringBootAdminClientAutoConfiguration { @Bean @ConditionalOnMissingBean public ApplicationRegistrator registrator(RegistrationClient registrationClient, ClientProperties client, ApplicationFactory applicationFactory) { return new DefaultApplicationRegistrator(applicationFactory, registrationClient, client.getAdminUrl(), client.isRegisterOnce()); } @Bean @ConditionalOnMissingBean public RegistrationApplicationListener registrationListener(ClientProperties client, ApplicationRegistrator registrator, Environment environment) { RegistrationApplicationListener listener = new RegistrationApplicationListener(registrator); listener.setAutoRegister(client.isAutoRegistration()); listener.setAutoDeregister(client.isAutoDeregistration(environment)); listener.setRegisterPeriod(client.getPeriod()); return listener; } @Bean @ConditionalOnMissingBean public StartupDateMetadataContributor startupDateMetadataContributor() { return new StartupDateMetadataContributor(); } @Configuration(proxyBeanMethods = false) @ConditionalOnWebApplication(type = Type.SERVLET) @AutoConfigureAfter(DispatcherServletAutoConfiguration.class) public static class ServletConfiguration { @Bean @Lazy(false) @ConditionalOnMissingBean public ApplicationFactory applicationFactory(InstanceProperties instance, ManagementServerProperties management, ServerProperties server, ServletContext servletContext, PathMappedEndpoints pathMappedEndpoints, WebEndpointProperties webEndpoint, ObjectProvider> metadataContributors, DispatcherServletPath dispatcherServletPath) { return new ServletApplicationFactory(instance, management, server, servletContext, pathMappedEndpoints, webEndpoint, new CompositeMetadataContributor(metadataContributors.getIfAvailable(Collections::emptyList)), dispatcherServletPath); } } @Configuration(proxyBeanMethods = false) @ConditionalOnWebApplication(type = Type.REACTIVE) public static class ReactiveConfiguration { @Bean @Lazy(false) @ConditionalOnMissingBean public ApplicationFactory applicationFactory(InstanceProperties instance, ManagementServerProperties management, ServerProperties server, PathMappedEndpoints pathMappedEndpoints, WebEndpointProperties webEndpoint, ObjectProvider> metadataContributors, WebFluxProperties webFluxProperties) { return new ReactiveApplicationFactory(instance, management, server, pathMappedEndpoints, webEndpoint, new CompositeMetadataContributor(metadataContributors.getIfAvailable(Collections::emptyList)), webFluxProperties); } } @Configuration(proxyBeanMethods = false) @ConditionalOnBean(RestClient.Builder.class) public static class RestClientRegistrationClientConfig { @Bean @ConditionalOnMissingBean public RegistrationClient registrationClient(ClientProperties client, RestClient.Builder restClientBuilder, ObjectProvider objectMapper) { var factorySettings = HttpClientSettings.defaults() .withConnectTimeout(client.getConnectTimeout()) .withReadTimeout(client.getReadTimeout()); var clientHttpRequestFactory = ClientHttpRequestFactoryBuilder.detect().build(factorySettings); restClientBuilder.requestFactory(clientHttpRequestFactory); objectMapper.ifAvailable((mapper) -> restClientBuilder.messageConverters((configurer) -> { configurer.removeIf(JacksonJsonHttpMessageConverter.class::isInstance); configurer.add(new JacksonJsonHttpMessageConverter(mapper)); })); if (client.getUsername() != null && client.getPassword() != null) { restClientBuilder .requestInterceptor(new BasicAuthenticationInterceptor(client.getUsername(), client.getPassword())); } var restClient = restClientBuilder.build(); return new RestClientRegistrationClient(restClient); } } } ================================================ FILE: spring-boot-admin-client/src/main/java/de/codecentric/boot/admin/client/config/SpringBootAdminClientCloudFoundryAutoConfiguration.java ================================================ /* * 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.config; import java.util.Collections; import java.util.List; import org.springframework.beans.factory.ObjectProvider; 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.AutoConfigureBefore; 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.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; import org.springframework.context.annotation.Lazy; import de.codecentric.boot.admin.client.registration.CloudFoundryApplicationFactory; import de.codecentric.boot.admin.client.registration.metadata.CloudFoundryMetadataContributor; import de.codecentric.boot.admin.client.registration.metadata.CompositeMetadataContributor; import de.codecentric.boot.admin.client.registration.metadata.MetadataContributor; @Configuration(proxyBeanMethods = false) @ConditionalOnWebApplication @ConditionalOnCloudPlatform(CloudPlatform.CLOUD_FOUNDRY) @Conditional(SpringBootAdminClientEnabledCondition.class) @EnableConfigurationProperties(CloudFoundryApplicationProperties.class) @AutoConfigureBefore({ SpringBootAdminClientAutoConfiguration.class }) public class SpringBootAdminClientCloudFoundryAutoConfiguration { @Bean @ConditionalOnMissingBean public CloudFoundryMetadataContributor cloudFoundryMetadataContributor( CloudFoundryApplicationProperties cloudFoundryApplicationProperties) { return new CloudFoundryMetadataContributor(cloudFoundryApplicationProperties); } @Bean @Lazy(false) @ConditionalOnMissingBean public CloudFoundryApplicationFactory applicationFactory(InstanceProperties instance, ManagementServerProperties management, ServerProperties server, PathMappedEndpoints pathMappedEndpoints, WebEndpointProperties webEndpoint, ObjectProvider> metadataContributors, CloudFoundryApplicationProperties cfApplicationProperties) { return new CloudFoundryApplicationFactory(instance, management, server, pathMappedEndpoints, webEndpoint, new CompositeMetadataContributor(metadataContributors.getIfAvailable(Collections::emptyList)), cfApplicationProperties); } } ================================================ FILE: spring-boot-admin-client/src/main/java/de/codecentric/boot/admin/client/config/SpringBootAdminClientEnabledCondition.java ================================================ /* * 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.config; import org.springframework.boot.autoconfigure.condition.ConditionOutcome; import org.springframework.boot.autoconfigure.condition.SpringBootCondition; import org.springframework.boot.context.properties.bind.Bindable; import org.springframework.boot.context.properties.bind.Binder; import org.springframework.context.annotation.ConditionContext; import org.springframework.core.type.AnnotatedTypeMetadata; /** * This condition checks if the client should be enabled. Two properties are checked: * spring.boot.admin.client.enabled and spring.boot.admin.client.url. The following table * shows under which conditions the client is active.
 *           | enabled: false | enabled: true (default) |
 * --------- | -------------- | ----------------------- |
 * url empty | inactive       | inactive                |
 * (default) |                |                         |
 * --------- | -------------- | ----------------------- |
 * url set   | inactive       | active                  |
 * 
*/ public class SpringBootAdminClientEnabledCondition extends SpringBootCondition { @Override public ConditionOutcome getMatchOutcome(ConditionContext context, AnnotatedTypeMetadata annotatedTypeMetadata) { ClientProperties clientProperties = getClientProperties(context); if (!clientProperties.isEnabled()) { return ConditionOutcome .noMatch("Spring Boot Client is disabled, because 'spring.boot.admin.client.enabled' is false."); } if (clientProperties.getUrl().length == 0) { return ConditionOutcome .noMatch("Spring Boot Client is disabled, because 'spring.boot.admin.client.url' is empty."); } return ConditionOutcome.match(); } private ClientProperties getClientProperties(ConditionContext context) { ClientProperties clientProperties = new ClientProperties(); Binder.get(context.getEnvironment()).bind("spring.boot.admin.client", Bindable.ofInstance(clientProperties)); return clientProperties; } } ================================================ FILE: spring-boot-admin-client/src/main/java/de/codecentric/boot/admin/client/config/SpringNativeClientAutoConfiguration.java ================================================ /* * 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.config; 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.restclient.autoconfigure.RestClientAutoConfiguration; import org.springframework.context.annotation.Conditional; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.ImportRuntimeHints; @Configuration(proxyBeanMethods = false) @ConditionalOnWebApplication @Conditional(SpringBootAdminClientEnabledCondition.class) @AutoConfigureAfter({ WebEndpointAutoConfiguration.class, RestClientAutoConfiguration.class }) @ImportRuntimeHints({ ClientRuntimeHints.class }) public class SpringNativeClientAutoConfiguration { } ================================================ FILE: spring-boot-admin-client/src/main/java/de/codecentric/boot/admin/client/config/package-info.java ================================================ /* * 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. */ @NullMarked package de.codecentric.boot.admin.client.config; import org.jspecify.annotations.NullMarked; ================================================ FILE: spring-boot-admin-client/src/main/java/de/codecentric/boot/admin/client/registration/Application.java ================================================ /* * 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.HashMap; import java.util.Map; import org.springframework.util.Assert; /** * Contains all information which is used when this application is registered. * * @author Johannes Edmeier */ @lombok.Data @lombok.ToString(exclude = "metadata") public class Application { private final String name; private final String managementUrl; private final String healthUrl; private final String serviceUrl; private final Map metadata; @lombok.Builder(builderClassName = "Builder") protected Application(String name, String managementUrl, String healthUrl, String serviceUrl, @lombok.Singular("metadata") Map metadata) { Assert.hasText(name, "name must not be empty!"); Assert.hasText(healthUrl, "healthUrl must not be empty!"); this.name = name; this.managementUrl = managementUrl; this.healthUrl = healthUrl; this.serviceUrl = serviceUrl; this.metadata = new HashMap<>(metadata); } public static Builder create(String name) { return Application.builder().name(name); } public Map getMetadata() { return Collections.unmodifiableMap(metadata); } public static class Builder { // Will be generated by lombok } } ================================================ FILE: spring-boot-admin-client/src/main/java/de/codecentric/boot/admin/client/registration/ApplicationFactory.java ================================================ /* * 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; /** * Classes implementing this interface are responsible for creating an {@link Application} * class which is used to register at the admin server. * * @author Johannes Edmeier */ public interface ApplicationFactory { /** * @return {@link Application} instance; */ Application createApplication(); } ================================================ FILE: spring-boot-admin-client/src/main/java/de/codecentric/boot/admin/client/registration/ApplicationRegistrator.java ================================================ /* * 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; /** * Interface for client application registration at spring-boot-admin-server */ 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 the * client has not registered against the admin server yet. */ String getRegisteredId(); } ================================================ FILE: spring-boot-admin-client/src/main/java/de/codecentric/boot/admin/client/registration/CloudFoundryApplicationFactory.java ================================================ /* * 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.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.web.server.autoconfigure.ServerProperties; import org.springframework.util.StringUtils; import de.codecentric.boot.admin.client.config.CloudFoundryApplicationProperties; import de.codecentric.boot.admin.client.config.InstanceProperties; import de.codecentric.boot.admin.client.registration.metadata.MetadataContributor; public class CloudFoundryApplicationFactory extends DefaultApplicationFactory { private final CloudFoundryApplicationProperties cfApplicationProperties; private final InstanceProperties instance; public CloudFoundryApplicationFactory(InstanceProperties instance, ManagementServerProperties management, ServerProperties server, PathMappedEndpoints pathMappedEndpoints, WebEndpointProperties webEndpoint, MetadataContributor metadataContributor, CloudFoundryApplicationProperties cfApplicationProperties) { super(instance, management, server, pathMappedEndpoints, webEndpoint, metadataContributor); this.cfApplicationProperties = cfApplicationProperties; this.instance = instance; } @Override protected String getServiceBaseUrl() { String baseUrl = this.instance.getServiceBaseUrl(); if (StringUtils.hasText(baseUrl)) { return baseUrl; } if (this.cfApplicationProperties.getUris().isEmpty()) { return super.getServiceBaseUrl(); } return "http://" + this.cfApplicationProperties.getUris().get(0); } } ================================================ FILE: spring-boot-admin-client/src/main/java/de/codecentric/boot/admin/client/registration/DefaultApplicationFactory.java ================================================ /* * 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.net.InetAddress; import java.net.UnknownHostException; 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.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.util.StringUtils; import org.springframework.web.util.UriComponentsBuilder; import de.codecentric.boot.admin.client.config.InstanceProperties; import de.codecentric.boot.admin.client.registration.metadata.MetadataContributor; /** * Default implementation for creating the {@link Application} instance which gets * registered at the admin server. * * @author Johannes Edmeier * @author Rene Felgenträger */ public class DefaultApplicationFactory implements ApplicationFactory { private final InstanceProperties instance; private final ServerProperties server; private final ManagementServerProperties management; private final PathMappedEndpoints pathMappedEndpoints; private final WebEndpointProperties webEndpoint; private final MetadataContributor metadataContributor; @Nullable private Integer localServerPort; @Nullable private Integer localManagementPort; public DefaultApplicationFactory(InstanceProperties instance, ManagementServerProperties management, ServerProperties server, PathMappedEndpoints pathMappedEndpoints, WebEndpointProperties webEndpoint, MetadataContributor metadataContributor) { this.instance = instance; this.management = management; this.server = server; this.pathMappedEndpoints = pathMappedEndpoints; this.webEndpoint = webEndpoint; this.metadataContributor = metadataContributor; } @Override public Application createApplication() { return Application.create(getName()) .healthUrl(getHealthUrl()) .managementUrl(getManagementUrl()) .serviceUrl(getServiceUrl()) .metadata(getMetadata()) .build(); } protected String getName() { return this.instance.getName(); } protected String getServiceUrl() { if (this.instance.getServiceUrl() != null) { return this.instance.getServiceUrl(); } return UriComponentsBuilder.fromUriString(getServiceBaseUrl()).path(getServicePath()).toUriString(); } protected String getServiceBaseUrl() { String baseUrl = this.instance.getServiceBaseUrl(); if (StringUtils.hasText(baseUrl)) { return baseUrl; } return UriComponentsBuilder.newInstance() .scheme(getScheme(this.server.getSsl())) .host(getServiceHost()) .port(getLocalServerPort()) .toUriString(); } protected String getServicePath() { String path = this.instance.getServicePath(); if (StringUtils.hasText(path)) { return path; } return "/"; } protected String getManagementUrl() { if (this.instance.getManagementUrl() != null) { return this.instance.getManagementUrl(); } return UriComponentsBuilder.fromUriString(getManagementBaseUrl()) .path("/") .path(getEndpointsWebPath()) .toUriString(); } protected String getManagementBaseUrl() { String baseUrl = this.instance.getManagementBaseUrl(); if (StringUtils.hasText(baseUrl)) { return baseUrl; } if (isManagementPortEqual()) { return this.getServiceUrl(); } Ssl ssl = (this.management.getSsl() != null) ? this.management.getSsl() : this.server.getSsl(); return UriComponentsBuilder.newInstance() .scheme(getScheme(ssl)) .host(getManagementHost()) .port(getLocalManagementPort()) .toUriString(); } protected boolean isManagementPortEqual() { return this.localManagementPort == null || this.localManagementPort.equals(this.localServerPort); } protected String getEndpointsWebPath() { return this.webEndpoint.getBasePath(); } protected String getHealthUrl() { if (this.instance.getHealthUrl() != null) { return this.instance.getHealthUrl(); } return UriComponentsBuilder.fromUriString(getManagementBaseUrl()) .path("/") .path(getHealthEndpointPath()) .toUriString(); } protected Map getMetadata() { Map metadata = new LinkedHashMap<>(); metadata.putAll(this.metadataContributor.getMetadata()); metadata.putAll(this.instance.getMetadata()); return metadata; } protected String getServiceHost() { InetAddress address = this.server.getAddress(); if (address == null) { address = getLocalHost(); } return getHost(address); } protected String getManagementHost() { InetAddress address = this.management.getAddress(); if (address != null) { return getHost(address); } return getServiceHost(); } protected InetAddress getLocalHost() { try { return InetAddress.getLocalHost(); } catch (UnknownHostException ex) { throw new IllegalArgumentException(ex.getMessage(), ex); } } protected Integer getLocalServerPort() { if (this.localServerPort == null) { throw new IllegalStateException( "couldn't determine local port. Please set spring.boot.admin.client.instance.service-base-url."); } return this.localServerPort; } protected Integer getLocalManagementPort() { if (this.localManagementPort == null) { return this.getLocalServerPort(); } return this.localManagementPort; } protected String getHealthEndpointPath() { String health = this.pathMappedEndpoints.getPath(EndpointId.of("health")); if (StringUtils.hasText(health)) { return health; } String status = this.pathMappedEndpoints.getPath(EndpointId.of("status")); if (StringUtils.hasText(status)) { return status; } throw new IllegalStateException("Either health or status endpoint must be enabled!"); } protected String getScheme(@Nullable Ssl ssl) { return ((ssl != null) && ssl.isEnabled()) ? "https" : "http"; } protected String getHost(InetAddress address) { if (this.instance.isPreferIp()) { return address.getHostAddress(); } return switch (this.instance.getServiceHostType()) { case IP -> address.getHostAddress(); case HOST_NAME -> address.getHostName(); default -> address.getCanonicalHostName(); }; } @EventListener public void onWebServerInitialized(WebServerInitializedEvent event) { String name = event.getApplicationContext().getServerNamespace(); if ("server".equals(name) || !StringUtils.hasText(name)) { this.localServerPort = event.getWebServer().getPort(); } else if ("management".equals(name)) { this.localManagementPort = event.getWebServer().getPort(); } } } ================================================ FILE: spring-boot-admin-client/src/main/java/de/codecentric/boot/admin/client/registration/DefaultApplicationRegistrator.java ================================================ /* * 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.concurrent.ConcurrentHashMap; import java.util.concurrent.atomic.AtomicReference; import java.util.concurrent.atomic.LongAdder; import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class DefaultApplicationRegistrator implements ApplicationRegistrator { private static final Logger LOGGER = LoggerFactory.getLogger(DefaultApplicationRegistrator.class); private final ConcurrentHashMap attempts = new ConcurrentHashMap<>(); private final AtomicReference registeredId = new AtomicReference<>(); private final ApplicationFactory applicationFactory; private final String[] adminUrls; private final boolean registerOnce; private final RegistrationClient registrationClient; public DefaultApplicationRegistrator(ApplicationFactory applicationFactory, RegistrationClient registrationClient, String[] adminUrls, boolean registerOnce) { this.applicationFactory = applicationFactory; this.adminUrls = adminUrls; this.registerOnce = registerOnce; this.registrationClient = registrationClient; } /** * Registers the client application at spring-boot-admin-server. * @return true if successful registration on at least one admin server */ @Override public boolean register() { Application application = this.applicationFactory.createApplication(); boolean isRegistrationSuccessful = false; for (String adminUrl : this.adminUrls) { LongAdder attempt = this.attempts.computeIfAbsent(adminUrl, (k) -> new LongAdder()); boolean successful = register(application, adminUrl, attempt.intValue() == 0); if (!successful) { attempt.increment(); } else { attempt.reset(); isRegistrationSuccessful = true; if (this.registerOnce) { break; } } } return isRegistrationSuccessful; } protected boolean register(Application application, String adminUrl, boolean firstAttempt) { try { String id = this.registrationClient.register(adminUrl, application); if (this.registeredId.compareAndSet(null, id)) { LOGGER.info("Application registered itself as {}", id); } else { LOGGER.debug("Application refreshed itself as {}", id); } return true; } catch (Exception ex) { if (firstAttempt) { LOGGER.warn( "Failed to register application as {} at spring-boot-admin ({}): {}. Further attempts are logged on DEBUG level", application, this.adminUrls, ex.getMessage(), ex); } else { LOGGER.debug("Failed to register application as {} at spring-boot-admin ({}): {}", application, this.adminUrls, ex.getMessage(), ex); } return false; } } @Override public void deregister() { String id = this.registeredId.get(); if (id == null) { return; } for (String adminUrl : this.adminUrls) { try { this.registrationClient.deregister(adminUrl, id); this.registeredId.compareAndSet(id, null); if (this.registerOnce) { break; } } catch (Exception ex) { LOGGER.warn("Failed to deregister application (id={}) at spring-boot-admin ({}): {}", id, adminUrl, ex.getMessage()); } } } @Override public String getRegisteredId() { return this.registeredId.get(); } } ================================================ FILE: spring-boot-admin-client/src/main/java/de/codecentric/boot/admin/client/registration/ReactiveApplicationFactory.java ================================================ /* * 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.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.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; import de.codecentric.boot.admin.client.config.InstanceProperties; import de.codecentric.boot.admin.client.registration.metadata.MetadataContributor; public class ReactiveApplicationFactory extends DefaultApplicationFactory { private final ManagementServerProperties management; private final ServerProperties server; private final WebFluxProperties webflux; private final InstanceProperties instance; public ReactiveApplicationFactory(InstanceProperties instance, ManagementServerProperties management, ServerProperties server, PathMappedEndpoints pathMappedEndpoints, WebEndpointProperties webEndpoint, MetadataContributor metadataContributor, WebFluxProperties webFluxProperties) { super(instance, management, server, pathMappedEndpoints, webEndpoint, metadataContributor); this.management = management; this.server = server; this.webflux = webFluxProperties; this.instance = instance; } @Override protected String getServiceUrl() { if (instance.getServiceUrl() != null) { return instance.getServiceUrl(); } return UriComponentsBuilder.fromUriString(getServiceBaseUrl()) .path(getServicePath()) .path(getWebfluxBasePath()) .toUriString(); } @Override protected String getManagementBaseUrl() { String baseUrl = this.instance.getManagementBaseUrl(); if (StringUtils.hasText(baseUrl)) { return baseUrl; } if (isManagementPortEqual()) { return this.getServiceUrl(); } Ssl ssl = (this.management.getSsl() != null) ? this.management.getSsl() : this.server.getSsl(); return UriComponentsBuilder.newInstance() .scheme(getScheme(ssl)) .host(getManagementHost()) .port(getLocalManagementPort()) .path(getManagementContextPath()) .toUriString(); } protected String getManagementContextPath() { return management.getBasePath(); } protected String getWebfluxBasePath() { return webflux.getBasePath(); } } ================================================ FILE: spring-boot-admin-client/src/main/java/de/codecentric/boot/admin/client/registration/RegistrationApplicationListener.java ================================================ /* * 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.concurrent.ScheduledFuture; import org.jspecify.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.DisposableBean; import org.springframework.beans.factory.InitializingBean; import org.springframework.boot.context.event.ApplicationReadyEvent; import org.springframework.context.event.ContextClosedEvent; import org.springframework.context.event.EventListener; import org.springframework.core.Ordered; import org.springframework.core.annotation.Order; import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler; /** * Listener responsible for starting and stopping the registration task when the * application is ready. * * @author Johannes Edmeier */ public class RegistrationApplicationListener implements InitializingBean, DisposableBean { private static final Logger LOGGER = LoggerFactory.getLogger(RegistrationApplicationListener.class); private final ApplicationRegistrator registrator; private final ThreadPoolTaskScheduler taskScheduler; private boolean autoDeregister = false; private boolean autoRegister = true; private Duration registerPeriod = Duration.ofSeconds(10); @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); taskScheduler.setRemoveOnCancelPolicy(true); taskScheduler.setThreadNamePrefix("registrationTask"); return taskScheduler; } @EventListener @Order(Ordered.LOWEST_PRECEDENCE) public void onApplicationReady(ApplicationReadyEvent event) { if (autoRegister) { startRegisterTask(); } } @EventListener @Order(Ordered.LOWEST_PRECEDENCE) public void onClosedContext(ContextClosedEvent event) { if (event.getApplicationContext().getParent() == null || "bootstrap".equals(event.getApplicationContext().getParent().getId())) { stopRegisterTask(); if (autoDeregister) { registrator.deregister(); } } } public void startRegisterTask() { if (scheduledTask != null && !scheduledTask.isDone()) { return; } scheduledTask = taskScheduler.scheduleAtFixedRate(registrator::register, registerPeriod); LOGGER.debug("Scheduled registration task for every {}ms", registerPeriod.toMillis()); } public void stopRegisterTask() { if (scheduledTask != null && !scheduledTask.isDone()) { scheduledTask.cancel(true); LOGGER.debug("Canceled registration task"); } } public void setAutoDeregister(boolean autoDeregister) { this.autoDeregister = autoDeregister; } public void setAutoRegister(boolean autoRegister) { this.autoRegister = autoRegister; } public void setRegisterPeriod(Duration registerPeriod) { this.registerPeriod = registerPeriod; } @Override public void afterPropertiesSet() { taskScheduler.afterPropertiesSet(); } @Override public void destroy() { taskScheduler.destroy(); } } ================================================ FILE: spring-boot-admin-client/src/main/java/de/codecentric/boot/admin/client/registration/RegistrationClient.java ================================================ /* * 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; public interface RegistrationClient { String register(String adminUrl, Application self); void deregister(String adminUrl, String id); } ================================================ FILE: spring-boot-admin-client/src/main/java/de/codecentric/boot/admin/client/registration/RestClientRegistrationClient.java ================================================ /* * Copyright 2014-2024 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.HttpHeaders; import org.springframework.http.MediaType; import org.springframework.web.client.RestClient; public class RestClientRegistrationClient implements RegistrationClient { private static final ParameterizedTypeReference> RESPONSE_TYPE = new ParameterizedTypeReference<>() { }; private final RestClient restClient; public RestClientRegistrationClient(RestClient restClient) { this.restClient = restClient; } @Override public String register(String adminUrl, Application application) { Map response = this.restClient.post() .uri(adminUrl) .headers(this::setRequestHeaders) .body(application) .retrieve() .body(RESPONSE_TYPE); return response.get("id").toString(); } @Override public void deregister(String adminUrl, String id) { this.restClient.delete().uri(adminUrl + '/' + id).retrieve().toBodilessEntity(); } protected void setRequestHeaders(HttpHeaders headers) { headers.setContentType(MediaType.APPLICATION_JSON); headers.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON)); } } ================================================ FILE: spring-boot-admin-client/src/main/java/de/codecentric/boot/admin/client/registration/ServletApplicationFactory.java ================================================ /* * 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 jakarta.servlet.ServletContext; 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.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; import de.codecentric.boot.admin.client.config.InstanceProperties; import de.codecentric.boot.admin.client.registration.metadata.MetadataContributor; public class ServletApplicationFactory extends DefaultApplicationFactory { private final ServletContext servletContext; private final ServerProperties server; private final ManagementServerProperties management; private final InstanceProperties instance; private final DispatcherServletPath dispatcherServletPath; public ServletApplicationFactory(InstanceProperties instance, ManagementServerProperties management, ServerProperties server, ServletContext servletContext, PathMappedEndpoints pathMappedEndpoints, WebEndpointProperties webEndpoint, MetadataContributor metadataContributor, DispatcherServletPath dispatcherServletPath) { super(instance, management, server, pathMappedEndpoints, webEndpoint, metadataContributor); this.servletContext = servletContext; this.server = server; this.management = management; this.instance = instance; this.dispatcherServletPath = dispatcherServletPath; } @Override protected String getServiceUrl() { if (instance.getServiceUrl() != null) { return instance.getServiceUrl(); } return UriComponentsBuilder.fromUriString(getServiceBaseUrl()) .path(getServicePath()) .path(getServerContextPath()) .toUriString(); } @Override protected String getManagementBaseUrl() { String baseUrl = instance.getManagementBaseUrl(); if (StringUtils.hasText(baseUrl)) { return baseUrl; } if (isManagementPortEqual()) { return UriComponentsBuilder.fromUriString(getServiceUrl()) .path("/") .path(getDispatcherServletPrefix()) .path(getManagementContextPath()) .toUriString(); } Ssl ssl = (management.getSsl() != null) ? management.getSsl() : server.getSsl(); return UriComponentsBuilder.newInstance() .scheme(getScheme(ssl)) .host(getManagementHost()) .port(getLocalManagementPort()) .path(getManagementContextPath()) .toUriString(); } protected String getManagementContextPath() { return management.getBasePath(); } protected String getServerContextPath() { return servletContext.getContextPath(); } protected String getDispatcherServletPrefix() { return this.dispatcherServletPath.getPrefix(); } } ================================================ FILE: spring-boot-admin-client/src/main/java/de/codecentric/boot/admin/client/registration/metadata/CloudFoundryMetadataContributor.java ================================================ /* * 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.metadata; import java.util.Collections; import java.util.HashMap; import java.util.Map; import org.springframework.util.StringUtils; import de.codecentric.boot.admin.client.config.CloudFoundryApplicationProperties; public class CloudFoundryMetadataContributor implements MetadataContributor { private final CloudFoundryApplicationProperties cfApplicationProperties; public CloudFoundryMetadataContributor(CloudFoundryApplicationProperties cfApplicationProperties) { this.cfApplicationProperties = cfApplicationProperties; } @Override public Map getMetadata() { if (StringUtils.hasText(this.cfApplicationProperties.getApplicationId()) && StringUtils.hasText(this.cfApplicationProperties.getInstanceIndex())) { Map map = new HashMap<>(); map.put("applicationId", this.cfApplicationProperties.getApplicationId()); map.put("instanceId", this.cfApplicationProperties.getInstanceIndex()); return map; } return Collections.emptyMap(); } } ================================================ FILE: spring-boot-admin-client/src/main/java/de/codecentric/boot/admin/client/registration/metadata/CompositeMetadataContributor.java ================================================ /* * 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.metadata; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; 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; } } ================================================ FILE: spring-boot-admin-client/src/main/java/de/codecentric/boot/admin/client/registration/metadata/MetadataContributor.java ================================================ /* * 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.metadata; import java.util.Map; @FunctionalInterface public interface MetadataContributor { Map getMetadata(); } ================================================ FILE: spring-boot-admin-client/src/main/java/de/codecentric/boot/admin/client/registration/metadata/StartupDateMetadataContributor.java ================================================ /* * 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.metadata; import java.time.OffsetDateTime; import java.time.format.DateTimeFormatter; import java.util.Map; import static java.util.Collections.singletonMap; public class StartupDateMetadataContributor implements MetadataContributor { private final OffsetDateTime timestamp = OffsetDateTime.now(); @Override public Map getMetadata() { return singletonMap("startup", this.timestamp.format(DateTimeFormatter.ISO_DATE_TIME)); } } ================================================ FILE: spring-boot-admin-client/src/main/java/de/codecentric/boot/admin/client/registration/metadata/package-info.java ================================================ /* * 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. */ @NullMarked package de.codecentric.boot.admin.client.registration.metadata; import org.jspecify.annotations.NullMarked; ================================================ FILE: spring-boot-admin-client/src/main/java/de/codecentric/boot/admin/client/registration/package-info.java ================================================ /* * 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. */ @NullMarked package de.codecentric.boot.admin.client.registration; import org.jspecify.annotations.NullMarked; ================================================ FILE: spring-boot-admin-client/src/main/resources/META-INF/additional-spring-configuration-metadata.json ================================================ {"groups": [ ],"properties": [ { "name": "spring.boot.admin.client.enabled", "type": "java.lang.Boolean", "description": "Enable Spring Admin Client.", "defaultValue": "true" } ]} ================================================ FILE: spring-boot-admin-client/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports ================================================ de.codecentric.boot.admin.client.config.SpringBootAdminClientAutoConfiguration de.codecentric.boot.admin.client.config.SpringBootAdminClientCloudFoundryAutoConfiguration de.codecentric.boot.admin.client.config.SpringNativeClientAutoConfiguration ================================================ FILE: spring-boot-admin-client/src/test/java/de/codecentric/boot/admin/client/AbstractClientApplicationTest.java ================================================ /* * 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; import java.net.InetAddress; import java.net.UnknownHostException; import java.util.Arrays; import java.util.concurrent.CountDownLatch; import java.util.stream.Stream; import com.github.tomakehurst.wiremock.WireMockServer; import com.github.tomakehurst.wiremock.client.ResponseDefinitionBuilder; import com.github.tomakehurst.wiremock.common.ConsoleNotifier; import com.github.tomakehurst.wiremock.matching.RequestPatternBuilder; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.SpringApplication; import org.springframework.boot.SpringBootConfiguration; import org.springframework.boot.WebApplicationType; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.context.event.ApplicationReadyEvent; import org.springframework.context.ConfigurableApplicationContext; import org.springframework.context.event.EventListener; import de.codecentric.boot.admin.client.registration.ApplicationRegistrator; import static com.github.tomakehurst.wiremock.client.WireMock.created; import static com.github.tomakehurst.wiremock.client.WireMock.equalTo; import static com.github.tomakehurst.wiremock.client.WireMock.matching; import static com.github.tomakehurst.wiremock.client.WireMock.matchingJsonPath; import static com.github.tomakehurst.wiremock.client.WireMock.post; import static com.github.tomakehurst.wiremock.client.WireMock.postRequestedFor; import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo; import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.options; import static org.awaitility.Awaitility.await; public abstract class AbstractClientApplicationTest { private final WireMockServer wireMock = new WireMockServer( options().dynamicPort().notifier(new ConsoleNotifier(true))); private SpringApplication application; private ConfigurableApplicationContext instance; private static final CountDownLatch cdl = new CountDownLatch(1); protected void setUp(WebApplicationType type) { setUpWiremock(); setUpApplication(type); } private void setUpWiremock() { wireMock.start(); ResponseDefinitionBuilder response = created().withHeader("Content-Type", "application/json") .withHeader("Connection", "close") .withHeader("Location", wireMock.url("/instances/abcdef")) .withBody("{ \"id\" : \"abcdef\" }"); wireMock.stubFor(post(urlEqualTo("/instances")).willReturn(response)); } private void setUpApplication(WebApplicationType type) { application = new SpringApplication(TestClientApplication.class); application.setWebApplicationType(type); } private void setUpApplicationContext(String... additionalArgs) { Stream defaultArgs = Stream.of("--spring.application.name=Test-Client", "--server.port=0", "--management.endpoints.web.base-path=/mgmt", "--endpoints.health.enabled=true", "--spring.boot.admin.client.url=" + wireMock.url("/")); String[] args = Stream.concat(defaultArgs, Arrays.stream(additionalArgs)).toArray(String[]::new); this.instance = application.run(args); } @AfterEach void tearDown() { wireMock.stop(); if (instance != null) { instance.close(); } } @Test public void test_context() throws InterruptedException, UnknownHostException { setUpApplicationContext(); String hostName = InetAddress.getLocalHost().getCanonicalHostName(); String serviceHost = "http://" + hostName + ":" + getServerPort(); String managementHost = "http://" + hostName + ":" + getManagementPort(); RequestPatternBuilder request = postRequestedFor(urlEqualTo("/instances")); request.withHeader("Content-Type", equalTo("application/json")) .withRequestBody(matchingJsonPath("$.name", equalTo("Test-Client"))) .withRequestBody(matchingJsonPath("$.healthUrl", equalTo(managementHost + "/mgmt/health"))) .withRequestBody(matchingJsonPath("$.managementUrl", equalTo(managementHost + "/mgmt"))) .withRequestBody(matchingJsonPath("$.serviceUrl", equalTo(serviceHost + "/"))) .withRequestBody(matchingJsonPath("$.metadata.startup", matching(".+"))); cdl.await(); await().untilAsserted(() -> wireMock.verify(request)); } @Test public void test_context_with_snake_case() throws InterruptedException, UnknownHostException { setUpApplicationContext("--spring.jackson.property-naming-strategy=SNAKE_CASE"); String hostName = InetAddress.getLocalHost().getCanonicalHostName(); String serviceHost = "http://" + hostName + ":" + getServerPort(); String managementHost = "http://" + hostName + ":" + getManagementPort(); RequestPatternBuilder request = postRequestedFor(urlEqualTo("/instances")); request.withHeader("Content-Type", equalTo("application/json")) .withRequestBody(matchingJsonPath("$.name", equalTo("Test-Client"))) .withRequestBody(matchingJsonPath("$.health_url", equalTo(managementHost + "/mgmt/health"))) .withRequestBody(matchingJsonPath("$.management_url", equalTo(managementHost + "/mgmt"))) .withRequestBody(matchingJsonPath("$.service_url", equalTo(serviceHost + "/"))) .withRequestBody(matchingJsonPath("$.metadata.startup", matching(".+"))); cdl.await(); await().untilAsserted(() -> wireMock.verify(request)); } private int getServerPort() { return instance.getEnvironment().getProperty("local.server.port", Integer.class, 0); } private int getManagementPort() { return instance.getEnvironment().getProperty("local.management.port", Integer.class, 0); } @SpringBootConfiguration @EnableAutoConfiguration public static class TestClientApplication { @Autowired private ApplicationRegistrator registrator; @EventListener public void ping(ApplicationReadyEvent ev) { new Thread(() -> { await().until(() -> registrator.getRegisteredId() != null); cdl.countDown(); }).start(); } } } ================================================ FILE: spring-boot-admin-client/src/test/java/de/codecentric/boot/admin/client/ClientReactiveApplicationTest.java ================================================ /* * 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; import org.junit.jupiter.api.BeforeEach; import org.springframework.boot.WebApplicationType; class ClientReactiveApplicationTest extends AbstractClientApplicationTest { @BeforeEach void setUp() { super.setUp(WebApplicationType.REACTIVE); } } ================================================ FILE: spring-boot-admin-client/src/test/java/de/codecentric/boot/admin/client/ClientServletApplicationTest.java ================================================ /* * 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; import org.junit.jupiter.api.BeforeEach; import org.springframework.boot.WebApplicationType; class ClientServletApplicationTest extends AbstractClientApplicationTest { @BeforeEach void setUp() { super.setUp(WebApplicationType.SERVLET); } } ================================================ FILE: spring-boot-admin-client/src/test/java/de/codecentric/boot/admin/client/config/ClientPropertiesTest.java ================================================ /* * 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.config; import org.junit.jupiter.api.Test; import org.springframework.mock.env.MockEnvironment; import static org.assertj.core.api.Assertions.assertThat; class ClientPropertiesTest { @Test void should_default_autoDeregister_to_false() { MockEnvironment env = new MockEnvironment(); ClientProperties clientProperties = new ClientProperties(); assertThat(clientProperties.isAutoDeregistration(env)).isFalse(); clientProperties.setAutoDeregistration(false); assertThat(clientProperties.isAutoDeregistration(env)).isFalse(); clientProperties.setAutoDeregistration(true); assertThat(clientProperties.isAutoDeregistration(env)).isTrue(); } @Test void should_default_autoDeregister_to_true() { MockEnvironment env = new MockEnvironment(); env.setProperty("VCAP_APPLICATION", ""); ClientProperties clientProperties = new ClientProperties(); assertThat(clientProperties.isAutoDeregistration(env)).isTrue(); clientProperties.setAutoDeregistration(false); assertThat(clientProperties.isAutoDeregistration(env)).isFalse(); clientProperties.setAutoDeregistration(true); assertThat(clientProperties.isAutoDeregistration(env)).isTrue(); } @Test void should_return_all_adminUrls() { ClientProperties clientProperties = new ClientProperties(); clientProperties.setApiPath("register"); clientProperties.setUrl(new String[] { "http://first", "http://second" }); assertThat(clientProperties.getAdminUrl()).containsExactly("http://first/register", "http://second/register"); } } ================================================ FILE: spring-boot-admin-client/src/test/java/de/codecentric/boot/admin/client/config/CloudFoundryApplicationPropertiesTest.java ================================================ /* * 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.config; import org.junit.jupiter.api.Test; import org.springframework.boot.cloud.CloudFoundryVcapEnvironmentPostProcessor; import org.springframework.boot.context.properties.bind.Bindable; import org.springframework.boot.context.properties.bind.Binder; import org.springframework.boot.logging.DeferredLogs; import org.springframework.mock.env.MockEnvironment; import static org.assertj.core.api.Assertions.assertThat; class CloudFoundryApplicationPropertiesTest { @Test void bind() { String vcap = "{\"application_users\":[]," + "\"application_id\":\"9958288f-9842-4ddc-93dd-1ea3c90634cd\"," + "\"instance_id\":\"bb7935245adf3e650dfb7c58a06e9ece\"," + "\"instance_index\":0,\"version\":\"3464e092-1c13-462e-a47c-807c30318a50\"," + "\"name\":\"foo\",\"uris\":[\"foo.cfapps.io\"]," + "\"started_at\":\"2013-05-29 02:37:59 +0000\"," + "\"started_at_timestamp\":1369795079," + "\"host\":\"0.0.0.0\",\"port\":61034," + "\"limits\":{\"mem\":128,\"disk\":1024,\"fds\":16384}," + "\"version\":\"3464e092-1c13-462e-a47c-807c30318a50\"," + "\"name\":\"dsyerenv\",\"uris\":[\"dsyerenv.cfapps.io\"]," + "\"users\":[],\"start\":\"2013-05-29 02:37:59 +0000\"," + "\"state_timestamp\":1369795079}"; MockEnvironment env = new MockEnvironment(); env.setProperty("VCAP_APPLICATION", vcap); new CloudFoundryVcapEnvironmentPostProcessor(new DeferredLogs()).postProcessEnvironment(env, null); CloudFoundryApplicationProperties cfProperties = Binder.get(env) .bind("vcap.application", Bindable.of(CloudFoundryApplicationProperties.class)) .get(); assertThat(cfProperties.getApplicationId()).isEqualTo("9958288f-9842-4ddc-93dd-1ea3c90634cd"); assertThat(cfProperties.getInstanceIndex()).isEqualTo("0"); } } ================================================ FILE: spring-boot-admin-client/src/test/java/de/codecentric/boot/admin/client/config/SpringBootAdminClientAutoConfigurationTest.java ================================================ /* * 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.config; import java.time.Duration; import org.junit.jupiter.api.Test; 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.logging.ConditionEvaluationReportLoggingListener; 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.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 de.codecentric.boot.admin.client.registration.ApplicationRegistrator; import de.codecentric.boot.admin.client.registration.RegistrationClient; import static org.assertj.core.api.Assertions.assertThat; class SpringBootAdminClientAutoConfigurationTest { private final WebApplicationContextRunner contextRunner = new WebApplicationContextRunner() .withConfiguration(AutoConfigurations.of(EndpointAutoConfiguration.class, WebEndpointAutoConfiguration.class, DispatcherServletAutoConfiguration.class, HttpClientAutoConfiguration.class, RestClientAutoConfiguration.class, SpringBootAdminClientAutoConfiguration.class)); @Test void not_active() { this.contextRunner.run((context) -> assertThat(context).doesNotHaveBean(ApplicationRegistrator.class)); } @Test void active() { this.contextRunner.withPropertyValues("spring.boot.admin.client.url:http://localhost:8081") .run((context) -> assertThat(context).hasSingleBean(ApplicationRegistrator.class)); } @Test void disabled() { this.contextRunner .withPropertyValues("spring.boot.admin.client.url:http://localhost:8081", "spring.boot.admin.client.enabled:false") .run((context) -> assertThat(context).doesNotHaveBean(ApplicationRegistrator.class)); } @Test void nonWebEnvironment() { ApplicationContextRunner nonWebContextRunner = new ApplicationContextRunner() .withConfiguration(AutoConfigurations.of(SpringBootAdminClientAutoConfiguration.class)); nonWebContextRunner.withPropertyValues("spring.boot.admin.client.url:http://localhost:8081") .run((context) -> assertThat(context).doesNotHaveBean(ApplicationRegistrator.class)); } @Test void reactiveEnvironment() { ReactiveWebApplicationContextRunner reactiveContextRunner = new ReactiveWebApplicationContextRunner() .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 restClientRegistrationClientInBlockingEnvironment() { WebApplicationContextRunner webApplicationContextRunner = new WebApplicationContextRunner().withConfiguration( AutoConfigurations.of(EndpointAutoConfiguration.class, WebEndpointAutoConfiguration.class, DispatcherServletAutoConfiguration.class, HttpClientAutoConfiguration.class, RestClientAutoConfiguration.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") .withInitializer(new ConditionEvaluationReportLoggingListener()) .run((context) -> { RegistrationClient registrationClient = context.getBean(RegistrationClient.class); RestClient restClient = (RestClient) ReflectionTestUtils.getField(registrationClient, "restClient"); assertThat(restClient).isNotNull(); ClientHttpRequestFactory requestFactory = (ClientHttpRequestFactory) ReflectionTestUtils .getField(restClient, "clientRequestFactory"); Integer connectTimeout = (Integer) ReflectionTestUtils.getField(requestFactory, "connectTimeout"); assertThat(connectTimeout).isEqualTo(1337); Duration readTimeout = (Duration) ReflectionTestUtils.getField(requestFactory, "readTimeout"); assertThat(readTimeout).isEqualTo(Duration.ofMillis(42)); }); } } ================================================ FILE: spring-boot-admin-client/src/test/java/de/codecentric/boot/admin/client/config/SpringBootAdminClientCloudFoundryAutoConfigurationTest.java ================================================ /* * 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.config; import org.junit.jupiter.api.Test; 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.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; import de.codecentric.boot.admin.client.registration.DefaultApplicationFactory; import de.codecentric.boot.admin.client.registration.metadata.CloudFoundryMetadataContributor; import static org.assertj.core.api.Assertions.assertThat; class SpringBootAdminClientCloudFoundryAutoConfigurationTest { private final WebApplicationContextRunner contextRunner = new WebApplicationContextRunner() .withConfiguration(AutoConfigurations.of(EndpointAutoConfiguration.class, WebEndpointAutoConfiguration.class, WebMvcAutoConfiguration.class, DispatcherServletAutoConfiguration.class, HttpClientAutoConfiguration.class, RestClientAutoConfiguration.class, SpringBootAdminClientAutoConfiguration.class, SpringBootAdminClientCloudFoundryAutoConfiguration.class)); @Test void non_cloud_platform() { this.contextRunner.withPropertyValues("spring.boot.admin.client.url:http://localhost:8081").run((context) -> { assertThat(context).doesNotHaveBean(CloudFoundryMetadataContributor.class); assertThat(context).getBean(ApplicationFactory.class).isInstanceOf(DefaultApplicationFactory.class); }); } @Test void cloudfoundry() { this.contextRunner.withPropertyValues("spring.boot.admin.client.url:http://localhost:8081") .withPropertyValues("VCAP_APPLICATION:{}") .run((context) -> { assertThat(context).hasSingleBean(CloudFoundryMetadataContributor.class); assertThat(context).getBean(ApplicationFactory.class) .isInstanceOf(CloudFoundryApplicationFactory.class); }); } @Test void cloudfoundry_sba_disabled() { this.contextRunner.withPropertyValues("VCAP_APPLICATION:{}").run((context) -> { assertThat(context).doesNotHaveBean(CloudFoundryMetadataContributor.class); assertThat(context).doesNotHaveBean(ApplicationFactory.class); }); } } ================================================ FILE: spring-boot-admin-client/src/test/java/de/codecentric/boot/admin/client/config/SpringBootAdminClientEnabledConditionTest.java ================================================ /* * 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.config; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.mockito.BDDMockito; import org.springframework.context.annotation.ConditionContext; import org.springframework.core.type.AnnotatedTypeMetadata; import org.springframework.mock.env.MockEnvironment; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.mock; class SpringBootAdminClientEnabledConditionTest { private SpringBootAdminClientEnabledCondition condition; private AnnotatedTypeMetadata annotatedTypeMetadata; private ConditionContext conditionContext; @BeforeEach void setUp() { condition = new SpringBootAdminClientEnabledCondition(); annotatedTypeMetadata = mock(AnnotatedTypeMetadata.class); conditionContext = mock(ConditionContext.class); } @Test void test_emptyUrl_enabled() { MockEnvironment environment = new MockEnvironment(); BDDMockito.given(conditionContext.getEnvironment()).willReturn(environment); assertThat(condition.getMatchOutcome(conditionContext, annotatedTypeMetadata).isMatch()).isFalse(); } @Test void test_emptyUrl_disabled() { MockEnvironment environment = new MockEnvironment(); environment.setProperty("spring.boot.admin.client.enabled", "false"); BDDMockito.given(conditionContext.getEnvironment()).willReturn(environment); assertThat(condition.getMatchOutcome(conditionContext, annotatedTypeMetadata).isMatch()).isFalse(); } @Test void test_nonEmptyUrl_disabled() { MockEnvironment environment = new MockEnvironment(); environment.setProperty("spring.boot.admin.client.enabled", "false"); environment.setProperty("spring.boot.admin.client.url", "http://localhost:8080/management"); BDDMockito.given(conditionContext.getEnvironment()).willReturn(environment); assertThat(condition.getMatchOutcome(conditionContext, annotatedTypeMetadata).isMatch()).isFalse(); } @Test void test_nonEmptyUrl_enabled() { MockEnvironment environment = new MockEnvironment(); environment.setProperty("spring.boot.admin.client.url", "http://localhost:8080/management"); BDDMockito.given(conditionContext.getEnvironment()).willReturn(environment); assertThat(condition.getMatchOutcome(conditionContext, annotatedTypeMetadata).isMatch()).isTrue(); } } ================================================ FILE: spring-boot-admin-client/src/test/java/de/codecentric/boot/admin/client/config/SpringBootAdminClientRegistrationClientAutoConfigurationTest.java ================================================ /* * 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 * * 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.config; import java.util.function.Function; import java.util.stream.Stream; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; 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.logging.ConditionEvaluationReportLoggingListener; import org.springframework.boot.http.client.ClientHttpRequestFactoryBuilder; import org.springframework.boot.test.context.runner.WebApplicationContextRunner; import org.springframework.boot.webmvc.autoconfigure.DispatcherServletAutoConfiguration; import org.springframework.web.client.RestClient; import de.codecentric.boot.admin.client.registration.RegistrationClient; import de.codecentric.boot.admin.client.registration.RestClientRegistrationClient; import static org.assertj.core.api.Assertions.assertThat; public class SpringBootAdminClientRegistrationClientAutoConfigurationTest { @ParameterizedTest(name = "{0}") @MethodSource("contextRunnerCustomizations") void autoConfiguresRegistrationClient(String testCaseName, Function customizer, Class expectedRegistrationClient) { WebApplicationContextRunner webApplicationContextRunner = new WebApplicationContextRunner() .withConfiguration( AutoConfigurations.of(EndpointAutoConfiguration.class, WebEndpointAutoConfiguration.class, DispatcherServletAutoConfiguration.class, SpringBootAdminClientAutoConfiguration.class)) .with(customizer) .withInitializer(new ConditionEvaluationReportLoggingListener()); webApplicationContextRunner.withPropertyValues("spring.boot.admin.client.url:http://localhost:8081") .run((context) -> { RegistrationClient registrationClient = context.getBean(RegistrationClient.class); assertThat(registrationClient).isInstanceOf(expectedRegistrationClient); }); } public static Stream contextRunnerCustomizations() { return Stream.of(// Arguments.of(// "RestClientBuilder with ClientHttpRequestFactoryBuilder", // customizer() // .withRestClientBuilder() .withClientHttpRequestFactoryBuilder() .build(), // RestClientRegistrationClient.class), Arguments.of(// "RestClientBuilder only", // customizer() // .withRestClientBuilder() .build(), // RestClientRegistrationClient.class) // ); } private static ContextRunnerCustomizerBuilder customizer() { return new ContextRunnerCustomizerBuilder(); } private static final class ContextRunnerCustomizerBuilder { private Function customizer = (runner) -> runner; ContextRunnerCustomizerBuilder withRestClientBuilder() { customizer = customizer.andThen((runner) -> runner.withBean(RestClient.Builder.class, RestClient::builder)); return this; } ContextRunnerCustomizerBuilder withClientHttpRequestFactoryBuilder() { customizer = customizer.andThen((runner) -> runner.withBean(ClientHttpRequestFactoryBuilder.class, ClientHttpRequestFactoryBuilder::detect)); return this; } Function build() { return customizer; } } } ================================================ FILE: spring-boot-admin-client/src/test/java/de/codecentric/boot/admin/client/registration/AbstractRegistrationClientTest.java ================================================ /* * 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 com.github.tomakehurst.wiremock.WireMockServer; import com.github.tomakehurst.wiremock.client.ResponseDefinitionBuilder; import com.github.tomakehurst.wiremock.common.ConsoleNotifier; import com.github.tomakehurst.wiremock.matching.RequestPatternBuilder; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import static com.github.tomakehurst.wiremock.client.WireMock.created; import static com.github.tomakehurst.wiremock.client.WireMock.delete; import static com.github.tomakehurst.wiremock.client.WireMock.deleteRequestedFor; import static com.github.tomakehurst.wiremock.client.WireMock.equalTo; import static com.github.tomakehurst.wiremock.client.WireMock.ok; import static com.github.tomakehurst.wiremock.client.WireMock.post; import static com.github.tomakehurst.wiremock.client.WireMock.postRequestedFor; import static com.github.tomakehurst.wiremock.client.WireMock.serverError; import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo; import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.options; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; public abstract class AbstractRegistrationClientTest { private final WireMockServer wireMock = new WireMockServer( options().dynamicPort().notifier(new ConsoleNotifier(true))); private final Application application = Application.create("AppName") .managementUrl("http://localhost:8080/mgmt") .healthUrl("http://localhost:8080/health") .serviceUrl("http://localhost:8080") .build(); private RegistrationClient registrationClient; public void setUp(RegistrationClient registrationClient) { this.registrationClient = registrationClient; } @BeforeEach void setUpWiremock() { wireMock.start(); } @AfterEach void tearDown() { wireMock.stop(); } @Test public void register_should_return_id_when_successful() { ResponseDefinitionBuilder response = created().withHeader("Content-Type", "application/json") .withHeader("Location", this.wireMock.url("/instances/abcdef")) .withBody("{ \"id\" : \"-id-\" }"); this.wireMock.stubFor(post(urlEqualTo("/instances")).willReturn(response)); assertThat(this.registrationClient.register(this.wireMock.url("/instances"), this.application)) .isEqualTo("-id-"); RequestPatternBuilder expectedRequest = postRequestedFor(urlEqualTo("/instances")) .withHeader("Accept", equalTo("application/json")) .withHeader("Content-Type", equalTo("application/json")); this.wireMock.verify(expectedRequest); } @Test public void register_should_throw() { this.wireMock.stubFor(post(urlEqualTo("/instances")).willReturn(serverError())); assertThatThrownBy(() -> this.registrationClient.register(this.wireMock.url("/instances"), this.application)) .isInstanceOf(Exception.class); } @Test public void deregister() { this.wireMock.stubFor(delete(urlEqualTo("/instances/-id-")).willReturn(ok())); this.registrationClient.deregister(this.wireMock.url("/instances"), "-id-"); this.wireMock.verify(deleteRequestedFor(urlEqualTo("/instances/-id-"))); } @Test public void deregister_should_trow() { this.wireMock.stubFor(delete(urlEqualTo("/instances/-id-")).willReturn(serverError())); assertThatThrownBy(() -> this.registrationClient.deregister(this.wireMock.url("/instances"), "-id-")) .isInstanceOf(Exception.class); } } ================================================ FILE: spring-boot-admin-client/src/test/java/de/codecentric/boot/admin/client/registration/ApplicationTest.java ================================================ /* * 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 com.jayway.jsonpath.DocumentContext; import com.jayway.jsonpath.JsonPath; import org.junit.jupiter.api.Test; import tools.jackson.databind.json.JsonMapper; import static org.assertj.core.api.Assertions.assertThat; class ApplicationTest { @Test void test_json_format() { JsonMapper jsonMapper = JsonMapper.builder().build(); Application app = Application.create("test") .healthUrl("http://health") .serviceUrl("http://service") .managementUrl("http://management") .build(); DocumentContext json = JsonPath.parse(jsonMapper.writeValueAsString(app)); assertThat((String) json.read("$.name")).isEqualTo("test"); assertThat((String) json.read("$.serviceUrl")).isEqualTo("http://service"); assertThat((String) json.read("$.managementUrl")).isEqualTo("http://management"); assertThat((String) json.read("$.healthUrl")).isEqualTo("http://health"); } @Test void test_equals_hashCode() { Application a1 = Application.create("foo") .healthUrl("healthUrl") .managementUrl("mgmt") .serviceUrl("svc") .build(); Application a2 = Application.create("foo") .healthUrl("healthUrl") .managementUrl("mgmt") .serviceUrl("svc") .build(); assertThat(a1).isEqualTo(a2).hasSameHashCodeAs(a2); Application a3 = Application.create("foo") .healthUrl("healthUrl2") .managementUrl("mgmt") .serviceUrl("svc") .build(); assertThat(a1).isNotEqualTo(a3); assertThat(a2).isNotEqualTo(a3); } @Test void should_not_return_sensitive_data_in_toString() { Application application = Application.create("app").healthUrl("HEALTH").metadata("password", "geheim").build(); assertThat(application.toString()).doesNotContain("geheim"); } } ================================================ FILE: spring-boot-admin-client/src/test/java/de/codecentric/boot/admin/client/registration/CloudFoundryApplicationFactoryTest.java ================================================ /* * 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.assertj.core.api.SoftAssertions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; 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.web.server.autoconfigure.ServerProperties; import de.codecentric.boot.admin.client.config.CloudFoundryApplicationProperties; import de.codecentric.boot.admin.client.config.InstanceProperties; import static java.util.Collections.singletonList; import static java.util.Collections.singletonMap; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; class CloudFoundryApplicationFactoryTest { private final InstanceProperties instanceProperties = new InstanceProperties(); private final ServerProperties server = new ServerProperties(); private final ManagementServerProperties management = new ManagementServerProperties(); private final PathMappedEndpoints pathMappedEndpoints = mock(PathMappedEndpoints.class); private final WebEndpointProperties webEndpoint = new WebEndpointProperties(); private final CloudFoundryApplicationProperties cfApplicationProperties = new CloudFoundryApplicationProperties(); private final CloudFoundryApplicationFactory factory = new CloudFoundryApplicationFactory(this.instanceProperties, this.management, this.server, this.pathMappedEndpoints, this.webEndpoint, () -> singletonMap("contributor", "test"), this.cfApplicationProperties); @BeforeEach void setup() { this.instanceProperties.setName("test"); } @Test void should_use_application_uri() { when(this.pathMappedEndpoints.getPath(EndpointId.of("health"))).thenReturn("/actuator/health"); this.cfApplicationProperties.setUris(singletonList("application/Uppercase")); Application app = this.factory.createApplication(); SoftAssertions softly = new SoftAssertions(); softly.assertThat(app.getManagementUrl()).isEqualTo("http://application/Uppercase/actuator"); softly.assertThat(app.getHealthUrl()).isEqualTo("http://application/Uppercase/actuator/health"); softly.assertThat(app.getServiceUrl()).isEqualTo("http://application/Uppercase/"); softly.assertAll(); } @Test void should_use_service_base_uri() { when(this.pathMappedEndpoints.getPath(EndpointId.of("health"))).thenReturn("/actuator/health"); this.cfApplicationProperties.setUris(singletonList("application/Uppercase")); this.instanceProperties.setServiceBaseUrl("https://serviceBaseUrl"); Application app = this.factory.createApplication(); SoftAssertions softly = new SoftAssertions(); softly.assertThat(app.getManagementUrl()).isEqualTo("https://serviceBaseUrl/actuator"); softly.assertThat(app.getHealthUrl()).isEqualTo("https://serviceBaseUrl/actuator/health"); softly.assertThat(app.getServiceUrl()).isEqualTo("https://serviceBaseUrl/"); softly.assertAll(); } } ================================================ FILE: spring-boot-admin-client/src/test/java/de/codecentric/boot/admin/client/registration/DefaultApplicationFactoryTest.java ================================================ /* * 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.net.InetAddress; import java.net.UnknownHostException; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; 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.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; import static java.util.Collections.singletonMap; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.assertj.core.api.Assertions.entry; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; class DefaultApplicationFactoryTest { private final InstanceProperties instanceProperties = new InstanceProperties(); private final ServerProperties server = new ServerProperties(); private final ManagementServerProperties management = new ManagementServerProperties(); private final PathMappedEndpoints pathMappedEndpoints = mock(PathMappedEndpoints.class); private final WebEndpointProperties webEndpoint = new WebEndpointProperties(); private final DefaultApplicationFactory factory = new DefaultApplicationFactory(instanceProperties, management, server, pathMappedEndpoints, webEndpoint, () -> singletonMap("contributor", "test")); @BeforeEach void setup() { instanceProperties.setName("test"); } @Test void test_mgmtPortPath() { webEndpoint.setBasePath("/admin"); when(pathMappedEndpoints.getPath(EndpointId.of("health"))).thenReturn("/admin/alive"); publishApplicationReadyEvent(factory, 8080, 8081); Application app = factory.createApplication(); assertThat(app.getManagementUrl()).isEqualTo("http://" + getHostname() + ":8081/admin"); assertThat(app.getHealthUrl()).isEqualTo("http://" + getHostname() + ":8081/admin/alive"); assertThat(app.getServiceUrl()).isEqualTo("http://" + getHostname() + ":8080/"); } @Test void test_default() { instanceProperties.setMetadata(singletonMap("instance", "test")); when(pathMappedEndpoints.getPath(EndpointId.of("health"))).thenReturn("/actuator/health"); publishApplicationReadyEvent(factory, 8080, null); Application app = factory.createApplication(); assertThat(app.getManagementUrl()).isEqualTo("http://" + getHostname() + ":8080/actuator"); assertThat(app.getHealthUrl()).isEqualTo("http://" + getHostname() + ":8080/actuator/health"); assertThat(app.getServiceUrl()).isEqualTo("http://" + getHostname() + ":8080/"); assertThat(app.getMetadata()).containsExactly(entry("contributor", "test"), entry("instance", "test")); } @Test void test_ssl() { server.setSsl(new Ssl()); server.getSsl().setEnabled(true); when(pathMappedEndpoints.getPath(EndpointId.of("health"))).thenReturn("/actuator/health"); publishApplicationReadyEvent(factory, 8080, null); Application app = factory.createApplication(); assertThat(app.getManagementUrl()).isEqualTo("https://" + getHostname() + ":8080/actuator"); assertThat(app.getHealthUrl()).isEqualTo("https://" + getHostname() + ":8080/actuator/health"); assertThat(app.getServiceUrl()).isEqualTo("https://" + getHostname() + ":8080/"); } @Test void test_ssl_management() { management.setSsl(new Ssl()); management.getSsl().setEnabled(true); when(pathMappedEndpoints.getPath(EndpointId.of("health"))).thenReturn("/actuator/alive"); publishApplicationReadyEvent(factory, 8080, 9090); Application app = factory.createApplication(); assertThat(app.getManagementUrl()).isEqualTo("https://" + getHostname() + ":9090/actuator"); assertThat(app.getHealthUrl()).isEqualTo("https://" + getHostname() + ":9090/actuator/alive"); assertThat(app.getServiceUrl()).isEqualTo("http://" + getHostname() + ":8080/"); } @Test void test_preferIpAddress_server_address_missing() { instanceProperties.setPreferIp(true); when(pathMappedEndpoints.getPath(EndpointId.of("health"))).thenReturn("/application/alive"); publishApplicationReadyEvent(factory, 8080, null); Application app = factory.createApplication(); assertThat(app.getServiceUrl()).matches("http://\\d{0,3}\\.\\d{0,3}\\.\\d{0,3}\\.\\d{0,3}:8080/"); } @Test void test_preferIpAddress_management_address_missing() { instanceProperties.setPreferIp(true); when(pathMappedEndpoints.getPath(EndpointId.of("health"))).thenReturn("/application/alive"); publishApplicationReadyEvent(factory, 8080, 8081); Application app = factory.createApplication(); assertThat(app.getManagementUrl()).matches("http://\\d{0,3}\\.\\d{0,3}\\.\\d{0,3}\\.\\d{0,3}:8081/actuator"); } @Test void test_preferIpAddress() throws UnknownHostException { instanceProperties.setPreferIp(true); server.setAddress(InetAddress.getByName("127.0.0.1")); management.setAddress(InetAddress.getByName("127.0.0.2")); when(pathMappedEndpoints.getPath(EndpointId.of("health"))).thenReturn("/actuator/health"); publishApplicationReadyEvent(factory, 8080, 8081); Application app = factory.createApplication(); assertThat(app.getManagementUrl()).isEqualTo("http://127.0.0.2:8081/actuator"); assertThat(app.getHealthUrl()).isEqualTo("http://127.0.0.2:8081/actuator/health"); assertThat(app.getServiceUrl()).isEqualTo("http://127.0.0.1:8080/"); } @Test void test_all_custom() { instanceProperties.setHealthUrl("http://health"); instanceProperties.setManagementUrl("http://management"); instanceProperties.setServiceUrl("http://service"); Application app = factory.createApplication(); assertThat(app.getServiceUrl()).isEqualTo("http://service"); assertThat(app.getManagementUrl()).isEqualTo("http://management"); assertThat(app.getHealthUrl()).isEqualTo("http://health"); } @Test void test_all_baseUrls() { instanceProperties.setManagementBaseUrl("http://management:8090"); instanceProperties.setServiceBaseUrl("http://service:80"); webEndpoint.setBasePath("/admin"); when(pathMappedEndpoints.getPath(EndpointId.of("health"))).thenReturn("/admin/health"); Application app = factory.createApplication(); assertThat(app.getServiceUrl()).isEqualTo("http://service:80/"); assertThat(app.getManagementUrl()).isEqualTo("http://management:8090/admin"); assertThat(app.getHealthUrl()).isEqualTo("http://management:8090/admin/health"); } @Test void test_service_baseUrl() { instanceProperties.setServiceBaseUrl("http://service:80"); webEndpoint.setBasePath("/admin"); when(pathMappedEndpoints.getPath(EndpointId.of("health"))).thenReturn("/admin/health"); Application app = factory.createApplication(); assertThat(app.getServiceUrl()).isEqualTo("http://service:80/"); assertThat(app.getManagementUrl()).isEqualTo("http://service:80/admin"); assertThat(app.getHealthUrl()).isEqualTo("http://service:80/admin/health"); } @Test void test_missing_ports() { assertThatThrownBy(factory::createApplication).isInstanceOf(IllegalStateException.class) .hasMessageContaining("service-base-url"); } @Test void test_service_path() { instanceProperties.setServiceBaseUrl("http://service:80"); instanceProperties.setServicePath("app"); webEndpoint.setBasePath("/admin"); when(pathMappedEndpoints.getPath(EndpointId.of("health"))).thenReturn("/admin/health"); Application app = factory.createApplication(); assertThat(app.getServiceUrl()).isEqualTo("http://service:80/app"); assertThat(app.getManagementUrl()).isEqualTo("http://service:80/app/admin"); assertThat(app.getHealthUrl()).isEqualTo("http://service:80/app/admin/health"); } @Test void test_service_path_default() { assertThat(factory.getServicePath()).isEqualTo("/"); } private String getHostname() { try { return InetAddress.getLocalHost().getCanonicalHostName(); } catch (UnknownHostException ex) { throw new IllegalStateException(ex); } } private void publishApplicationReadyEvent(DefaultApplicationFactory factory, Integer serverport, Integer managementport) { factory.onWebServerInitialized(new TestWebServerInitializedEvent("server", serverport)); factory.onWebServerInitialized(new TestWebServerInitializedEvent("management", (managementport != null) ? managementport : serverport)); } private static final class TestWebServerInitializedEvent extends WebServerInitializedEvent { private final WebServer server = mock(WebServer.class); private final WebServerApplicationContext context = mock(WebServerApplicationContext.class); private TestWebServerInitializedEvent(String name, int port) { super(mock(WebServer.class)); when(server.getPort()).thenReturn(port); when(context.getServerNamespace()).thenReturn(name); } @Override public WebServerApplicationContext getApplicationContext() { return context; } @Override public WebServer getWebServer() { return this.server; } } } ================================================ FILE: spring-boot-admin-client/src/test/java/de/codecentric/boot/admin/client/registration/DefaultApplicationRegistratorTest.java ================================================ /* * 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.Test; import org.springframework.web.client.RestClientException; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; class DefaultApplicationRegistratorTest { private final Application application = Application.create("AppName") .managementUrl("http://localhost:8080/mgmt") .healthUrl("http://localhost:8080/health") .serviceUrl("http://localhost:8080") .build(); private final RegistrationClient registrationClient = mock(RegistrationClient.class); @Test void register_should_return_true_when_successful() { ApplicationRegistrator registrator = new DefaultApplicationRegistrator(() -> this.application, this.registrationClient, new String[] { "http://sba:8080/instances", "http://sba2:8080/instances" }, true); when(this.registrationClient.register(any(), eq(this.application))).thenReturn("-id-"); assertThat(registrator.register()).isTrue(); assertThat(registrator.getRegisteredId()).isEqualTo("-id-"); } @Test void register_should_return_false_when_failed() { ApplicationRegistrator registrator = new DefaultApplicationRegistrator(() -> this.application, this.registrationClient, new String[] { "http://sba:8080/instances", "http://sba2:8080/instances" }, true); when(this.registrationClient.register(any(), eq(this.application))).thenThrow(new RestClientException("Error")); assertThat(registrator.register()).isFalse(); assertThat(registrator.register()).isFalse(); assertThat(registrator.getRegisteredId()).isNull(); } @Test void register_should_try_next_on_error() { ApplicationRegistrator registrator = new DefaultApplicationRegistrator(() -> this.application, this.registrationClient, new String[] { "http://sba:8080/instances", "http://sba2:8080/instances" }, true); when(this.registrationClient.register("http://sba:8080/instances", this.application)) .thenThrow(new RestClientException("Error")); when(this.registrationClient.register("http://sba2:8080/instances", this.application)).thenReturn("-id-"); assertThat(registrator.register()).isTrue(); assertThat(registrator.getRegisteredId()).isEqualTo("-id-"); } @Test void deregister_should_deregister_at_server() { ApplicationRegistrator registrator = new DefaultApplicationRegistrator(() -> this.application, this.registrationClient, new String[] { "http://sba:8080/instances", "http://sba2:8080/instances" }, true); when(this.registrationClient.register(any(), eq(this.application))).thenReturn("-id-"); registrator.register(); registrator.deregister(); assertThat(registrator.getRegisteredId()).isNull(); verify(this.registrationClient).deregister("http://sba:8080/instances", "-id-"); } @Test void deregister_should_not_deregister_when_not_registered() { ApplicationRegistrator registrator = new DefaultApplicationRegistrator(() -> this.application, this.registrationClient, new String[] { "http://sba:8080/instances", "http://sba2:8080/instances" }, true); registrator.deregister(); verify(this.registrationClient, never()).deregister(any(), any()); } @Test void deregister_should_try_next_on_error() { ApplicationRegistrator registrator = new DefaultApplicationRegistrator(() -> this.application, this.registrationClient, new String[] { "http://sba:8080/instances", "http://sba2:8080/instances" }, true); when(this.registrationClient.register(any(), eq(this.application))).thenReturn("-id-"); doThrow(new RestClientException("Error")).when(this.registrationClient) .deregister("http://sba:8080/instances", "-id-"); registrator.register(); registrator.deregister(); assertThat(registrator.getRegisteredId()).isNull(); verify(this.registrationClient).deregister("http://sba:8080/instances", "-id-"); verify(this.registrationClient).deregister("http://sba2:8080/instances", "-id-"); } @Test void register_should_register_on_multiple_servers() { ApplicationRegistrator registrator = new DefaultApplicationRegistrator(() -> this.application, this.registrationClient, new String[] { "http://sba:8080/instances", "http://sba2:8080/instances" }, false); when(this.registrationClient.register(any(), eq(this.application))).thenReturn("-id-"); assertThat(registrator.register()).isTrue(); assertThat(registrator.getRegisteredId()).isEqualTo("-id-"); verify(this.registrationClient).register("http://sba:8080/instances", this.application); verify(this.registrationClient).register("http://sba2:8080/instances", this.application); } @Test void deregister_should_deregister_on_multiple_servers() { ApplicationRegistrator registrator = new DefaultApplicationRegistrator(() -> this.application, this.registrationClient, new String[] { "http://sba:8080/instances", "http://sba2:8080/instances" }, false); when(this.registrationClient.register(any(), eq(this.application))).thenReturn("-id-"); registrator.register(); registrator.deregister(); assertThat(registrator.getRegisteredId()).isNull(); verify(this.registrationClient).deregister("http://sba:8080/instances", "-id-"); verify(this.registrationClient).deregister("http://sba2:8080/instances", "-id-"); } } ================================================ FILE: spring-boot-admin-client/src/test/java/de/codecentric/boot/admin/client/registration/ReactiveApplicationFactoryTest.java ================================================ /* * 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.net.InetAddress; import java.net.UnknownHostException; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; 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.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; import static java.util.Collections.singletonMap; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; class ReactiveApplicationFactoryTest { private final InstanceProperties instanceProperties = new InstanceProperties(); private final ServerProperties server = new ServerProperties(); private final ManagementServerProperties management = new ManagementServerProperties(); private final PathMappedEndpoints pathMappedEndpoints = mock(PathMappedEndpoints.class); private final WebEndpointProperties webEndpoint = new WebEndpointProperties(); private final WebFluxProperties webflux = new WebFluxProperties(); private final ReactiveApplicationFactory factory = new ReactiveApplicationFactory(instanceProperties, management, server, pathMappedEndpoints, webEndpoint, () -> singletonMap("contributor", "test"), webflux); @BeforeEach void setup() { instanceProperties.setName("test"); } @Test void test_contextPath_mgmtPath() { webflux.setBasePath("/app"); webEndpoint.setBasePath("/admin"); when(pathMappedEndpoints.getPath(EndpointId.of("health"))).thenReturn("/admin/health"); publishApplicationReadyEvent(factory, 8080, null); Application app = factory.createApplication(); assertThat(app.getManagementUrl()).isEqualTo("http://" + getHostname() + ":8080/app/admin"); assertThat(app.getHealthUrl()).isEqualTo("http://" + getHostname() + ":8080/app/admin/health"); assertThat(app.getServiceUrl()).isEqualTo("http://" + getHostname() + ":8080/app"); } @Test void test_contextPath_mgmtPortPath() { webflux.setBasePath("/app"); webEndpoint.setBasePath("/admin"); when(pathMappedEndpoints.getPath(EndpointId.of("health"))).thenReturn("/admin/health"); publishApplicationReadyEvent(factory, 8080, 8081); Application app = factory.createApplication(); assertThat(app.getManagementUrl()).isEqualTo("http://" + getHostname() + ":8081/admin"); assertThat(app.getHealthUrl()).isEqualTo("http://" + getHostname() + ":8081/admin/health"); assertThat(app.getServiceUrl()).isEqualTo("http://" + getHostname() + ":8080/app"); } @Test void test_basePath() { webflux.setBasePath("/app"); when(pathMappedEndpoints.getPath(EndpointId.of("health"))).thenReturn("/actuator/health"); publishApplicationReadyEvent(factory, 80, null); Application app = factory.createApplication(); assertThat(app.getManagementUrl()).isEqualTo("http://" + getHostname() + ":80/app/actuator"); assertThat(app.getHealthUrl()).isEqualTo("http://" + getHostname() + ":80/app/actuator/health"); assertThat(app.getServiceUrl()).isEqualTo("http://" + getHostname() + ":80/app"); } @Test void test_noBasePath() { when(pathMappedEndpoints.getPath(EndpointId.of("health"))).thenReturn("/actuator/health"); publishApplicationReadyEvent(factory, 80, null); Application app = factory.createApplication(); assertThat(app.getManagementUrl()).isEqualTo("http://" + getHostname() + ":80/actuator"); assertThat(app.getHealthUrl()).isEqualTo("http://" + getHostname() + ":80/actuator/health"); assertThat(app.getServiceUrl()).isEqualTo("http://" + getHostname() + ":80/"); } @Test void test_mgmtBasePath_mgmtPortPath() { webflux.setBasePath("/app"); management.setBasePath("/mgnt"); when(pathMappedEndpoints.getPath(EndpointId.of("health"))).thenReturn("/actuator/health"); publishApplicationReadyEvent(factory, 8080, 8081); Application app = factory.createApplication(); assertThat(app.getManagementUrl()).isEqualTo("http://" + getHostname() + ":8081/mgnt/actuator"); assertThat(app.getHealthUrl()).isEqualTo("http://" + getHostname() + ":8081/mgnt/actuator/health"); assertThat(app.getServiceUrl()).isEqualTo("http://" + getHostname() + ":8080/app"); } private String getHostname() { try { return InetAddress.getLocalHost().getCanonicalHostName(); } catch (UnknownHostException ex) { throw new IllegalStateException(ex); } } private void publishApplicationReadyEvent(DefaultApplicationFactory factory, Integer serverport, Integer managementport) { factory.onWebServerInitialized(new TestWebServerInitializedEvent("server", serverport)); factory.onWebServerInitialized(new TestWebServerInitializedEvent("management", (managementport != null) ? managementport : serverport)); } private static final class TestWebServerInitializedEvent extends WebServerInitializedEvent { private final WebServer server = mock(WebServer.class); private final WebServerApplicationContext context = mock(WebServerApplicationContext.class); private TestWebServerInitializedEvent(String name, int port) { super(mock(WebServer.class)); when(server.getPort()).thenReturn(port); when(context.getServerNamespace()).thenReturn(name); } @Override public WebServerApplicationContext getApplicationContext() { return context; } @Override public WebServer getWebServer() { return this.server; } } } ================================================ FILE: spring-boot-admin-client/src/test/java/de/codecentric/boot/admin/client/registration/RegistrationApplicationListenerTest.java ================================================ /* * 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.concurrent.ScheduledFuture; import org.junit.jupiter.api.Test; import org.springframework.boot.SpringApplication; import org.springframework.boot.context.event.ApplicationReadyEvent; import org.springframework.context.ApplicationContext; import org.springframework.context.event.ContextClosedEvent; import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler; import org.springframework.web.context.ConfigurableWebApplicationContext; import org.springframework.web.context.WebApplicationContext; import static java.time.Duration.ZERO; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.ArgumentMatchers.isA; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; class RegistrationApplicationListenerTest { @Test void should_schedule_register_task() { ApplicationRegistrator registrator = mock(ApplicationRegistrator.class); ThreadPoolTaskScheduler scheduler = mock(ThreadPoolTaskScheduler.class); RegistrationApplicationListener listener = new RegistrationApplicationListener(registrator, scheduler); listener.onApplicationReady(new ApplicationReadyEvent(mock(SpringApplication.class), null, mock(ConfigurableWebApplicationContext.class), ZERO)); verify(scheduler).scheduleAtFixedRate(isA(Runnable.class), eq(Duration.ofSeconds(10))); } @Test void should_no_schedule_register_task_when_not_autoRegister() { ApplicationRegistrator registrator = mock(ApplicationRegistrator.class); ThreadPoolTaskScheduler scheduler = mock(ThreadPoolTaskScheduler.class); RegistrationApplicationListener listener = new RegistrationApplicationListener(registrator, scheduler); listener.setAutoRegister(false); listener.onApplicationReady(new ApplicationReadyEvent(mock(SpringApplication.class), null, mock(ConfigurableWebApplicationContext.class), ZERO)); verify(scheduler, never()).scheduleAtFixedRate(isA(Runnable.class), eq(Duration.ofSeconds(10))); } @Test void should_cancel_register_task_on_context_close() { ApplicationRegistrator registrator = mock(ApplicationRegistrator.class); ThreadPoolTaskScheduler scheduler = mock(ThreadPoolTaskScheduler.class); RegistrationApplicationListener listener = new RegistrationApplicationListener(registrator, scheduler); ScheduledFuture task = mock(ScheduledFuture.class); when(scheduler.scheduleAtFixedRate(isA(Runnable.class), eq(Duration.ofSeconds(10)))).then((invocation) -> task); listener.onApplicationReady(new ApplicationReadyEvent(mock(SpringApplication.class), null, mock(ConfigurableWebApplicationContext.class), ZERO)); verify(scheduler).scheduleAtFixedRate(isA(Runnable.class), eq(Duration.ofSeconds(10))); listener.onClosedContext(new ContextClosedEvent(mock(WebApplicationContext.class))); verify(task).cancel(true); } @Test void should_start_and_cancel_task_on_request() { ApplicationRegistrator registrator = mock(ApplicationRegistrator.class); ThreadPoolTaskScheduler scheduler = mock(ThreadPoolTaskScheduler.class); RegistrationApplicationListener listener = new RegistrationApplicationListener(registrator, scheduler); ScheduledFuture task = mock(ScheduledFuture.class); when(scheduler.scheduleAtFixedRate(isA(Runnable.class), eq(Duration.ofSeconds(10)))).then((invocation) -> task); listener.startRegisterTask(); verify(scheduler).scheduleAtFixedRate(isA(Runnable.class), eq(Duration.ofSeconds(10))); listener.stopRegisterTask(); verify(task).cancel(true); } @Test void should_not_deregister_when_not_autoDeregister() { ApplicationRegistrator registrator = mock(ApplicationRegistrator.class); ThreadPoolTaskScheduler scheduler = mock(ThreadPoolTaskScheduler.class); RegistrationApplicationListener listener = new RegistrationApplicationListener(registrator, scheduler); listener.onClosedContext(new ContextClosedEvent(mock(WebApplicationContext.class))); verify(registrator, never()).deregister(); } @Test void should_deregister_when_autoDeregister() { ApplicationRegistrator registrator = mock(ApplicationRegistrator.class); ThreadPoolTaskScheduler scheduler = mock(ThreadPoolTaskScheduler.class); RegistrationApplicationListener listener = new RegistrationApplicationListener(registrator, scheduler); listener.setAutoDeregister(true); listener.onClosedContext(new ContextClosedEvent(mock(ApplicationContext.class))); verify(registrator).deregister(); } @Test void should_deregister_when_autoDeregister_and_parent_is_bootstrap_context() { ApplicationRegistrator registrator = mock(ApplicationRegistrator.class); ThreadPoolTaskScheduler scheduler = mock(ThreadPoolTaskScheduler.class); RegistrationApplicationListener listener = new RegistrationApplicationListener(registrator, scheduler); listener.setAutoDeregister(true); ApplicationContext parentContext = mock(ApplicationContext.class); when(parentContext.getId()).thenReturn("bootstrap"); ApplicationContext mockContext = mock(ApplicationContext.class); when(mockContext.getParent()).thenReturn(parentContext); listener.onClosedContext(new ContextClosedEvent(mockContext)); verify(registrator).deregister(); } @Test void should_not_deregister_when_autoDeregister_and_not_root() { ApplicationRegistrator registrator = mock(ApplicationRegistrator.class); ThreadPoolTaskScheduler scheduler = mock(ThreadPoolTaskScheduler.class); RegistrationApplicationListener listener = new RegistrationApplicationListener(registrator, scheduler); listener.setAutoDeregister(true); ApplicationContext mockContext = mock(ApplicationContext.class); when(mockContext.getParent()).thenReturn(mock(ApplicationContext.class)); listener.onClosedContext(new ContextClosedEvent(mockContext)); verify(registrator, never()).deregister(); } @Test void should_init_and_shutdown_taskScheduler() { ApplicationRegistrator registrator = mock(ApplicationRegistrator.class); ThreadPoolTaskScheduler scheduler = mock(ThreadPoolTaskScheduler.class); RegistrationApplicationListener listener = new RegistrationApplicationListener(registrator, scheduler); listener.afterPropertiesSet(); verify(scheduler, times(1)).afterPropertiesSet(); listener.destroy(); verify(scheduler, times(1)).destroy(); } } ================================================ FILE: spring-boot-admin-client/src/test/java/de/codecentric/boot/admin/client/registration/RestClientRegistrationClientTest.java ================================================ /* * Copyright 2014-2024 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.RestClient; class RestClientRegistrationClientTest extends AbstractRegistrationClientTest { @BeforeEach void setUp() { super.setUp(new RestClientRegistrationClient(RestClient.create())); } } ================================================ FILE: spring-boot-admin-client/src/test/java/de/codecentric/boot/admin/client/registration/ServletApplicationFactoryTest.java ================================================ /* * 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.net.InetAddress; import java.net.UnknownHostException; import java.util.Collections; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; 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.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; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; class ServletApplicationFactoryTest { private final InstanceProperties instance = new InstanceProperties(); private final ServerProperties server = new ServerProperties(); private final ManagementServerProperties management = new ManagementServerProperties(); private final MockServletContext servletContext = new MockServletContext(); private final PathMappedEndpoints pathMappedEndpoints = mock(PathMappedEndpoints.class); private final WebEndpointProperties webEndpoint = new WebEndpointProperties(); private final DispatcherServletPath dispatcherServletPath = mock(DispatcherServletPath.class); private final ServletApplicationFactory factory = new ServletApplicationFactory(instance, management, server, servletContext, pathMappedEndpoints, webEndpoint, Collections::emptyMap, dispatcherServletPath); @BeforeEach void setup() { instance.setName("test"); when(dispatcherServletPath.getPrefix()).thenReturn(""); } @Test void test_contextPath_mgmtPath() { servletContext.setContextPath("app"); webEndpoint.setBasePath("/admin"); when(pathMappedEndpoints.getPath(EndpointId.of("health"))).thenReturn("/admin/health"); publishApplicationReadyEvent(factory, 8080, null); Application app = factory.createApplication(); assertThat(app.getManagementUrl()).isEqualTo("http://" + getHostname() + ":8080/app/admin"); assertThat(app.getHealthUrl()).isEqualTo("http://" + getHostname() + ":8080/app/admin/health"); assertThat(app.getServiceUrl()).isEqualTo("http://" + getHostname() + ":8080/app"); } @Test void test_contextPath_mgmtPortPath() { servletContext.setContextPath("app"); webEndpoint.setBasePath("/admin"); when(pathMappedEndpoints.getPath(EndpointId.of("health"))).thenReturn("/admin/health"); publishApplicationReadyEvent(factory, 8080, 8081); Application app = factory.createApplication(); assertThat(app.getManagementUrl()).isEqualTo("http://" + getHostname() + ":8081/admin"); assertThat(app.getHealthUrl()).isEqualTo("http://" + getHostname() + ":8081/admin/health"); assertThat(app.getServiceUrl()).isEqualTo("http://" + getHostname() + ":8080/app"); } @Test void test_contextPath() { servletContext.setContextPath("app"); when(pathMappedEndpoints.getPath(EndpointId.of("health"))).thenReturn("/actuator/health"); publishApplicationReadyEvent(factory, 80, null); Application app = factory.createApplication(); assertThat(app.getManagementUrl()).isEqualTo("http://" + getHostname() + ":80/app/actuator"); assertThat(app.getHealthUrl()).isEqualTo("http://" + getHostname() + ":80/app/actuator/health"); assertThat(app.getServiceUrl()).isEqualTo("http://" + getHostname() + ":80/app"); } @Test void test_servletPath() { when(dispatcherServletPath.getPrefix()).thenReturn("app"); servletContext.setContextPath("srv"); when(pathMappedEndpoints.getPath(EndpointId.of("health"))).thenReturn("/actuator/health"); publishApplicationReadyEvent(factory, 80, null); Application app = factory.createApplication(); assertThat(app.getManagementUrl()).isEqualTo("http://" + getHostname() + ":80/srv/app/actuator"); assertThat(app.getHealthUrl()).isEqualTo("http://" + getHostname() + ":80/srv/app/actuator/health"); assertThat(app.getServiceUrl()).isEqualTo("http://" + getHostname() + ":80/srv"); } @Test void test_servicePath() { servletContext.setContextPath("app"); when(pathMappedEndpoints.getPath(EndpointId.of("health"))).thenReturn("/actuator/health"); publishApplicationReadyEvent(factory, 80, null); instance.setServicePath("/servicePath/"); Application app = factory.createApplication(); assertThat(app.getManagementUrl()).isEqualTo("http://" + getHostname() + ":80/servicePath/app/actuator"); assertThat(app.getHealthUrl()).isEqualTo("http://" + getHostname() + ":80/servicePath/app/actuator/health"); assertThat(app.getServiceUrl()).isEqualTo("http://" + getHostname() + ":80/servicePath/app"); } private String getHostname() { try { return InetAddress.getLocalHost().getCanonicalHostName(); } catch (UnknownHostException ex) { throw new IllegalStateException(ex); } } private void publishApplicationReadyEvent(DefaultApplicationFactory factory, Integer serverport, Integer managementport) { factory.onWebServerInitialized(new TestWebServerInitializedEvent("server", serverport)); factory.onWebServerInitialized(new TestWebServerInitializedEvent("management", (managementport != null) ? managementport : serverport)); } private static final class TestWebServerInitializedEvent extends WebServerInitializedEvent { private final WebServer server = mock(WebServer.class); private final WebServerApplicationContext context = mock(WebServerApplicationContext.class); private TestWebServerInitializedEvent(String name, int port) { super(mock(WebServer.class)); when(server.getPort()).thenReturn(port); when(context.getServerNamespace()).thenReturn(name); } @Override public WebServerApplicationContext getApplicationContext() { return context; } @Override public WebServer getWebServer() { return this.server; } } } ================================================ FILE: spring-boot-admin-client/src/test/java/de/codecentric/boot/admin/client/registration/metadata/CloudFoundryMetadataContributorTest.java ================================================ /* * 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.metadata; import org.junit.jupiter.api.Test; import de.codecentric.boot.admin.client.config.CloudFoundryApplicationProperties; import static org.assertj.core.api.Assertions.assertThat; class CloudFoundryMetadataContributorTest { @Test void should_return_empty_metadata() { CloudFoundryMetadataContributor contributor = new CloudFoundryMetadataContributor( new CloudFoundryApplicationProperties()); assertThat(contributor.getMetadata()).isEmpty(); } @Test void should_return_metadata() { CloudFoundryApplicationProperties cfApplicationProperties = new CloudFoundryApplicationProperties(); cfApplicationProperties.setApplicationId("appId"); cfApplicationProperties.setInstanceIndex("1"); CloudFoundryMetadataContributor contributor = new CloudFoundryMetadataContributor(cfApplicationProperties); assertThat(contributor.getMetadata()).containsEntry("applicationId", "appId").containsEntry("instanceId", "1"); } } ================================================ FILE: spring-boot-admin-client/src/test/java/de/codecentric/boot/admin/client/registration/metadata/CompositeMetadataContributorTest.java ================================================ /* * 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.metadata; import java.util.Map; import org.junit.jupiter.api.Test; import static java.util.Arrays.asList; import static java.util.Collections.emptyList; import static java.util.Collections.singletonMap; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.entry; class CompositeMetadataContributorTest { @Test void should_merge_metadata() { CompositeMetadataContributor contributor = new CompositeMetadataContributor( asList(() -> singletonMap("a", "first"), () -> singletonMap("b", "second"), () -> singletonMap("b", "second-new"))); Map metadata = contributor.getMetadata(); assertThat(metadata).containsExactly(entry("a", "first"), entry("b", "second-new")); } @Test void should_return_empty_metadata() { CompositeMetadataContributor contributor = new CompositeMetadataContributor(emptyList()); Map metadata = contributor.getMetadata(); assertThat(metadata).isEmpty(); } } ================================================ FILE: spring-boot-admin-client/src/test/java/de/codecentric/boot/admin/client/registration/metadata/StartupDateMetadataContributorTest.java ================================================ /* * 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.metadata; import java.util.Map; import org.junit.jupiter.api.Test; import static org.assertj.core.api.Assertions.assertThat; class StartupDateMetadataContributorTest { @Test void should_return_startupdate() { StartupDateMetadataContributor contributor = new StartupDateMetadataContributor(); Map metadata = contributor.getMetadata(); assertThat(metadata).hasSize(1).hasEntrySatisfying("startup", (value) -> assertThat(value).isNotEmpty()); } } ================================================ FILE: spring-boot-admin-client/src/test/resources/application.yml ================================================ server: shutdown: immediate ================================================ FILE: spring-boot-admin-client/src/test/resources/junit-platform.properties ================================================ junit.jupiter.execution.timeout.test.method.default=1m ================================================ FILE: spring-boot-admin-client/src/test/resources/logback-test.xml ================================================ ================================================ FILE: spring-boot-admin-dependencies/pom.xml ================================================ 4.0.0 spring-boot-admin-dependencies pom Spring Boot Admin Dependencies Spring Boot Admin Dependencies de.codecentric spring-boot-admin ${revision} ../pom.xml de.codecentric spring-boot-admin-server ${revision} de.codecentric spring-boot-admin-server-ui ${revision} de.codecentric spring-boot-admin-client ${revision} de.codecentric spring-boot-admin-starter-client ${revision} de.codecentric spring-boot-admin-starter-server ${revision} org.codehaus.mojo flatten-maven-plugin false flatten process-resources flatten true bom remove remove resolve include-cloud !excludeSpringCloud de.codecentric spring-boot-admin-server-cloud ${revision} ================================================ FILE: spring-boot-admin-docs/pom.xml ================================================ 4.0.0 spring-boot-admin-docs pom Spring Boot Admin Docs Spring Boot Admin Docs de.codecentric spring-boot-admin-build ${revision} ../spring-boot-admin-build com.github.eirslett frontend-maven-plugin src/site production ${project.version} install-node-and-npm install-node-and-npm pre-site ${node.version} npm-install pre-site npm ci --prefer-offline --no-progress --no-audit --silent npm-build site npm run build:prod org.rodnansol spring-configuration-property-documenter-maven-plugin generate-adoc generate-and-aggregate-documents pre-site ADOC spring-boot-admin-server ../spring-boot-admin-server ${project.build.directory}/aggregated-adoc.adoc ================================================ FILE: spring-boot-admin-docs/src/site/.gitignore ================================================ # Dependencies /node_modules # Production /build # Generated files .docusaurus .cache-loader # Misc .DS_Store .env.local .env.development.local .env.test.local .env.production.local npm-debug.log* current/index.html current/404.html ================================================ FILE: spring-boot-admin-docs/src/site/README.md ================================================ # Website This website is built using [Docusaurus](https://docusaurus.io/), a modern static website generator. ### Installation ``` $ npm install ``` ### Local Development ``` $ npm run start ``` This command starts a local development server and opens up a browser window. Most changes are reflected live without having to restart the server. ### Build ``` $ npm run build ``` This command generates static content into the `build` directory and can be served using any static contents hosting service. ================================================ FILE: spring-boot-admin-docs/src/site/babel.config.js ================================================ module.exports = { presets: [require.resolve('@docusaurus/core/lib/babel/preset')], }; ================================================ FILE: spring-boot-admin-docs/src/site/current/404.template.html ================================================ Page not found · GitHub Pages

404

There isn't a GitHub Pages site here.

Did you mean to visit docs.spring-boot-admin.com? Please note that this site belongs to a GitHub user and is not an official GitHub site.

================================================ FILE: spring-boot-admin-docs/src/site/current/index.template.html ================================================ Redirecting...

Redirecting...

================================================ FILE: spring-boot-admin-docs/src/site/docs/01-getting-started/10-server-setup.md ================================================ --- 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 ================================================ FILE: spring-boot-admin-docs/src/site/docs/01-getting-started/20-client-registration.md ================================================ --- 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 ================================================ FILE: spring-boot-admin-docs/src/site/docs/01-getting-started/50-snapshots.md ================================================ --- sidebar_custom_props: icon: 'package' --- # 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-boot-admin-snapshot Spring Boot Admin Snapshots https://maven.pkg.github.com/codecentric/spring-boot-admin true false spring-milestone false https://repo.spring.io/milestone spring-snapshot true http:s//repo.spring.io/snapshot ``` ================================================ FILE: spring-boot-admin-docs/src/site/docs/01-getting-started/_category_.json ================================================ { "label": "Getting Started", "position": 1 } ================================================ FILE: spring-boot-admin-docs/src/site/docs/01-getting-started/index.md ================================================ --- 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. ::: ================================================ FILE: spring-boot-admin-docs/src/site/docs/02-server/01-server.mdx ================================================ --- sidebar_custom_props: icon: 'server' --- 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 server ## Running Behind a Front-end Proxy Server In case the Spring Boot Admin server is running behind a reverse proxy, it may be requried to configure the public url where the server is reachable via (`spring.boot.admin.ui.public-url`). In addition, when the reverse proxy terminates the https connection, it may be necessary to configure `server.forward-headers-strategy=native` (also see [Spring Boot Reference Guide](https://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#howto-use-tomcat-behind-a-proxy-server)). ## Spring Cloud Discovery The Spring Boot Admin Server can use Spring Clouds `DiscoveryClient` to discover applications. The advantage is that the clients don’t have to include the `spring-boot-admin-starter-client`. You just have to add a `DiscoveryClient` implementation to your admin server - everything else is done by AutoConfiguration. ### Static Configuration using SimpleDiscoveryClient Spring Cloud provides a `SimpleDiscoveryClient`. It allows you to specify client applications via static configuration: ```xml title="pom.xml" org.springframework.cloud spring-cloud-starter ``` ```yaml title="application.yml" spring: cloud: discovery: client: simple: instances: test: - uri: http://instance1.intern:8080 metadata: management.context-path: /actuator - uri: http://instance2.intern:8080 metadata: management.context-path: /actuator ``` ### 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. See the [integration guides](../04-integration/) for detailed setup instructions. ### Converting ServiceInstances The information from the service registry are converted by the `ServiceInstanceConverter`. Spring Boot Admin ships with a default and Eureka converter implementation. The correct one is selected by AutoConfiguration. :::tip You can modify how the information from the registry is used to register the application by using SBA Server configuration options and instance metadata. The values from the metadata takes precedence over the server config. If the plenty of options don’t fit your needs you can provide your own ServiceInstanceConverter. ::: :::tip When using Eureka, the healthCheckUrl known to Eureka is used for health-checking, which can be set on your client using eureka.instance.healthCheckUrl. ::: ### CloudFoundry If you are deploying your applications to CloudFoundry then `vcap.application.application_id` and `vcap.application.instance_index` **_must_** be added to the metadata for proper registration of applications with Spring Boot Admin Server. Here is a sample configuration for Eureka: ```yml title="application.yml" eureka: instance: hostname: ${vcap.application.uris[0]} nonSecurePort: 80 metadata-map: applicationId: ${vcap.application.application_id} instanceId: ${vcap.application.instance_index} ``` ================================================ FILE: spring-boot-admin-docs/src/site/docs/02-server/02-security.md ================================================ --- 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(); } ``` ================================================ FILE: spring-boot-admin-docs/src/site/docs/02-server/10-Events.mdx ================================================ --- sidebar_custom_props: icon: 'server' --- # Server(-Sent) Events Spring Boot Admin uses event sourcing per default to track changes of registered applications. Every change (see relevant events below) of an application is stored as an event in memory. This allows to reconstruct the state of an application at any point in time, as far as the server is not restarted. :::info Server Events in Spring Boot Admin are not the same as Spring Boot's AuditEvent actuator events. Server Events track changes and lifecycle of registered instances for monitoring and UI purposes, while AuditEvents are used for auditing application actions and security events. ::: ## Event Endpoints Spring Boot Admin utilizes several endpoints for accessing instance events via the `InstancesController`: ### List All Events - **GET `/instances/events`** - Returns all instance events as a JSON array. - Use this to retrieve the complete event history for all registered instances on UI startup. ### Stream All Events - **GET `/instances/events`** (with `Accept: text/event-stream`) - Returns a continuous stream of instance events using [Server-Sent Events (SSE)](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events). - Useful for real-time monitoring and UI updates. ### Stream Events for a Specific Instance - **GET `/instances/{id}`** (with `Accept: text/event-stream`) - Streams events for a single instance, identified by its ID. ## Available Events ### InstanceRegisteredEvent Indicates that a new instance has been registered with Spring Boot Admin. This event is used to add the instance to SBA and starts monitoring its status and endpoints. ### InstanceRegistrationUpdatedEvent Signals that the registration details of an instance have changed (e.g., new management URL or changed registration source). This event is used to update how the instance is accessed and monitored. ### InstanceEndpointsDetectedEvent Signals that the endpoints (such as actuator endpoints) of an instance have been detected or updated. This event is used to refresh the available operations and monitoring features for the instance in the UI. ### InstanceInfoChangedEvent Occurs when the metadata or information (e.g., version, build info, tags) of an instance changes. This event is used to update the displayed details about the instance in the UI. ### InstanceStatusChangedEvent Occurs when the status of an instance changes (e.g., from UP to DOWN, or vice versa). This event is used to update the health indicator and status badge in the UI, and may trigger notifications or alerts. ### InstanceDeregisteredEvent Indicates that an instance has been unregistered from Spring Boot Admin. This event is used to remove the instance from the UI and stop monitoring its status and endpoints. ## Viewing Events in the UI Journal All instance events described above can be viewed in the Journal section of the Spring Boot Admin UI. The Journal provides a chronological log of all relevant events for each registered instance, allowing users to track changes, status updates, and lifecycle actions directly from the web interface. This feature helps administrators and operators to: - Audit the history of instance registrations, status changes, and endpoint updates - Troubleshoot issues by reviewing the sequence of events - Gain insights into the operational state of monitored applications ================================================ FILE: spring-boot-admin-docs/src/site/docs/02-server/20-Clustering.mdx ================================================ --- sidebar_custom_props: icon: 'server' --- import metadataServer 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 Spring Boot Admin Server supports cluster replication via Hazelcast. It is automatically enabled when a `HazelcastConfig`\- or `HazelcastInstance`\-Bean is present. You can also configure the Hazelcast instance to be persistent, to keep the status over restarts. Also have a look at the [Spring Boot support for Hazelcast](http://docs.spring.io/spring-boot/docs/current-SNAPSHOT/reference/htmlsingle/#boot-features-hazelcast/). When using clustering, Spring Boot Admin Events and Notifications are replicated across the members in the cluster. The applications are not replicated, each instance of Spring Boot Admin will have its own set of applications. This means that each instance has to monitor all applications, which may lead to increased load on the monitored services. Otherwise, you would have to ensure that each application is only monitored by one instance of Spring Boot Admin. ![Architecture](hazelcast-component-diagram.png) :::info In the setup shown in the picture above, both Spring Boot Admin instances poll the monitored services for health checks. This means each service receives health check requests from every SBA node in the cluster. ::: ## Example Configuration The following example shows a simple Hazelcast configuration, which should work in most environments. It uses multicast for discovery, which may not be available in all networks and does not require a dedicated Hazelcast server. All instances (aka members) of the Spring Boot Admin Server when using this config, will form a cluster automatically. Keep in mind that Hazelcast has a lot of options to configure the network and discovery. Please refer to the [Hazelcast Documentation](https://docs.hazelcast.com/) for more details, as we do not offer any kind of support for Hazelcast itself. Also, this is just a basic example, you should adapt the configuration to your needs, especially regarding production readiness and network configuration. 1. Add Hazelcast to your dependencies: ```xml title="pom.xml" com.hazelcast hazelcast ``` 2. Instantiate a HazelcastConfig: ```java title="HazelcastConfig.java" @Bean public Config hazelcastConfig() { // This map is used to store the events. // It should be configured to reliably hold all the data, // Spring Boot Admin will compact the events, if there are too many MapConfig eventStoreMap = new MapConfig(DEFAULT_NAME_EVENT_STORE_MAP).setInMemoryFormat(InMemoryFormat.OBJECT) .setBackupCount(1) .setMergePolicyConfig(new MergePolicyConfig(PutIfAbsentMergePolicy.class.getName(), 100)); // This map is used to deduplicate the notifications. // If data in this map gets lost it should not be a big issue as it will atmost // lead to // the same notification to be sent by multiple instances MapConfig sentNotificationsMap = new MapConfig(DEFAULT_NAME_SENT_NOTIFICATIONS_MAP) .setInMemoryFormat(InMemoryFormat.OBJECT) .setBackupCount(1) .setEvictionConfig( new EvictionConfig().setEvictionPolicy(EvictionPolicy.LRU).setMaxSizePolicy(MaxSizePolicy.PER_NODE)) .setMergePolicyConfig(new MergePolicyConfig(PutIfAbsentMergePolicy.class.getName(), 100)); Config config = new Config(); config.addMapConfig(eventStoreMap); config.addMapConfig(sentNotificationsMap); config.setProperty("hazelcast.jmx", "true"); // network and join configuration (simple defaults good for local/dev) NetworkConfig network = config.getNetworkConfig(); network.setPort(5701).setPortAutoIncrement(true); JoinConfig join = network.getJoin(); join.getMulticastConfig().setEnabled(true); return config; } ``` ================================================ FILE: spring-boot-admin-docs/src/site/docs/02-server/30-persistence.md ================================================ --- 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 ================================================ FILE: spring-boot-admin-docs/src/site/docs/02-server/40-instance-registry.md ================================================ --- 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 ================================================ FILE: spring-boot-admin-docs/src/site/docs/02-server/99-server-properties.mdx ================================================ --- 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"; ================================================ FILE: spring-boot-admin-docs/src/site/docs/02-server/_category_.json ================================================ { "position": 2, "label": "Server" } ================================================ FILE: spring-boot-admin-docs/src/site/docs/02-server/index.md ================================================ --- sidebar_custom_props: icon: 'server' --- import DocCardList from '@theme/DocCardList'; # Spring Boot Admin Server The Spring Boot Admin Server acts as the core component for managing and monitoring multiple Spring Boot applications. It collects health, metrics, and runtime information from registered applications and displays them on a user-friendly web interface. To set up the Spring Boot Admin Server, you'll need to create a Spring Boot application and add the Spring Boot Admin Server Starter dependency. The server can operate as a Servlet or Reactive (WebFlux) application, depending on your project setup. **Key Features:** * **Application Registration**: Clients (Spring Boot applications) register with the Admin Server via HTTP or service discovery mechanisms like Eureka or Consul. * **Health Monitoring**: Provides an overview of the health status of registered applications via Spring Boot Actuator endpoints. * **Metrics and Logs**: Displays key performance metrics and logs in real-time. * **Management Actions**: Allows interaction with client applications for tasks like restarting, updating configurations, or triggering garbage collection. The Admin Server itself is stateless, meaning it relies on its registered applications to periodically poll their status. Once configured, the Admin Server dashboard provides a central view for managing all of your Spring Boot services. ================================================ FILE: spring-boot-admin-docs/src/site/docs/02-server/notifications/90-custom-notifiers.md ================================================ --- 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 ================================================ FILE: spring-boot-admin-docs/src/site/docs/02-server/notifications/_category_.json ================================================ { "label": "Notifications", "description": "Configure notifications to alert teams about instance status changes via email, Slack, Teams, and other channels." } ================================================ FILE: spring-boot-admin-docs/src/site/docs/02-server/notifications/index.mdx ================================================ --- sidebar_position: 80 sidebar_custom_props: icon: 'bell-ring' --- # Notifications 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`. ```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()); } }); } } ``` ## Notification Proxy Settings All Notifiers which are using a `RestTemplate` can be configured to use a proxy. ## Notification Reminder The `RemindingNotifier` sends reminders for down/offline applications, it delegates the sending of notifications to another notifier. By default, a reminder is triggered when a registered application changes to `DOWN` or `OFFLINE`. You can alter this behaviour via `setReminderStatuses()`. The reminder ends when either the status changes to a non-triggering status or the regarding application gets deregistered. By default, the reminders are sent every 10 minutes, to change this use `setReminderPeriod()`. The `RemindingNotifier` itself doesn’t start the background thread to send the reminders, you need to take care of this as shown in the given example below; How to configure reminders ```java title="NotifierConfiguration.java" @Configuration public class NotifierConfiguration { @Autowired private Notifier notifier; @Primary @Bean(initMethod = "start", destroyMethod = "stop") public RemindingNotifier remindingNotifier() { RemindingNotifier notifier = new RemindingNotifier(notifier, repository); notifier.setReminderPeriod(Duration.ofMinutes(10)); // (1) notifier.setCheckReminderInverval(Duration.ofSeconds(10)); //(2) return notifier; } } ``` 1. The reminders will be sent every 10 minutes. 2. Schedules sending of due reminders every 10 seconds. ## Filtering Notifications The `FilteringNotifier` allows you to filter certain notification based on rules you can add/remove at runtime. It delegates the sending of notifications to another notifier. If you add a `FilteringNotifier` to your `ApplicationContext` a RESTful interface on `notifications/filter` gets available. The restful interface provides the following methods for getting, adding, and deleting notification filters: * `GET notifications/filter` * Returns a list of all registered notification filters. Each containing the attributes `id`, `applicationName`, `expiry`, and `expired`. * `POST notifications/filters?instanceId=&applicationName=&ttl=` * Posts a new notification filter for the application/instance of the given `instanceId` or `applicationName`. Either `instanceId` or `applicationName` must be set. The parameter `ttl` is optional and represents the expiration of the filter as an instant (the number of seconds from the epoch of `1970-01-01T00:00:00Z`). * `DELETE notifications/filters/{id}` * Deletes the notification filter with the requested id from the filters. You may as well access all notification filter configurations via the main applications view inside SBA client, as seen in the screenshot below. ![Sample notification filters](notification-filter.png) A `FilteringNotifier` might be useful, for instance, if you don’t want to receive notifications when deploying your applications. Before stopping the application, you can add an (expiring) filter via a `POST` request. How to configure filtering ```java title="NotifierConfig.java" @Configuration(proxyBeanMethods = false) public class NotifierConfig { private final InstanceRepository repository; private final ObjectProvider> otherNotifiers; public NotifierConfig(InstanceRepository repository, ObjectProvider> otherNotifiers) { this.repository = repository; this.otherNotifiers = otherNotifiers; } @Bean public FilteringNotifier filteringNotifier() { // (1) CompositeNotifier delegate = new CompositeNotifier(this.otherNotifiers.getIfAvailable(Collections::emptyList)); return new FilteringNotifier(delegate, this.repository); } @Primary @Bean(initMethod = "start", destroyMethod = "stop") public RemindingNotifier remindingNotifier() { // (2) RemindingNotifier notifier = new RemindingNotifier(filteringNotifier(), this.repository); notifier.setReminderPeriod(Duration.ofMinutes(10)); notifier.setCheckReminderInverval(Duration.ofSeconds(10)); return notifier; } } ``` 1. Add the `FilteringNotifier` bean using a delegate (e.g. `MailNotifier` when configured) 2. Add the `RemindingNotifier` as primary bean using the `FilteringNotifier` as delegate. :::tip This example combines the reminding and filtering notifiers. This allows you to get notifications after the deployed application hasn’t restarted in a certain amount of time (until the filter expires). ::: ## Shipped Notifiers ================================================ FILE: spring-boot-admin-docs/src/site/docs/02-server/notifications/notifier-dingtalk.mdx ================================================ --- sidebar_custom_props: icon: 'notifications' --- 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 To enable [DingTalk](https://www.dingtalk.com/) notifications you need to create and authorize a dingtalk bot and set the appropriate configuration properties for webhookUrl and secret. ================================================ FILE: spring-boot-admin-docs/src/site/docs/02-server/notifications/notifier-discord.mdx ================================================ --- sidebar_custom_props: icon: 'notifications' --- 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 To enable Discord notifications you need to create a webhook and set the appropriate configuration property. ================================================ FILE: spring-boot-admin-docs/src/site/docs/02-server/notifications/notifier-hipchat.mdx ================================================ --- sidebar_custom_props: icon: 'notifications' --- 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 To enable [Hipchat](https://www.hipchat.com/) notifications you need to create an API token on your Hipchat account and set the appropriate configuration properties. ================================================ FILE: spring-boot-admin-docs/src/site/docs/02-server/notifications/notifier-lets-chat.mdx ================================================ --- sidebar_custom_props: icon: 'notifications' --- 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 To enable [Let’s Chat](https://sdelements.github.io/lets-chat/) notifications you need to add the host url and add the API token and username from Let’s Chat ================================================ FILE: spring-boot-admin-docs/src/site/docs/02-server/notifications/notifier-mail.mdx ================================================ --- sidebar_custom_props: icon: 'notifications' --- 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 Mail notifications will be delivered as HTML emails rendered using https://www.thymeleaf.org/[Thymeleaf] templates. To enable Mail notifications, configure a `JavaMailSender` using `spring-boot-starter-mail` and set a recipient.
![mail-notification.png](mail-notification.png)
Sample Mail Notification with default template
:::info To prevent disclosure of sensitive information, the default mail template doesn’t show any metadata of the instance. If you want to you show some of the metadata you can use a custom template. ::: ```xml title="Add spring-boot-starter-mail to your dependencies" org.springframework.boot spring-boot-starter-mail ``` ```properties title="application.properties" spring.mail.host=smtp.example.com spring.boot.admin.notify.mail.to=admin@example.com ``` ================================================ FILE: spring-boot-admin-docs/src/site/docs/02-server/notifications/notifier-mattermost.mdx ================================================ --- sidebar_custom_props: icon: 'notifications' --- 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 To enable [Mattermost](https://mattermost.com/) notifications you need to add a bot account under integrations on your Mattermost server and configure it appropriately. ================================================ FILE: spring-boot-admin-docs/src/site/docs/02-server/notifications/notifier-msteams.mdx ================================================ --- sidebar_custom_props: icon: 'notifications' --- 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 To enable Microsoft Teams notifications you need to set up a connector webhook url and set the appropriate configuration property. ================================================ FILE: spring-boot-admin-docs/src/site/docs/02-server/notifications/notifier-rocketchat.mdx ================================================ --- sidebar_custom_props: icon: 'notifications' --- 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 To enable [Rocket.Chat](https://www.rocket.chat/) notifications you need a personal token access and create a room to send message with this token ================================================ FILE: spring-boot-admin-docs/src/site/docs/02-server/notifications/notifier-slack.mdx ================================================ --- sidebar_custom_props: icon: 'bell' --- 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 To enable [Slack](https://slack.com/) notifications you need to add an incoming Webhook under custom integrations on your Slack account and configure it appropriately. ================================================ FILE: spring-boot-admin-docs/src/site/docs/02-server/notifications/notifier-telegram.mdx ================================================ --- sidebar_custom_props: icon: 'notifications' --- 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 To enable [Telegram](https://telegram.org/) notifications you need to create and authorize a telegram bot and set the appropriate configuration properties for auth-token and chat-id. ================================================ FILE: spring-boot-admin-docs/src/site/docs/02-server/notifications/notifier-webex.mdx ================================================ --- sidebar_custom_props: icon: 'notifications' --- 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 To enable [Webex](https://www.webex.com/) notifications, you need to set the appropriate configuration properties for `auth-token` and `room-id`. ================================================ FILE: spring-boot-admin-docs/src/site/docs/03-client/10-client-features.md ================================================ --- sidebar_custom_props: icon: 'features' --- # Features ## 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 **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" org.springframework.boot spring-boot-maven-plugin build-info ``` To generate the build-info in a gradle project, add the following snippet to your `build.gradle`: ```groovy title="build.gradle" springBoot { buildInfo() } ``` ## JMX-Bean Management To interact with JMX-beans in the admin UI you have to include [Jolokia](https://jolokia.org/) in your application and expose it via the actuator endpoint. As Jolokia is servlet based there is no support for reactive applications. 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. See https://jolokia.org/reference/html/manual/spring.html for more details. ```xml title="pom.xml" org.jolokia jolokia-support-springboot-3 2.5.0 ``` ### Spring Boot 2 App You can still monitor Spring Boot 2 applications with Jolokia endpoint using a Spring Boot Admin 3 server. Spring Boot 2 provided the actuator itself, so you only need the plain jolokia dependency. ```xml title="pom.xml" org.jolokia jolokia-core ``` ## 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`. 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. 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) logging.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 (2) ``` 1. Destination the logfile is written to. Enables the logfile actuator endpoint. 2. File log pattern using ANSI colors. ## 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. ```properties title="application.properties" #using the metadata spring.boot.admin.client.instance.metadata.tags.environment=test #using the info endpoint 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. :::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. ::: ================================================ FILE: spring-boot-admin-docs/src/site/docs/03-client/20-registration.md ================================================ --- 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 ================================================ FILE: spring-boot-admin-docs/src/site/docs/03-client/30-metadata.md ================================================ --- 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 ================================================ FILE: spring-boot-admin-docs/src/site/docs/03-client/40-service-discovery.md ================================================ --- 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 ================================================ FILE: spring-boot-admin-docs/src/site/docs/03-client/80-configuration.md ================================================ --- sidebar_custom_props: icon: 'configuration' --- # Configuration In addition to discovering and registering services, Spring Boot Admin makes use of metadata to control how individual clients are handled by the server. Metadata allows you to fine-tune the behavior of the admin server for each registered service and can influence how services are displayed, monitored, or interacted with. A set of well-defined metadata attributes is recognized and evaluated directly on the server side. Because the Spring Boot Admin client itself relies on the same Spring Cloud interfaces, these metadata properties can also be applied consistently on the client, ensuring a unified configuration approach across your entire system. __Instance metadata options__ | Property name | Description | |------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------| | `disable-url` | Disables links of this instance in UI. Useful, when the URL does not point to a UI. | | `group` | Assign a group name. Used in UI to aggregate instances not by application but by assigned group. | | `hide-url` | Hide URLs of the instance in UI. Useful, when running in a cluster, exposing a non routable URL. | | `service-url` | Override the service url of the registered service. Allows to specify the actual URL to the UI. This does not affect management url. | | `sidebar.links.N.label`
`sidebar.links.N.url`
`sidebar.links.N.iframe` | **label:** Shown in sidebar
**url:** URL used as href in link or iframe.
**iframe:** boolean value that allows to include URL as iframe. | | `tags.*` | Tags as key-value-pairs to be associated with this instance. | | `user.name`
`user.password` | Credentials being used to access the endpoints. | ================================================ FILE: spring-boot-admin-docs/src/site/docs/03-client/99-properties.mdx ================================================ --- 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"; ================================================ FILE: spring-boot-admin-docs/src/site/docs/03-client/_category_.json ================================================ { "position": 3, "label": "Clients" } ================================================ FILE: spring-boot-admin-docs/src/site/docs/03-client/index.md ================================================ --- 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. 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. 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:** * **Automatic Registration:** The client can self-register with the Admin Server by sending regular status updates. * **Health and Metrics Exposure:** The client leverages Spring Boot Actuator to expose endpoints for monitoring health status, system metrics, application logs, and other runtime data. * **Management Actions:** The Admin Server can interact with the client for actions such as restarting the application, clearing caches, or triggering log file downloads. * **Secure Communication:** Spring Boot Admin supports configuring authentication and SSL to ensure secure communication between the client and the server. ================================================ FILE: spring-boot-admin-docs/src/site/docs/04-integration/10-eureka.md ================================================ --- 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 ================================================ FILE: spring-boot-admin-docs/src/site/docs/04-integration/20-consul.md ================================================ --- 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 ================================================ FILE: spring-boot-admin-docs/src/site/docs/04-integration/30-zookeeper.md ================================================ --- 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 ================================================ FILE: spring-boot-admin-docs/src/site/docs/04-integration/40-hazelcast.md ================================================ --- 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 ================================================ FILE: spring-boot-admin-docs/src/site/docs/04-integration/_category_.json ================================================ { "position": 4, "label": "Integration", } ================================================ FILE: spring-boot-admin-docs/src/site/docs/04-integration/index.md ================================================ --- 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 ================================================ FILE: spring-boot-admin-docs/src/site/docs/05-security/10-server-authentication.md ================================================ --- 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 ================================================ FILE: spring-boot-admin-docs/src/site/docs/05-security/20-actuator-security.md ================================================ --- 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 ================================================ FILE: spring-boot-admin-docs/src/site/docs/05-security/30-csrf-protection.md ================================================ --- 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) ================================================ FILE: spring-boot-admin-docs/src/site/docs/05-security/_category_.json ================================================ { "position": 5, "label": "Security" } ================================================ FILE: spring-boot-admin-docs/src/site/docs/05-security/index.md ================================================ --- 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) ================================================ FILE: spring-boot-admin-docs/src/site/docs/06-customization/_category_.json ================================================ { "position": 6, "label": "Customization" } ================================================ FILE: spring-boot-admin-docs/src/site/docs/06-customization/index.md ================================================ import DocCardList from '@theme/DocCardList'; # Customization ================================================ FILE: spring-boot-admin-docs/src/site/docs/06-customization/monitoring/01-instance-filters.md ================================================ --- 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) ================================================ FILE: spring-boot-admin-docs/src/site/docs/06-customization/monitoring/02-custom-health-status.md ================================================ --- 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) ================================================ FILE: spring-boot-admin-docs/src/site/docs/06-customization/monitoring/_category_.json ================================================ { "label": "Monitoring", } ================================================ FILE: spring-boot-admin-docs/src/site/docs/06-customization/server/04-endpoint-detection.md ================================================ --- 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 ``` ================================================ FILE: spring-boot-admin-docs/src/site/docs/06-customization/server/_category_.json ================================================ { "label": "Server" } ================================================ FILE: spring-boot-admin-docs/src/site/docs/08-third-party/_category_.json ================================================ { "position": 8, "label": "Third Party Integrations" } ================================================ FILE: spring-boot-admin-docs/src/site/docs/08-third-party/index.md ================================================ --- sidebar_custom_props: icon: 'puzzle' --- import DocCardList from '@theme/DocCardList'; # Third Party Integrations ================================================ FILE: spring-boot-admin-docs/src/site/docs/08-third-party/pyctuator.md ================================================ --- 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). ================================================ FILE: spring-boot-admin-docs/src/site/docs/09-samples/10-sample-servlet.md ================================================ --- 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) ================================================ FILE: spring-boot-admin-docs/src/site/docs/09-samples/20-sample-reactive.md ================================================ --- 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) ================================================ FILE: spring-boot-admin-docs/src/site/docs/09-samples/30-sample-eureka.md ================================================ --- 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) ================================================ FILE: spring-boot-admin-docs/src/site/docs/09-samples/40-sample-consul.md ================================================ --- 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) ================================================ FILE: spring-boot-admin-docs/src/site/docs/09-samples/50-sample-zookeeper.md ================================================ --- 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) ================================================ FILE: spring-boot-admin-docs/src/site/docs/09-samples/60-sample-hazelcast.md ================================================ --- 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) ================================================ FILE: spring-boot-admin-docs/src/site/docs/09-samples/70-sample-custom-ui.md ================================================ --- 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/) ================================================ FILE: spring-boot-admin-docs/src/site/docs/09-samples/_category_.json ================================================ { "position": 9, "label": "Samples" } ================================================ FILE: spring-boot-admin-docs/src/site/docs/09-samples/index.md ================================================ --- 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 ================================================ FILE: spring-boot-admin-docs/src/site/docs/10-reference/10-event-types.md ================================================ --- 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) ================================================ FILE: spring-boot-admin-docs/src/site/docs/10-reference/20-rest-api.md ================================================ --- 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/) ================================================ FILE: spring-boot-admin-docs/src/site/docs/10-reference/60-actuator-endpoints.mdx ================================================ --- 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. | Actuator Endpoint | Documentation Link | |-----------------------------------|---------------------------------------------------------------------------------------------------------------------------------------| | `/actuator/auditevents` | [Audit Events](https://docs.spring.io/spring-boot/api/rest/actuator/auditevents.html#page-title) | | `/actuator/beans` | [Beans](https://docs.spring.io/spring-boot/api/rest/actuator/beans.html#page-title) | | `/actuator/conditions` | [Conditions](https://docs.spring.io/spring-boot/api/rest/actuator/conditions.html#page-title) | | `/actuator/configprops` | [Config Props](https://docs.spring.io/spring-boot/api/rest/actuator/configprops.html#page-title) | | `/actuator/env` | [Environment](https://docs.spring.io/spring-boot/api/rest/actuator/env.html#page-title) | | `/actuator/flyway` | [Flyway](https://docs.spring.io/spring-boot/api/rest/actuator/flyway.html#page-title) | | `/actuator/health` | [Health](https://docs.spring.io/spring-boot/api/rest/actuator/health.html#page-title) | | `/actuator/httptrace` | [HTTP Trace](https://docs.spring.io/spring-boot/docs/2.1.7.RELEASE/actuator-api/html/#http-trace) | | `/actuator/httpexchanges` | [HTTP Exchanges](https://docs.spring.io/spring-boot/api/rest/actuator/httpexchanges.html#page-title) | | `/actuator/info` | [Info](https://docs.spring.io/spring-boot/api/rest/actuator/info.html#page-title) | | `/actuator/jolokia` | [Jolokia](https://jolokia.org/reference/html/manual/spring.html) | | `/actuator/liquibase` | [Liquibase](https://docs.spring.io/spring-boot/api/rest/actuator/liquibase.html#page-title) | | `/actuator/logfile` | [Logfile](https://docs.spring.io/spring-boot/api/rest/actuator/logfile.html#page-title) | | `/actuator/loggers` | [Loggers](https://docs.spring.io/spring-boot/api/rest/actuator/loggers.html#page-title) | | `/actuator/mappings` | [Mappings](https://docs.spring.io/spring-boot/api/rest/actuator/mappings.html#page-title) | | `/actuator/metrics` | [Metrics](https://docs.spring.io/spring-boot/api/rest/actuator/metrics.html#page-title) | | `/actuator/quartz` | [Quartz](https://docs.spring.io/spring-boot/api/rest/actuator/quartz.html#page-title) | | `/actuator/quartz/jobs` | [Quartz Jobs](https://docs.spring.io/spring-boot/api/rest/actuator/quartz.html#page-title) | | `/actuator/quartz/triggers` | [Quartz Triggers](https://docs.spring.io/spring-boot/api/rest/actuator/quartz.html#page-title) | | `/actuator/sbom` | [SBOM](https://docs.spring.io/spring-boot/api/rest/actuator/sbom.html#page-title) | | `/actuator/scheduledtasks` | [Scheduled Tasks](https://docs.spring.io/spring-boot/api/rest/actuator/scheduledtasks.html#page-title) | | `/actuator/sessions` | [Sessions](https://docs.spring.io/spring-boot/api/rest/actuator/sessions.html#page-title) | | `/actuator/shutdown` | [Shutdown](https://docs.spring.io/spring-boot/api/rest/actuator/shutdown.html#page-title) | | `/actuator/startup` | [Startup](https://docs.spring.io/spring-boot/api/rest/actuator/startup.html#page-title) | | `/actuator/threaddump` | [Thread Dump](https://docs.spring.io/spring-boot/api/rest/actuator/threaddump.html#page-title) | | `/actuator/refresh` | [Refresh](https://docs.spring.io/spring-cloud-commons/reference/spring-cloud-commons/application-context-services.html#refresh-scope) | | `/actuator/busrefresh` | [Bus Refresh](https://docs.spring.io/spring-cloud-bus/docs/current/reference/html/index.html) | | `/actuator/caches` | [Caches](https://docs.spring.io/spring-boot/api/rest/actuator/caches.html#page-title) | | `/actuator/restart` | [Restart](https://docs.spring.io/spring-cloud-commons/reference/spring-cloud-commons/application-context-services.html#endpoints) | | `/actuator/gateway/globalfilters` | [Global Filters](https://cloud.spring.io/spring-cloud-gateway/multi/multi__actuator_api.html) | | `/actuator/gateway/routes` | [Routes](https://cloud.spring.io/spring-cloud-gateway/multi/multi__actuator_api.html) | | `/actuator/gateway/refresh` | [Refresh](https://cloud.spring.io/spring-cloud-gateway/multi/multi__actuator_api.html) | ================================================ FILE: spring-boot-admin-docs/src/site/docs/10-reference/_category_.json ================================================ { "position": 10, "label": "Reference" } ================================================ FILE: spring-boot-admin-docs/src/site/docs/10-reference/index.md ================================================ --- 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/) ================================================ FILE: spring-boot-admin-docs/src/site/docs/11-upgrading/01-spring-boot-admin-4.md ================================================ --- 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. ================================================ FILE: spring-boot-admin-docs/src/site/docs/11-upgrading/_category_.json ================================================ { "position": 11, "label": "Upgrading" } ================================================ FILE: spring-boot-admin-docs/src/site/docs/11-upgrading/index.md ================================================ --- sidebar_custom_props: icon: 'arrow-up' --- import DocCardList from '@theme/DocCardList'; # Upgrading ================================================ FILE: spring-boot-admin-docs/src/site/docs/index.mdx ================================================ --- 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}
)} />
================================================ FILE: spring-boot-admin-docs/src/site/docs/index.module.css ================================================ .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; } ================================================ FILE: spring-boot-admin-docs/src/site/docusaurus.config.ts ================================================ 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, } const config: Config = { title: 'Spring Boot Admin', favicon: 'img/favicon.png', url: 'https://docs.spring-boot-admin.com/', baseUrl: process.env.VERSION ? `/${process.env.VERSION}/` : '/', organizationName: 'codecentric', projectName: 'spring-boot-admin', onBrokenLinks: 'warn', onBrokenAnchors: 'warn', i18n: { defaultLocale: "en", locales: ["en"] }, presets: [ [ "classic", { theme: { customCss: ['./src/css/custom.css'], }, docs: { sidebarPath: "./sidebars.ts", }, blog: { showReadingTime: true, feedOptions: { type: ["rss", "atom"], xslt: true }, onInlineTags: "warn", onInlineAuthors: "warn", onUntruncatedBlogPosts: "warn" }, } 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) { content = content.replaceAll('@'+variable+'@', globalVariables[variable]); } return content }, }, themes: ['@docusaurus/theme-mermaid'], themeConfig: { image: "img/social-card.jpg", tableOfContents: { minHeadingLevel: 2, maxHeadingLevel: 5 }, algolia: { appId: "GUDRYGX7B3", apiKey: "d6ff502875993f3160598cbd257cc532", indexName: "spring-boot-admin", contextualSearch: true, searchParameters: {}, searchPagePath: false, insights: false }, navbar: { title: "Spring Boot Admin", logo: { alt: "Spring Boot logo with pulse line in front of it", src: "img/logo.png" }, items: [ { type: "docSidebar", sidebarId: "sidebar", position: "left", label: "Documentation" }, { type: "docSidebar", sidebarId: "sidebar", position: "left", label: "FAQ", href: "/faq" }, { href: "https://github.com/codecentric/spring-boot-admin", label: "GitHub", position: "right" } ] }, footer: { style: "dark", links: [ { title: "Docs", items: [ { label: "Overview", to: "/docs/getting-started/" }, { label: "FAQ", to: "/faq" } ] }, { title: "Community", items: [ { label: "Stack Overflow", href: "https://stackoverflow.com/questions/tagged/spring-boot-admin" } ] }, { title: "More", items: [ { label: "GitHub", href: "https://github.com/codecentric/spring-boot-admin" }, { label: "Impressum", to: "/impressum" }, { label: "Privacy", to: "/privacy" } ] } ], copyright: `© ${new Date().getFullYear()} codecentric AG` }, prism: { theme: prismThemes.github, darkTheme: prismThemes.vsDark, additionalLanguages: ["java", "bash", "javascript", "typescript", "docker", "gradle", "groovy", "yaml"] } } satisfies Preset.ThemeConfig }; export default config; ================================================ FILE: spring-boot-admin-docs/src/site/package.json ================================================ { "name": "site", "version": "0.0.0", "private": true, "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" }, "dependencies": { "@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", "glob": "^13.0.0", "lodash.merge": "^4.6.2", "node-html-markdown": "^2.0.0", "prism-react-renderer": "^2.4.0", "react": "^19.0.0", "react-dom": "^19.0.0", "react-multi-carousel": "^2.8.5", "remark-deflist": "^1.0.0", "typescript": "~5.9.0" }, "browserslist": { "development": [ "last 3 chrome version", "last 3 firefox version", "last 5 safari version" ], "production": [ ">0.5%", "not dead", "not op_mini all" ] } } ================================================ FILE: spring-boot-admin-docs/src/site/sidebars.ts ================================================ import type {SidebarsConfig} from '@docusaurus/plugin-content-docs'; const sidebars: SidebarsConfig = { sidebar: [ {type: 'autogenerated', dirName: '.'} ], }; export default sidebars; ================================================ FILE: spring-boot-admin-docs/src/site/src/components/CopyButton.module.css ================================================ .copyButton { display: inline-flex; align-items: center; gap: 6px; padding: 5px 8px; background-color: var(--ifm-color-primary-light); /* Spring Boot green */ color: #fff; font-size: 14px; font-weight: 500; border: none; border-radius: 6px; cursor: pointer; transition: background-color 0.2s ease-in-out, transform 0.1s ease-in-out; } .copyButton:hover { background-color: var(--ifm-color-primary-dark); /* darker green on hover */ } .copyButton:active { transform: scale(0.97); /* subtle click effect */ } .icon { width: 12px; aspect-ratio: 1; } ================================================ FILE: spring-boot-admin-docs/src/site/src/components/CopyButton.tsx ================================================ import React, { useState } from "react"; import styles from "./CopyButton.module.css"; export function CopyButton({ text }) { const [copied, setCopied] = useState(false); const handleCopy = async () => { try { await navigator.clipboard.writeText(text); setCopied(true); setTimeout(() => setCopied(false), 2000); } catch (err) { console.error("Failed to copy: ", err); } }; return ( ); } ================================================ FILE: spring-boot-admin-docs/src/site/src/components/HexMesh.module.css ================================================ .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; } } ================================================ FILE: spring-boot-admin-docs/src/site/src/components/HexMesh.tsx ================================================ /* * 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}
); } ================================================ FILE: spring-boot-admin-docs/src/site/src/components/PropertyTable.module.css ================================================ .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; } } .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; } dl { margin-top: 0; margin-bottom: 0; } } ================================================ FILE: spring-boot-admin-docs/src/site/src/components/PropertyTable.tsx ================================================ import { filterPropertiesByName } from "@site/src/propertiesUtil"; import styles from "./PropertyTable.module.css"; import { CopyButton } from "@site/src/components/CopyButton"; type Props = { title?: string; properties: Array; filter?: Array; includeOnly?: boolean; additionalProperties?: Array; } 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, ...additionalProperties ]; const hasDefaultValueOrType = (property: SpringPropertyDefinition) => { return property.defaultValue || property.type; }; return ( {title && } {propertiesToShow.map((a) => ( <> ))}
{title}
Property
{a.name}

{hasDefaultValueOrType(a) && (

{a.type && (
Type: 
{a.type}
)} {a.defaultValue && (
Default: 
{JSON.stringify(a.defaultValue)}
)}
)}
); } ================================================ FILE: spring-boot-admin-docs/src/site/src/components/Screenshot.module.css ================================================ .screenshot { position: relative; } .screenshot__description { font-size: .9rem; position: absolute; text-align: center; bottom: 2.4rem; z-index: 1; width: 90%; backdrop-filter: blur(10px); background: rgba(0,0,0,0.3); color: #fff; border-radius: 6px; padding: 5px 10px; left: 50%; transform: translateX(-50%); } ================================================ FILE: spring-boot-admin-docs/src/site/src/components/Screenshot.tsx ================================================ import Carousel from "react-multi-carousel"; import "react-multi-carousel/lib/styles.css"; import styles from "./Screenshot.module.css"; export function Screenshot({ images }: { images: {src: string, description: string}[] }) { return ( {images.map((image, index) => { const { default: imgSrc } = require(`../../assets/screens/${image.src}`); return (
{image.description}
); })}
); } ================================================ FILE: spring-boot-admin-docs/src/site/src/css/custom.css ================================================ :root { --ifm-color-primary: #18674e; --ifm-color-primary-dark: #165d46; --ifm-color-primary-darker: #145842; --ifm-color-primary-darkest: #114837; --ifm-color-primary-light: #1a7156; --ifm-color-primary-lighter: #1c765a; --ifm-color-primary-lightest: #1f8665; --ifm-background-color: #fff; } [data-theme='dark'] { --ifm-color-primary: #45d3a6; --ifm-color-primary-dark: #30cc9a; --ifm-color-primary-darker: #2ec092; --ifm-color-primary-darkest: #259f78; --ifm-color-primary-light: #5cd8b1; --ifm-color-primary-lighter: #67dbb6; --ifm-color-primary-lightest: #89e3c7; } main[class^='docMainContainer'] { position: relative; } main > .container { max-width: initial !important; } .dl-horizontal { * { margin: 0; } div { display: grid; gap: .5rem; grid-template-columns: repeat(3, 1fr); dd { grid-column: span 2 / span 2; } } } ================================================ FILE: spring-boot-admin-docs/src/site/src/global.d.ts ================================================ export {}; declare global { type SpringPropertyDefinition = { name: string; description: string; type?: string; defaultValue?: string; } } ================================================ FILE: spring-boot-admin-docs/src/site/src/pages/faq.md ================================================ --- toc_min_heading_level: 2 toc_max_heading_level: 2 --- # FAQ 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? 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) ::: ================================================ FILE: spring-boot-admin-docs/src/site/src/pages/impressum.md ================================================ # Impressum ## Hauptsitz der Gesellschaft codecentric AG Hochstraße 11 42697 Solingen Telefon: +49 (0) 212 23 36 28 0 Telefax: +49 (0) 212.23 36 28 79 E-Mail: info@codecentric.de ## Vorstand Rainer Vehns (Vorsitzender) Verena Deller Stefan Riedel Lars Rückemann ## Handelsregister Amtsgericht Wuppertal, HRB 25917 ## Umsatzsteueridentifikationsnummer DE 119437798 ## Inhaltlich Verantwortlicher Inhaltlich Verantwortlicher gemäß § 18 Abs. 2 MStV: Rainer Vehns ================================================ FILE: spring-boot-admin-docs/src/site/src/pages/index.tsx ================================================ import { Redirect } from "@docusaurus/router"; export default function Home() { return ; } ================================================ FILE: spring-boot-admin-docs/src/site/src/pages/markdown-page.md ================================================ --- title: Markdown page example --- # Markdown page example You don't need React to write simple standalone pages. ================================================ FILE: spring-boot-admin-docs/src/site/src/pages/privacy.md ================================================ # Datenschutzerklärung nach der DSGVO ## Name und Anschrift der Verantwortlichen Die Verantwortliche im Sinne der Datenschutz-Grundverordnung und anderer nationaler Datenschutzgesetze der Mitgliedsstaaten sowie sonstiger datenschutzrechtlicher Bestimmungen ist die: *codecentric AG* Hochstraße 11 42697 Solingen Deutschland Tel.: +49 [0] 212 23 36 28 0 E-Mail: office@codecentric.de Website: www.codecentric.de ## Anschrift des Datenschutzbeauftragten Der Datenschutzbeauftragte der Verantwortlichen ist wie folgt zu erreichen: *codecentric AG* Hochstraße 11 42697 Solingen Deutschland E-Mail: datenschutz@codecentric.de ## Allgemeines zur Datenverarbeitung ### Umfang der Verarbeitung personenbezogener Daten Wir verarbeiten personenbezogene Daten unserer Nutzer grundsätzlich nur, soweit dies zur Bereitstellung einer funktionsfähigen Website sowie unserer Inhalte und Leistungen erforderlich ist. Die Verarbeitung personenbezogener Daten unserer Nutzer erfolgt regelmäßig nur nach Einwilligung des Nutzers. Eine Ausnahme gilt in solchen Fällen, in denen eine vorherige Einholung einer Einwilligung aus tatsächlichen Gründen nicht möglich ist und die Verarbeitung der Daten durch gesetzliche Vorschriften gestattet ist. ### Rechtsgrundlage für die Verarbeitung personenbezogener Daten Soweit wir für Verarbeitungsvorgänge personenbezogener Daten eine Einwilligung der betroffenen Person einholen, dient Art. 6 Abs. 1 lit. a EU-Datenschutzgrundverordnung (DSGVO) als Rechtsgrundlage. Bei der Verarbeitung von personenbezogenen Daten, die zur Erfüllung eines Vertrages, dessen Vertragspartei die betroffene Person ist, erforderlich ist, dient Art. 6 Abs. 1 lit. b DSGVO als Rechtsgrundlage. Dies gilt auch für Verarbeitungsvorgänge, die zur Durchführung vorvertraglicher Maßnahmen erforderlich sind. Soweit eine Verarbeitung personenbezogener Daten zur Erfüllung einer rechtlichen Verpflichtung erforderlich ist, der unser Unternehmen unterliegt, dient Art. 6 Abs. 1 lit. c DSGVO als Rechtsgrundlage. Für den Fall, dass lebenswichtige Interessen der betroffenen Person oder einer anderen natürlichen Person eine Verarbeitung personenbezogener Daten erforderlich machen, dient Art. 6 Abs. 1 lit. d DSGVO als Rechtsgrundlage. Ist die Verarbeitung zur Wahrung eines berechtigten Interesses unseres Unternehmens oder eines Dritten erforderlich und überwiegen die Interessen, Grundrechte und Grundfreiheiten des Betroffenen das erstgenannte Interesse nicht, so dient Art. 6 Abs. 1 lit. f DSGVO als Rechtsgrundlage für die Verarbeitung. ### Datenlöschung und Speicherdauer Die personenbezogenen Daten der betroffenen Person werden gelöscht oder gesperrt, sobald der Zweck der Speicherung entfällt. Eine Speicherung kann darüber hinaus erfolgen, wenn dies durch den europäischen oder nationalen Gesetzgeber in unionsrechtlichen Verordnungen, Gesetzen oder sonstigen Vorschriften, denen der Verantwortliche unterliegt, vorgesehen wurde. Eine Sperrung oder Löschung der Daten erfolgt auch dann, wenn eine durch die genannten Normen vorgeschriebene Speicherfrist abläuft, es sei denn, dass eine Erforderlichkeit zur weiteren Speicherung der Daten für einen Vertragsabschluss oder eine Vertragserfüllung besteht. ## Bereitstellung der Website und Erstellung von Logfiles ### Beschreibung und Umfang der Datenverarbeitung Bei jedem Aufruf unserer Internetseite erfasst unser System automatisiert Daten und Informationen vom Computersystem des aufrufenden Rechners. Folgende Daten werden hierbei erhoben: 1. Informationen über den Browsertyp und die verwendete Version 2. Das Betriebssystem des Nutzers 3. Den Internet-Service-Provider des Nutzers 4. Die IP-Adresse des Nutzers 5. Datum und Uhrzeit des Zugriffs 6. Websites, von denen das System des Nutzers auf unsere Internetseite gelangt 7. Websites, die vom System des Nutzers über unsere Website aufgerufen werden Die Daten werden ebenfalls in den Logfiles unseres Systems gespeichert. Eine Speicherung dieser Daten zusammen mit anderen personenbezogenen Daten des Nutzers findet nicht statt. ### Rechtsgrundlage für die Datenverarbeitung Rechtsgrundlage für die vorübergehende Speicherung der Daten und der Logfiles ist Art. 6 Abs. 1 lit. f DSGVO. ### Zweck der Datenverarbeitung Die vorübergehende Speicherung der IP-Adresse durch das System ist notwendig, um eine Auslieferung der Website an den Rechner des Nutzers zu ermöglichen. Hierfür muss die IP-Adresse des Nutzers für die Dauer der Sitzung gespeichert bleiben. Die Speicherung in Logfiles erfolgt, um die Funktionsfähigkeit der Website sicherzustellen. Zudem dienen uns die Daten zur technischen Optimierung der Website und zur Sicherstellung der Sicherheit unserer informationstechnischen Systeme. Eine Auswertung der Daten zu Marketingzwecken findet in diesem Zusammenhang nicht statt. In diesen Zwecken liegt auch unser berechtigtes Interesse an der Datenverarbeitung nach Art. 6 Abs. 1 lit. f DSGVO. ### Dauer der Speicherung Die Daten werden gelöscht, sobald sie für die Erreichung des Zweckes ihrer Erhebung nicht mehr erforderlich sind. Im Falle der Erfassung der Daten zur Bereitstellung der Website ist dies der Fall, wenn die jeweilige Sitzung beendet ist. Im Falle der Speicherung der Daten in Logfiles ist dies nach spätestens sieben Tagen der Fall. Eine darüberhinausgehende Speicherung ist möglich. In diesem Fall werden die IP-Adressen der Nutzer gelöscht oder verfremdet, sodass eine Zuordnung des aufrufenden Clients nicht mehr möglich ist. ### Widerspruchs- und Beseitigungsmöglichkeit Die Erfassung der Daten zur Bereitstellung der Website und die Speicherung der Daten in Logfiles ist für den Betrieb der Internetseite zwingend erforderlich. Es besteht folglich seitens des Nutzers keine Widerspruchsmöglichkeit. ## Rechte der betroffenen Person Werden personenbezogene Daten von Ihnen verarbeitet, sind Sie Betroffener i.S.d. DSGVO und es stehen Ihnen folgende Rechte gegenüber dem Verantwortlichen zu: ### Auskunftsrecht Sie können von dem Verantwortlichen eine Bestätigung darüber verlangen, ob personenbezogene Daten, die Sie betreffen, von uns verarbeitet werden. Liegt eine solche Verarbeitung vor, können Sie von dem Verantwortlichen über folgende Informationen Auskunft verlangen: (1) die Zwecke, zu denen die personenbezogenen Daten verarbeitet werden; (2) die Kategorien von personenbezogenen Daten, welche verarbeitet werden; (3) die Empfänger bzw. die Kategorien von Empfängern, gegenüber denen die Sie betreffenden personenbezogenen Daten offengelegt wurden oder noch offengelegt werden; (4) die geplante Dauer der Speicherung der Sie betreffenden personenbezogenen Daten oder, falls konkrete Angaben hierzu nicht möglich sind, Kriterien für die Festlegung der Speicherdauer; (5) das Bestehen eines Rechts auf Berichtigung oder Löschung der Sie betreffenden personenbezogenen Daten, eines Rechts auf Einschränkung der Verarbeitung durch den Verantwortlichen oder eines Widerspruchsrechts gegen diese Verarbeitung; (6) das Bestehen eines Beschwerderechts bei einer Aufsichtsbehörde; (7) alle verfügbaren Informationen über die Herkunft der Daten, wenn die personenbezogenen Daten nicht bei der betroffenen Person erhoben werden; (8) das Bestehen einer automatisierten Entscheidungsfindung, einschließlich Profiling gemäß Art. 22 Abs. 1 und 4 DSGVO und – zumindest in diesen Fällen – aussagekräftige Informationen über die involvierte Logik sowie die Tragweite und die angestrebten Auswirkungen einer derartigen Verarbeitung für die betroffene Person. Ihnen steht das Recht zu, Auskunft darüber zu verlangen, ob die Sie betreffenden personenbezogenen Daten in ein Drittland oder an eine internationale Organisation übermittelt werden. In diesem Zusammenhang können Sie verlangen, über die geeigneten Garantien gem. Art. 46 DSGVO im Zusammenhang mit der Übermittlung unterrichtet zu werden. ### Recht auf Berichtigung Sie haben ein Recht auf Berichtigung und/oder Vervollständigung gegenüber dem Verantwortlichen, sofern die verarbeiteten personenbezogenen Daten, die Sie betreffen, unrichtig oder unvollständig sind. Der Verantwortliche hat die Berichtigung unverzüglich vorzunehmen. ### Recht auf Einschränkung der Verarbeitung Unter den folgenden Voraussetzungen können Sie die Einschränkung der Verarbeitung der Sie betreffenden personenbezogenen Daten verlangen: (1) wenn Sie die Richtigkeit der Sie betreffenden personenbezogenen für eine Dauer bestreiten, die es dem Verantwortlichen ermöglicht, die Richtigkeit der personenbezogenen Daten zu überprüfen; (2) die Verarbeitung unrechtmäßig ist und Sie die Löschung der personenbezogenen Daten ablehnen und stattdessen die Einschränkung der Nutzung der personenbezogenen Daten verlangen; (3) der Verantwortliche die personenbezogenen Daten für die Zwecke der Verarbeitung nicht länger benötigt, Sie diese jedoch zur Geltendmachung, Ausübung oder Verteidigung von Rechtsansprüchen benötigen, oder (4) wenn Sie Widerspruch gegen die Verarbeitung gemäß Art. 21 Abs. 1 DSGVO eingelegt haben und noch nicht feststeht, ob die berechtigten Gründe des Verantwortlichen gegenüber Ihren Gründen überwiegen. Wurde die Verarbeitung der Sie betreffenden personenbezogenen Daten eingeschränkt, dürfen diese Daten – von ihrer Speicherung abgesehen – nur mit Ihrer Einwilligung oder zur Geltendmachung, Ausübung oder Verteidigung von Rechtsansprüchen oder zum Schutz der Rechte einer anderen natürlichen oder juristischen Person oder aus Gründen eines wichtigen öffentlichen Interesses der Union oder eines Mitgliedstaats verarbeitet werden. Wurde die Einschränkung der Verarbeitung nach den o.g. Voraussetzungen eingeschränkt, werden Sie von dem Verantwortlichen unterrichtet, bevor die Einschränkung aufgehoben wird. ### Recht auf Löschung #### Löschungspflicht Sie können von dem Verantwortlichen verlangen, dass die Sie betreffenden personenbezogenen Daten unverzüglich gelöscht werden, und der Verantwortliche ist verpflichtet, diese Daten unverzüglich zu löschen, sofern einer der folgenden Gründe zutrifft: (1) Die Sie betreffenden personenbezogenen Daten sind für die Zwecke, für die sie erhoben oder auf sonstige Weise verarbeitet wurden, nicht mehr notwendig. (2) Sie widerrufen Ihre Einwilligung, auf die sich die Verarbeitung gem. Art. 6 Abs. 1 lit. a oder Art. 9 Abs. 2 lit. a DSGVO stützte, und es fehlt an einer anderweitigen Rechtsgrundlage für die Verarbeitung. (3) Sie legen gem. Art. 21 Abs. 1 DSGVO Widerspruch gegen die Verarbeitung ein und es liegen keine vorrangigen berechtigten Gründe für die Verarbeitung vor, oder Sie legen gem. Art. 21 Abs. 2 DSGVO Widerspruch gegen die Verarbeitung ein. (4) Die Sie betreffenden personenbezogenen Daten wurden unrechtmäßig verarbeitet. (5) Die Löschung der Sie betreffenden personenbezogenen Daten ist zur Erfüllung einer rechtlichen Verpflichtung nach dem Unionsrecht oder dem Recht der Mitgliedstaaten erforderlich, dem der Verantwortliche unterliegt. (6) Die Sie betreffenden personenbezogenen Daten wurden in Bezug auf angebotene Dienste der Informationsgesellschaft gemäß Art. 8 Abs. 1 DSGVO erhoben. #### Information an Dritte Hat der Verantwortliche die Sie betreffenden personenbezogenen Daten öffentlich gemacht und ist er gem. Art. 17 Abs. 1 DSGVO zu deren Löschung verpflichtet, so trifft er unter Berücksichtigung der verfügbaren Technologie und der Implementierungskosten angemessene Maßnahmen, auch technischer Art, um für die Datenverarbeitung Verantwortliche, die die personenbezogenen Daten verarbeiten, darüber zu informieren, dass Sie als betroffene Person von ihnen die Löschung aller Links zu diesen personenbezogenen Daten oder von Kopien oder Replikationen dieser personenbezogenen Daten verlangt haben. #### Ausnahmen Das Recht auf Löschung besteht nicht, soweit die Verarbeitung erforderlich ist (1) zur Ausübung des Rechts auf freie Meinungsäußerung und Information; (2) zur Erfüllung einer rechtlichen Verpflichtung, die die Verarbeitung nach dem Recht der Union oder der Mitgliedstaaten, dem der Verantwortliche unterliegt, erfordert, oder zur Wahrnehmung einer Aufgabe, die im öffentlichen Interesse liegt oder in Ausübung öffentlicher Gewalt erfolgt, die dem Verantwortlichen übertragen wurde; (3) aus Gründen des öffentlichen Interesses im Bereich der öffentlichen Gesundheit gemäß Art. 9 Abs. 2 lit. h und i sowie Art. 9 Abs. 3 DSGVO; (4) für im öffentlichen Interesse liegende Archivzwecke, wissenschaftliche oder historische Forschungszwecke oder für statistische Zwecke gem. Art. 89 Abs. 1 DSGVO, soweit das unter Abschnitt a) genannte Recht voraussichtlich die Verwirklichung der Ziele dieser Verarbeitung unmöglich macht oder ernsthaft beeinträchtigt, oder (5) zur Geltendmachung, Ausübung oder Verteidigung von Rechtsansprüchen. ### Recht auf Unterrichtung Haben Sie das Recht auf Berichtigung, Löschung oder Einschränkung der Verarbeitung gegenüber dem Verantwortlichen geltend gemacht, ist dieser verpflichtet, allen Empfängern, denen die Sie betreffenden personenbezogenen Daten offengelegt wurden, diese Berichtigung oder Löschung der Daten oder Einschränkung der Verarbeitung mitzuteilen, es sei denn, dies erweist sich als unmöglich oder ist mit einem unverhältnismäßigen Aufwand verbunden. Ihnen steht gegenüber dem Verantwortlichen das Recht zu, über diese Empfänger unterrichtet zu werden. ### Recht auf Datenübertragbarkeit Sie haben das Recht, die Sie betreffenden personenbezogenen Daten, die Sie dem Verantwortlichen bereitgestellt haben, in einem strukturierten, gängigen und maschinenlesbaren Format zu erhalten. Außerdem haben Sie das Recht diese Daten einem anderen Verantwortlichen ohne Behinderung durch den Verantwortlichen, dem die personenbezogenen Daten bereitgestellt wurden, zu übermitteln, sofern (1) die Verarbeitung auf einer Einwilligung gem. Art. 6 Abs. 1 lit. a DSGVO oder Art. 9 Abs. 2 lit. a DSGVO oder auf einem Vertrag gem. Art. 6 Abs. 1 lit. b DSGVO beruht und (2) die Verarbeitung mithilfe automatisierter Verfahren erfolgt. In Ausübung dieses Rechts haben Sie ferner das Recht, zu erwirken, dass die Sie betreffenden personenbezogenen Daten direkt von einem Verantwortlichen einem anderen Verantwortlichen übermittelt werden, soweit dies technisch machbar ist. Freiheiten und Rechte anderer Personen dürfen hierdurch nicht beeinträchtigt werden. Das Recht auf Datenübertragbarkeit gilt nicht für eine Verarbeitung personenbezogener Daten, die für die Wahrnehmung einer Aufgabe erforderlich ist, die im öffentlichen Interesse liegt oder in Ausübung öffentlicher Gewalt erfolgt, die dem Verantwortlichen übertragen wurde. ### Widerspruchsrecht Sie haben das Recht, aus Gründen, die sich aus ihrer besonderen Situation ergeben, jederzeit gegen die Verarbeitung der Sie betreffenden personenbezogenen Daten, die aufgrund von Art. 6 Abs. 1 lit. e oder f DSGVO erfolgt, Widerspruch einzulegen; dies gilt auch für ein auf diese Bestimmungen gestütztes Profiling. Der Verantwortliche verarbeitet die Sie betreffenden personenbezogenen Daten nicht mehr, es sei denn, er kann zwingende schutzwürdige Gründe für die Verarbeitung nachweisen, die Ihre Interessen, Rechte und Freiheiten überwiegen, oder die Verarbeitung dient der Geltendmachung, Ausübung oder Verteidigung von Rechtsansprüchen. Werden die Sie betreffenden personenbezogenen Daten verarbeitet, um Direktwerbung zu betreiben, haben Sie das Recht, jederzeit Widerspruch gegen die Verarbeitung der Sie betreffenden personenbezogenen Daten zum Zwecke derartiger Werbung einzulegen; dies gilt auch für das Profiling, soweit es mit solcher Direktwerbung in Verbindung steht. Widersprechen Sie der Verarbeitung für Zwecke der Direktwerbung, so werden die Sie betreffenden personenbezogenen Daten nicht mehr für diese Zwecke verarbeitet. Sie haben die Möglichkeit, im Zusammenhang mit der Nutzung von Diensten der Informationsgesellschaft – ungeachtet der Richtlinie 2002/58/EG – Ihr Widerspruchsrecht mittels automatisierter Verfahren auszuüben, bei denen technische Spezifikationen verwendet werden. ### Recht auf Widerruf der datenschutzrechtlichen Einwilligungserklärung Sie haben das Recht, Ihre datenschutzrechtliche Einwilligungserklärung jederzeit zu widerrufen. Durch den Widerruf der Einwilligung wird die Rechtmäßigkeit der aufgrund der Einwilligung bis zum Widerruf erfolgten Verarbeitung nicht berührt. ### Automatisierte Entscheidung im Einzelfall einschließlich Profiling Sie haben das Recht, nicht einer ausschließlich auf einer automatisierten Verarbeitung – einschließlich Profiling – beruhenden Entscheidung unterworfen zu werden, die Ihnen gegenüber rechtliche Wirkung entfaltet oder Sie in ähnlicher Weise erheblich beeinträchtigt. Dies gilt nicht, wenn die Entscheidung (1) für den Abschluss oder die Erfüllung eines Vertrags zwischen Ihnen und dem Verantwortlichen erforderlich ist, (2) aufgrund von Rechtsvorschriften der Union oder der Mitgliedstaaten, denen der Verantwortliche unterliegt, zulässig ist und diese Rechtsvorschriften angemessene Maßnahmen zur Wahrung Ihrer Rechte und Freiheiten sowie Ihrer berechtigten Interessen enthalten oder (3) mit Ihrer ausdrücklichen Einwilligung erfolgt. Allerdings dürfen diese Entscheidungen nicht auf besonderen Kategorien personenbezogener Daten nach Art. 9 Abs. 1 DSGVO beruhen, sofern nicht Art. 9 Abs. 2 lit. a oder g DSGVO gilt und angemessene Maßnahmen zum Schutz der Rechte und Freiheiten sowie Ihrer berechtigten Interessen getroffen wurden. Hinsichtlich der in (1) und (3) genannten Fälle trifft der Verantwortliche angemessene Maßnahmen, um die Rechte und Freiheiten sowie Ihre berechtigten Interessen zu wahren, wozu mindestens das Recht auf Erwirkung des Eingreifens einer Person seitens des Verantwortlichen, auf Darlegung des eigenen Standpunkts und auf Anfechtung der Entscheidung gehört. ### Recht auf Beschwerde bei einer Aufsichtsbehörde Unbeschadet eines anderweitigen verwaltungsrechtlichen oder gerichtlichen Rechtsbehelfs steht Ihnen das Recht auf Beschwerde bei einer Aufsichtsbehörde, insbesondere in dem Mitgliedstaat ihres Aufenthaltsorts, ihres Arbeitsplatzes oder des Orts des mutmaßlichen Verstoßes, zu, wenn Sie der Ansicht sind, dass die Verarbeitung der Sie betreffenden personenbezogenen Daten gegen die DSGVO verstößt. Die Aufsichtsbehörde, bei der die Beschwerde eingereicht wurde, unterrichtet den Beschwerdeführer über den Stand und die Ergebnisse der Beschwerde, einschließlich der Möglichkeit eines gerichtlichen Rechtsbehelfs nach Art. 78 DSGVO. ================================================ FILE: spring-boot-admin-docs/src/site/src/propertiesUtil.ts ================================================ /** * 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[], includeOnly: boolean = false ) => { if (!includeOnly) { return properties.filter(property => !containsKeywordIgnoreCase(property.name, keywords)); } return properties.filter(property => containsKeywordIgnoreCase(property.name, keywords)); }; function containsKeywordIgnoreCase(str: string, keywords: string[]): boolean { const searchContext = str.toLowerCase(); return keywords.some(keyword => { const searchTerm = keyword.toLowerCase(); const isIncluded = searchContext.includes(searchTerm); return isIncluded; }); } ================================================ FILE: spring-boot-admin-docs/src/site/src/theme/DocCard/index.js ================================================ import React from "react"; import clsx from "clsx"; import Link from "@docusaurus/Link"; import { findFirstSidebarItemLink, useDocById } from "@docusaurus/plugin-content-docs/client"; import { usePluralForm } from "@docusaurus/theme-common"; 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"; const ICON_MAP = { 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(); return (count) => selectMessage( count, translate( { message: "1 item|{count} items", id: "theme.docs.DocCard.categoryDescription.plurals", description: "The default description for a category card in the generated index about how many items this category includes" }, { count } ) ); } function CardContainer({ href, children }) { return ( {children} ); } function CardLayout({ href, icon, title, description }) { return ( {icon} {title} {description && (

{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(); // Unexpected: categories that don't have a link have been filtered upfront if (!href) { return null; } return ( ); } function CardLink({ item }) { const doc = useDocById(item.docId ?? undefined); return ( <> ); } ================================================ FILE: spring-boot-admin-docs/src/site/src/theme/DocCard/styles.module.css ================================================ .cardContainer { --ifm-link-color: var(--ifm-color-emphasis-800); --ifm-link-hover-color: var(--ifm-color-emphasis-700); --ifm-link-hover-decoration: none; box-shadow: 0 1.5px 3px 0 rgb(0 0 0 / 15%); border: 1px solid var(--ifm-color-emphasis-200); transition: all var(--ifm-transition-fast) ease; transition-property: border, box-shadow; } .cardContainer:hover { border-color: var(--ifm-color-primary); box-shadow: 0 3px 6px 0 rgb(0 0 0 / 20%); } .cardContainer *:last-child { margin-bottom: 0; } .cardTitle { font-size: 1.2rem; display: inline-flex; gap: 0.25rem; } .cardDescription { font-size: 0.8rem; } ================================================ FILE: spring-boot-admin-docs/src/site/src/theme/MDXComponents.ts ================================================ import MDXComponents from '@theme-original/MDXComponents'; import { Icon } from '@iconify/react'; export default { // Re-use the default mapping ...MDXComponents, IIcon: Icon, // Make the iconify Icon component available in MDX as . }; ================================================ FILE: spring-boot-admin-docs/src/site/static/.nojekyll ================================================ ================================================ FILE: spring-boot-admin-docs/src/site/tsconfig.json ================================================ { // This file is not used in compilation. It is here just for a nice editor experience. "extends": "@docusaurus/tsconfig", "compilerOptions": { "target": "es2023", "lib": [ "es2023", "dom" ], "baseUrl": ".", "paths": { "@sba": [ "/../../.." ] } }, "include": [ "**/*.mdx" ] } ================================================ FILE: spring-boot-admin-samples/pom.xml ================================================ 4.0.0 spring-boot-admin-samples pom Spring Boot Admin Samples Spring Boot Admin Samples de.codecentric spring-boot-admin-build ${revision} ../spring-boot-admin-build spring-boot-admin-sample-custom-ui spring-boot-admin-sample-servlet spring-boot-admin-sample-reactive spring-boot-admin-sample-war spring-boot-admin-sample-hazelcast de.codecentric spring-boot-admin-sample-custom-ui ${revision} ${project.basedir}/src/main/resources true **/application*.yml **/application*.yaml **/application*.properties ${project.basedir}/src/main/resources **/application*.yml **/application*.yaml **/application*.properties org.apache.maven.plugins maven-javadoc-plugin none include-cloud !excludeSpringCloud spring-boot-admin-sample-eureka spring-boot-admin-sample-consul spring-boot-admin-sample-zookeeper travis env.TRAVIS true org.jacoco jacoco-maven-plugin true ================================================ FILE: spring-boot-admin-samples/spring-boot-admin-sample-consul/docker-compose.yml ================================================ services: consul: image: hashicorp/consul ports: - "8500:8500" ================================================ FILE: spring-boot-admin-samples/spring-boot-admin-sample-consul/pom.xml ================================================ 4.0.0 spring-boot-admin-sample-consul Spring Boot Admin Sample Consul Spring Boot Admin Sample using Consul de.codecentric spring-boot-admin-samples ${revision} ../pom.xml org.springframework.boot spring-boot-starter-webmvc org.springframework.boot spring-boot-starter-security de.codecentric spring-boot-admin-starter-server org.springframework.cloud spring-cloud-starter-consul-discovery org.springframework.boot spring-boot-starter-test test ${project.artifactId} org.springframework.boot spring-boot-maven-plugin repackage build-info de.codecentric.boot.admin.sample.SpringBootAdminConsulApplication false ================================================ FILE: spring-boot-admin-samples/spring-boot-admin-sample-consul/src/main/java/de/codecentric/boot/admin/sample/SpringBootAdminConsulApplication.java ================================================ /* * 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.sample; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.client.discovery.EnableDiscoveryClient; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Profile; import org.springframework.security.config.Customizer; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler; import org.springframework.security.web.csrf.CookieCsrfTokenRepository; 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; @SpringBootApplication @EnableDiscoveryClient @EnableAdminServer public class SpringBootAdminConsulApplication { public static void main(String[] args) { SpringApplication.run(SpringBootAdminConsulApplication.class, args); } @Profile("insecure") @Configuration(proxyBeanMethods = false) public static class SecurityPermitAllConfig { private final String adminContextPath; public SecurityPermitAllConfig(AdminServerProperties adminServerProperties) { this.adminContextPath = adminServerProperties.getContextPath(); } @Bean protected SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http.authorizeHttpRequests((authorizeRequests) -> authorizeRequests.anyRequest().permitAll()) .csrf((csrf) -> csrf.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()) .ignoringRequestMatchers( PathPatternRequestMatcher.withDefaults() .matcher(POST, this.adminContextPath + "/instances"), PathPatternRequestMatcher.withDefaults() .matcher(DELETE, this.adminContextPath + "/instances/*"), PathPatternRequestMatcher.withDefaults().matcher(this.adminContextPath + "/actuator/**"))); return http.build(); } } @Profile("secure") @Configuration(proxyBeanMethods = false) public static class SecuritySecureConfig { private final String adminContextPath; public SecuritySecureConfig(AdminServerProperties adminServerProperties) { this.adminContextPath = adminServerProperties.getContextPath(); } @Bean protected SecurityFilterChain filterChain(HttpSecurity http) throws Exception { SavedRequestAwareAuthenticationSuccessHandler successHandler = new SavedRequestAwareAuthenticationSuccessHandler(); successHandler.setTargetUrlParameter("redirectTo"); successHandler.setDefaultTargetUrl(this.adminContextPath + "/"); http.authorizeHttpRequests((authorizeRequests) -> authorizeRequests .requestMatchers(PathPatternRequestMatcher.withDefaults().matcher(this.adminContextPath + "/assets/**")) .permitAll() .requestMatchers(PathPatternRequestMatcher.withDefaults().matcher(this.adminContextPath + "/login")) .permitAll() .anyRequest() .authenticated()) .formLogin((formLogin) -> formLogin.loginPage(this.adminContextPath + "/login") .successHandler(successHandler)) .logout((logout) -> logout.logoutUrl(this.adminContextPath + "/logout")) .httpBasic(Customizer.withDefaults()) .csrf((csrf) -> csrf.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()) .ignoringRequestMatchers( PathPatternRequestMatcher.withDefaults() .matcher(POST, this.adminContextPath + "/instances"), PathPatternRequestMatcher.withDefaults() .matcher(DELETE, this.adminContextPath + "/instances/*"), PathPatternRequestMatcher.withDefaults().matcher(this.adminContextPath + "/actuator/**"))); return http.build(); } } } ================================================ FILE: spring-boot-admin-samples/spring-boot-admin-sample-consul/src/main/resources/application-dev.yml ================================================ spring: boot: admin: ui: cache: no-cache: true template-location: file:../../spring-boot-admin-server-ui/target/dist/ resource-locations: file:../../spring-boot-admin-server-ui/target/dist/ cache-templates: false ================================================ FILE: spring-boot-admin-samples/spring-boot-admin-sample-consul/src/main/resources/application-insecure.yml ================================================ info.tags.security: insecure ================================================ FILE: spring-boot-admin-samples/spring-boot-admin-sample-consul/src/main/resources/application-secure.yml ================================================ spring: security: user: name: "user" password: "password" ================================================ FILE: spring-boot-admin-samples/spring-boot-admin-sample-consul/src/main/resources/application.yml ================================================ spring: application: name: consul-example cloud: config: enabled: false consul: host: localhost port: 8500 discovery: metadata: management-context-path: /foo health-path: /ping user-name: user user-password: password profiles: active: - secure boot: admin: discovery: ignored-services: consul management: endpoints: web: exposure: include: "*" path-mapping: health: /ping base-path: /foo endpoint: health: show-details: ALWAYS ================================================ FILE: spring-boot-admin-samples/spring-boot-admin-sample-consul/src/test/java/de/codecentric/boot/admin/sample/SpringBootAdminConsulApplicationTest.java ================================================ /* * 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.sample; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.context.junit.jupiter.SpringExtension; @ExtendWith(SpringExtension.class) @SpringBootTest(classes = { SpringBootAdminConsulApplication.class }, properties = { "spring.cloud.consul.enabled=false" }) class SpringBootAdminConsulApplicationTest { @Test void contextLoads() { } } ================================================ FILE: spring-boot-admin-samples/spring-boot-admin-sample-custom-ui/.gitignore ================================================ .DS_Store node_modules /dist # local env files .env.local .env.*.local # Log files npm-debug.log* yarn-debug.log* yarn-error.log* # Editor directories and files .idea .vscode *.suo *.ntvs* *.njsproj *.sln *.sw* ================================================ FILE: spring-boot-admin-samples/spring-boot-admin-sample-custom-ui/README.md ================================================ spring-boot-admin-sample-custom-ui ================================ ### Building this module The jar **can be build with Maven** using the frontend-maven-plugin. This will download node.js and npm automatically. If you don't want to use the maven exec run the following commands: ### Running Spring Boot Admin Server for development To develop the ui on an running server the best to do is 1. Running the ui build in watch mode so the resources get updated: ```shell npm run build:watch ``` 2. Run a Spring Boot Admin Server instances with the template-location and resource-location pointing to the build output and disable caching: ``` spring.boot.admin.ui.cache.no-cache: true spring.boot.admin.ui.extension-resource-locations: file:../spring-boot-admin-sample-custom-ui/target/dist/ spring.boot.admin.ui.cache-templates: false ``` Or just start the [spring-boot-admin-sample-servlet](../spring-boot-admin-sample-servlet) project using the `dev` profile. ### Build ```shell npm install npm run build ``` Repeated build with watching the files: ```shell npm run build:watch ``` ================================================ FILE: spring-boot-admin-samples/spring-boot-admin-sample-custom-ui/babel.config.js ================================================ module.exports = { presets: [ '@vue/cli-plugin-babel/preset' ] } ================================================ FILE: spring-boot-admin-samples/spring-boot-admin-sample-custom-ui/package.json ================================================ { "name": "spring-boot-admin-sample-custom-ui", "version": "0.0.0", "private": true, "type": "module", "scripts": { "build": "vite build", "build:dev": "NODE_ENV=development vite build --emptyOutDir --sourcemap --mode development", "build:watch": "NODE_ENV=development vite build --emptyOutDir --watch --sourcemap --mode development", "preview": "vite preview" }, "dependencies": { "vue": "3.5.30" }, "devDependencies": { "@vitejs/plugin-vue": "6.0.5", "vite": "7.3.1", "vite-plugin-static-copy": "^3.0.0" }, "browserslist": [ "> 1%", "last 2 versions", "not ie <= 8" ] } ================================================ FILE: spring-boot-admin-samples/spring-boot-admin-sample-custom-ui/pom.xml ================================================ 4.0.0 spring-boot-admin-sample-custom-ui Spring Boot Admin Server custom UI Spring Boot Admin Server custom UI de.codecentric spring-boot-admin-samples ${revision} ../pom.xml com.github.eirslett frontend-maven-plugin install-node-and-npm install-node-and-npm ${node.version} npm-install npm ci --prefer-offline --no-progress --no-audit --silent npm-build npm run build ${project.version} 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 false ================================================ FILE: spring-boot-admin-samples/spring-boot-admin-sample-custom-ui/src/custom-endpoint.vue ================================================ ================================================ FILE: spring-boot-admin-samples/spring-boot-admin-sample-custom-ui/src/custom-subitem.vue ================================================ ================================================ FILE: spring-boot-admin-samples/spring-boot-admin-sample-custom-ui/src/custom.css ================================================ .mx-1 { margin-left: .5rem; margin-right: .5rem; } .m-4 { margin: 2rem; } ================================================ FILE: spring-boot-admin-samples/spring-boot-admin-sample-custom-ui/src/custom.vue ================================================ ================================================ FILE: spring-boot-admin-samples/spring-boot-admin-sample-custom-ui/src/handle.vue ================================================ ================================================ FILE: spring-boot-admin-samples/spring-boot-admin-sample-custom-ui/src/index.js ================================================ /* * Copyright 2014-2020 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. */ /* global SBA */ import customEndpoint from "./custom-endpoint.vue"; import customSubitem from "./custom-subitem.vue"; import custom from "./custom.vue"; import handle from "./handle.vue"; // tag::customization-ui-toplevel[] 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", }, }); }, }); // end::customization-ui-toplevel[] // tag::customization-ui-child[] SBA.viewRegistry.addView({ name: "customSub", parent: "custom", // <1> path: "/customSub", // <2> component: customSubitem, label: "Custom Sub", order: 1000, }); // end::customization-ui-child[] SBA.viewRegistry.addView({ name: "customSubUser", parent: "user", // <1> path: "/customSub", // <2> component: customSubitem, label: "Custom Sub In Usermenu", order: 1000, }); // tag::customization-ui-endpoint[] 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> }); // end::customization-ui-endpoint[] // tag::customization-ui-groups[] SBA.viewRegistry.setGroupIcon( "custom", //<1> ` ` //<2> ); // end::customization-ui-groups[] ================================================ FILE: spring-boot-admin-samples/spring-boot-admin-sample-custom-ui/src/routes.txt ================================================ /custom/** /customSub/** ================================================ FILE: spring-boot-admin-samples/spring-boot-admin-sample-custom-ui/vite.config.js ================================================ import vue from "@vitejs/plugin-vue"; import path from "path"; import { defineConfig } from "vite"; import { viteStaticCopy } from "vite-plugin-static-copy"; export default defineConfig({ plugins: [ vue(), viteStaticCopy({ targets: [ { src: "src/routes.txt", dest: "./", }, ], }), ], build: { target: "es2015", sourcemap: true, minify: false, outDir: "target/dist", lib: { entry: path.resolve(__dirname, "src/index.js"), name: "CustomUi", formats: ["umd"], fileName: () => "custom-ui.js", }, define: { __VUE_PROD_DEVTOOLS__: process.env.NODE_ENV === "development", }, rollupOptions: { external: ["vue"], output: { globals: { vue: "Vue", }, }, }, }, }); ================================================ FILE: spring-boot-admin-samples/spring-boot-admin-sample-eureka/docker-compose.yml ================================================ version: '2' services: eureka: image: springcloud/eureka container_name: eureka ports: - "8761:8761" networks: - "discovery" environment: - EUREKA_INSTANCE_PREFERIPADDRESS=true admin: build: context: . dockerfile: ./src/main/docker/Dockerfile depends_on: - eureka container_name: admin ports: - "8080:8080" networks: - "discovery" environment: - EUREKA_SERVICE_URL=http://eureka:8761 - EUREKA_INSTANCE_PREFER_IP_ADDRESS=true - LOGGING_FILE=/tmp/admin.log config: image: springcloud/configserver container_name: config depends_on: - eureka ports: - "8888:8888" networks: - "discovery" environment: - EUREKA_SERVICE_URL=http://eureka:8761 - EUREKA_INSTANCE_PREFER_IP_ADDRESS=true customers: image: springcloud/customers depends_on: - config - rabbit networks: - "discovery" environment: - EUREKA_INSTANCE_PREFER_IP_ADDRESS=true - CONFIG_SERVER_URI=http://config:8888 - RABBITMQ_HOST=rabbit - RABBITMQ_PORT=5672 stores: image: springcloud/stores depends_on: - config - rabbit - mongodb networks: - "discovery" environment: - EUREKA_INSTANCE_PREFER_IP_ADDRESS=true - CONFIG_SERVER_URI=http://config:8888 - RABBITMQ_HOST=rabbit - RABBITMQ_PORT=5672 - MONGODB_HOST=mongodb - MONGODB_PORT=27017 customersui: image: springcloud/customersui depends_on: - config - customers - stores ports: - "80:80" links: - "config" networks: - "discovery" environment: - SERVER_PORT=80 - EUREKA_INSTANCE_PREFER_IP_ADDRESS=true - CONFIG_SERVER_URI=http://config:8888 mongodb: image: tutum/mongodb container_name: mongodb ports: - "27017:27017" networks: - "discovery" environment: - AUTH=no rabbit: image: "rabbitmq:4" container_name: rabbit ports: - "5672:5672" networks: - "discovery" networks: discovery: ================================================ FILE: spring-boot-admin-samples/spring-boot-admin-sample-eureka/pom.xml ================================================ 4.0.0 spring-boot-admin-sample-eureka Spring Boot Admin Sample Eureka Spring Boot Admin Sample using Eureka de.codecentric spring-boot-admin-samples ${revision} ../pom.xml de.codecentric spring-boot-admin-starter-server org.springframework.boot spring-boot-starter-webmvc org.springframework.boot spring-boot-starter-tomcat org.springframework.boot spring-boot-starter-security org.springframework.cloud spring-cloud-starter-netflix-eureka-client org.springframework.boot spring-boot-starter-test test ${project.artifactId} org.springframework.boot spring-boot-maven-plugin repackage build-info de.codecentric.boot.admin.SpringBootAdminEurekaApplication false ================================================ FILE: spring-boot-admin-samples/spring-boot-admin-sample-eureka/src/main/docker/Dockerfile ================================================ FROM eclipse-temurin:25 VOLUME /tmp ADD target/spring-boot-admin-sample-eureka.jar /app.jar RUN bash -c 'touch /app.jar' EXPOSE 8080 ENTRYPOINT ["java","-jar","/app.jar"] ================================================ FILE: spring-boot-admin-samples/spring-boot-admin-sample-eureka/src/main/java/de/codecentric/boot/admin/SpringBootAdminEurekaApplication.java ================================================ /* * 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; import java.net.URI; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.cloud.client.discovery.EnableDiscoveryClient; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Profile; import org.springframework.security.config.Customizer; import org.springframework.security.config.web.server.ServerHttpSecurity; import org.springframework.security.web.server.SecurityWebFilterChain; import org.springframework.security.web.server.authentication.RedirectServerAuthenticationSuccessHandler; import org.springframework.security.web.server.authentication.ServerAuthenticationSuccessHandler; import org.springframework.security.web.server.authentication.logout.RedirectServerLogoutSuccessHandler; import org.springframework.security.web.server.authentication.logout.ServerLogoutSuccessHandler; import de.codecentric.boot.admin.server.config.AdminServerProperties; import de.codecentric.boot.admin.server.config.EnableAdminServer; @Configuration(proxyBeanMethods = false) @EnableAutoConfiguration @EnableDiscoveryClient @EnableAdminServer 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); } @Bean @Profile("insecure") public SecurityWebFilterChain securityWebFilterChainPermitAll(ServerHttpSecurity http) { return http.authorizeExchange((authorizeExchange) -> authorizeExchange.anyExchange().permitAll()) .csrf(ServerHttpSecurity.CsrfSpec::disable) .build(); } @Bean @Profile("secure") public SecurityWebFilterChain securityWebFilterChainSecure(ServerHttpSecurity http) { return http .authorizeExchange( (authorizeExchange) -> authorizeExchange.pathMatchers(this.adminServer.path("/assets/**")) .permitAll() .pathMatchers("/actuator/health/**") .permitAll() .pathMatchers(this.adminServer.path("/login")) .permitAll() .anyExchange() .authenticated()) .formLogin((formLogin) -> formLogin.loginPage(this.adminServer.path("/login")) .authenticationSuccessHandler(loginSuccessHandler(this.adminServer.path("/")))) .logout((logout) -> logout.logoutUrl(this.adminServer.path("/logout")) .logoutSuccessHandler(logoutSuccessHandler(this.adminServer.path("/login?logout")))) .httpBasic(Customizer.withDefaults()) .csrf(ServerHttpSecurity.CsrfSpec::disable) .build(); } // The following two methods are only required when setting a custom base-path (see // 'basepath' profile in application.yml) private ServerLogoutSuccessHandler logoutSuccessHandler(String uri) { RedirectServerLogoutSuccessHandler successHandler = new RedirectServerLogoutSuccessHandler(); successHandler.setLogoutSuccessUrl(URI.create(uri)); return successHandler; } private ServerAuthenticationSuccessHandler loginSuccessHandler(String uri) { RedirectServerAuthenticationSuccessHandler successHandler = new RedirectServerAuthenticationSuccessHandler(); successHandler.setLocation(URI.create(uri)); return successHandler; } } ================================================ FILE: spring-boot-admin-samples/spring-boot-admin-sample-eureka/src/main/resources/application-dev.yml ================================================ spring: boot: admin: ui: cache: no-cache: true template-location: file:../../spring-boot-admin-server-ui/target/dist/ resource-locations: file:../../spring-boot-admin-server-ui/target/dist/ cache-templates: false ================================================ FILE: spring-boot-admin-samples/spring-boot-admin-sample-eureka/src/main/resources/application-insecure.yml ================================================ info.tags.security: insecure ================================================ FILE: spring-boot-admin-samples/spring-boot-admin-sample-eureka/src/main/resources/application-secure.yml ================================================ spring: security: user: name: "user" password: "password" boot: admin: client: username: "user" #These two are needed so that the client password: "password" #can register at the protected server api instance: metadata: user.name: "user" #These two are needed so that the server user.password: "password" #can access the protected client endpoints ================================================ FILE: spring-boot-admin-samples/spring-boot-admin-sample-eureka/src/main/resources/application.yml ================================================ spring: application: name: spring-boot-admin-sample-eureka profiles: active: - secure # tag::configuration-eureka[] eureka: #<1> 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: ${EUREKA_SERVICE_URL:http://localhost:8761}/eureka/ management: endpoints: web: exposure: include: "*" #<2> endpoint: health: show-details: ALWAYS # end::configuration-eureka[] ================================================ FILE: spring-boot-admin-samples/spring-boot-admin-sample-eureka/src/test/java/de/codecentric/boot/admin/SpringBootAdminEurekaApplicationTest.java ================================================ /* * 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; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.context.junit.jupiter.SpringExtension; @ExtendWith(SpringExtension.class) @SpringBootTest(classes = { SpringBootAdminEurekaApplication.class }) class SpringBootAdminEurekaApplicationTest { @Test void contextLoads() { } } ================================================ FILE: spring-boot-admin-samples/spring-boot-admin-sample-hazelcast/pom.xml ================================================ 4.0.0 spring-boot-admin-sample-hazelcast Spring Boot Admin Sample Hazelcast Spring Boot Admin Sample using Hazelcast de.codecentric spring-boot-admin-samples ${revision} ../pom.xml org.springframework.boot spring-boot-starter-webmvc org.springframework.boot spring-boot-starter-security de.codecentric spring-boot-admin-starter-server de.codecentric spring-boot-admin-starter-client com.hazelcast hazelcast org.springframework.boot spring-boot-starter-test test ${project.artifactId} org.springframework.boot spring-boot-maven-plugin repackage build-info de.codecentric.boot.admin.sample.SpringBootAdminHazelcastApplication false ================================================ FILE: spring-boot-admin-samples/spring-boot-admin-sample-hazelcast/src/main/java/de/codecentric/boot/admin/sample/SpringBootAdminHazelcastApplication.java ================================================ /* * 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.sample; import com.hazelcast.config.Config; import com.hazelcast.config.EvictionConfig; import com.hazelcast.config.EvictionPolicy; import com.hazelcast.config.InMemoryFormat; import com.hazelcast.config.MapConfig; import com.hazelcast.config.MaxSizePolicy; import com.hazelcast.config.MergePolicyConfig; import com.hazelcast.config.TcpIpConfig; import com.hazelcast.spi.merge.PutIfAbsentMergePolicy; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Profile; import org.springframework.security.config.Customizer; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler; import org.springframework.security.web.csrf.CookieCsrfTokenRepository; import org.springframework.security.web.servlet.util.matcher.PathPatternRequestMatcher; import reactor.core.publisher.Mono; import de.codecentric.boot.admin.server.config.AdminServerProperties; import de.codecentric.boot.admin.server.config.EnableAdminServer; import de.codecentric.boot.admin.server.notify.Notifier; import static de.codecentric.boot.admin.server.config.AdminServerHazelcastAutoConfiguration.DEFAULT_NAME_EVENT_STORE_MAP; import static de.codecentric.boot.admin.server.config.AdminServerHazelcastAutoConfiguration.DEFAULT_NAME_SENT_NOTIFICATIONS_MAP; import static java.util.Collections.singletonList; import static org.springframework.http.HttpMethod.DELETE; import static org.springframework.http.HttpMethod.POST; @SpringBootApplication @EnableAdminServer public class SpringBootAdminHazelcastApplication { private static final Logger log = LoggerFactory.getLogger(SpringBootAdminHazelcastApplication.class); public static void main(String[] args) { SpringApplication.run(SpringBootAdminHazelcastApplication.class, args); } // tag::application-hazelcast[] @Bean public Config hazelcastConfig() { // This map is used to store the events. // It should be configured to reliably hold all the data, // Spring Boot Admin will compact the events, if there are too many MapConfig eventStoreMap = new MapConfig(DEFAULT_NAME_EVENT_STORE_MAP).setInMemoryFormat(InMemoryFormat.OBJECT) .setBackupCount(1) .setMergePolicyConfig(new MergePolicyConfig(PutIfAbsentMergePolicy.class.getName(), 100)); // This map is used to deduplicate the notifications. // If data in this map gets lost it should not be a big issue as it will utmost // lead to // the same notification to be sent by multiple instances MapConfig sentNotificationsMap = new MapConfig(DEFAULT_NAME_SENT_NOTIFICATIONS_MAP) .setInMemoryFormat(InMemoryFormat.OBJECT) .setBackupCount(1) .setEvictionConfig( new EvictionConfig().setEvictionPolicy(EvictionPolicy.LRU).setMaxSizePolicy(MaxSizePolicy.PER_NODE)) .setMergePolicyConfig(new MergePolicyConfig(PutIfAbsentMergePolicy.class.getName(), 100)); Config config = new Config(); config.addMapConfig(eventStoreMap); config.addMapConfig(sentNotificationsMap); config.setProperty("hazelcast.jmx", "true"); // WARNING: This setups a local cluster, you change it to fit your needs. config.getNetworkConfig().getJoin().getMulticastConfig().setEnabled(false); TcpIpConfig tcpIpConfig = config.getNetworkConfig().getJoin().getTcpIpConfig(); tcpIpConfig.setEnabled(true); tcpIpConfig.setMembers(singletonList("127.0.0.1")); return config; } // end::application-hazelcast[] @Bean public Notifier loggingNotifier() { return (event) -> Mono.fromRunnable(() -> log.info("Event occurred: {}", event)); } @Profile("insecure") @Configuration(proxyBeanMethods = false) public static class SecurityPermitAllConfig { private final AdminServerProperties adminServer; public SecurityPermitAllConfig(AdminServerProperties adminServer) { this.adminServer = adminServer; } @Bean protected SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http.authorizeHttpRequests((authorizeRequests) -> authorizeRequests.anyRequest().permitAll()) .csrf((csrf) -> csrf.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()) .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()) .ignoringRequestMatchers( PathPatternRequestMatcher.withDefaults().matcher(POST, this.adminServer.path("/instances")), PathPatternRequestMatcher.withDefaults() .matcher(DELETE, this.adminServer.path("/instances/*")), PathPatternRequestMatcher.withDefaults().matcher(this.adminServer.path("/actuator/**")))); return http.build(); } } @Profile("secure") @Configuration(proxyBeanMethods = false) public static class SecuritySecureConfig { private final AdminServerProperties adminServer; public SecuritySecureConfig(AdminServerProperties adminServer) { this.adminServer = adminServer; } @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(PathPatternRequestMatcher.withDefaults().matcher(this.adminServer.path("/assets/**"))) .permitAll() .requestMatchers(PathPatternRequestMatcher.withDefaults().matcher(this.adminServer.path("/login"))) .permitAll() .anyRequest() .authenticated()) .formLogin((formLogin) -> formLogin.loginPage(this.adminServer.path("/login")) .successHandler(successHandler)) .logout((logout) -> logout.logoutUrl(this.adminServer.path("/logout"))) .httpBasic(Customizer.withDefaults()) .csrf((csrf) -> csrf.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()) .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()) .ignoringRequestMatchers( PathPatternRequestMatcher.withDefaults().matcher(POST, this.adminServer.path("/instances")), PathPatternRequestMatcher.withDefaults() .matcher(DELETE, this.adminServer.path("/instances/*")), PathPatternRequestMatcher.withDefaults().matcher(this.adminServer.path("/actuator/**")))); return http.build(); } } } ================================================ FILE: spring-boot-admin-samples/spring-boot-admin-sample-hazelcast/src/main/resources/application-dev.yml ================================================ spring: boot: admin: ui: cache: no-cache: true template-location: file:../../spring-boot-admin-server-ui/target/dist/ resource-locations: file:../../spring-boot-admin-server-ui/target/dist/ cache-templates: false ================================================ FILE: spring-boot-admin-samples/spring-boot-admin-sample-hazelcast/src/main/resources/application-insecure.yml ================================================ info.tags.security: insecure ================================================ FILE: spring-boot-admin-samples/spring-boot-admin-sample-hazelcast/src/main/resources/application-secure.yml ================================================ spring: security: user: name: "user" password: "password" boot: admin: client: username: "user" #These two are needed so that the client password: "password" #can register at the protected server api instance: metadata: user.name: "user" #These two are needed so that the server user.password: "password" #can access the protected client endpoints ================================================ FILE: spring-boot-admin-samples/spring-boot-admin-sample-hazelcast/src/main/resources/application.yml ================================================ --- logging: file: name: "target/boot-admin-sample-hazelcast.log" management: endpoints: web: exposure: include: "*" endpoint: health: show-details: ALWAYS spring: application: name: spring-boot-admin-sample-hazelcast boot: admin: client: url: http://localhost:${server.port:8080} profiles: active: - insecure ================================================ FILE: spring-boot-admin-samples/spring-boot-admin-sample-hazelcast/src/test/java/de/codecentric/boot/admin/sample/SpringBootAdminHazelcastApplicationTest.java ================================================ /* * 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.sample; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.context.junit.jupiter.SpringExtension; @ExtendWith(SpringExtension.class) @SpringBootTest(classes = { SpringBootAdminHazelcastApplication.class }) class SpringBootAdminHazelcastApplicationTest { @Test void contextLoads() { } } ================================================ FILE: spring-boot-admin-samples/spring-boot-admin-sample-reactive/pom.xml ================================================ 4.0.0 spring-boot-admin-sample-reactive Spring Boot Admin Sample Reactive Spring Boot Admin Sample Reactive de.codecentric spring-boot-admin-samples ${revision} ../pom.xml de.codecentric spring-boot-admin-starter-server org.springframework.boot spring-boot-starter-security de.codecentric spring-boot-admin-starter-client org.springframework.boot spring-boot-starter-test test org.springframework.boot spring-boot-devtools true ${project.artifactId} org.springframework.boot spring-boot-maven-plugin repackage build-info de.codecentric.boot.admin.sample.SpringBootAdminReactiveApplication false ================================================ FILE: spring-boot-admin-samples/spring-boot-admin-sample-reactive/src/main/java/de/codecentric/boot/admin/sample/SpringBootAdminReactiveApplication.java ================================================ /* * 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.sample; import java.net.URI; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Profile; import org.springframework.security.config.Customizer; import org.springframework.security.config.web.server.ServerHttpSecurity; import org.springframework.security.web.server.SecurityWebFilterChain; import org.springframework.security.web.server.authentication.RedirectServerAuthenticationSuccessHandler; import org.springframework.security.web.server.authentication.ServerAuthenticationSuccessHandler; import org.springframework.security.web.server.authentication.logout.RedirectServerLogoutSuccessHandler; import org.springframework.security.web.server.authentication.logout.ServerLogoutSuccessHandler; import reactor.core.publisher.Mono; import de.codecentric.boot.admin.server.config.AdminServerProperties; import de.codecentric.boot.admin.server.config.EnableAdminServer; import de.codecentric.boot.admin.server.notify.Notifier; @SpringBootApplication @EnableAdminServer public class SpringBootAdminReactiveApplication { private final AdminServerProperties adminServer; public SpringBootAdminReactiveApplication(AdminServerProperties adminServer) { this.adminServer = adminServer; } public static void main(String[] args) { SpringApplication.run(SpringBootAdminReactiveApplication.class, args); } @Bean @Profile("insecure") public SecurityWebFilterChain securityWebFilterChainPermitAll(ServerHttpSecurity http) { return http.authorizeExchange((authorizeExchange) -> authorizeExchange.anyExchange().permitAll()) .csrf(ServerHttpSecurity.CsrfSpec::disable) .build(); } @Bean @Profile("secure") public SecurityWebFilterChain securityWebFilterChainSecure(ServerHttpSecurity http) { return http .authorizeExchange( (authorizeExchange) -> authorizeExchange.pathMatchers(this.adminServer.path("/assets/**")) .permitAll() .pathMatchers("/actuator/health/**") .permitAll() .pathMatchers(this.adminServer.path("/login")) .permitAll() .anyExchange() .authenticated()) .formLogin((formLogin) -> formLogin.loginPage(this.adminServer.path("/login")) .authenticationSuccessHandler(loginSuccessHandler(this.adminServer.path("/")))) .logout((logout) -> logout.logoutUrl(this.adminServer.path("/logout")) .logoutSuccessHandler(logoutSuccessHandler(this.adminServer.path("/login?logout")))) .httpBasic(Customizer.withDefaults()) .csrf(ServerHttpSecurity.CsrfSpec::disable) .build(); } // The following two methods are only required when setting a custom base-path (see // 'basepath' profile in application.yml) private ServerLogoutSuccessHandler logoutSuccessHandler(String uri) { RedirectServerLogoutSuccessHandler successHandler = new RedirectServerLogoutSuccessHandler(); successHandler.setLogoutSuccessUrl(URI.create(uri)); return successHandler; } private ServerAuthenticationSuccessHandler loginSuccessHandler(String uri) { RedirectServerAuthenticationSuccessHandler successHandler = new RedirectServerAuthenticationSuccessHandler(); successHandler.setLocation(URI.create(uri)); return successHandler; } @Bean public Notifier notifier() { return (e) -> Mono.empty(); } } ================================================ FILE: spring-boot-admin-samples/spring-boot-admin-sample-reactive/src/main/resources/application-dev.yml ================================================ spring: boot: admin: ui: cache: no-cache: true template-location: file:../../spring-boot-admin-server-ui/target/dist/ resource-locations: file:../../spring-boot-admin-server-ui/target/dist/ cache-templates: false ================================================ FILE: spring-boot-admin-samples/spring-boot-admin-sample-reactive/src/main/resources/application-insecure.yml ================================================ info.tags.security: insecure ================================================ FILE: spring-boot-admin-samples/spring-boot-admin-sample-reactive/src/main/resources/application-secure.yml ================================================ spring: security: user: name: "user" password: "password" boot: admin: client: username: "user" #These two are needed so that the client password: "password" #can register at the protected server api instance: metadata: user.name: "user" #These two are needed so that the server user.password: "password" #can access the protected client endpoints ================================================ FILE: spring-boot-admin-samples/spring-boot-admin-sample-reactive/src/main/resources/application.yml ================================================ info: scm-url: "@scm.url@" build-url: "https://travis-ci.org/codecentric/spring-boot-admin" logging: file: name: "target/boot-admin-sample-reactive.log" management: endpoints: web: exposure: include: "*" endpoint: health: show-details: ALWAYS spring: application: name: spring-boot-admin-sample-reactive boot: admin: client: url: http://localhost:8080 profiles: active: - insecure ================================================ FILE: spring-boot-admin-samples/spring-boot-admin-sample-reactive/src/test/java/de/codecentric/boot/admin/sample/SpringBootAdminReactiveApplicationTest.java ================================================ /* * 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.sample; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.context.junit.jupiter.SpringExtension; @ExtendWith(SpringExtension.class) @SpringBootTest(classes = { SpringBootAdminReactiveApplication.class }) class SpringBootAdminReactiveApplicationTest { @Test void contextLoads() { } } ================================================ FILE: spring-boot-admin-samples/spring-boot-admin-sample-servlet/pom.xml ================================================ 4.0.0 spring-boot-admin-sample-servlet Spring Boot Admin Sample Servlet Spring Boot Admin Sample Servlet de.codecentric spring-boot-admin-samples ${revision} ../pom.xml de.codecentric spring-boot-admin-sample-custom-ui de.codecentric spring-boot-admin-starter-server org.springframework.boot spring-boot-starter-security org.springframework.boot spring-boot-starter-webmvc org.springframework.cloud spring-cloud-starter org.springframework.boot spring-boot-starter-mail de.codecentric spring-boot-admin-starter-client org.springframework.session spring-session-core org.springframework.session spring-session-jdbc org.springframework.cloud spring-cloud-starter-config org.hsqldb hsqldb org.jolokia jolokia-support-springboot org.springframework.boot spring-boot-starter-test test ${project.artifactId} org.cyclonedx cyclonedx-maven-plugin generate-resources makeAggregateBom application ${project.build.outputDirectory} bom json true org.springframework.boot spring-boot-maven-plugin repackage build-info de.codecentric.boot.admin.sample.SpringBootAdminServletApplication false ================================================ FILE: spring-boot-admin-samples/spring-boot-admin-sample-servlet/src/main/java/de/codecentric/boot/admin/sample/CustomCsrfFilter.java ================================================ /* * 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.sample; 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); } } ================================================ FILE: spring-boot-admin-samples/spring-boot-admin-sample-servlet/src/main/java/de/codecentric/boot/admin/sample/CustomEndpoint.java ================================================ /* * 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.sample; import org.springframework.boot.actuate.endpoint.annotation.Endpoint; import org.springframework.boot.actuate.endpoint.annotation.ReadOperation; @Endpoint(id = "custom") public class CustomEndpoint { @ReadOperation public String invoke() { return "Hello World!"; } } ================================================ FILE: spring-boot-admin-samples/spring-boot-admin-sample-servlet/src/main/java/de/codecentric/boot/admin/sample/CustomNotifier.java ================================================ /* * 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.sample; import org.slf4j.Logger; import org.slf4j.LoggerFactory; 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.events.InstanceEvent; import de.codecentric.boot.admin.server.domain.events.InstanceStatusChangedEvent; import de.codecentric.boot.admin.server.notify.AbstractEventNotifier; // tag::customization-notifiers[] 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()); } }); } } // end::customization-notifiers[] ================================================ FILE: spring-boot-admin-samples/spring-boot-admin-sample-servlet/src/main/java/de/codecentric/boot/admin/sample/NotifierConfig.java ================================================ /* * 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.sample; import java.time.Duration; import java.util.Collections; import java.util.List; import org.springframework.beans.factory.ObjectProvider; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Primary; import de.codecentric.boot.admin.server.domain.entities.InstanceRepository; import de.codecentric.boot.admin.server.notify.CompositeNotifier; import de.codecentric.boot.admin.server.notify.Notifier; import de.codecentric.boot.admin.server.notify.RemindingNotifier; import de.codecentric.boot.admin.server.notify.filter.FilteringNotifier; // tag::configuration-filtering-notifier[] @Configuration(proxyBeanMethods = false) public class NotifierConfig { private final InstanceRepository repository; private final ObjectProvider> otherNotifiers; public NotifierConfig(InstanceRepository repository, ObjectProvider> otherNotifiers) { this.repository = repository; this.otherNotifiers = otherNotifiers; } @Bean public FilteringNotifier filteringNotifier() { // <1> CompositeNotifier delegate = new CompositeNotifier(this.otherNotifiers.getIfAvailable(Collections::emptyList)); return new FilteringNotifier(delegate, this.repository); } @Primary @Bean(initMethod = "start", destroyMethod = "stop") public RemindingNotifier remindingNotifier() { // <2> RemindingNotifier notifier = new RemindingNotifier(filteringNotifier(), this.repository); notifier.setReminderPeriod(Duration.ofMinutes(10)); notifier.setCheckReminderInverval(Duration.ofSeconds(10)); return notifier; } } // end::configuration-filtering-notifier[] ================================================ FILE: spring-boot-admin-samples/spring-boot-admin-sample-servlet/src/main/java/de/codecentric/boot/admin/sample/SecurityPermitAllConfig.java ================================================ /* * 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.sample; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Profile; 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; @Profile("insecure") @Configuration(proxyBeanMethods = false) public class SecurityPermitAllConfig { private final AdminServerProperties adminServer; public SecurityPermitAllConfig(AdminServerProperties adminServer) { this.adminServer = adminServer; } @Bean protected SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http.authorizeHttpRequests((authorizeRequest) -> authorizeRequest.anyRequest().permitAll()); http.addFilterAfter(new CustomCsrfFilter(), BasicAuthenticationFilter.class) .csrf((csrf) -> csrf.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()) .csrfTokenRequestHandler(new CsrfTokenRequestAttributeHandler()) .ignoringRequestMatchers( PathPatternRequestMatcher.withDefaults().matcher(POST, this.adminServer.path("/instances")), PathPatternRequestMatcher.withDefaults() .matcher(POST, this.adminServer.path("/notifications/**")), PathPatternRequestMatcher.withDefaults() .matcher(DELETE, this.adminServer.path("/notifications/**")), PathPatternRequestMatcher.withDefaults().matcher(DELETE, this.adminServer.path("/instances/*")), PathPatternRequestMatcher.withDefaults().matcher(this.adminServer.path("/actuator/**")))); return http.build(); } } ================================================ FILE: spring-boot-admin-samples/spring-boot-admin-sample-servlet/src/main/java/de/codecentric/boot/admin/sample/SecuritySecureConfig.java ================================================ /* * 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.sample; import java.util.UUID; import jakarta.servlet.DispatcherType; 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; 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; @Profile("secure") // tag::configuration-spring-security[] @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(PathPatternRequestMatcher.withDefaults().matcher(this.adminServer.path("/assets/**"))) .permitAll() // <1> .requestMatchers( PathPatternRequestMatcher.withDefaults().matcher((this.adminServer.path("/actuator/info")))) .permitAll() .requestMatchers(PathPatternRequestMatcher.withDefaults().matcher(adminServer.path("/actuator/health"))) .permitAll() .requestMatchers(PathPatternRequestMatcher.withDefaults().matcher(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( PathPatternRequestMatcher.withDefaults().matcher(POST, this.adminServer.path("/instances")), // <6> PathPatternRequestMatcher.withDefaults().matcher(DELETE, this.adminServer.path("/instances/*")), // <6> PathPatternRequestMatcher.withDefaults().matcher(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(); } } // end::configuration-spring-security[] ================================================ FILE: spring-boot-admin-samples/spring-boot-admin-sample-servlet/src/main/java/de/codecentric/boot/admin/sample/SpringBootAdminServletApplication.java ================================================ /* * 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.sample; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.boot.SpringApplication; import org.springframework.boot.actuate.audit.AuditEventRepository; import org.springframework.boot.actuate.audit.InMemoryAuditEventRepository; import org.springframework.boot.actuate.web.exchanges.HttpExchangeRepository; import org.springframework.boot.actuate.web.exchanges.InMemoryHttpExchangeRepository; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.context.metrics.buffering.BufferingApplicationStartup; import org.springframework.cache.CacheManager; import org.springframework.cache.annotation.EnableCaching; import org.springframework.cache.concurrent.ConcurrentMapCacheManager; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Lazy; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; import org.springframework.jdbc.datasource.embedded.EmbeddedDatabase; import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder; import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType; import de.codecentric.boot.admin.server.config.EnableAdminServer; import de.codecentric.boot.admin.server.domain.entities.InstanceRepository; import de.codecentric.boot.admin.server.web.client.HttpHeadersProvider; import de.codecentric.boot.admin.server.web.client.InstanceExchangeFilterFunction; @SpringBootApplication @EnableAdminServer @Lazy(false) @EnableCaching public class SpringBootAdminServletApplication { private static final Logger log = LoggerFactory.getLogger(SpringBootAdminServletApplication.class); public static void main(String[] args) { SpringApplication app = new SpringApplication(SpringBootAdminServletApplication.class); app.setApplicationStartup(new BufferingApplicationStartup(1500)); app.run(args); } @Bean public CacheManager cacheManager() { return new ConcurrentMapCacheManager("books"); } // tag::customization-instance-exchange-filter-function[] @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()); } }); } // end::customization-instance-exchange-filter-function[] @Bean public CustomNotifier customNotifier(InstanceRepository repository) { return new CustomNotifier(repository); } @Bean public CustomEndpoint customEndpoint() { return new CustomEndpoint(); } // tag::customization-http-headers-providers[] @Bean public HttpHeadersProvider customHttpHeadersProvider() { return (instance) -> { HttpHeaders httpHeaders = new HttpHeaders(); httpHeaders.add("X-CUSTOM", "My Custom Value"); return httpHeaders; }; } // end::customization-http-headers-providers[] @Bean public HttpExchangeRepository httpTraceRepository() { return new InMemoryHttpExchangeRepository(); } @Bean public AuditEventRepository auditEventRepository() { return new InMemoryAuditEventRepository(); } @Bean public EmbeddedDatabase dataSource() { return new EmbeddedDatabaseBuilder().setType(EmbeddedDatabaseType.HSQL) .addScript("org/springframework/session/jdbc/schema-hsqldb.sql") .build(); } } ================================================ FILE: spring-boot-admin-samples/spring-boot-admin-sample-servlet/src/main/resources/application-dev.yml ================================================ spring: boot: admin: ui: cache: no-cache: true template-location: file:../../spring-boot-admin-server-ui/target/dist/ resource-locations: file:../../spring-boot-admin-server-ui/target/dist/ cache-templates: false extension-resource-locations: file:../spring-boot-admin-sample-custom-ui/target/dist/ ================================================ FILE: spring-boot-admin-samples/spring-boot-admin-sample-servlet/src/main/resources/application-insecure.yml ================================================ info.tags.security: insecure ================================================ FILE: spring-boot-admin-samples/spring-boot-admin-sample-servlet/src/main/resources/application-secure.yml ================================================ spring: security: user: name: "user" password: "password" boot: admin: client: username: "user" #These two are needed so that the client password: "password" #can register at the protected server api instance: metadata: user.name: "user" #These two are needed so that the server user.password: "password" #can access the protected client endpoints info.tags.security: secured ================================================ FILE: spring-boot-admin-samples/spring-boot-admin-sample-servlet/src/main/resources/application-themed.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" ================================================ FILE: spring-boot-admin-samples/spring-boot-admin-sample-servlet/src/main/resources/application.yml ================================================ --- info: scm-url: "@scm.url@" build-url: "https://travis-ci.org/codecentric/spring-boot-admin" logging: level: ROOT: info de.codecentric: info org.springframework.web: info file: name: "target/boot-admin-sample-servlet.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" group: Spring Boot Admin: - de.codecentric.boot.admin.server - de.codecentric.boot.admin.client management: endpoints: web: exposure: include: "*" endpoint: refresh: enabled: true restart: enabled: true shutdown: enabled: true env: post: enabled: true health: show-details: ALWAYS spring: application: name: spring-boot-admin-sample-servlet cloud: discovery: client: simple: instances: SBA: - uri: http://localhost:8080 metadata: management.context-path: /actuator service-url: http://localhost:8080 service-path: / boot: admin: client: url: http://localhost:8080 instance: service-host-type: IP metadata: service-path: /foo service-url: http://localhost:8080 hide-url: true tags: environment: test de-service-test-1: A large content de-service-test-2: A large content de-service-test-3: A large content de-service-test-4: A large content de-service-test-5: A large content de-service-test-6: A large content kubectl.kubernetes.io/last-applied-configuration: '{"name":"jvm.threads.peak","description":"The peak live thread count since the Java virtual machine started or peak was reset","baseUnit":"threads","measurements":[{"statistic":"VALUE","value":64.0}],"availableTags":[]}' config: import: optional:configserver:http://localhost:8888/ jmx: enabled: true main: lazy-initialization: true --- # tag::customization-external-views-simple-link[] spring: boot: admin: ui: external-views: - label: "🚀" #<1> url: "https://codecentric.de" #<2> order: 2000 #<3> # end::customization-external-views-simple-link[] --- # tag::customization-external-views-dropdown-with-links[] 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 # end::customization-external-views-dropdown-with-links[] --- # tag::customization-external-views-dropdown-is-link-with-links-as-children[] 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 # end::customization-external-views-dropdown-is-link-with-links-as-children[] --- # tag::customization-view-settings[] spring: boot: admin: ui: view-settings: - name: "journal" enabled: false # end::customization-view-settings[] management: endpoint: sbom: application: location: optional:classpath:bom.json ================================================ FILE: spring-boot-admin-samples/spring-boot-admin-sample-servlet/src/test/java/de/codecentric/boot/admin/sample/SpringBootAdminServletApplicationTest.java ================================================ /* * 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.sample; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.context.junit.jupiter.SpringExtension; @ExtendWith(SpringExtension.class) @SpringBootTest(classes = { SpringBootAdminServletApplication.class }) class SpringBootAdminServletApplicationTest { @Test void contextLoads() { } } ================================================ FILE: spring-boot-admin-samples/spring-boot-admin-sample-servlet-graalvm/Readme.md ================================================ # Spring Boot Admin GraalVM sample application This is a sample project running a Spring Boot Admin server which works with GraalVM and Native Image Builder. In order to show basic functionalities, the server itself is registered as a client. ## Build project Make sure to use a GraalVM with a v17-BaseJDK to build the project (e.g. GraalVM Oracle 17.0.8). If you're using sdkman: ```bash sdk install java 17.0.8-graal ``` Build the application with the `native` profile: ```bash mvn -Pnative native:compile ``` The native application will now be build in the target folder. ```bash cd target ./spring-boot-admin-sample-servlet-graalvm ``` You should now be able to access Spring Boot Admin locally under http://localhost:8080/ ## Build an OCI image that can be run with Docker ```bash mvn spring-boot:build-image -Pnative -Dspring-boot.build-image.imageName=spring-boot-admin-sample-servlet-graalvm:latest ``` Depending on your OS, you might want to change the builder in your `pom.xml`. Right now, `dashaun/native-builder:focal-arm64` is a good choice for ARM64. In most other cases `paketobuildpacks/builder:tiny` should do the job. ## Running the example ```bash docker run --rm -p 8080:8080 docker.io/library/spring-boot-admin-sample-servlet-graalvm:latest ``` You should now be able to access Spring Boot Admin locally under http://localhost:8080/ ## Current limitations of Spring Boot's native image build feature Keep in mind that currently not all Spring modules have built-in support. Therefore, you might need to tell the AOT compiler about the usage of reflection, dynamic proxies etc. There are several ways to deal with these concerns. A good starting point for specifying additional native configuration can be found in the official [Spring documentation](https://docs.spring.io/spring-framework/docs/6.0.0/reference/html/core.html#aot-hints). Some features like gc and memory metrics are not supported by GraalVM yet. So some views (e.g. gc-details) are currently not working. ================================================ FILE: spring-boot-admin-samples/spring-boot-admin-sample-servlet-graalvm/pom.xml ================================================ 4.0.0 spring-boot-admin-sample-servlet-graalvm Spring Boot Admin Sample Servlet GraalVM Spring Boot Admin Sample Servlet de.codecentric spring-boot-admin-samples ${revision} ../pom.xml 1.5.7-7 0.11.5 true de.codecentric spring-boot-admin-starter-server de.codecentric spring-boot-admin-starter-client org.projectlombok lombok true com.github.luben zstd-jni ${zstd-jni.version} io.netty netty-tcnative-boringssl-static compile ${project.artifactId} org.graalvm.buildtools native-maven-plugin ${native-build-tools-plugin.version} true native org.springframework.boot spring-boot-maven-plugin org.projectlombok lombok dashaun/native-builder:focal-arm64 true repackage build-info process-aot process-aot org.graalvm.buildtools native-maven-plugin true -H:+IncludeAllLocales -H:-CheckToolchain -H:+ReportExceptionStackTraces ${project.build.outputDirectory} true add-reachability-metadata add-reachability-metadata nativeTest org.junit.platform junit-platform-launcher test org.springframework.boot spring-boot-maven-plugin process-test-aot process-test-aot org.graalvm.buildtools native-maven-plugin ${project.build.outputDirectory} true native-test test ================================================ FILE: spring-boot-admin-samples/spring-boot-admin-sample-servlet-graalvm/src/main/java/de/codecentric/boot/admin/sample/SpringBootAdminServletApplication.java ================================================ /* * 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.sample; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import de.codecentric.boot.admin.server.config.EnableAdminServer; @SpringBootApplication @EnableAdminServer public class SpringBootAdminServletApplication { public static void main(String[] args) { SpringApplication.run(SpringBootAdminServletApplication.class, args); } } ================================================ FILE: spring-boot-admin-samples/spring-boot-admin-sample-servlet-graalvm/src/main/resources/application.yml ================================================ --- info: scm-url: "@scm.url@" build-url: "https://travis-ci.org/codecentric/spring-boot-admin" logging: level: ROOT: info de.codecentric: trace org.springframework.web: debug file: name: "target/spring-boot-admin-3-graalvm.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" management: info: java: enabled: true env: enabled: true endpoints: web: exposure: include: "*" spring: application: name: spring-boot-admin-3-graalvm main: lazy-initialization: true boot: admin: client: url: http://localhost:8080 ui: external-views: - label: 'Is it Christmas yet?' url: https://isitchristmas.com/ ================================================ FILE: spring-boot-admin-samples/spring-boot-admin-sample-war/.gitignore ================================================ /.springBeans ================================================ FILE: spring-boot-admin-samples/spring-boot-admin-sample-war/pom.xml ================================================ 4.0.0 spring-boot-admin-sample-war war Spring Boot Admin Sample War Spring Boot Admin Sample packaged as war de.codecentric spring-boot-admin-samples ${revision} ../pom.xml org.springframework.boot spring-boot-starter-tomcat provided org.springframework.boot spring-boot-starter-webmvc org.springframework.boot spring-boot-starter-security de.codecentric spring-boot-admin-starter-client de.codecentric spring-boot-admin-server de.codecentric spring-boot-admin-server-ui ${project.artifactId} org.apache.maven.plugins maven-war-plugin false true org.springframework.boot spring-boot-maven-plugin repackage build-info false ================================================ FILE: spring-boot-admin-samples/spring-boot-admin-sample-war/src/main/java/de/codecentric/boot/admin/sample/SpringBootAdminWarApplication.java ================================================ /* * 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.sample; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.builder.SpringApplicationBuilder; import org.springframework.boot.web.servlet.support.SpringBootServletInitializer; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Profile; import org.springframework.security.config.Customizer; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler; import org.springframework.security.web.csrf.CookieCsrfTokenRepository; 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; @SpringBootApplication @EnableAdminServer public class SpringBootAdminWarApplication extends SpringBootServletInitializer { public static void main(String[] args) { SpringApplication.run(SpringBootAdminWarApplication.class, args); } @Override protected SpringApplicationBuilder configure(SpringApplicationBuilder application) { return application; } @Profile("insecure") @Configuration(proxyBeanMethods = false) public static class SecurityPermitAllConfig { private final AdminServerProperties adminServer; public SecurityPermitAllConfig(AdminServerProperties adminServer) { this.adminServer = adminServer; } @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http.authorizeHttpRequests((authorizeRequests) -> authorizeRequests.anyRequest().permitAll()) .csrf((csrf) -> csrf.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()) .ignoringRequestMatchers( PathPatternRequestMatcher.withDefaults().matcher(POST, this.adminServer.path("/instances")), PathPatternRequestMatcher.withDefaults() .matcher(DELETE, this.adminServer.path("/instances/*")), PathPatternRequestMatcher.withDefaults().matcher(this.adminServer.path("/actuator/**")))); return http.build(); } } @Profile("secure") @Configuration(proxyBeanMethods = false) public static class SecuritySecureConfig { private final AdminServerProperties adminServer; public SecuritySecureConfig(AdminServerProperties adminServer) { this.adminServer = adminServer; } @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(PathPatternRequestMatcher.withDefaults().matcher(this.adminServer.path("/assets/**"))) .permitAll() .requestMatchers(PathPatternRequestMatcher.withDefaults().matcher(this.adminServer.path("/login"))) .permitAll() .anyRequest() .authenticated()) .formLogin((formLogin) -> formLogin.loginPage(this.adminServer.path("/login")) .successHandler(successHandler)) .logout((logout) -> logout.logoutUrl(this.adminServer.path("/logout"))) .httpBasic(Customizer.withDefaults()) .csrf((csrf) -> csrf.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()) .ignoringRequestMatchers( PathPatternRequestMatcher.withDefaults().matcher(POST, this.adminServer.path("/instances")), PathPatternRequestMatcher.withDefaults() .matcher(DELETE, this.adminServer.path("/instances/*")), PathPatternRequestMatcher.withDefaults().matcher(this.adminServer.path("/actuator/**")))); return http.build(); } } } ================================================ FILE: spring-boot-admin-samples/spring-boot-admin-sample-war/src/main/resources/application-dev.yml ================================================ spring: boot: admin: ui: cache: no-cache: true template-location: file:../../spring-boot-admin-server-ui/target/dist/ resource-locations: file:../../spring-boot-admin-server-ui/target/dist/ cache-templates: false ================================================ FILE: spring-boot-admin-samples/spring-boot-admin-sample-war/src/main/resources/application-insecure.yml ================================================ info.tags.security: insecure ================================================ FILE: spring-boot-admin-samples/spring-boot-admin-sample-war/src/main/resources/application-secure.yml ================================================ spring: security: user: name: "user" password: "password" boot: admin: client: username: "user" #These two are needed so that the client password: "password" #can register at the protected server api instance: metadata: user.name: "user" #These two are needed so that the server user.password: "password" #can access the protected client endpoints ================================================ FILE: spring-boot-admin-samples/spring-boot-admin-sample-war/src/main/resources/application.yml ================================================ spring: application: name: spring-boot-admin-sample-war boot: admin: client: url: http://localhost:8080 instance: service-base-url: http://localhost:8080 profiles: active: - secure management: endpoints: web: exposure: include: "*" endpoint: health: show-details: ALWAYS ================================================ FILE: spring-boot-admin-samples/spring-boot-admin-sample-zookeeper/docker-compose.yml ================================================ services: zookeeper: image: zookeeper ports: - "2181:2181" ================================================ FILE: spring-boot-admin-samples/spring-boot-admin-sample-zookeeper/pom.xml ================================================ 4.0.0 spring-boot-admin-sample-zookeeper Spring Boot Admin Sample Zookeeper Spring Boot Admin Sample using Zookeeper de.codecentric spring-boot-admin-samples ${revision} ../pom.xml org.springframework.boot spring-boot-starter-webmvc org.springframework.boot spring-boot-starter-security de.codecentric spring-boot-admin-starter-server org.springframework.cloud spring-cloud-starter-zookeeper-discovery org.springframework.cloud spring-cloud-context org.springframework.boot spring-boot-starter-test test ${project.artifactId} org.springframework.boot spring-boot-maven-plugin repackage build-info de.codecentric.boot.admin.sample.SpringBootAdminZookeeperApplication false ================================================ FILE: spring-boot-admin-samples/spring-boot-admin-sample-zookeeper/src/main/java/de/codecentric/boot/admin/sample/SpringBootAdminZookeeperApplication.java ================================================ /* * 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.sample; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.client.discovery.EnableDiscoveryClient; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Profile; import org.springframework.security.config.Customizer; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler; import org.springframework.security.web.csrf.CookieCsrfTokenRepository; 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; @SpringBootApplication @EnableDiscoveryClient @EnableAdminServer public class SpringBootAdminZookeeperApplication { public static void main(String[] args) { SpringApplication.run(SpringBootAdminZookeeperApplication.class, args); } @Profile("insecure") @Configuration(proxyBeanMethods = false) public static class SecurityPermitAllConfig { private final AdminServerProperties adminServer; public SecurityPermitAllConfig(AdminServerProperties adminServer) { this.adminServer = adminServer; } @Bean protected SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http.authorizeHttpRequests((authorizeRequests) -> authorizeRequests.anyRequest().permitAll()) .csrf((csrf) -> csrf.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()) .ignoringRequestMatchers( PathPatternRequestMatcher.withDefaults().matcher(POST, this.adminServer.path("/instances")), PathPatternRequestMatcher.withDefaults() .matcher(DELETE, this.adminServer.path("/instances/*")), PathPatternRequestMatcher.withDefaults().matcher(this.adminServer.path("/actuator/**")))); return http.build(); } } @Profile("secure") @Configuration(proxyBeanMethods = false) public static class SecuritySecureConfig { private final AdminServerProperties adminServer; public SecuritySecureConfig(AdminServerProperties adminServer) { this.adminServer = adminServer; } @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(PathPatternRequestMatcher.withDefaults().matcher(this.adminServer.path("/assets/**"))) .permitAll() .requestMatchers(PathPatternRequestMatcher.withDefaults().matcher(this.adminServer.path("/login"))) .permitAll() .anyRequest() .authenticated()) .formLogin((formLogin) -> formLogin.loginPage(this.adminServer.path("/login")) .successHandler(successHandler)) .logout((logout) -> logout.logoutUrl(this.adminServer.path("/logout"))) .httpBasic(Customizer.withDefaults()) .csrf((csrf) -> csrf.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()) .ignoringRequestMatchers( PathPatternRequestMatcher.withDefaults().matcher(POST, this.adminServer.path("/instances")), PathPatternRequestMatcher.withDefaults() .matcher(DELETE, this.adminServer.path("/instances/*")), PathPatternRequestMatcher.withDefaults().matcher(this.adminServer.path("/actuator/**")))); return http.build(); } } } ================================================ FILE: spring-boot-admin-samples/spring-boot-admin-sample-zookeeper/src/main/resources/application.yml ================================================ spring: application: name: zookeeper-example cloud: config: enabled: false zookeeper: connect-string: localhost:2181 discovery: metadata: management.context-path: /foo health.path: /ping user.name: user user.password: password profiles: active: - secure management: endpoints: web: exposure: include: "*" path-mapping: health: /ping base-path: /foo endpoint: health: show-details: ALWAYS --- spring: config: activate: on-profile: insecure --- spring: security: user: name: "user" password: "password" config: activate: on-profile: secure ================================================ FILE: spring-boot-admin-samples/spring-boot-admin-sample-zookeeper/src/test/java/de/codecentric/boot/admin/sample/SpringBootAdminZookeeperApplicationTest.java ================================================ /* * 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.sample; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.context.junit.jupiter.SpringExtension; @ExtendWith(SpringExtension.class) @SpringBootTest(classes = { SpringBootAdminZookeeperApplication.class }, properties = { "spring.cloud.zookeeper.enabled=false" }) class SpringBootAdminZookeeperApplicationTest { @Test void contextLoads() { } } ================================================ FILE: spring-boot-admin-server/pom.xml ================================================ 4.0.0 spring-boot-admin-server Spring Boot Admin Server Spring Boot Admin Server de.codecentric spring-boot-admin-build ${revision} ../spring-boot-admin-build org.springframework.boot spring-boot-starter org.springframework.boot spring-boot-starter-webclient org.springframework.boot spring-boot-starter-hazelcast true org.springframework.boot spring-boot-starter-webflux org.springframework.boot spring-boot-starter-webmvc true org.springframework.boot spring-boot-starter-thymeleaf org.springframework.boot spring-boot-starter-actuator org.apache.httpcomponents.client5 httpclient5 io.projectreactor.addons reactor-extra org.projectlombok lombok true org.springframework.boot spring-boot-starter-mail true com.hazelcast hazelcast true org.springframework.boot spring-boot-autoconfigure-processor true org.springframework.boot spring-boot-configuration-processor true org.springframework.boot spring-boot-starter-test test org.junit.vintage junit-vintage-engine org.springframework.boot spring-boot-starter-security test tools.jackson.datatype jackson-datatype-json-org test io.projectreactor reactor-test test com.hazelcast hazelcast tests test org.wiremock wiremock-standalone test org.eclipse.jetty jetty-alpn-server test org.awaitility awaitility test org.testcontainers testcontainers test org.testcontainers testcontainers-junit-jupiter test ================================================ FILE: spring-boot-admin-server/src/main/java/de/codecentric/boot/admin/server/config/AdminServerAutoConfiguration.java ================================================ /* * 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.server.config; import java.time.Duration; import lombok.extern.slf4j.Slf4j; import org.reactivestreams.Publisher; import org.springframework.boot.autoconfigure.AutoConfigureAfter; import org.springframework.boot.autoconfigure.ImportAutoConfiguration; import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.boot.webclient.autoconfigure.WebClientAutoConfiguration; 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 de.codecentric.boot.admin.server.domain.entities.InstanceRepository; import de.codecentric.boot.admin.server.domain.entities.SnapshottingInstanceRepository; import de.codecentric.boot.admin.server.domain.events.InstanceEvent; import de.codecentric.boot.admin.server.eventstore.InMemoryEventStore; import de.codecentric.boot.admin.server.eventstore.InstanceEventPublisher; import de.codecentric.boot.admin.server.eventstore.InstanceEventStore; import de.codecentric.boot.admin.server.services.ApiMediaTypeHandler; import de.codecentric.boot.admin.server.services.ApplicationRegistry; import de.codecentric.boot.admin.server.services.EndpointDetectionTrigger; import de.codecentric.boot.admin.server.services.EndpointDetector; import de.codecentric.boot.admin.server.services.HashingInstanceUrlIdGenerator; import de.codecentric.boot.admin.server.services.InfoUpdateTrigger; import de.codecentric.boot.admin.server.services.InfoUpdater; import de.codecentric.boot.admin.server.services.InstanceFilter; import de.codecentric.boot.admin.server.services.InstanceIdGenerator; import de.codecentric.boot.admin.server.services.InstanceRegistry; import de.codecentric.boot.admin.server.services.StatusUpdateTrigger; import de.codecentric.boot.admin.server.services.StatusUpdater; import de.codecentric.boot.admin.server.services.endpoints.ChainingStrategy; 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(proxyBeanMethods = false) @Conditional(SpringBootAdminServerEnabledCondition.class) @ConditionalOnBean(AdminServerMarkerConfiguration.Marker.class) @EnableConfigurationProperties(AdminServerProperties.class) @ImportAutoConfiguration({ AdminServerInstanceWebClientConfiguration.class, AdminServerWebConfiguration.class }) @AutoConfigureAfter({ WebClientAutoConfiguration.class }) @Slf4j @Lazy(false) public class AdminServerAutoConfiguration { private final AdminServerProperties adminServerProperties; public AdminServerAutoConfiguration(AdminServerProperties adminServerProperties) { this.adminServerProperties = adminServerProperties; } @Bean @ConditionalOnMissingBean public InstanceFilter instanceFilter() { return (instance) -> true; } @Bean @ConditionalOnMissingBean public InstanceRegistry instanceRegistry(InstanceRepository instanceRepository, InstanceIdGenerator instanceIdGenerator, InstanceFilter instanceFilter) { return new InstanceRegistry(instanceRepository, instanceIdGenerator, instanceFilter); } @Bean @ConditionalOnMissingBean public ApplicationRegistry applicationRegistry(InstanceRegistry instanceRegistry, InstanceEventPublisher instanceEventPublisher) { return new ApplicationRegistry(instanceRegistry, instanceEventPublisher); } @Bean @ConditionalOnMissingBean public InstanceIdGenerator instanceIdGenerator() { return new HashingInstanceUrlIdGenerator(); } @Bean @ConditionalOnMissingBean public StatusUpdater statusUpdater(InstanceRepository instanceRepository, InstanceWebClient.Builder instanceWebClientBuilder) { return new StatusUpdater(instanceRepository, instanceWebClientBuilder.build(), new ApiMediaTypeHandler()); } @Bean(initMethod = "start", destroyMethod = "stop") @ConditionalOnMissingBean public StatusUpdateTrigger statusUpdateTrigger(StatusUpdater statusUpdater, Publisher events) { AdminServerProperties.MonitorProperties monitorProperties = this.adminServerProperties.getMonitor(); Duration defaultTimeout = monitorProperties.getDefaultTimeout(); Duration statusInterval = monitorProperties.getStatusInterval(); if (defaultTimeout.compareTo(statusInterval) > 0) { log.warn( "Default timeout ({}) is larger than status interval ({}), hence status interval will be used as timeout.", defaultTimeout, statusInterval); } return new StatusUpdateTrigger(statusUpdater, events, monitorProperties.getStatusInterval(), monitorProperties.getStatusLifetime(), monitorProperties.getStatusMaxBackoff()); } @Bean @ConditionalOnMissingBean public EndpointDetector endpointDetector(InstanceRepository instanceRepository, InstanceWebClient.Builder instanceWebClientBuilder) { InstanceWebClient instanceWebClient = instanceWebClientBuilder.build(); ChainingStrategy strategy = new ChainingStrategy( new QueryIndexEndpointStrategy(instanceWebClient, new ApiMediaTypeHandler()), new ProbeEndpointsStrategy(instanceWebClient, this.adminServerProperties.getProbedEndpoints())); return new EndpointDetector(instanceRepository, strategy); } @Bean(initMethod = "start", destroyMethod = "stop") @ConditionalOnMissingBean public EndpointDetectionTrigger endpointDetectionTrigger(EndpointDetector endpointDetector, Publisher events) { return new EndpointDetectionTrigger(endpointDetector, events); } @Bean @ConditionalOnMissingBean public InfoUpdater infoUpdater(InstanceRepository instanceRepository, InstanceWebClient.Builder instanceWebClientBuilder) { return new InfoUpdater(instanceRepository, instanceWebClientBuilder.build(), new ApiMediaTypeHandler()); } @Bean(initMethod = "start", destroyMethod = "stop") @ConditionalOnMissingBean public InfoUpdateTrigger infoUpdateTrigger(InfoUpdater infoUpdater, Publisher events) { return new InfoUpdateTrigger(infoUpdater, events, this.adminServerProperties.getMonitor().getInfoInterval(), this.adminServerProperties.getMonitor().getInfoLifetime(), this.adminServerProperties.getMonitor().getInfoMaxBackoff()); } @Bean @ConditionalOnMissingBean(InstanceEventStore.class) public InMemoryEventStore eventStore() { return new InMemoryEventStore(); } @Bean(initMethod = "start", destroyMethod = "stop") @ConditionalOnMissingBean(InstanceRepository.class) public SnapshottingInstanceRepository instanceRepository(InstanceEventStore eventStore) { return new SnapshottingInstanceRepository(eventStore); } } ================================================ FILE: spring-boot-admin-server/src/main/java/de/codecentric/boot/admin/server/config/AdminServerCloudFoundryAutoConfiguration.java ================================================ /* * 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.server.config; import org.springframework.boot.autoconfigure.AutoConfigureBefore; import org.springframework.boot.autoconfigure.condition.ConditionalOnCloudPlatform; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.cloud.CloudPlatform; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Lazy; import de.codecentric.boot.admin.server.services.CloudFoundryInstanceIdGenerator; import de.codecentric.boot.admin.server.services.HashingInstanceUrlIdGenerator; import de.codecentric.boot.admin.server.services.InstanceIdGenerator; import de.codecentric.boot.admin.server.web.client.CloudFoundryHttpHeaderProvider; @Configuration(proxyBeanMethods = false) @ConditionalOnCloudPlatform(CloudPlatform.CLOUD_FOUNDRY) @AutoConfigureBefore({ AdminServerAutoConfiguration.class }) @Lazy(false) public class AdminServerCloudFoundryAutoConfiguration { @Bean @ConditionalOnMissingBean public InstanceIdGenerator instanceIdGenerator() { return new CloudFoundryInstanceIdGenerator(new HashingInstanceUrlIdGenerator()); } @Bean @ConditionalOnMissingBean public CloudFoundryHttpHeaderProvider cloudFoundryHttpHeaderProvider() { return new CloudFoundryHttpHeaderProvider(); } } ================================================ FILE: spring-boot-admin-server/src/main/java/de/codecentric/boot/admin/server/config/AdminServerHazelcastAutoConfiguration.java ================================================ /* * 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.server.config; import java.util.List; import com.hazelcast.core.HazelcastInstance; import com.hazelcast.map.IMap; import org.reactivestreams.Publisher; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.autoconfigure.AutoConfigureAfter; import org.springframework.boot.autoconfigure.AutoConfigureBefore; import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.autoconfigure.condition.ConditionalOnSingleCandidate; import org.springframework.boot.hazelcast.autoconfigure.HazelcastAutoConfiguration; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Lazy; 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 de.codecentric.boot.admin.server.notify.HazelcastNotificationTrigger; import de.codecentric.boot.admin.server.notify.NotificationTrigger; import de.codecentric.boot.admin.server.notify.Notifier; @Configuration(proxyBeanMethods = false) @ConditionalOnBean(AdminServerMarkerConfiguration.Marker.class) @ConditionalOnSingleCandidate(HazelcastInstance.class) @ConditionalOnProperty(prefix = "spring.boot.admin.hazelcast", name = "enabled", matchIfMissing = true) @AutoConfigureBefore({ AdminServerAutoConfiguration.class, AdminServerNotifierAutoConfiguration.class }) @AutoConfigureAfter(HazelcastAutoConfiguration.class) @Lazy(false) public class AdminServerHazelcastAutoConfiguration { public static final String DEFAULT_NAME_EVENT_STORE_MAP = "spring-boot-admin-event-store"; public static final String DEFAULT_NAME_SENT_NOTIFICATIONS_MAP = "spring-boot-admin-sent-notifications"; @Value("${spring.boot.admin.hazelcast.event-store:" + DEFAULT_NAME_EVENT_STORE_MAP + "}") private final String nameEventStoreMap = DEFAULT_NAME_EVENT_STORE_MAP; @Bean @ConditionalOnMissingBean(InstanceEventStore.class) public HazelcastEventStore eventStore(HazelcastInstance hazelcastInstance) { IMap> map = hazelcastInstance.getMap(this.nameEventStoreMap); return new HazelcastEventStore(map); } @Configuration(proxyBeanMethods = false) @ConditionalOnBean(Notifier.class) public static class NotifierTriggerConfiguration { @Value("${spring.boot.admin.hazelcast.sent-notifications:" + DEFAULT_NAME_SENT_NOTIFICATIONS_MAP + "}") private final String nameSentNotificationsMap = DEFAULT_NAME_SENT_NOTIFICATIONS_MAP; @Bean(initMethod = "start", destroyMethod = "stop") @ConditionalOnMissingBean(NotificationTrigger.class) public NotificationTrigger notificationTrigger(HazelcastInstance hazelcastInstance, Notifier notifier, Publisher events) { return new HazelcastNotificationTrigger(notifier, events, hazelcastInstance.getMap(this.nameSentNotificationsMap)); } } } ================================================ FILE: spring-boot-admin-server/src/main/java/de/codecentric/boot/admin/server/config/AdminServerInstanceWebClientConfiguration.java ================================================ /* * 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.server.config; import java.net.CookiePolicy; import java.util.List; import org.reactivestreams.Publisher; import org.springframework.beans.factory.ObjectProvider; import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Lazy; import org.springframework.context.annotation.Scope; import org.springframework.core.annotation.Order; import org.springframework.web.reactive.function.client.WebClient; import de.codecentric.boot.admin.server.domain.events.InstanceEvent; import de.codecentric.boot.admin.server.web.client.BasicAuthHttpHeaderProvider; import de.codecentric.boot.admin.server.web.client.CompositeHttpHeadersProvider; import de.codecentric.boot.admin.server.web.client.HttpHeadersProvider; import de.codecentric.boot.admin.server.web.client.InstanceExchangeFilterFunction; import de.codecentric.boot.admin.server.web.client.InstanceExchangeFilterFunctions; import de.codecentric.boot.admin.server.web.client.InstanceWebClient; import de.codecentric.boot.admin.server.web.client.InstanceWebClientCustomizer; import de.codecentric.boot.admin.server.web.client.LegacyEndpointConverter; import de.codecentric.boot.admin.server.web.client.LegacyEndpointConverters; import de.codecentric.boot.admin.server.web.client.cookies.CookieStoreCleanupTrigger; import de.codecentric.boot.admin.server.web.client.cookies.JdkPerInstanceCookieStore; import de.codecentric.boot.admin.server.web.client.cookies.PerInstanceCookieStore; import de.codecentric.boot.admin.server.web.client.reactive.CompositeReactiveHttpHeadersProvider; import de.codecentric.boot.admin.server.web.client.reactive.ReactiveHttpHeadersProvider; @Configuration(proxyBeanMethods = false) @Lazy(false) public class AdminServerInstanceWebClientConfiguration { private final InstanceWebClient.Builder instanceWebClientBuilder; public AdminServerInstanceWebClientConfiguration(ObjectProvider customizers, WebClient.Builder webClient) { this.instanceWebClientBuilder = InstanceWebClient.builder(webClient); customizers.orderedStream().forEach((customizer) -> customizer.customize(this.instanceWebClientBuilder)); } @Bean @ConditionalOnMissingBean @Scope("prototype") public InstanceWebClient.Builder instanceWebClientBuilder() { return this.instanceWebClientBuilder.clone(); } @Configuration(proxyBeanMethods = false) protected static class InstanceExchangeFiltersConfiguration { @Bean @ConditionalOnBean(InstanceExchangeFilterFunction.class) @ConditionalOnMissingBean(name = "filterInstanceWebClientCustomizer") public InstanceWebClientCustomizer filterInstanceWebClientCustomizer( List filters) { return (builder) -> builder.filters((f) -> f.addAll(filters)); } @Configuration(proxyBeanMethods = false) protected static class DefaultInstanceExchangeFiltersConfiguration { @Bean @Order(0) @ConditionalOnBean(HttpHeadersProvider.class) @ConditionalOnMissingBean(name = "addHeadersInstanceExchangeFilter") public InstanceExchangeFilterFunction addHeadersInstanceExchangeFilter( List headersProviders) { return InstanceExchangeFilterFunctions.addHeaders(new CompositeHttpHeadersProvider(headersProviders)); } @Bean @Order(0) @ConditionalOnBean(ReactiveHttpHeadersProvider.class) @ConditionalOnMissingBean(name = "addReactiveHeadersInstanceExchangeFilter") public InstanceExchangeFilterFunction addReactiveHeadersInstanceExchangeFilter( List reactiveHeadersProviders) { return InstanceExchangeFilterFunctions .addHeadersReactive(new CompositeReactiveHttpHeadersProvider(reactiveHeadersProviders)); } @Bean @Order(10) @ConditionalOnMissingBean(name = "rewriteEndpointUrlInstanceExchangeFilter") public InstanceExchangeFilterFunction rewriteEndpointUrlInstanceExchangeFilter() { return InstanceExchangeFilterFunctions.rewriteEndpointUrl(); } @Bean @Order(20) @ConditionalOnMissingBean(name = "setDefaultAcceptHeaderInstanceExchangeFilter") public InstanceExchangeFilterFunction setDefaultAcceptHeaderInstanceExchangeFilter() { return InstanceExchangeFilterFunctions.setDefaultAcceptHeader(); } @Bean @Order(30) @ConditionalOnBean(LegacyEndpointConverter.class) @ConditionalOnMissingBean(name = "legacyEndpointConverterInstanceExchangeFilter") public InstanceExchangeFilterFunction legacyEndpointConverterInstanceExchangeFilter( List converters) { return InstanceExchangeFilterFunctions.convertLegacyEndpoints(converters); } @Bean @Order(40) @ConditionalOnMissingBean(name = "logfileAcceptWorkaround") public InstanceExchangeFilterFunction logfileAcceptWorkaround() { return InstanceExchangeFilterFunctions.logfileAcceptWorkaround(); } @Bean @Order(50) @ConditionalOnMissingBean(name = "cookieHandlingInstanceExchangeFilter") public InstanceExchangeFilterFunction cookieHandlingInstanceExchangeFilter( final PerInstanceCookieStore store) { return InstanceExchangeFilterFunctions.handleCookies(store); } @Bean @Order(100) @ConditionalOnMissingBean(name = "retryInstanceExchangeFilter") public InstanceExchangeFilterFunction retryInstanceExchangeFilter( AdminServerProperties adminServerProperties) { AdminServerProperties.MonitorProperties monitor = adminServerProperties.getMonitor(); return InstanceExchangeFilterFunctions.retry(monitor.getDefaultRetries(), monitor.getRetries()); } @Bean @Order(200) @ConditionalOnMissingBean(name = "timeoutInstanceExchangeFilter") public InstanceExchangeFilterFunction timeoutInstanceExchangeFilter( AdminServerProperties adminServerProperties) { AdminServerProperties.MonitorProperties monitor = adminServerProperties.getMonitor(); return InstanceExchangeFilterFunctions.timeout(monitor.getDefaultTimeout(), monitor.getTimeout()); } } } @Configuration(proxyBeanMethods = false) protected static class HttpHeadersProviderConfiguration { @Bean @ConditionalOnMissingBean public BasicAuthHttpHeaderProvider basicAuthHttpHeadersProvider(AdminServerProperties adminServerProperties) { AdminServerProperties.InstanceAuthProperties instanceAuth = adminServerProperties.getInstanceAuth(); if (instanceAuth.isEnabled()) { return new BasicAuthHttpHeaderProvider(instanceAuth.getDefaultUserName(), instanceAuth.getDefaultPassword(), instanceAuth.getServiceMap()); } else { return new BasicAuthHttpHeaderProvider(); } } } @Configuration(proxyBeanMethods = false) protected static class LegacyEndpointConvertersConfiguration { @Bean @ConditionalOnMissingBean(name = "healthLegacyEndpointConverter") public LegacyEndpointConverter healthLegacyEndpointConverter() { return LegacyEndpointConverters.health(); } @Bean @ConditionalOnMissingBean(name = "infoLegacyEndpointConverter") public LegacyEndpointConverter infoLegacyEndpointConverter() { return LegacyEndpointConverters.info(); } @Bean @ConditionalOnMissingBean(name = "envLegacyEndpointConverter") public LegacyEndpointConverter envLegacyEndpointConverter() { return LegacyEndpointConverters.env(); } @Bean @ConditionalOnMissingBean(name = "httptraceLegacyEndpointConverter") public LegacyEndpointConverter httptraceLegacyEndpointConverter() { return LegacyEndpointConverters.httptrace(); } @Bean @ConditionalOnMissingBean(name = "threaddumpLegacyEndpointConverter") public LegacyEndpointConverter threaddumpLegacyEndpointConverter() { return LegacyEndpointConverters.threaddump(); } @Bean @ConditionalOnMissingBean(name = "liquibaseLegacyEndpointConverter") public LegacyEndpointConverter liquibaseLegacyEndpointConverter() { return LegacyEndpointConverters.liquibase(); } @Bean @ConditionalOnMissingBean(name = "flywayLegacyEndpointConverter") public LegacyEndpointConverter flywayLegacyEndpointConverter() { return LegacyEndpointConverters.flyway(); } @Bean @ConditionalOnMissingBean(name = "beansLegacyEndpointConverter") public LegacyEndpointConverter beansLegacyEndpointConverter() { return LegacyEndpointConverters.beans(); } @Bean @ConditionalOnMissingBean(name = "configpropsLegacyEndpointConverter") public LegacyEndpointConverter configpropsLegacyEndpointConverter() { return LegacyEndpointConverters.configprops(); } @Bean @ConditionalOnMissingBean(name = "mappingsLegacyEndpointConverter") public LegacyEndpointConverter mappingsLegacyEndpointConverter() { return LegacyEndpointConverters.mappings(); } @Bean @ConditionalOnMissingBean(name = "startupLegacyEndpointConverter") public LegacyEndpointConverter startupLegacyEndpointConverter() { return LegacyEndpointConverters.startup(); } } @Configuration(proxyBeanMethods = false) protected static class CookieStoreConfiguration { /** * Creates a default {@link PerInstanceCookieStore} that should be used. * @return the cookie store */ @Bean @ConditionalOnMissingBean public PerInstanceCookieStore cookieStore() { return new JdkPerInstanceCookieStore(CookiePolicy.ACCEPT_ORIGINAL_SERVER); } /** * Creates a default trigger to cleanup the cookie store on deregistering of an * {@link de.codecentric.boot.admin.server.domain.entities.Instance}. * @param publisher publisher of {@link InstanceEvent}s events * @param cookieStore the store to inform about deregistration of an * {@link de.codecentric.boot.admin.server.domain.entities.Instance} * @return a new trigger */ @Bean(initMethod = "start", destroyMethod = "stop") @ConditionalOnMissingBean public CookieStoreCleanupTrigger cookieStoreCleanupTrigger(final Publisher publisher, final PerInstanceCookieStore cookieStore) { return new CookieStoreCleanupTrigger(publisher, cookieStore); } } } ================================================ FILE: spring-boot-admin-server/src/main/java/de/codecentric/boot/admin/server/config/AdminServerMarkerConfiguration.java ================================================ /* * 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.server.config; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration(proxyBeanMethods = false) public class AdminServerMarkerConfiguration { @Bean public Marker adminServerMarker() { return new Marker(); } public static class Marker { } } ================================================ FILE: spring-boot-admin-server/src/main/java/de/codecentric/boot/admin/server/config/AdminServerNotifierAutoConfiguration.java ================================================ /* * Copyright 2014-2024 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.server.config; import java.nio.charset.StandardCharsets; import java.util.List; import org.apache.hc.client5.http.auth.AuthScope; import org.apache.hc.client5.http.auth.Credentials; import org.apache.hc.client5.http.auth.UsernamePasswordCredentials; import org.apache.hc.client5.http.impl.auth.BasicCredentialsProvider; import org.apache.hc.client5.http.impl.classic.HttpClientBuilder; import org.apache.hc.core5.http.HttpHost; import org.reactivestreams.Publisher; import org.springframework.boot.autoconfigure.AutoConfigureAfter; import org.springframework.boot.autoconfigure.AutoConfigureBefore; import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.autoconfigure.condition.ConditionalOnSingleCandidate; import org.springframework.boot.autoconfigure.condition.NoneNestedConditions; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.ApplicationContext; 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.context.annotation.Primary; import org.springframework.http.client.HttpComponentsClientHttpRequestFactory; import org.springframework.mail.MailSender; import org.springframework.mail.javamail.JavaMailSender; import org.springframework.web.client.RestTemplate; import org.thymeleaf.TemplateEngine; import org.thymeleaf.spring6.SpringTemplateEngine; import org.thymeleaf.templatemode.TemplateMode; import org.thymeleaf.templateresolver.ClassLoaderTemplateResolver; import de.codecentric.boot.admin.server.domain.entities.InstanceRepository; import de.codecentric.boot.admin.server.domain.events.InstanceEvent; import de.codecentric.boot.admin.server.notify.CompositeNotifier; import de.codecentric.boot.admin.server.notify.DingTalkNotifier; import de.codecentric.boot.admin.server.notify.DiscordNotifier; import de.codecentric.boot.admin.server.notify.FeiShuNotifier; import de.codecentric.boot.admin.server.notify.HipchatNotifier; import de.codecentric.boot.admin.server.notify.LetsChatNotifier; import de.codecentric.boot.admin.server.notify.MailNotifier; import de.codecentric.boot.admin.server.notify.MattermostNotifier; import de.codecentric.boot.admin.server.notify.MicrosoftTeamsNotifier; import de.codecentric.boot.admin.server.notify.NotificationTrigger; import de.codecentric.boot.admin.server.notify.Notifier; import de.codecentric.boot.admin.server.notify.NotifierProxyProperties; import de.codecentric.boot.admin.server.notify.OpsGenieNotifier; import de.codecentric.boot.admin.server.notify.PagerdutyNotifier; import de.codecentric.boot.admin.server.notify.RocketChatNotifier; import de.codecentric.boot.admin.server.notify.SlackNotifier; import de.codecentric.boot.admin.server.notify.TelegramNotifier; import de.codecentric.boot.admin.server.notify.WebexNotifier; import de.codecentric.boot.admin.server.notify.filter.FilteringNotifier; import de.codecentric.boot.admin.server.notify.filter.web.NotificationFilterController; @Configuration(proxyBeanMethods = false) @Conditional(SpringBootAdminServerEnabledCondition.class) @EnableConfigurationProperties(NotifierProxyProperties.class) @AutoConfigureAfter(name = { "org.springframework.boot.mail.autoconfigure.MailSenderAutoConfiguration" }) public class AdminServerNotifierAutoConfiguration { private static RestTemplate createNotifierRestTemplate(NotifierProxyProperties proxyProperties) { RestTemplate restTemplate = new RestTemplate(); if (proxyProperties.getHost() != null) { HttpClientBuilder builder = HttpClientBuilder.create(); builder.setProxy(new HttpHost(proxyProperties.getHost(), proxyProperties.getPort())); if (proxyProperties.getUsername() != null && proxyProperties.getPassword() != null) { AuthScope authScope = new AuthScope(proxyProperties.getHost(), proxyProperties.getPort()); Credentials usernamePasswordCredentials = new UsernamePasswordCredentials(proxyProperties.getUsername(), proxyProperties.getPassword().toCharArray()); BasicCredentialsProvider credsProvider = new BasicCredentialsProvider(); credsProvider.setCredentials(authScope, usernamePasswordCredentials); builder.setDefaultCredentialsProvider(credsProvider); } restTemplate.setRequestFactory(new HttpComponentsClientHttpRequestFactory(builder.build())); } return restTemplate; } @Configuration(proxyBeanMethods = false) @ConditionalOnBean(Notifier.class) @Lazy(false) public static class NotifierTriggerConfiguration { @Bean(initMethod = "start", destroyMethod = "stop") @ConditionalOnMissingBean(NotificationTrigger.class) public NotificationTrigger notificationTrigger(Notifier notifier, Publisher events) { return new NotificationTrigger(notifier, events); } } @Configuration(proxyBeanMethods = false) @ConditionalOnBean(Notifier.class) @AutoConfigureBefore({ NotifierTriggerConfiguration.class }) @Lazy(false) public static class CompositeNotifierConfiguration { @Bean @Primary @Conditional(NoSingleNotifierCandidateCondition.class) public CompositeNotifier compositeNotifier(List notifiers) { return new CompositeNotifier(notifiers); } static class NoSingleNotifierCandidateCondition extends NoneNestedConditions { NoSingleNotifierCandidateCondition() { super(ConfigurationPhase.REGISTER_BEAN); } @ConditionalOnSingleCandidate(Notifier.class) static class HasSingleNotifierInstance { } } } @Configuration(proxyBeanMethods = false) @ConditionalOnSingleCandidate(FilteringNotifier.class) @Lazy(false) public static class FilteringNotifierWebConfiguration { private final FilteringNotifier filteringNotifier; public FilteringNotifierWebConfiguration(FilteringNotifier filteringNotifier) { this.filteringNotifier = filteringNotifier; } @Bean public NotificationFilterController notificationFilterController() { return new NotificationFilterController(this.filteringNotifier); } } @Configuration(proxyBeanMethods = false) @AutoConfigureBefore({ NotifierTriggerConfiguration.class, CompositeNotifierConfiguration.class }) @ConditionalOnBean(MailSender.class) @Lazy(false) public static class MailNotifierConfiguration { private final ApplicationContext applicationContext; public MailNotifierConfiguration(ApplicationContext applicationContext) { this.applicationContext = applicationContext; } @Bean @ConditionalOnMissingBean @ConfigurationProperties("spring.boot.admin.notify.mail") public MailNotifier mailNotifier(JavaMailSender mailSender, InstanceRepository repository, TemplateEngine mailNotifierTemplateEngine) { return new MailNotifier(mailSender, repository, mailNotifierTemplateEngine); } @Bean public TemplateEngine mailNotifierTemplateEngine() { ClassLoaderTemplateResolver resolver = new ClassLoaderTemplateResolver(); resolver.setTemplateMode(TemplateMode.HTML); resolver.setCharacterEncoding(StandardCharsets.UTF_8.name()); SpringTemplateEngine templateEngine = new SpringTemplateEngine(); templateEngine.setTemplateResolver(resolver); return templateEngine; } } @Configuration(proxyBeanMethods = false) @ConditionalOnProperty(prefix = "spring.boot.admin.notify.hipchat", name = "url") @AutoConfigureBefore({ NotifierTriggerConfiguration.class, CompositeNotifierConfiguration.class }) @Lazy(false) public static class HipchatNotifierConfiguration { @Bean @ConditionalOnMissingBean @ConfigurationProperties("spring.boot.admin.notify.hipchat") public HipchatNotifier hipchatNotifier(InstanceRepository repository, NotifierProxyProperties proxyProperties) { return new HipchatNotifier(repository, createNotifierRestTemplate(proxyProperties)); } } @Configuration(proxyBeanMethods = false) @ConditionalOnProperty(prefix = "spring.boot.admin.notify.slack", name = "webhook-url") @AutoConfigureBefore({ NotifierTriggerConfiguration.class, CompositeNotifierConfiguration.class }) @Lazy(false) public static class SlackNotifierConfiguration { @Bean @ConditionalOnMissingBean @ConfigurationProperties("spring.boot.admin.notify.slack") public SlackNotifier slackNotifier(InstanceRepository repository, NotifierProxyProperties proxyProperties) { return new SlackNotifier(repository, createNotifierRestTemplate(proxyProperties)); } } @Configuration(proxyBeanMethods = false) @ConditionalOnProperty(prefix = "spring.boot.admin.notify.mattermost", name = "api-url") @AutoConfigureBefore({ NotifierTriggerConfiguration.class, CompositeNotifierConfiguration.class }) @Lazy(false) public static class MattermostNotifierConfiguration { @Bean @ConditionalOnMissingBean @ConfigurationProperties("spring.boot.admin.notify.mattermost") public MattermostNotifier mattermostNotifier(InstanceRepository repository, NotifierProxyProperties proxyProperties) { return new MattermostNotifier(repository, createNotifierRestTemplate(proxyProperties)); } } @Configuration(proxyBeanMethods = false) @ConditionalOnProperty(prefix = "spring.boot.admin.notify.letschat", name = "url") @AutoConfigureBefore({ NotifierTriggerConfiguration.class, CompositeNotifierConfiguration.class }) @Lazy(false) public static class LetsChatNotifierConfiguration { @Bean @ConditionalOnMissingBean @ConfigurationProperties("spring.boot.admin.notify.letschat") public LetsChatNotifier letsChatNotifier(InstanceRepository repository, NotifierProxyProperties proxyProperties) { return new LetsChatNotifier(repository, createNotifierRestTemplate(proxyProperties)); } } @Configuration(proxyBeanMethods = false) @ConditionalOnProperty(prefix = "spring.boot.admin.notify.pagerduty", name = "service-key") @AutoConfigureBefore({ NotifierTriggerConfiguration.class, CompositeNotifierConfiguration.class }) @Lazy(false) public static class PagerdutyNotifierConfiguration { @Bean @ConditionalOnMissingBean @ConfigurationProperties("spring.boot.admin.notify.pagerduty") public PagerdutyNotifier pagerdutyNotifier(InstanceRepository repository, NotifierProxyProperties proxyProperties) { return new PagerdutyNotifier(repository, createNotifierRestTemplate(proxyProperties)); } } @Configuration(proxyBeanMethods = false) @ConditionalOnProperty(prefix = "spring.boot.admin.notify.opsgenie", name = "api-key") @AutoConfigureBefore({ NotifierTriggerConfiguration.class, CompositeNotifierConfiguration.class }) @Lazy(false) public static class OpsGenieNotifierConfiguration { @Bean @ConditionalOnMissingBean @ConfigurationProperties("spring.boot.admin.notify.opsgenie") public OpsGenieNotifier opsgenieNotifier(InstanceRepository repository, NotifierProxyProperties proxyProperties) { return new OpsGenieNotifier(repository, createNotifierRestTemplate(proxyProperties)); } } @Configuration(proxyBeanMethods = false) @ConditionalOnProperty(prefix = "spring.boot.admin.notify.ms-teams", name = "webhook-url") @AutoConfigureBefore({ NotifierTriggerConfiguration.class, CompositeNotifierConfiguration.class }) @Lazy(false) public static class MicrosoftTeamsNotifierConfiguration { @Bean @ConditionalOnMissingBean @ConfigurationProperties("spring.boot.admin.notify.ms-teams") public MicrosoftTeamsNotifier microsoftTeamsNotifier(InstanceRepository repository, NotifierProxyProperties proxyProperties) { return new MicrosoftTeamsNotifier(repository, createNotifierRestTemplate(proxyProperties)); } } @Configuration(proxyBeanMethods = false) @ConditionalOnProperty(prefix = "spring.boot.admin.notify.telegram", name = "auth-token") @AutoConfigureBefore({ NotifierTriggerConfiguration.class, CompositeNotifierConfiguration.class }) @Lazy(false) public static class TelegramNotifierConfiguration { @Bean @ConditionalOnMissingBean @ConfigurationProperties("spring.boot.admin.notify.telegram") public TelegramNotifier telegramNotifier(InstanceRepository repository, NotifierProxyProperties proxyProperties) { return new TelegramNotifier(repository, createNotifierRestTemplate(proxyProperties)); } } @Configuration(proxyBeanMethods = false) @ConditionalOnProperty(prefix = "spring.boot.admin.notify.discord", name = "webhook-url") @AutoConfigureBefore({ NotifierTriggerConfiguration.class, CompositeNotifierConfiguration.class }) @Lazy(false) public static class DiscordNotifierConfiguration { @Bean @ConditionalOnMissingBean @ConfigurationProperties("spring.boot.admin.notify.discord") public DiscordNotifier discordNotifier(InstanceRepository repository, NotifierProxyProperties proxyProperties) { return new DiscordNotifier(repository, createNotifierRestTemplate(proxyProperties)); } } @Configuration(proxyBeanMethods = false) @ConditionalOnProperty(prefix = "spring.boot.admin.notify.dingtalk", name = "webhook-url") @AutoConfigureBefore({ NotifierTriggerConfiguration.class, CompositeNotifierConfiguration.class }) @Lazy(false) public static class DingTalkNotifierConfiguration { @Bean @ConditionalOnMissingBean @ConfigurationProperties("spring.boot.admin.notify.dingtalk") public DingTalkNotifier dingTalkNotifier(InstanceRepository repository, NotifierProxyProperties proxyProperties) { return new DingTalkNotifier(repository, createNotifierRestTemplate(proxyProperties)); } } @Configuration(proxyBeanMethods = false) @ConditionalOnProperty(prefix = "spring.boot.admin.notify.rocketchat", name = "url") @AutoConfigureBefore({ NotifierTriggerConfiguration.class, CompositeNotifierConfiguration.class }) @Lazy(false) public static class RocketChatNotifierConfiguration { @Bean @ConditionalOnMissingBean @ConfigurationProperties("spring.boot.admin.notify.rocketchat") public RocketChatNotifier rocketChatNotifier(InstanceRepository repository, NotifierProxyProperties proxyProperties) { return new RocketChatNotifier(repository, createNotifierRestTemplate(proxyProperties)); } } @Configuration(proxyBeanMethods = false) @ConditionalOnProperty(prefix = "spring.boot.admin.notify.feishu", name = "webhook-url") @AutoConfigureBefore({ NotifierTriggerConfiguration.class, CompositeNotifierConfiguration.class }) @Lazy(false) public static class FeiShuNotifierConfiguration { @Bean @ConditionalOnMissingBean @ConfigurationProperties("spring.boot.admin.notify.feishu") public FeiShuNotifier feiShuNotifier(InstanceRepository repository, NotifierProxyProperties proxyProperties) { return new FeiShuNotifier(repository, createNotifierRestTemplate(proxyProperties)); } } @Configuration(proxyBeanMethods = false) @ConditionalOnProperty(prefix = "spring.boot.admin.notify.webex", name = "auth-token") @AutoConfigureBefore({ NotifierTriggerConfiguration.class, CompositeNotifierConfiguration.class }) @Lazy(false) public static class WebexNotifierConfiguration { @Bean @ConditionalOnMissingBean @ConfigurationProperties("spring.boot.admin.notify.webex") public WebexNotifier webexNotifier(InstanceRepository repository, NotifierProxyProperties proxyProperties) { return new WebexNotifier(repository, createNotifierRestTemplate(proxyProperties)); } } } ================================================ FILE: spring-boot-admin-server/src/main/java/de/codecentric/boot/admin/server/config/AdminServerProperties.java ================================================ /* * 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.server.config; import java.time.Duration; import java.time.temporal.ChronoUnit; import java.util.HashMap; import java.util.HashSet; import java.util.Map; import java.util.Set; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.boot.convert.DurationUnit; import de.codecentric.boot.admin.server.web.PathUtils; import de.codecentric.boot.admin.server.web.client.BasicAuthHttpHeaderProvider.InstanceCredentials; import static java.util.Arrays.asList; @lombok.Data @ConfigurationProperties("spring.boot.admin") public class AdminServerProperties { /** * The context-path prefixes the path where the Admin Servers static assets and api * should be served, relative to the Dispatcher-Servlet. */ private String contextPath = ""; private ServerProperties server = new ServerProperties(); private MonitorProperties monitor = new MonitorProperties(); private InstanceAuthProperties instanceAuth = new InstanceAuthProperties(); private InstanceProxyProperties instanceProxy = new InstanceProxyProperties(); /** * The metadata keys which should be sanitized when serializing to json */ private String[] metadataKeysToSanitize = new String[] { ".*password$", ".*secret$", ".*key$", ".*token$", ".*credentials.*", ".*vcap_services$" }; /** * For Spring Boot 2.x applications the endpoints should be discovered automatically * using the actuator links. For Spring Boot 1.x applications SBA probes for the * specified endpoints using an OPTIONS request. If the path differs from the id you * can specify this as id:path (e.g. health:ping). */ private String[] probedEndpoints = { "health", "env", "metrics", "httptrace:trace", "httptrace", "threaddump:dump", "threaddump", "jolokia", "info", "logfile", "refresh", "flyway", "liquibase", "heapdump", "loggers", "auditevents", "mappings", "scheduledtasks", "configprops", "caches", "beans" }; public void setContextPath(String contextPath) { this.contextPath = PathUtils.normalizePath(contextPath); } /** * @param path the partial path within the admin context-path * @return the full path within the admin context-path */ public String path(String path) { return this.contextPath + path; } @lombok.Data public static class ServerProperties { /** * Enable Spring Boot Admin Server Default: true */ private boolean enabled = true; } @lombok.Data public static class MonitorProperties { /** * Time interval to check the status of instances, must be greater than 1 second. */ @DurationUnit(ChronoUnit.MILLIS) private Duration statusInterval = Duration.ofMillis(10_000L); /** * Lifetime of status. The status won't be updated as long the last status isn't * expired. */ @DurationUnit(ChronoUnit.MILLIS) private Duration statusLifetime = Duration.ofMillis(10_000L); /** * The maximal backoff for status check retries (retry after error has exponential * backoff, minimum backoff is 1 second). */ @DurationUnit(ChronoUnit.MILLIS) private Duration statusMaxBackoff = Duration.ofMillis(60_000L); /** * Time interval to check the info of instances, */ @DurationUnit(ChronoUnit.MILLIS) private Duration infoInterval = Duration.ofMinutes(1L); /** * The maximal backoff for info check retries (retry after error has exponential * backoff, minimum backoff is 1 second). */ @DurationUnit(ChronoUnit.MILLIS) private Duration infoMaxBackoff = Duration.ofMinutes(10); /** * Lifetime of info. The info won't be updated as long the last info isn't * expired. */ @DurationUnit(ChronoUnit.MILLIS) private Duration infoLifetime = Duration.ofMinutes(1L); /** * Default number of retries for failed requests. Individual values for specific * endpoints can be overriden using `spring.boot.admin.monitor.retries.*`. */ private int defaultRetries = 0; /** * Number of retries per endpointId. Defaults to default-retry. */ private Map retries = new HashMap<>(); /** * Default timeout when making requests. Individual values for specific endpoints * can be overriden using `spring.boot.admin.monitor.timeout.*`. */ @DurationUnit(ChronoUnit.MILLIS) private Duration defaultTimeout = Duration.ofMillis(10_000L); /** * timeout per endpointId. Defaults to default-timeout. */ @DurationUnit(ChronoUnit.MILLIS) private Map timeout = new HashMap<>(); } @lombok.Data public static class InstanceAuthProperties { /** * Whether or not to use configuration properties as a source for instance * credentials
* Default: true */ private boolean enabled = true; /** * Default username used for authentication to each instance. Individual values * for specific instances can be overriden using * `spring.boot.admin.instance-auth.service-map.*.user-name`.
* Default: null */ private String defaultUserName = null; /** * Default userpassword used for authentication to each instance. Individual * values for specific instances can be overriden using * `spring.boot.admin.instance-auth.service-map.*.user-password`.
* Default: null */ private String defaultPassword = null; /** * Map of instance credentials per registered service name */ private Map serviceMap = new HashMap<>(); } @lombok.Data public static class InstanceProxyProperties { /** * Headers not to be forwarded when making requests to clients. */ private Set ignoredHeaders = new HashSet<>(asList("Cookie", "Set-Cookie", "Authorization")); } } ================================================ FILE: spring-boot-admin-server/src/main/java/de/codecentric/boot/admin/server/config/AdminServerWebConfiguration.java ================================================ /* * Copyright 2014-2024 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.server.config; import org.springframework.boot.autoconfigure.AutoConfigureAfter; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; import org.springframework.boot.webmvc.autoconfigure.WebMvcAutoConfiguration; import org.springframework.context.ApplicationEventPublisher; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.accept.ContentNegotiationManager; import org.springframework.web.reactive.accept.RequestedContentTypeResolver; import tools.jackson.databind.module.SimpleModule; import de.codecentric.boot.admin.server.eventstore.InstanceEventStore; import de.codecentric.boot.admin.server.services.ApplicationRegistry; import de.codecentric.boot.admin.server.services.InstanceRegistry; import de.codecentric.boot.admin.server.utils.jackson.AdminServerModule; import de.codecentric.boot.admin.server.web.ApplicationsController; import de.codecentric.boot.admin.server.web.InstancesController; import de.codecentric.boot.admin.server.web.client.InstanceWebClient; @Configuration(proxyBeanMethods = false) public class AdminServerWebConfiguration { private final AdminServerProperties adminServerProperties; public AdminServerWebConfiguration(AdminServerProperties adminServerProperties) { this.adminServerProperties = adminServerProperties; } @Bean public SimpleModule adminJacksonModule() { return new AdminServerModule(this.adminServerProperties.getMetadataKeysToSanitize()); } @Bean @ConditionalOnMissingBean public InstancesController instancesController(InstanceRegistry instanceRegistry, InstanceEventStore eventStore) { return new InstancesController(instanceRegistry, eventStore); } @Bean @ConditionalOnMissingBean public ApplicationsController applicationsController(ApplicationRegistry applicationRegistry, ApplicationEventPublisher applicationEventPublisher) { return new ApplicationsController(applicationRegistry, applicationEventPublisher); } @Configuration(proxyBeanMethods = false) @ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.REACTIVE) public static class ReactiveRestApiConfiguration { private final AdminServerProperties adminServerProperties; public ReactiveRestApiConfiguration(AdminServerProperties adminServerProperties) { this.adminServerProperties = adminServerProperties; } @Bean @ConditionalOnMissingBean public de.codecentric.boot.admin.server.web.reactive.InstancesProxyController instancesProxyController( InstanceRegistry instanceRegistry, InstanceWebClient.Builder instanceWebClientBuilder) { return new de.codecentric.boot.admin.server.web.reactive.InstancesProxyController( this.adminServerProperties.getContextPath(), this.adminServerProperties.getInstanceProxy().getIgnoredHeaders(), instanceRegistry, instanceWebClientBuilder.build()); } @Bean public org.springframework.web.reactive.result.method.annotation.RequestMappingHandlerMapping adminHandlerMapping( RequestedContentTypeResolver webFluxContentTypeResolver) { org.springframework.web.reactive.result.method.annotation.RequestMappingHandlerMapping mapping = new de.codecentric.boot.admin.server.web.reactive.AdminControllerHandlerMapping( this.adminServerProperties.getContextPath()); mapping.setOrder(0); mapping.setContentTypeResolver(webFluxContentTypeResolver); return mapping; } } @Configuration(proxyBeanMethods = false) @ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET) @AutoConfigureAfter(WebMvcAutoConfiguration.class) public static class ServletRestApiConfiguration { private final AdminServerProperties adminServerProperties; public ServletRestApiConfiguration(AdminServerProperties adminServerProperties) { this.adminServerProperties = adminServerProperties; } @Bean @ConditionalOnMissingBean public de.codecentric.boot.admin.server.web.servlet.InstancesProxyController instancesProxyController( InstanceRegistry instanceRegistry, InstanceWebClient.Builder instanceWebClientBuilder) { return new de.codecentric.boot.admin.server.web.servlet.InstancesProxyController( this.adminServerProperties.getContextPath(), this.adminServerProperties.getInstanceProxy().getIgnoredHeaders(), instanceRegistry, instanceWebClientBuilder.build()); } @Bean public org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping adminHandlerMapping( ContentNegotiationManager contentNegotiationManager) { org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping mapping = new de.codecentric.boot.admin.server.web.servlet.AdminControllerHandlerMapping( this.adminServerProperties.getContextPath()); mapping.setOrder(0); mapping.setContentNegotiationManager(contentNegotiationManager); return mapping; } } } ================================================ FILE: spring-boot-admin-server/src/main/java/de/codecentric/boot/admin/server/config/EnableAdminServer.java ================================================ /* * 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.server.config; import java.lang.annotation.Documented; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; import org.springframework.context.annotation.Import; /** * @author Dennis Schulte */ @Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @Documented @Import(AdminServerMarkerConfiguration.class) public @interface EnableAdminServer { } ================================================ FILE: spring-boot-admin-server/src/main/java/de/codecentric/boot/admin/server/config/SpringBootAdminServerEnabledCondition.java ================================================ /* * 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.server.config; import org.springframework.boot.autoconfigure.condition.ConditionOutcome; import org.springframework.boot.autoconfigure.condition.SpringBootCondition; import org.springframework.boot.context.properties.bind.Bindable; import org.springframework.boot.context.properties.bind.Binder; import org.springframework.context.annotation.ConditionContext; import org.springframework.core.type.AnnotatedTypeMetadata; /** * This condition checks if the sever should be enabled. Property * spring.boot.admin.server.enabled is checked. */ public class SpringBootAdminServerEnabledCondition extends SpringBootCondition { @Override public ConditionOutcome getMatchOutcome(ConditionContext context, AnnotatedTypeMetadata annotatedTypeMetadata) { AdminServerProperties serverProperties = getClientProperties(context); if (!serverProperties.getServer().isEnabled()) { return ConditionOutcome .noMatch("Spring Boot Server is disabled, because 'spring.boot.admin.server.enabled' is false."); } return ConditionOutcome.match(); } private AdminServerProperties getClientProperties(ConditionContext context) { AdminServerProperties serverProperties = new AdminServerProperties(); Binder.get(context.getEnvironment()).bind("spring.boot.admin", Bindable.ofInstance(serverProperties)); return serverProperties; } } ================================================ FILE: spring-boot-admin-server/src/main/java/de/codecentric/boot/admin/server/config/package-info.java ================================================ /* * 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. */ // Spring Boot Admin Server - core configuration package. @NullMarked package de.codecentric.boot.admin.server.config; import org.jspecify.annotations.NullMarked; ================================================ FILE: spring-boot-admin-server/src/main/java/de/codecentric/boot/admin/server/domain/entities/Application.java ================================================ /* * 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.server.domain.entities; import java.time.Instant; import java.util.ArrayList; import java.util.Collections; import java.util.List; import org.jspecify.annotations.Nullable; import org.springframework.util.Assert; import de.codecentric.boot.admin.server.domain.values.BuildVersion; import de.codecentric.boot.admin.server.domain.values.StatusInfo; @lombok.Data public final class Application { private final String name; @Nullable private final BuildVersion buildVersion; private final String status; private final Instant statusTimestamp; private final List instances; @lombok.Builder(builderClassName = "Builder", toBuilder = true) private Application(String name, @Nullable BuildVersion buildVersion, @Nullable String status, @Nullable Instant statusTimestamp, List instances) { Assert.notNull(name, "'name' must not be null"); this.name = name; this.buildVersion = buildVersion; this.status = (status != null) ? status : StatusInfo.STATUS_UNKNOWN; this.statusTimestamp = (statusTimestamp != null) ? statusTimestamp : Instant.now(); if (instances.isEmpty()) { this.instances = Collections.emptyList(); } else { this.instances = new ArrayList<>(instances); } } public static Application.Builder create(String name) { return builder().name(name); } public static class Builder { // Will be generated by lombok } } ================================================ FILE: spring-boot-admin-server/src/main/java/de/codecentric/boot/admin/server/domain/entities/EventsourcingInstanceRepository.java ================================================ /* * 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.server.domain.entities; import java.util.function.BiFunction; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import reactor.util.retry.Retry; 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.InstanceEventStore; import de.codecentric.boot.admin.server.eventstore.OptimisticLockingException; /** * InstanceRepository storing instances using an event log. * * @author Johannes Edmeier */ public class EventsourcingInstanceRepository implements InstanceRepository { private static final Logger log = LoggerFactory.getLogger(EventsourcingInstanceRepository.class); private final InstanceEventStore eventStore; private final Retry retryOptimisticLockException = Retry.max(10) .doBeforeRetry((s) -> log.debug("Retrying after OptimisticLockingException", s.failure())) .filter(OptimisticLockingException.class::isInstance); public EventsourcingInstanceRepository(InstanceEventStore eventStore) { this.eventStore = eventStore; } @Override public Mono save(Instance instance) { return this.eventStore.append(instance.getUnsavedEvents()).then(Mono.just(instance.clearUnsavedEvents())); } @Override public Flux findAll() { return this.eventStore.findAll() .groupBy(InstanceEvent::getInstance) .flatMap((f) -> f.reduce(Instance.create(f.key()), Instance::apply)); } @Override public Mono find(InstanceId id) { return this.eventStore.find(id) .collectList() .filter((e) -> !e.isEmpty()) .map((e) -> Instance.create(id).apply(e)); } @Override public Flux findByName(String name) { return findAll().filter((a) -> a.isRegistered() && name.equals(a.getRegistration().getName())); } @Override public Mono compute(InstanceId id, BiFunction> remappingFunction) { return this.find(id) .flatMap((application) -> remappingFunction.apply(id, application)) .switchIfEmpty(Mono.defer(() -> remappingFunction.apply(id, null))) .flatMap(this::save) .retryWhen(this.retryOptimisticLockException); } @Override public Mono computeIfPresent(InstanceId id, BiFunction> remappingFunction) { return this.find(id) .flatMap((application) -> remappingFunction.apply(id, application)) .flatMap(this::save) .retryWhen(this.retryOptimisticLockException); } } ================================================ FILE: spring-boot-admin-server/src/main/java/de/codecentric/boot/admin/server/domain/entities/Instance.java ================================================ /* * 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.server.domain.entities; import java.io.Serializable; import java.time.Instant; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.List; import java.util.Map; import java.util.Objects; import org.jspecify.annotations.Nullable; import org.springframework.util.Assert; import de.codecentric.boot.admin.server.domain.events.InstanceDeregisteredEvent; import de.codecentric.boot.admin.server.domain.events.InstanceEndpointsDetectedEvent; import de.codecentric.boot.admin.server.domain.events.InstanceEvent; import de.codecentric.boot.admin.server.domain.events.InstanceInfoChangedEvent; import de.codecentric.boot.admin.server.domain.events.InstanceRegisteredEvent; import de.codecentric.boot.admin.server.domain.events.InstanceRegistrationUpdatedEvent; import de.codecentric.boot.admin.server.domain.events.InstanceStatusChangedEvent; import de.codecentric.boot.admin.server.domain.values.BuildVersion; import de.codecentric.boot.admin.server.domain.values.Endpoint; import de.codecentric.boot.admin.server.domain.values.Endpoints; import de.codecentric.boot.admin.server.domain.values.Info; import de.codecentric.boot.admin.server.domain.values.InstanceId; import de.codecentric.boot.admin.server.domain.values.Registration; import de.codecentric.boot.admin.server.domain.values.StatusInfo; import de.codecentric.boot.admin.server.domain.values.Tags; import static java.util.Collections.emptyList; import static java.util.Collections.emptyMap; import static java.util.Collections.unmodifiableList; /** * The aggregate representing a registered application instance. * * @author Johannes Edmeier */ @lombok.Data @lombok.EqualsAndHashCode(exclude = { "unsavedEvents", "statusTimestamp" }) @lombok.ToString(exclude = "unsavedEvents") public final class Instance implements Serializable { private final InstanceId id; private final long version; @Nullable private final Registration registration; private final boolean registered; private final StatusInfo statusInfo; private final Instant statusTimestamp; private final Info info; private final List unsavedEvents; private final Endpoints endpoints; @Nullable private final BuildVersion buildVersion; private final Tags tags; private Instance(InstanceId id) { this(id, -1L, null, false, StatusInfo.ofUnknown(), Instant.EPOCH, Info.empty(), Endpoints.empty(), null, Tags.empty(), emptyList()); } private Instance(InstanceId id, long version, @Nullable Registration registration, boolean registered, StatusInfo statusInfo, Instant statusTimestamp, Info info, Endpoints endpoints, @Nullable BuildVersion buildVersion, Tags tags, List unsavedEvents) { Assert.notNull(id, "'id' must not be null"); Assert.notNull(endpoints, "'endpoints' must not be null"); Assert.notNull(info, "'info' must not be null"); Assert.notNull(statusInfo, "'statusInfo' must not be null"); this.id = id; this.version = version; this.registration = registration; this.registered = registered; this.statusInfo = statusInfo; this.statusTimestamp = statusTimestamp; this.info = info; this.endpoints = (registered && (registration != null)) ? endpoints.withEndpoint(Endpoint.HEALTH, registration.getHealthUrl()) : endpoints; this.unsavedEvents = unsavedEvents; this.buildVersion = buildVersion; this.tags = tags; } public static Instance create(InstanceId id) { Assert.notNull(id, "'id' must not be null"); return new Instance(id); } public Instance register(Registration registration) { Assert.notNull(registration, "'registration' must not be null"); if (!this.isRegistered()) { return this.apply(new InstanceRegisteredEvent(this.id, this.nextVersion(), registration), true); } if (!Objects.equals(this.registration, registration)) { return this.apply(new InstanceRegistrationUpdatedEvent(this.id, this.nextVersion(), registration), true); } return this; } public Instance deregister() { if (this.isRegistered()) { return this.apply(new InstanceDeregisteredEvent(this.id, this.nextVersion()), true); } return this; } public Instance withInfo(Info info) { Assert.notNull(info, "'info' must not be null"); if (Objects.equals(this.info, info)) { return this; } return this.apply(new InstanceInfoChangedEvent(this.id, this.nextVersion(), info), true); } public Instance withStatusInfo(StatusInfo statusInfo) { Assert.notNull(statusInfo, "'statusInfo' must not be null"); if (Objects.equals(this.statusInfo.getStatus(), statusInfo.getStatus())) { return this; } return this.apply(new InstanceStatusChangedEvent(this.id, this.nextVersion(), statusInfo), true); } public Instance withEndpoints(Endpoints endpoints) { Assert.notNull(endpoints, "'endpoints' must not be null"); Endpoints endpointsWithHealth = (this.registration != null) ? endpoints.withEndpoint(Endpoint.HEALTH, this.registration.getHealthUrl()) : endpoints; if (Objects.equals(this.endpoints, endpointsWithHealth)) { return this; } return this.apply(new InstanceEndpointsDetectedEvent(this.id, this.nextVersion(), endpoints), true); } public boolean isRegistered() { return this.registered; } public Registration getRegistration() { if (this.registration == null) { throw new IllegalStateException("Application '" + this.id + "' has no valid registration."); } return this.registration; } List getUnsavedEvents() { return unmodifiableList(this.unsavedEvents); } Instance clearUnsavedEvents() { return new Instance(this.id, this.version, this.registration, this.registered, this.statusInfo, this.statusTimestamp, this.info, this.endpoints, this.buildVersion, this.tags, emptyList()); } Instance apply(Collection events) { Assert.notNull(events, "'events' must not be null"); Instance instance = this; for (InstanceEvent event : events) { instance = instance.apply(event); } return instance; } Instance apply(InstanceEvent event) { return this.apply(event, false); } private Instance apply(InstanceEvent event, boolean isNewEvent) { Assert.notNull(event, "'event' must not be null"); Assert.isTrue(this.id.equals(event.getInstance()), "'event' must refer the same instance"); Assert.isTrue(event.getVersion() >= this.nextVersion(), () -> "Event " + event.getVersion() + " must be greater or equal to " + this.nextVersion()); List unsavedEvents = appendToEvents(event, isNewEvent); if (event instanceof InstanceRegisteredEvent registeredEvent) { Registration registration = registeredEvent.getRegistration(); return new Instance(this.id, event.getVersion(), registration, true, StatusInfo.ofUnknown(), event.getTimestamp(), Info.empty(), Endpoints.empty(), updateBuildVersion(registration.getMetadata()), updateTags(registration.getMetadata()), unsavedEvents); } else if (event instanceof InstanceRegistrationUpdatedEvent updatedEvent) { Registration registration = updatedEvent.getRegistration(); return new Instance(this.id, event.getVersion(), registration, this.registered, this.statusInfo, this.statusTimestamp, this.info, this.endpoints, updateBuildVersion(registration.getMetadata(), this.info.getValues()), updateTags(registration.getMetadata(), this.info.getValues()), unsavedEvents); } else if (event instanceof InstanceStatusChangedEvent statusChangedEvent) { StatusInfo statusInfo = statusChangedEvent.getStatusInfo(); return new Instance(this.id, event.getVersion(), this.registration, this.registered, statusInfo, event.getTimestamp(), this.info, this.endpoints, this.buildVersion, this.tags, unsavedEvents); } else if (event instanceof InstanceEndpointsDetectedEvent endpointsDetectedEvent) { Endpoints endpoints = endpointsDetectedEvent.getEndpoints(); return new Instance(this.id, event.getVersion(), this.registration, this.registered, this.statusInfo, this.statusTimestamp, this.info, endpoints, this.buildVersion, this.tags, unsavedEvents); } else if (event instanceof InstanceInfoChangedEvent infoChangedEvent) { Info info = infoChangedEvent.getInfo(); Map metaData = (this.registration != null) ? this.registration.getMetadata() : emptyMap(); return new Instance(this.id, event.getVersion(), this.registration, this.registered, this.statusInfo, this.statusTimestamp, info, this.endpoints, updateBuildVersion(metaData, info.getValues()), updateTags(metaData, info.getValues()), unsavedEvents); } else if (event instanceof InstanceDeregisteredEvent) { return new Instance(this.id, event.getVersion(), this.registration, false, StatusInfo.ofUnknown(), event.getTimestamp(), Info.empty(), Endpoints.empty(), null, Tags.empty(), unsavedEvents); } return this; } private long nextVersion() { return this.version + 1L; } private List appendToEvents(InstanceEvent event, boolean isNewEvent) { if (!isNewEvent) { return this.unsavedEvents; } ArrayList events = new ArrayList<>(this.unsavedEvents.size() + 1); events.addAll(this.unsavedEvents); events.add(event); return events; } @Nullable @SafeVarargs private BuildVersion updateBuildVersion(Map... sources) { return Arrays.stream(sources).map(BuildVersion::from).filter(Objects::nonNull).findFirst().orElse(null); } @SafeVarargs private Tags updateTags(Map... sources) { return Arrays.stream(sources).map((source) -> Tags.from(source, "tags")).reduce(Tags.empty(), Tags::append); } } ================================================ FILE: spring-boot-admin-server/src/main/java/de/codecentric/boot/admin/server/domain/entities/InstanceRepository.java ================================================ /* * 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.server.domain.entities; import java.util.function.BiFunction; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import de.codecentric.boot.admin.server.domain.values.InstanceId; /** * Responsible for storing instances. * * @author Johannes Edmeier */ public interface InstanceRepository { /** * Saves the Instance * @param app instance to save * @return the saved instance */ Mono save(Instance app); /** * @return all instances in the repository; */ Flux findAll(); /** * @param id the instances id * @return the instance with the specified id; */ Mono find(InstanceId id); /** * @param name the instances name * @return all instance with the specified name; */ Flux findByName(String name); /** * Updates the instance associated with the id using the remapping function. If there * is no associated instance the function will be called with the id and null. * @param id instance to update * @param remappingFunction function to apply * @return the saved istance */ Mono compute(InstanceId id, BiFunction> remappingFunction); /** * Updates the instance associated with the id using the remapping function. If there * is no associated instance the function will not be called. * @param id instance to update * @param remappingFunction function to apply * @return the saved istance */ Mono computeIfPresent(InstanceId id, BiFunction> remappingFunction); } ================================================ FILE: spring-boot-admin-server/src/main/java/de/codecentric/boot/admin/server/domain/entities/SnapshottingInstanceRepository.java ================================================ /* * 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.server.domain.entities; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import java.util.function.Function; import org.jspecify.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import reactor.core.Disposable; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; 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.InstanceEventStore; import de.codecentric.boot.admin.server.eventstore.OptimisticLockingException; /** * InstanceRepository storing instances using an event log. * * @author Johannes Edmeier */ public class SnapshottingInstanceRepository extends EventsourcingInstanceRepository { private static final Logger log = LoggerFactory.getLogger(SnapshottingInstanceRepository.class); private final ConcurrentMap snapshots = new ConcurrentHashMap<>(); private final Set outdatedSnapshots = ConcurrentHashMap.newKeySet(); private final InstanceEventStore eventStore; @Nullable private Disposable subscription; public SnapshottingInstanceRepository(InstanceEventStore eventStore) { super(eventStore); this.eventStore = eventStore; } @Override public Flux findAll() { return Mono.fromSupplier(this.snapshots::values).flatMapIterable(Function.identity()); } @Override public Mono find(InstanceId id) { return Mono.defer(() -> { if (!this.outdatedSnapshots.contains(id)) { return Mono.justOrEmpty(this.snapshots.get(id)); } else { return rehydrateSnapshot(id).doOnSuccess((v) -> this.outdatedSnapshots.remove(v.getId())); } }); } @Override public Mono save(Instance instance) { return super.save(instance).doOnError(OptimisticLockingException.class, (e) -> this.outdatedSnapshots.add(instance.getId())); } public void start() { this.subscription = this.eventStore.findAll().concatWith(this.eventStore).subscribe(this::updateSnapshot); } public void stop() { if (this.subscription != null) { this.subscription.dispose(); this.subscription = null; } } protected Mono rehydrateSnapshot(InstanceId id) { return super.find(id).map((instance) -> this.snapshots.compute(id, (key, snapshot) -> { // check if the loaded version hasn't been already outdated by a snapshot if (snapshot == null || instance.getVersion() >= snapshot.getVersion()) { return instance; } else { return snapshot; } })); } protected void updateSnapshot(InstanceEvent event) { try { this.snapshots.compute(event.getInstance(), (key, old) -> { Instance instance = (old != null) ? old : Instance.create(key); if (event.getVersion() > instance.getVersion()) { return instance.apply(event); } return instance; }); } catch (Exception ex) { log.warn("Error while updating the snapshot with event {}", event, ex); } } } ================================================ FILE: spring-boot-admin-server/src/main/java/de/codecentric/boot/admin/server/domain/entities/package-info.java ================================================ /* * 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. */ // Spring Boot Admin Server - entity package. @NullMarked package de.codecentric.boot.admin.server.domain.entities; import org.jspecify.annotations.NullMarked; ================================================ FILE: spring-boot-admin-server/src/main/java/de/codecentric/boot/admin/server/domain/events/InstanceDeregisteredEvent.java ================================================ /* * 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.server.domain.events; import java.io.Serial; import java.time.Instant; import de.codecentric.boot.admin.server.domain.values.InstanceId; /** * This event gets emitted when an instance is unregistered. * * @author Johannes Edmeier */ @lombok.Value @lombok.EqualsAndHashCode(callSuper = true) @lombok.ToString(callSuper = true) public class InstanceDeregisteredEvent extends InstanceEvent { public static final String TYPE = "DEREGISTERED"; @Serial private static final long serialVersionUID = 1L; public InstanceDeregisteredEvent(InstanceId instance, long version) { this(instance, version, Instant.now()); } public InstanceDeregisteredEvent(InstanceId instance, long version, Instant timestamp) { super(instance, version, TYPE, timestamp); } } ================================================ FILE: spring-boot-admin-server/src/main/java/de/codecentric/boot/admin/server/domain/events/InstanceEndpointsDetectedEvent.java ================================================ /* * 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.server.domain.events; import java.io.Serial; import java.time.Instant; import de.codecentric.boot.admin.server.domain.values.Endpoints; import de.codecentric.boot.admin.server.domain.values.InstanceId; /** * This event gets emitted when all instance's endpoints are discovered. * * @author Johannes Edmeier */ @lombok.Value @lombok.EqualsAndHashCode(callSuper = true) @lombok.ToString(callSuper = true) public class InstanceEndpointsDetectedEvent extends InstanceEvent { public static final String TYPE = "ENDPOINTS_DETECTED"; @Serial private static final long serialVersionUID = 1L; Endpoints endpoints; public InstanceEndpointsDetectedEvent(InstanceId instance, long version, Endpoints endpoints) { this(instance, version, Instant.now(), endpoints); } public InstanceEndpointsDetectedEvent(InstanceId instance, long version, Instant timestamp, Endpoints endpoints) { super(instance, version, TYPE, timestamp); this.endpoints = endpoints; } } ================================================ FILE: spring-boot-admin-server/src/main/java/de/codecentric/boot/admin/server/domain/events/InstanceEvent.java ================================================ /* * 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.server.domain.events; import java.io.Serial; import java.io.Serializable; import java.time.Instant; import org.springframework.util.Assert; import de.codecentric.boot.admin.server.domain.values.InstanceId; /** * Abstract Event regarding registered instances * * @author Johannes Edmeier */ @lombok.Data public abstract class InstanceEvent implements Serializable { @Serial private static final long serialVersionUID = 1L; private final InstanceId instance; private final long version; private final Instant timestamp; private final String type; protected InstanceEvent(InstanceId instance, long version, String type, Instant timestamp) { Assert.notNull(instance, "'instance' must not be null"); Assert.notNull(timestamp, "'timestamp' must not be null"); Assert.hasText(type, "'type' must not be empty"); this.instance = instance; this.version = version; this.timestamp = timestamp; this.type = type; } } ================================================ FILE: spring-boot-admin-server/src/main/java/de/codecentric/boot/admin/server/domain/events/InstanceInfoChangedEvent.java ================================================ /* * 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.server.domain.events; import java.io.Serial; import java.time.Instant; import de.codecentric.boot.admin.server.domain.values.Info; import de.codecentric.boot.admin.server.domain.values.InstanceId; /** * This event gets emitted when an instance information changes. * * @author Johannes Edmeier */ @lombok.Value @lombok.EqualsAndHashCode(callSuper = true) @lombok.ToString(callSuper = true) public class InstanceInfoChangedEvent extends InstanceEvent { public static final String TYPE = "INFO_CHANGED"; @Serial private static final long serialVersionUID = 1L; Info info; public InstanceInfoChangedEvent(InstanceId instance, long version, Info info) { this(instance, version, Instant.now(), info); } public InstanceInfoChangedEvent(InstanceId instance, long version, Instant timestamp, Info info) { super(instance, version, TYPE, timestamp); this.info = info; } } ================================================ FILE: spring-boot-admin-server/src/main/java/de/codecentric/boot/admin/server/domain/events/InstanceRegisteredEvent.java ================================================ /* * 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.server.domain.events; import java.io.Serial; import java.time.Instant; import de.codecentric.boot.admin.server.domain.values.InstanceId; import de.codecentric.boot.admin.server.domain.values.Registration; /** * This event gets emitted when an instance is registered. * * @author Johannes Edmeier */ @lombok.Value @lombok.EqualsAndHashCode(callSuper = true) @lombok.ToString(callSuper = true) public class InstanceRegisteredEvent extends InstanceEvent { public static final String TYPE = "REGISTERED"; @Serial private static final long serialVersionUID = 1L; Registration registration; public InstanceRegisteredEvent(InstanceId instance, long version, Registration registration) { this(instance, version, Instant.now(), registration); } public InstanceRegisteredEvent(InstanceId instance, long version, Instant timestamp, Registration registration) { super(instance, version, TYPE, timestamp); this.registration = registration; } } ================================================ FILE: spring-boot-admin-server/src/main/java/de/codecentric/boot/admin/server/domain/events/InstanceRegistrationUpdatedEvent.java ================================================ /* * 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.server.domain.events; import java.io.Serial; import java.time.Instant; import de.codecentric.boot.admin.server.domain.values.InstanceId; import de.codecentric.boot.admin.server.domain.values.Registration; /** * This event gets emitted when an instance updates it's registration. * * @author Johannes Edmeier */ @lombok.Value @lombok.EqualsAndHashCode(callSuper = true) @lombok.ToString(callSuper = true) public class InstanceRegistrationUpdatedEvent extends InstanceEvent { public static final String TYPE = "REGISTRATION_UPDATED"; @Serial private static final long serialVersionUID = 1L; Registration registration; public InstanceRegistrationUpdatedEvent(InstanceId instance, long version, Registration registration) { this(instance, version, Instant.now(), registration); } public InstanceRegistrationUpdatedEvent(InstanceId instance, long version, Instant timestamp, Registration registration) { super(instance, version, TYPE, timestamp); this.registration = registration; } } ================================================ FILE: spring-boot-admin-server/src/main/java/de/codecentric/boot/admin/server/domain/events/InstanceStatusChangedEvent.java ================================================ /* * 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.server.domain.events; import java.io.Serial; import java.time.Instant; import de.codecentric.boot.admin.server.domain.values.InstanceId; import de.codecentric.boot.admin.server.domain.values.StatusInfo; /** * This event gets emitted when an instance changes its status. * * @author Johannes Edmeier */ @lombok.Value @lombok.EqualsAndHashCode(callSuper = true) @lombok.ToString(callSuper = true) public class InstanceStatusChangedEvent extends InstanceEvent { public static final String TYPE = "STATUS_CHANGED"; @Serial private static final long serialVersionUID = 1L; StatusInfo statusInfo; public InstanceStatusChangedEvent(InstanceId instance, long version, StatusInfo statusInfo) { this(instance, version, Instant.now(), statusInfo); } public InstanceStatusChangedEvent(InstanceId instance, long version, Instant timestamp, StatusInfo statusInfo) { super(instance, version, TYPE, timestamp); this.statusInfo = statusInfo; } } ================================================ FILE: spring-boot-admin-server/src/main/java/de/codecentric/boot/admin/server/domain/events/package-info.java ================================================ /* * 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. */ // Spring Boot Admin Server - events package. @NullMarked package de.codecentric.boot.admin.server.domain.events; import org.jspecify.annotations.NullMarked; ================================================ FILE: spring-boot-admin-server/src/main/java/de/codecentric/boot/admin/server/domain/values/BuildVersion.java ================================================ /* * 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.server.domain.values; import java.io.Serializable; import java.util.Map; import java.util.Scanner; import org.jspecify.annotations.Nullable; import org.springframework.util.StringUtils; @lombok.Data public final class BuildVersion implements Serializable, Comparable { private static final String DEFAULT_VERSION = "UNKNOWN"; private final String value; private BuildVersion(String value) { if (!StringUtils.hasText(value)) { this.value = DEFAULT_VERSION; } else { this.value = value; } } public static BuildVersion valueOf(String s) { return new BuildVersion(s); } @Nullable public static BuildVersion from(Map map) { if (map.isEmpty()) { return null; } Object build = map.get("build"); if (build instanceof Map) { Object version = ((Map) build).get("version"); if (version instanceof String versionString) { return valueOf(versionString); } } Object version = map.get("build.version"); if (version instanceof String versionString) { return valueOf(versionString); } version = map.get("version"); if (version instanceof String versionString) { return valueOf(versionString); } return null; } @Override public String toString() { return this.value; } @Override public int compareTo(BuildVersion other) { try (Scanner s1 = new Scanner(this.value); Scanner s2 = new Scanner(other.value)) { s1.useDelimiter("[.\\-+]"); s2.useDelimiter("[.\\-+]"); while (s1.hasNext() && s2.hasNext()) { int c; if (s1.hasNextInt() && s2.hasNextInt()) { c = Integer.compare(s1.nextInt(), s2.nextInt()); } else { c = s1.next().compareTo(s2.next()); } if (c != 0) { return c; } } if (s1.hasNext()) { return 1; } else if (s2.hasNext()) { return -1; } } return 0; } } ================================================ FILE: spring-boot-admin-server/src/main/java/de/codecentric/boot/admin/server/domain/values/Endpoint.java ================================================ /* * 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.server.domain.values; import java.io.Serializable; import org.springframework.util.Assert; @lombok.Data public final class Endpoint implements Serializable { public static final String INFO = "info"; public static final String HEALTH = "health"; public static final String LOGFILE = "logfile"; public static final String ENV = "env"; public static final String HTTPTRACE = "httptrace"; public static final String HTTPEXCHANGES = "httpexchanges"; public static final String THREADDUMP = "threaddump"; public static final String LIQUIBASE = "liquibase"; public static final String FLYWAY = "flyway"; public static final String ACTUATOR_INDEX = "actuator-index"; public static final String BEANS = "beans"; public static final String CONFIGPROPS = "configprops"; public static final String MAPPINGS = "mappings"; public static final String STARTUP = "startup"; private final String id; private final String url; private Endpoint(String id, String url) { Assert.hasText(id, "'id' must not be empty."); Assert.hasText(url, "'url' must not be empty."); this.id = id; this.url = url; } public static Endpoint of(String id, String url) { return new Endpoint(id, url); } } ================================================ FILE: spring-boot-admin-server/src/main/java/de/codecentric/boot/admin/server/domain/values/Endpoints.java ================================================ /* * 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.server.domain.values; import java.io.Serializable; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.Iterator; import java.util.Map; import java.util.Optional; import java.util.function.Function; import java.util.stream.Stream; import org.jspecify.annotations.Nullable; import static java.util.stream.Collectors.toMap; @lombok.EqualsAndHashCode @lombok.ToString public final class Endpoints implements Iterable, Serializable { private static final Endpoints EMPTY = new Endpoints(Collections.emptyList()); private final Map endpoints; private Endpoints(Collection endpoints) { if (endpoints.isEmpty()) { this.endpoints = Collections.emptyMap(); } else { this.endpoints = endpoints.stream().collect(toMap(Endpoint::getId, Function.identity())); } } public static Endpoints empty() { return EMPTY; } public static Endpoints single(String id, String url) { return new Endpoints(Collections.singletonList(Endpoint.of(id, url))); } public static Endpoints of(@Nullable Collection endpoints) { if (endpoints == null || endpoints.isEmpty()) { return empty(); } return new Endpoints(endpoints); } public Optional get(String id) { return Optional.ofNullable(this.endpoints.get(id)); } public boolean isPresent(String id) { return this.endpoints.containsKey(id); } @Override public Iterator iterator() { return new UnmodifiableIterator<>(this.endpoints.values().iterator()); } public Endpoints withEndpoint(String id, String url) { Endpoint endpoint = Endpoint.of(id, url); HashMap newEndpoints = new HashMap<>(this.endpoints); newEndpoints.put(endpoint.getId(), endpoint); return new Endpoints(newEndpoints.values()); } public Stream stream() { return this.endpoints.values().stream(); } private record UnmodifiableIterator(Iterator delegate) implements Iterator { @Override public boolean hasNext() { return this.delegate.hasNext(); } @Override public T next() { return this.delegate.next(); } } } ================================================ FILE: spring-boot-admin-server/src/main/java/de/codecentric/boot/admin/server/domain/values/Info.java ================================================ /* * 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.server.domain.values; import java.io.Serializable; import java.util.Collections; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.Map; import com.fasterxml.jackson.annotation.JsonAnyGetter; import com.fasterxml.jackson.annotation.JsonAnySetter; import org.jspecify.annotations.Nullable; /** * Represents the info fetched from the info actuator endpoint * * @author Johannes Edmeier */ @lombok.Data public final class Info implements Serializable { private static final Info EMPTY = new Info(Collections.emptyMap()); private Map values = new HashMap<>(); public Info() { } private Info(Map values) { if (!values.isEmpty()) { this.values = Collections.unmodifiableMap(new LinkedHashMap<>(values)); } } public static Info from(@Nullable Map values) { if (values == null || values.isEmpty()) { return empty(); } return new Info(values); } public static Info empty() { return EMPTY; } @JsonAnySetter public void put(String key, Object value) { this.values.put(key, value); } @JsonAnyGetter public Map getValues() { return Collections.unmodifiableMap(values); } } ================================================ FILE: spring-boot-admin-server/src/main/java/de/codecentric/boot/admin/server/domain/values/InstanceId.java ================================================ /* * 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.server.domain.values; import java.io.Serializable; import org.springframework.util.Assert; /** * Value type for the instance identifier */ @lombok.Data public final class InstanceId implements Serializable, Comparable { private final String value; private InstanceId(String value) { Assert.hasText(value, "'value' must have text"); this.value = value; } public static InstanceId of(String value) { return new InstanceId(value); } @Override public String toString() { return value; } @Override public int compareTo(InstanceId that) { return this.value.compareTo(that.value); } } ================================================ FILE: spring-boot-admin-server/src/main/java/de/codecentric/boot/admin/server/domain/values/Registration.java ================================================ /* * 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.server.domain.values; import java.io.Serializable; import java.net.URI; import java.net.URISyntaxException; import java.util.Collections; import java.util.LinkedHashMap; import java.util.Map; import lombok.ToString; import org.jspecify.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.util.Assert; import org.springframework.util.StringUtils; /** * Registration info for the instance registers with (including metadata) * * @author Johannes Edmeier */ @lombok.Data @ToString(exclude = "metadata") public final class Registration implements Serializable { private static final Logger log = LoggerFactory.getLogger(Registration.class); private final String name; /** * Base URL of the Actuator (management) endpoints. May run on a different port or * context than the service itself. Must be an absolute URL when present. Example: * https://example.com/actuator */ @Nullable private final String managementUrl; /** * Absolute URL of the Actuator health endpoint. Required and used by Spring Boot * Admin to determine the instance status. Example: * https://example.com/actuator/health */ private final String healthUrl; /** * Public base URL of the business application (not the Actuator base). Used by Spring * Boot Admin to link to the running app (e.g., "Open application"). Must be an * absolute URL; can be overridden via metadata keys "service-url" and "service-path". */ @Nullable private final String serviceUrl; private final String source; private final Map metadata; @lombok.Builder(builderClassName = "Builder", toBuilder = true) private Registration(String name, @Nullable String managementUrl, String healthUrl, @Nullable String serviceUrl, String source, @lombok.Singular("metadata") Map metadata) { Assert.hasText(name, "'name' must not be empty."); Assert.hasText(healthUrl, "'healthUrl' must not be empty."); Assert.isTrue(checkUrl(healthUrl), "'healthUrl' is not valid: " + healthUrl); Assert.isTrue(!StringUtils.hasText(managementUrl) || checkUrl(managementUrl), "'managementUrl' is not valid: " + managementUrl); Assert.isTrue(!StringUtils.hasText(serviceUrl) || checkUrl(serviceUrl), "'serviceUrl' is not valid: " + serviceUrl); this.name = name; this.managementUrl = managementUrl; this.healthUrl = healthUrl; this.serviceUrl = this.getServiceUrl(serviceUrl, metadata); this.source = source; this.metadata = new LinkedHashMap<>(); for (Map.Entry entry : metadata.entrySet()) { String key = entry.getKey(); String value = entry.getValue(); this.metadata.put(key, value); } } public static Registration.Builder create(String name, String healthUrl) { return builder().name(name).healthUrl(healthUrl); } public static Registration.Builder copyOf(Registration registration) { return registration.toBuilder(); } /** * Determines the service url. It might be overriden by metadata entries to override * the service url. * @param serviceUrl original serviceUrl * @param metadata metadata information of registered instance * @return the actual service url */ @Nullable private String getServiceUrl(@Nullable String serviceUrl, Map metadata) { if (serviceUrl == null) { return null; } String url = metadata.getOrDefault("service-url", serviceUrl); try { URI baseUri = new URI(url); return baseUri.toString(); } catch (URISyntaxException ex) { log.warn("Invalid service url: " + serviceUrl, ex); } return serviceUrl; } public Map getMetadata() { return Collections.unmodifiableMap(this.metadata); } /** * Checks the syntax of the given URL. * @param url the URL. * @return true, if valid. */ private boolean checkUrl(String url) { try { URI uri = new URI(url); return uri.isAbsolute(); } catch (URISyntaxException ex) { return false; } } public static class Builder { // Will be generated by lombok } } ================================================ FILE: spring-boot-admin-server/src/main/java/de/codecentric/boot/admin/server/domain/values/StatusInfo.java ================================================ /* * 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.server.domain.values; import java.io.Serializable; import java.util.Collections; import java.util.Comparator; import java.util.HashMap; import java.util.List; import java.util.Map; import org.jspecify.annotations.Nullable; import org.springframework.util.Assert; import static java.util.Arrays.asList; /** * Instance status with details fetched from the info endpoint. * * @author Johannes Edmeier */ @lombok.Data public final class StatusInfo implements Serializable { 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"; private static final List STATUS_ORDER = asList(STATUS_DOWN, STATUS_OUT_OF_SERVICE, STATUS_OFFLINE, STATUS_UNKNOWN, STATUS_RESTRICTED, STATUS_UP); private final String status; private final Map details; private StatusInfo(String status, @Nullable Map details) { Assert.hasText(status, "'status' must not be empty."); this.status = status.toUpperCase(); this.details = (details != null) ? new HashMap<>(details) : Collections.emptyMap(); } public static StatusInfo valueOf(String statusCode, @Nullable Map details) { return new StatusInfo(statusCode, details); } public static StatusInfo valueOf(String statusCode) { return valueOf(statusCode, null); } public static StatusInfo ofUnknown() { return valueOf(STATUS_UNKNOWN, null); } public static StatusInfo ofUp() { return ofUp(null); } public static StatusInfo ofDown() { return ofDown(null); } public static StatusInfo ofOffline() { return ofOffline(null); } public static StatusInfo ofOutOfService() { return ofOutOfService(null); } public static StatusInfo ofRestricted() { return ofRestricted(null); } public static StatusInfo ofUp(@Nullable Map details) { return valueOf(STATUS_UP, details); } public static StatusInfo ofDown(@Nullable Map details) { return valueOf(STATUS_DOWN, details); } public static StatusInfo ofOffline(@Nullable Map details) { return valueOf(STATUS_OFFLINE, details); } public static StatusInfo ofOutOfService(@Nullable Map details) { return valueOf(STATUS_OUT_OF_SERVICE, details); } public static StatusInfo ofRestricted(@Nullable Map details) { return valueOf(STATUS_RESTRICTED, details); } public Map getDetails() { return Collections.unmodifiableMap(details); } public boolean isUp() { return STATUS_UP.equals(status); } public boolean isOffline() { return STATUS_OFFLINE.equals(status); } public boolean isDown() { return STATUS_DOWN.equals(status); } public boolean isUnknown() { return STATUS_UNKNOWN.equals(status); } public boolean isOutOfService() { return STATUS_OUT_OF_SERVICE.equals(status); } public boolean isRestricted() { return STATUS_RESTRICTED.equals(status); } public static Comparator severity() { return Comparator.comparingInt(STATUS_ORDER::indexOf); } @SuppressWarnings("unchecked") public static StatusInfo from(Map body) { Map details = Collections.emptyMap(); /* * Key "details" is present when accessing Spring Boot Actuator Health using * Accept-Header {@link org.springframework.boot.actuate.endpoint.ApiVersion#V2}. */ if (body.containsKey("details")) { details = (Map) body.get("details"); } else if (body.containsKey("components")) { details = (Map) body.get("components"); } return StatusInfo.valueOf((String) body.get("status"), details); } } ================================================ FILE: spring-boot-admin-server/src/main/java/de/codecentric/boot/admin/server/domain/values/Tags.java ================================================ /* * 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.server.domain.values; import java.io.Serializable; import java.util.Collections; import java.util.LinkedHashMap; import java.util.Map; import java.util.Objects; import java.util.function.Function; import java.util.stream.Collector; import lombok.Getter; import org.jspecify.annotations.Nullable; import org.springframework.util.StringUtils; import static java.util.stream.Collectors.toMap; @Getter @lombok.EqualsAndHashCode @lombok.ToString public final class Tags implements Serializable { private static final Tags EMPTY = new Tags(Collections.emptyMap()); private final Map values; private Tags(Map tags) { if (tags.isEmpty()) { this.values = Collections.emptyMap(); } else { this.values = Collections.unmodifiableMap(new LinkedHashMap<>(tags)); } } public static Tags empty() { return EMPTY; } public static Tags from(Map map) { return from(map, null); } @SuppressWarnings("unchecked") public static Tags from(Map map, @Nullable String prefix) { if (map.isEmpty()) { return empty(); } if (StringUtils.hasText(prefix)) { Object nestedTags = map.get(prefix); if (nestedTags instanceof Map) { return from((Map) nestedTags); } String flatPrefix = prefix + "."; return from(map.entrySet() .stream() .filter((e) -> e.getKey() != null) .filter((e) -> e.getKey().toLowerCase().startsWith(flatPrefix)) .collect(toLinkedHashMap((e) -> e.getKey().substring(flatPrefix.length()), Map.Entry::getValue))); } return new Tags(map.entrySet() .stream() .filter((e) -> e.getKey() != null) .collect(toLinkedHashMap(Map.Entry::getKey, (e) -> Objects.toString(e.getValue())))); } private static Collector> toLinkedHashMap( Function keyMapper, Function valueMapper) { return toMap(keyMapper, valueMapper, (u, v) -> { throw new IllegalStateException(String.format("Duplicate key %s", u)); }, LinkedHashMap::new); } public Tags append(Tags other) { Map newTags = new LinkedHashMap<>(this.values); newTags.putAll(other.values); return new Tags(newTags); } } ================================================ FILE: spring-boot-admin-server/src/main/java/de/codecentric/boot/admin/server/domain/values/package-info.java ================================================ /* * 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. */ // Spring Boot Admin Server - domain objects package. @NullMarked package de.codecentric.boot.admin.server.domain.values; import org.jspecify.annotations.NullMarked; ================================================ FILE: spring-boot-admin-server/src/main/java/de/codecentric/boot/admin/server/eventstore/ConcurrentMapEventStore.java ================================================ /* * 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.server.eventstore; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Optional; import java.util.concurrent.ConcurrentMap; import java.util.function.BinaryOperator; import java.util.function.Function; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import de.codecentric.boot.admin.server.domain.events.InstanceEvent; import de.codecentric.boot.admin.server.domain.values.InstanceId; import static java.util.Comparator.comparing; import static java.util.stream.Collectors.groupingBy; import static java.util.stream.Collectors.reducing; public abstract class ConcurrentMapEventStore extends InstanceEventPublisher implements InstanceEventStore { private static final Logger log = LoggerFactory.getLogger(ConcurrentMapEventStore.class); private static final Comparator byTimestampAndIdAndVersion = comparing(InstanceEvent::getTimestamp) .thenComparing(InstanceEvent::getInstance) .thenComparing(InstanceEvent::getVersion); private final int maxLogSizePerAggregate; private final ConcurrentMap> eventLog; protected ConcurrentMapEventStore(int maxLogSizePerAggregate, ConcurrentMap> eventLog) { this.eventLog = eventLog; this.maxLogSizePerAggregate = maxLogSizePerAggregate; } @Override public Flux findAll() { return Flux.defer(() -> Flux.fromIterable(eventLog.values()) .flatMapIterable(Function.identity()) .sort(byTimestampAndIdAndVersion)); } @Override public Flux find(InstanceId id) { return Flux.defer(() -> Flux.fromIterable(eventLog.getOrDefault(id, Collections.emptyList()))); } @Override public Mono append(List events) { return Mono.fromRunnable(() -> { while (true) { if (doAppend(events)) { return; } } }); } protected boolean doAppend(List events) { if (events.isEmpty()) { return true; } InstanceId id = events.get(0).getInstance(); if (!events.stream().allMatch((event) -> event.getInstance().equals(id))) { throw new IllegalArgumentException("'events' must only refer to the same instance."); } List oldEvents = eventLog.computeIfAbsent(id, (key) -> new ArrayList<>(maxLogSizePerAggregate + 1)); long lastVersion = getLastVersion(oldEvents); if (lastVersion >= events.get(0).getVersion()) { throw createOptimisticLockException(events.get(0), lastVersion); } List newEvents = new ArrayList<>(oldEvents); newEvents.addAll(events); if (newEvents.size() > maxLogSizePerAggregate) { log.debug("Threshold for {} reached. Compacting events", id); compact(newEvents); } if (eventLog.replace(id, oldEvents, newEvents)) { log.debug("Events appended to log {}", events); return true; } log.debug("Unsuccessful attempt append the events {} ", events); return false; } private void compact(List events) { BinaryOperator latestEvent = (e1, e2) -> (e1.getVersion() > e2.getVersion()) ? e1 : e2; Map, Optional> latestPerType = events.stream() .collect(groupingBy(InstanceEvent::getClass, reducing(latestEvent))); events.removeIf((e) -> !Objects.equals(e, latestPerType.get(e.getClass()).orElse(null))); } private OptimisticLockingException createOptimisticLockException(InstanceEvent event, long lastVersion) { return new OptimisticLockingException( "Version " + event.getVersion() + " was overtaken by " + lastVersion + " for " + event.getInstance()); } protected static long getLastVersion(List events) { return events.isEmpty() ? -1 : events.get(events.size() - 1).getVersion(); } } ================================================ FILE: spring-boot-admin-server/src/main/java/de/codecentric/boot/admin/server/eventstore/HazelcastEventStore.java ================================================ /* * 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.server.eventstore; import java.util.List; import com.hazelcast.core.EntryAdapter; import com.hazelcast.core.EntryEvent; import com.hazelcast.map.IMap; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import de.codecentric.boot.admin.server.domain.events.InstanceEvent; import de.codecentric.boot.admin.server.domain.values.InstanceId; /** * Event-Store backed by a Hazelcast-map. * * @author Johannes Edmeier */ public class HazelcastEventStore extends ConcurrentMapEventStore { private static final Logger log = LoggerFactory.getLogger(HazelcastEventStore.class); public HazelcastEventStore(IMap> eventLogs) { this(100, eventLogs); } public HazelcastEventStore(int maxLogSizePerAggregate, IMap> eventLog) { super(maxLogSizePerAggregate, eventLog); eventLog.addEntryListener(new EntryAdapter>() { @Override public void entryUpdated(EntryEvent> event) { log.debug("Updated {}", event); long lastKnownVersion = getLastVersion(event.getOldValue()); List newEvents = event.getValue() .stream() .filter((e) -> e.getVersion() > lastKnownVersion) .toList(); HazelcastEventStore.this.publish(newEvents); } }, true); } } ================================================ FILE: spring-boot-admin-server/src/main/java/de/codecentric/boot/admin/server/eventstore/InMemoryEventStore.java ================================================ /* * 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.server.eventstore; import java.util.List; import java.util.concurrent.ConcurrentHashMap; import reactor.core.publisher.Mono; import de.codecentric.boot.admin.server.domain.events.InstanceEvent; /** * Event-Store backed by a ConcurrentHashMap. * * @author Johannes Edmeier */ public class InMemoryEventStore extends ConcurrentMapEventStore { public InMemoryEventStore() { this(100); } public InMemoryEventStore(int maxLogSizePerAggregate) { super(maxLogSizePerAggregate, new ConcurrentHashMap<>()); } @Override public Mono append(List events) { return super.append(events).then(Mono.fromRunnable(() -> this.publish(events))); } } ================================================ FILE: spring-boot-admin-server/src/main/java/de/codecentric/boot/admin/server/eventstore/InstanceEventPublisher.java ================================================ /* * 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.server.eventstore; import java.util.List; import org.reactivestreams.Publisher; import org.reactivestreams.Subscriber; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import reactor.core.publisher.Flux; import reactor.core.publisher.Sinks; import de.codecentric.boot.admin.server.domain.events.InstanceEvent; public class InstanceEventPublisher implements Publisher { private static final Logger log = LoggerFactory.getLogger(InstanceEventPublisher.class); private final Flux publishedFlux; private final Sinks.Many unicast; private final Sinks.EmitFailureHandler emitFailureHandler = (signalType, emitResult) -> emitResult .equals(Sinks.EmitResult.FAIL_NON_SERIALIZED); protected InstanceEventPublisher() { this.unicast = Sinks.many().unicast().onBackpressureBuffer(); this.publishedFlux = this.unicast.asFlux().publish().autoConnect(0); } protected void publish(List events) { events.forEach((event) -> { log.debug("Event published {}", event); this.unicast.emitNext(event, emitFailureHandler); }); } @Override public void subscribe(Subscriber s) { this.publishedFlux.subscribe(s); } } ================================================ FILE: spring-boot-admin-server/src/main/java/de/codecentric/boot/admin/server/eventstore/InstanceEventStore.java ================================================ /* * 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.server.eventstore; import java.util.List; import org.reactivestreams.Publisher; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import de.codecentric.boot.admin.server.domain.events.InstanceEvent; import de.codecentric.boot.admin.server.domain.values.InstanceId; /** * Interface for storing all instance related Events * * @author Johannes Edmeier */ public interface InstanceEventStore extends Publisher { Flux findAll(); Flux find(InstanceId id); Mono append(List events); } ================================================ FILE: spring-boot-admin-server/src/main/java/de/codecentric/boot/admin/server/eventstore/OptimisticLockingException.java ================================================ /* * 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.server.eventstore; public class OptimisticLockingException extends RuntimeException { public OptimisticLockingException(String message) { super(message); } } ================================================ FILE: spring-boot-admin-server/src/main/java/de/codecentric/boot/admin/server/eventstore/package-info.java ================================================ /* * 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. */ // Spring Boot Admin Server - event store package. @NullMarked package de.codecentric.boot.admin.server.eventstore; import org.jspecify.annotations.NullMarked; ================================================ FILE: spring-boot-admin-server/src/main/java/de/codecentric/boot/admin/server/notify/AbstractEventNotifier.java ================================================ /* * 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.server.notify; import org.slf4j.Logger; import org.slf4j.LoggerFactory; 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.events.InstanceEvent; /** * Abstract Notifier which allows disabling and filtering of events. * * @author Johannes Edmeier */ public abstract class AbstractEventNotifier implements Notifier { private final InstanceRepository repository; /** * Enables the notification. */ private boolean enabled = true; protected AbstractEventNotifier(InstanceRepository repository) { this.repository = repository; } @Override public Mono notify(InstanceEvent event) { if (!enabled) { return Mono.empty(); } return repository.find(event.getInstance()) .filter((instance) -> shouldNotify(event, instance)) .flatMap((instance) -> doNotify(event, instance)) .doOnError((ex) -> getLogger().error("Couldn't notify for event {} ", event, ex)) .then(); } protected boolean shouldNotify(InstanceEvent event, Instance instance) { return true; } protected abstract Mono doNotify(InstanceEvent event, Instance instance); private Logger getLogger() { return LoggerFactory.getLogger(this.getClass()); } public void setEnabled(boolean enabled) { this.enabled = enabled; } public boolean isEnabled() { return enabled; } } ================================================ FILE: spring-boot-admin-server/src/main/java/de/codecentric/boot/admin/server/notify/AbstractStatusChangeNotifier.java ================================================ /* * 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.server.notify; import java.util.Arrays; import java.util.HashMap; 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.events.InstanceDeregisteredEvent; import de.codecentric.boot.admin.server.domain.events.InstanceEvent; import de.codecentric.boot.admin.server.domain.events.InstanceStatusChangedEvent; import de.codecentric.boot.admin.server.domain.values.InstanceId; /** * Abstract Notifier for status change which allows filtering of certain status changes. * * @author Johannes Edmeier */ public abstract class AbstractStatusChangeNotifier extends AbstractEventNotifier { private final Map lastStatuses = new HashMap<>(); /** * List of changes to ignore. Must be in Format OLD:NEW, for any status use * as * wildcard, e.g. *:UP or OFFLINE:* */ private String[] ignoreChanges = { "UNKNOWN:UP" }; public AbstractStatusChangeNotifier(InstanceRepository repository) { super(repository); } @Override public Mono notify(InstanceEvent event) { return super.notify(event).then(Mono.fromRunnable(() -> updateLastStatus(event))); } @Override protected boolean shouldNotify(InstanceEvent event, Instance instance) { if (event instanceof InstanceStatusChangedEvent statusChangedEvent) { String from = getLastStatus(event.getInstance()); String to = statusChangedEvent.getStatusInfo().getStatus(); return Arrays.binarySearch(ignoreChanges, from + ":" + to) < 0 && Arrays.binarySearch(ignoreChanges, "*:" + to) < 0 && Arrays.binarySearch(ignoreChanges, from + ":*") < 0; } return false; } protected final String getLastStatus(InstanceId instanceId) { return lastStatuses.getOrDefault(instanceId, "UNKNOWN"); } protected void updateLastStatus(InstanceEvent event) { if (event instanceof InstanceDeregisteredEvent) { lastStatuses.remove(event.getInstance()); } if (event instanceof InstanceStatusChangedEvent statusChangedEvent) { lastStatuses.put(event.getInstance(), statusChangedEvent.getStatusInfo().getStatus()); } } public void setIgnoreChanges(String[] ignoreChanges) { String[] copy = Arrays.copyOf(ignoreChanges, ignoreChanges.length); Arrays.sort(copy); this.ignoreChanges = copy; } public String[] getIgnoreChanges() { return ignoreChanges; } } ================================================ FILE: spring-boot-admin-server/src/main/java/de/codecentric/boot/admin/server/notify/CompositeNotifier.java ================================================ /* * 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.server.notify; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.util.Assert; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import de.codecentric.boot.admin.server.domain.events.InstanceEvent; /** * A notifier delegating notifications to all specified notifiers. * * @author Sebastian Meiser */ public class CompositeNotifier implements Notifier { private static final Logger log = LoggerFactory.getLogger(CompositeNotifier.class); private final Iterable delegates; public CompositeNotifier(Iterable delegates) { Assert.notNull(delegates, "'delegates' must not be null!"); this.delegates = delegates; } @Override public Mono notify(InstanceEvent event) { return Flux.fromIterable(delegates).flatMap((d) -> d.notify(event).onErrorResume((error) -> { log.warn("Unexpected exception while triggering notifications. Notification might not be sent.", error); return Mono.empty(); })).then(); } } ================================================ FILE: spring-boot-admin-server/src/main/java/de/codecentric/boot/admin/server/notify/DingTalkNotifier.java ================================================ /* * 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.server.notify; import java.net.URLEncoder; import java.nio.charset.StandardCharsets; import java.util.HashMap; import java.util.Map; import javax.crypto.Mac; import javax.crypto.spec.SecretKeySpec; import lombok.extern.slf4j.Slf4j; import org.apache.hc.client5.http.utils.Base64; import org.jspecify.annotations.Nullable; import org.springframework.http.HttpEntity; import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; import org.springframework.web.client.RestTemplate; 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.events.InstanceEvent; import de.codecentric.boot.admin.server.notify.filter.AbstractContentNotifier; /** * Notifier submitting events to DingTalk. * * @author Mask */ @Slf4j public class DingTalkNotifier extends AbstractContentNotifier { private static final String DEFAULT_MESSAGE = "#{name} #{id} is #{status}"; private RestTemplate restTemplate; /** * Webhook URI for the DingTalk API. */ private String webhookUrl; /** * Secret for DingTalk. */ @Nullable private String secret; public DingTalkNotifier(InstanceRepository repository, RestTemplate restTemplate) { super(repository); this.restTemplate = restTemplate; } @Override protected Mono doNotify(InstanceEvent event, Instance instance) { return Mono .fromRunnable(() -> restTemplate.postForEntity(buildUrl(), createMessage(event, instance), Void.class)); } private String buildUrl() { Long timestamp = System.currentTimeMillis(); return String.format("%s×tamp=%s&sign=%s", webhookUrl, timestamp, getSign(timestamp)); } protected Object createMessage(InstanceEvent event, Instance instance) { Map messageJson = new HashMap<>(); messageJson.put("msgtype", "text"); Map content = new HashMap<>(); content.put("content", createContent(event, instance)); messageJson.put("text", content); HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_JSON); return new HttpEntity<>(messageJson, headers); } @Override protected String getDefaultMessage() { return DEFAULT_MESSAGE; } private String getSign(Long timestamp) { try { String stringToSign = timestamp + "\n" + secret; Mac mac = Mac.getInstance("HmacSHA256"); mac.init(new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), "HmacSHA256")); byte[] signData = mac.doFinal(stringToSign.getBytes(StandardCharsets.UTF_8)); return URLEncoder.encode(new String(Base64.encodeBase64(signData)), StandardCharsets.UTF_8); } catch (Exception ex) { log.warn("Failed to sign message", ex); } return ""; } public void setRestTemplate(RestTemplate restTemplate) { this.restTemplate = restTemplate; } public String getWebhookUrl() { return webhookUrl; } public void setWebhookUrl(String webhookUrl) { this.webhookUrl = webhookUrl; } @Nullable public String getSecret() { return secret; } public void setSecret(@Nullable String secret) { this.secret = secret; } } ================================================ FILE: spring-boot-admin-server/src/main/java/de/codecentric/boot/admin/server/notify/DiscordNotifier.java ================================================ /* * 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.server.notify; import java.net.URI; import java.util.HashMap; import java.util.Map; import org.jspecify.annotations.Nullable; import org.springframework.http.HttpEntity; import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; import org.springframework.web.client.RestTemplate; 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.events.InstanceEvent; import de.codecentric.boot.admin.server.notify.filter.AbstractContentNotifier; /** * Notifier submitting events to Discord by webhooks. * * @author Movitz Sunar * @see https://discordapp.com/developers/docs/resources/webhook#execute-webhook */ public class DiscordNotifier extends AbstractContentNotifier { private static final String DEFAULT_MESSAGE = "*#{name}* (#{id}) is *#{status}*"; private RestTemplate restTemplate; /** * Webhook URI for the Discord API (i.e. * https://discordapp.com/api/webhooks/{webhook.id}/{webhook.token}) */ @Nullable private URI webhookUrl; /** * If the message is a text to speech message. False by default. */ private boolean tts = false; /** * Optional username. Default is set in Discord. */ @Nullable private String username; /** * Optional URL to avatar. */ @Nullable private String avatarUrl; public DiscordNotifier(InstanceRepository repository, RestTemplate restTemplate) { super(repository); this.restTemplate = restTemplate; } @Override protected Mono doNotify(InstanceEvent event, Instance instance) { if (webhookUrl == null) { return Mono.error(new IllegalStateException("'webhookUrl' must not be null.")); } return Mono.fromRunnable( () -> restTemplate.postForEntity(webhookUrl, createDiscordNotification(event, instance), Void.class)); } protected Object createDiscordNotification(InstanceEvent event, Instance instance) { Map body = new HashMap<>(); body.put("content", createContent(event, instance)); body.put("tts", tts); if (avatarUrl != null) { body.put("avatar_url", avatarUrl); } if (username != null) { body.put("username", username); } HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_JSON); headers.add(HttpHeaders.USER_AGENT, "RestTemplate"); return new HttpEntity<>(body, headers); } @Override protected String getDefaultMessage() { return DEFAULT_MESSAGE; } @Nullable public URI getWebhookUrl() { return webhookUrl; } public void setWebhookUrl(@Nullable URI webhookUrl) { this.webhookUrl = webhookUrl; } public boolean isTts() { return tts; } public void setTts(boolean tts) { this.tts = tts; } @Nullable public String getUsername() { return username; } public void setUsername(@Nullable String username) { this.username = username; } @Nullable public String getAvatarUrl() { return avatarUrl; } public void setAvatarUrl(@Nullable String avatarUrl) { this.avatarUrl = avatarUrl; } public void setRestTemplate(RestTemplate restTemplate) { this.restTemplate = restTemplate; } } ================================================ FILE: spring-boot-admin-server/src/main/java/de/codecentric/boot/admin/server/notify/FeiShuNotifier.java ================================================ /* * Copyright 2014-2022 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.server.notify; import java.net.URI; import java.nio.charset.StandardCharsets; import java.time.Instant; import java.util.ArrayList; import java.util.Base64; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.UUID; import javax.crypto.Mac; import javax.crypto.spec.SecretKeySpec; import lombok.extern.slf4j.Slf4j; import org.springframework.http.HttpEntity; import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.util.StringUtils; import org.springframework.web.client.RestTemplate; import reactor.core.publisher.Mono; import tools.jackson.databind.json.JsonMapper; 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.notify.filter.AbstractContentNotifier; /** * Notifier submitting events to FeiShu by webhooks. * * @author sweeter * @see https://open.feishu.cn/document/ukTMukTMukTM/ucTM5YjL3ETO24yNxkjN * */ @Slf4j public class FeiShuNotifier extends AbstractContentNotifier { private RestTemplate restTemplate; /** * Webhook URL for the FeiShu(飞书) chat group API (i.e. * https://open.feishu.cn/open-apis/bot/v2/hook/xxx). */ private URI webhookUrl; /** * {@literal @}all */ private boolean atAll = true; /** * The secret of the chat group robot from the FeiShu setup. */ private String secret; /** * FeiShu message type: text(文本) interactive(消息卡片) */ private MessageType messageType = MessageType.interactive; /** * Card theme message */ private Card card = new Card(); public FeiShuNotifier(InstanceRepository repository, RestTemplate restTemplate) { super(repository); this.restTemplate = restTemplate; } @Override protected Mono doNotify(InstanceEvent event, Instance instance) { if (webhookUrl == null) { return Mono.error(new IllegalStateException("'webhookUrl' must not be null.")); } return Mono.fromRunnable(() -> { ResponseEntity responseEntity = this.restTemplate.postForEntity(this.webhookUrl, this.createNotification(event, instance), String.class); log.debug("Send a notification message to the FeiShu group,returns the parameter:{}", responseEntity.getBody()); }); } private String generateSign(String secret, long timestamp) { try { String stringToSign = timestamp + "\n" + secret; Mac mac = Mac.getInstance("HmacSHA256"); mac.init(new SecretKeySpec(stringToSign.getBytes(StandardCharsets.UTF_8), "HmacSHA256")); byte[] signData = mac.doFinal(new byte[] {}); return new String(Base64.getEncoder().encode(signData)); } catch (Exception ex) { log.error("Description Failed to generate the Webhook signature of the FeiShu:{}", ex.getMessage()); } return null; } protected HttpEntity> createNotification(InstanceEvent event, Instance instance) { Map body = new HashMap<>(); body.put("receive_id", UUID.randomUUID().toString()); if (StringUtils.hasText(this.secret)) { long timestamp = Instant.now().getEpochSecond(); body.put("timestamp", timestamp); body.put("sign", this.generateSign(this.secret, timestamp)); } body.put("msg_type", this.messageType); switch (this.messageType) { case interactive: body.put("card", this.createCardContent(event, instance)); break; case text: default: body.put("content", this.createTextContent(event, instance)); } HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_JSON); headers.add("User-Agent", "Codecentric's Spring Boot Admin"); return new HttpEntity<>(body, headers); } @Override protected Map buildContentModel(InstanceEvent event, Instance instance) { Map root = new HashMap<>(); root.put("event", event); root.put("instance", instance); root.put("lastStatus", this.getLastStatus(event.getInstance())); return root; } @Override protected String getDefaultMessage() { return "ServiceName: #{instance.registration.name}(#{instance.id}) \nServiceUrl: #{instance.registration.serviceUrl} \nStatus: changed status from [#{lastStatus}] to [#{event.statusInfo.status}]"; } private String createTextContent(InstanceEvent event, Instance instance) { Map textContent = new HashMap<>(); String content = this.createContent(event, instance); if (this.atAll) { content += "\n@all"; } textContent.put("text", content); return this.toJsonString(textContent); } private String createCardContent(InstanceEvent event, Instance instance) { String content = this.createContent(event, instance); Map header = new HashMap<>(); header.put("template", StringUtils.hasText(this.card.getThemeColor()) ? "red" : this.card.getThemeColor()); Map titleContent = new HashMap<>(); titleContent.put("tag", "plain_text"); titleContent.put("content", this.card.getTitle()); header.put("title", titleContent); List> elements = new ArrayList<>(); Map item = new HashMap<>(); item.put("tag", "div"); Map text = new HashMap<>(); text.put("tag", "plain_text"); text.put("content", content); item.put("text", text); elements.add(item); if (this.atAll) { Map atItem = new HashMap<>(); atItem.put("tag", "div"); Map atText = new HashMap<>(); atText.put("tag", "lark_md"); atText.put("content", ""); atItem.put("text", atText); elements.add(atItem); } Map cardContent = new HashMap<>(); cardContent.put("header", header); cardContent.put("elements", elements); return this.toJsonString(cardContent); } private String toJsonString(Object o) { try { JsonMapper jsonMapper = JsonMapper.builder().build(); return jsonMapper.writeValueAsString(o); } catch (Exception ex) { log.warn("Failed to serialize JSON object", ex); } return null; } public URI getWebhookUrl() { return this.webhookUrl; } public void setWebhookUrl(URI webhookUrl) { this.webhookUrl = webhookUrl; } public void setRestTemplate(RestTemplate restTemplate) { this.restTemplate = restTemplate; } public boolean isAtAll() { return atAll; } public void setAtAll(boolean atAll) { this.atAll = atAll; } public String getSecret() { return secret; } public void setSecret(String secret) { this.secret = secret; } public MessageType getMessageType() { return messageType; } public void setMessageType(MessageType messageType) { this.messageType = messageType; } public Card getCard() { return card; } public void setCard(Card card) { this.card = card; } public enum MessageType { text, interactive } public static class Card { /** * This is header title. */ private String title = "Codecentric's Spring Boot Admin notice"; private String themeColor = "red"; public String getTitle() { return title; } public void setTitle(String title) { this.title = title; } public String getThemeColor() { return themeColor; } public void setThemeColor(String themeColor) { this.themeColor = themeColor; } } } ================================================ FILE: spring-boot-admin-server/src/main/java/de/codecentric/boot/admin/server/notify/HazelcastNotificationTrigger.java ================================================ /* * 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.server.notify; import java.util.concurrent.ConcurrentMap; import org.reactivestreams.Publisher; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import reactor.core.publisher.Mono; import de.codecentric.boot.admin.server.domain.events.InstanceEvent; import de.codecentric.boot.admin.server.domain.values.InstanceId; public class HazelcastNotificationTrigger extends NotificationTrigger { private static final Logger log = LoggerFactory.getLogger(HazelcastNotificationTrigger.class); private final ConcurrentMap sentNotifications; public HazelcastNotificationTrigger(Notifier notifier, Publisher events, ConcurrentMap sentNotifications) { super(notifier, events); this.sentNotifications = sentNotifications; } @Override protected Mono sendNotifications(InstanceEvent event) { while (true) { Long lastSentEvent = this.sentNotifications.getOrDefault(event.getInstance(), -1L); if (lastSentEvent >= event.getVersion()) { log.debug("Notifications already sent. Not triggering notifiers for {}", event); return Mono.empty(); } if (lastSentEvent < 0) { if (this.sentNotifications.putIfAbsent(event.getInstance(), event.getVersion()) == null) { log.debug("Triggering notifiers for {}", event); return super.sendNotifications(event); } } else { if (this.sentNotifications.replace(event.getInstance(), lastSentEvent, event.getVersion())) { log.debug("Triggering notifiers for {}", event); return super.sendNotifications(event); } } } } } ================================================ FILE: spring-boot-admin-server/src/main/java/de/codecentric/boot/admin/server/notify/HipchatNotifier.java ================================================ /* * 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.server.notify; import java.net.URI; import java.util.HashMap; import java.util.Map; import org.jspecify.annotations.Nullable; import org.springframework.http.HttpEntity; import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; import org.springframework.web.client.RestTemplate; 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.events.InstanceEvent; import de.codecentric.boot.admin.server.domain.events.InstanceStatusChangedEvent; import de.codecentric.boot.admin.server.domain.values.StatusInfo; import de.codecentric.boot.admin.server.notify.filter.AbstractContentNotifier; /** * Notifier submitting events to HipChat. * * @author Jamie Brown */ public class HipchatNotifier extends AbstractContentNotifier { private static final String DEFAULT_DESCRIPTION = "#{name}/#{id} is #{status}"; private RestTemplate restTemplate; /** * Base URL for HipChat API (i.e. https://ACCOUNT_NAME.hipchat.com/v2 */ @Nullable private URI url; /** * API token that has access to notify in the room */ @Nullable private String authToken; /** * Id of the room to notify */ @Nullable private String roomId; /** * TRUE will cause OS notification, FALSE will only notify to room */ private boolean notify = false; public HipchatNotifier(InstanceRepository repository, RestTemplate restTemplate) { super(repository); this.restTemplate = restTemplate; } @Override protected Mono doNotify(InstanceEvent event, Instance instance) { return Mono.fromRunnable( () -> restTemplate.postForEntity(buildUrl(), createHipChatNotification(event, instance), Void.class)); } protected String buildUrl() { if (url == null) { throw new IllegalStateException("'url' must not be null."); } return String.format("%s/room/%s/notification?auth_token=%s", url, roomId, authToken); } protected HttpEntity> createHipChatNotification(InstanceEvent event, Instance instance) { Map body = new HashMap<>(); body.put("color", getColor(event)); body.put("message", createContent(event, instance)); body.put("notify", getNotify()); body.put("message_format", "html"); HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_JSON); return new HttpEntity<>(body, headers); } protected boolean getNotify() { return notify; } @Override protected String getDefaultMessage() { return DEFAULT_DESCRIPTION; } protected String getColor(InstanceEvent event) { if (event instanceof InstanceStatusChangedEvent statusChangedEvent) { return StatusInfo.STATUS_UP.equals(statusChangedEvent.getStatusInfo().getStatus()) ? "green" : "red"; } else { return "gray"; } } @Nullable public URI getUrl() { return url; } public void setUrl(@Nullable URI url) { this.url = url; } @Nullable public String getAuthToken() { return authToken; } public void setAuthToken(@Nullable String authToken) { this.authToken = authToken; } @Nullable public String getRoomId() { return roomId; } public void setRoomId(@Nullable String roomId) { this.roomId = roomId; } public boolean isNotify() { return notify; } public void setNotify(boolean notify) { this.notify = notify; } public void setRestTemplate(RestTemplate restTemplate) { this.restTemplate = restTemplate; } } ================================================ FILE: spring-boot-admin-server/src/main/java/de/codecentric/boot/admin/server/notify/LetsChatNotifier.java ================================================ /* * 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.server.notify; import java.net.URI; import java.nio.charset.StandardCharsets; import java.util.Base64; import java.util.HashMap; import java.util.Map; import org.jspecify.annotations.Nullable; import org.springframework.http.HttpEntity; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; import org.springframework.http.MediaType; import org.springframework.web.client.RestTemplate; 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.events.InstanceEvent; import de.codecentric.boot.admin.server.notify.filter.AbstractContentNotifier; /** * Notifier submitting events to let´s Chat. * * @author Rico Pahlisch */ public class LetsChatNotifier extends AbstractContentNotifier { private static final String DEFAULT_MESSAGE = "*#{name}* (#{id}) is *#{status}*"; private RestTemplate restTemplate; /** * Host URL for Let´s Chat */ @Nullable private URI url; /** * Name of the room */ @Nullable private String room; /** * Token for the Let´s chat API */ @Nullable private String token; /** * username which sends notification */ private String username = "Spring Boot Admin"; public LetsChatNotifier(InstanceRepository repository, RestTemplate restTemplate) { super(repository); this.restTemplate = restTemplate; } @Override protected Mono doNotify(InstanceEvent event, Instance instance) { HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_JSON); // Let's Chat requires the token as basic username, the password can be an // arbitrary string. String auth = Base64.getEncoder() .encodeToString(String.format("%s:%s", token, username).getBytes(StandardCharsets.UTF_8)); headers.add(HttpHeaders.AUTHORIZATION, String.format("Basic %s", auth)); return Mono.fromRunnable(() -> restTemplate.exchange(createUrl(), HttpMethod.POST, new HttpEntity<>(createMessage(event, instance), headers), Void.class)); } private URI createUrl() { if (url == null) { throw new IllegalStateException("'url' must not be null."); } return URI.create(String.format("%s/rooms/%s/messages", url, room)); } protected Object createMessage(InstanceEvent event, Instance instance) { Map messageJson = new HashMap<>(); messageJson.put("text", createContent(event, instance)); return messageJson; } @Override protected String getDefaultMessage() { return DEFAULT_MESSAGE; } public void setRestTemplate(RestTemplate restTemplate) { this.restTemplate = restTemplate; } @Nullable public URI getUrl() { return url; } public void setUrl(@Nullable URI url) { this.url = url; } public String getUsername() { return username; } public void setUsername(String username) { this.username = username; } @Nullable public String getRoom() { return room; } public void setRoom(@Nullable String room) { this.room = room; } @Nullable public String getToken() { return token; } public void setToken(@Nullable String token) { this.token = token; } } ================================================ FILE: spring-boot-admin-server/src/main/java/de/codecentric/boot/admin/server/notify/LoggingNotifier.java ================================================ /* * 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.server.notify; import org.slf4j.Logger; import org.slf4j.LoggerFactory; 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.events.InstanceEvent; import de.codecentric.boot.admin.server.domain.events.InstanceStatusChangedEvent; /** * Notifier that just writes to a logger. * * @author Johannes Edmeier */ public class LoggingNotifier extends AbstractStatusChangeNotifier { private static final Logger LOGGER = LoggerFactory.getLogger(LoggingNotifier.class); public LoggingNotifier(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()); } }); } } ================================================ FILE: spring-boot-admin-server/src/main/java/de/codecentric/boot/admin/server/notify/MailNotifier.java ================================================ /* * 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.server.notify; import java.nio.charset.StandardCharsets; import java.util.Arrays; import java.util.HashMap; import java.util.Map; import jakarta.mail.MessagingException; import jakarta.mail.internet.MimeMessage; import org.jspecify.annotations.Nullable; import org.springframework.mail.javamail.JavaMailSender; import org.springframework.mail.javamail.MimeMessageHelper; import org.thymeleaf.TemplateEngine; import org.thymeleaf.context.Context; 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.events.InstanceEvent; import static java.util.Collections.singleton; /** * Notifier sending emails using Thymeleaf templates. * * @author Johannes Edmeier */ public class MailNotifier extends AbstractStatusChangeNotifier { private final JavaMailSender mailSender; private final TemplateEngine templateEngine; /** * recipients of the mail */ private String[] to = { "root@localhost" }; /** * cc-recipients of the mail */ private String[] cc = {}; /** * sender of the change */ private String from = "Spring Boot Admin "; /** * Additional properties to be set for the template */ private Map additionalProperties = new HashMap<>(); /** * Base-URL used for hyperlinks in mail */ @Nullable private String baseUrl; /** * Thymeleaf template for mail */ private String template = "META-INF/spring-boot-admin-server/mail/status-changed.html"; public MailNotifier(JavaMailSender mailSender, InstanceRepository repository, TemplateEngine templateEngine) { super(repository); this.mailSender = mailSender; this.templateEngine = templateEngine; } @Override protected Mono doNotify(InstanceEvent event, Instance instance) { return Mono.fromRunnable(() -> { Context ctx = new Context(); ctx.setVariables(additionalProperties); ctx.setVariable("baseUrl", this.baseUrl); ctx.setVariable("event", event); ctx.setVariable("instance", instance); ctx.setVariable("lastStatus", getLastStatus(event.getInstance())); try { MimeMessage mimeMessage = mailSender.createMimeMessage(); MimeMessageHelper message = new MimeMessageHelper(mimeMessage, StandardCharsets.UTF_8.name()); message.setText(getBody(ctx).replaceAll("\\s+\\n", "\n"), true); message.setSubject(getSubject(ctx)); message.setTo(this.to); message.setCc(this.cc); message.setFrom(this.from); mailSender.send(mimeMessage); } catch (MessagingException ex) { throw new RuntimeException("Error sending mail notification", ex); } }); } protected String getBody(Context ctx) { return templateEngine.process(this.template, ctx); } protected String getSubject(Context ctx) { return templateEngine.process(this.template, singleton("subject"), ctx).trim(); } public String[] getTo() { return Arrays.copyOf(to, to.length); } public void setTo(String[] to) { this.to = Arrays.copyOf(to, to.length); } public String[] getCc() { return Arrays.copyOf(cc, cc.length); } public void setCc(String[] cc) { this.cc = Arrays.copyOf(cc, cc.length); } public String getFrom() { return from; } public void setFrom(String from) { this.from = from; } public String getTemplate() { return template; } public void setTemplate(String template) { this.template = template; } @Nullable public String getBaseUrl() { return baseUrl; } public void setBaseUrl(@Nullable String baseUrl) { this.baseUrl = baseUrl; } public Map getAdditionalProperties() { return additionalProperties; } public void setAdditionalProperties(Map additionalProperties) { this.additionalProperties = additionalProperties; } } ================================================ FILE: spring-boot-admin-server/src/main/java/de/codecentric/boot/admin/server/notify/MattermostNotifier.java ================================================ /* * 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 * * 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.server.notify; import java.net.URI; import java.util.Collections; import java.util.HashMap; import java.util.Map; import org.jspecify.annotations.Nullable; import org.springframework.http.HttpEntity; import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; import org.springframework.web.client.RestTemplate; 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.events.InstanceEvent; import de.codecentric.boot.admin.server.domain.events.InstanceStatusChangedEvent; import de.codecentric.boot.admin.server.domain.values.StatusInfo; import de.codecentric.boot.admin.server.notify.filter.AbstractContentNotifier; /** * Notifier submitting events to Mattermost. * * @author Emir Boyaci */ public class MattermostNotifier extends AbstractContentNotifier { private static final String DEFAULT_MESSAGE = "**#{name}** (#{id}) is **#{status}**"; private RestTemplate restTemplate; /** * API url for Mattermost (i.e. https://example.mattermost.com/api/v4/posts) */ @Nullable private URI apiUrl; /** * Bot access token (i.e. dufc8q78hjgeccwsfhe37pcq1w) */ @Nullable private String botAccessToken; /** * Optional channel name without # sign (i.e. h616jh436pysjpopp3259mhwxc) */ @Nullable private String channelId; public MattermostNotifier(InstanceRepository repository, RestTemplate restTemplate) { super(repository); this.restTemplate = restTemplate; } @Override protected Mono doNotify(InstanceEvent event, Instance instance) { if (apiUrl == null) { return Mono.error(new IllegalStateException("'url' must not be null.")); } return Mono.fromRunnable(() -> restTemplate.postForEntity(apiUrl, createMessage(event, instance), Void.class)); } public void setRestTemplate(RestTemplate restTemplate) { this.restTemplate = restTemplate; } protected Object createMessage(InstanceEvent event, Instance instance) { Map messageJson = new HashMap<>(); if (channelId != null) { messageJson.put("channel_id", channelId); } Map attachments = new HashMap<>(); attachments.put("text", createContent(event, instance)); attachments.put("fallback", createContent(event, instance)); attachments.put("color", getColor(event)); Map props = new HashMap<>(); props.put("attachments", Collections.singletonList(attachments)); messageJson.put("props", props); HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_JSON); headers.setBearerAuth(botAccessToken); return new HttpEntity<>(messageJson, headers); } @Override protected String getDefaultMessage() { return DEFAULT_MESSAGE; } protected String getColor(InstanceEvent event) { if (event instanceof InstanceStatusChangedEvent statusChangedEvent) { return StatusInfo.STATUS_UP.equals(statusChangedEvent.getStatusInfo().getStatus()) ? "#2eb885" : "#a30100"; } else { return "#439FE0"; } } @Nullable public URI getApiUrl() { return apiUrl; } public void setApiUrl(@Nullable URI apiUrl) { this.apiUrl = apiUrl; } @Nullable public String getChannelId() { return channelId; } public void setChannelId(@Nullable String channelId) { this.channelId = channelId; } @Nullable public String getBotAccessToken() { return botAccessToken; } public void setBotAccessToken(@Nullable String botAccessToken) { this.botAccessToken = botAccessToken; } } ================================================ FILE: spring-boot-admin-server/src/main/java/de/codecentric/boot/admin/server/notify/MicrosoftTeamsNotifier.java ================================================ /* * 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.server.notify; import java.net.URI; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Objects; import lombok.Builder; import lombok.Data; import lombok.Getter; import lombok.Setter; import org.jspecify.annotations.Nullable; import org.springframework.expression.EvaluationContext; import org.springframework.expression.Expression; import org.springframework.expression.ParserContext; import org.springframework.expression.spel.standard.SpelExpressionParser; import org.springframework.expression.spel.support.DataBindingPropertyAccessor; import org.springframework.expression.spel.support.MapAccessor; import org.springframework.expression.spel.support.SimpleEvaluationContext; import org.springframework.http.HttpEntity; import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; import org.springframework.web.client.RestTemplate; 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.events.InstanceDeregisteredEvent; import de.codecentric.boot.admin.server.domain.events.InstanceEvent; import de.codecentric.boot.admin.server.domain.events.InstanceRegisteredEvent; import de.codecentric.boot.admin.server.domain.events.InstanceStatusChangedEvent; import static java.util.Collections.singletonList; public class MicrosoftTeamsNotifier extends AbstractStatusChangeNotifier { private static final String STATUS_KEY = "Status"; private static final String SERVICE_URL_KEY = "Service URL"; private static final String HEALTH_URL_KEY = "Health URL"; private static final String MANAGEMENT_URL_KEY = "Management URL"; private static final String SOURCE_KEY = "Source"; private static final String DEFAULT_THEME_COLOR_EXPRESSION = "#{event.type == 'STATUS_CHANGED' ? (event.statusInfo.status=='UP' ? '6db33f' : 'b32d36') : '439fe0'}"; private static final String DEFAULT_DEREGISTER_ACTIVITY_SUBTITLE_EXPRESSION = "#{instance.registration.name} with id #{instance.id} has de-registered from Spring Boot Admin"; private static final String DEFAULT_REGISTER_ACTIVITY_SUBTITLE_EXPRESSION = "#{instance.registration.name} with id #{instance.id} has registered with Spring Boot Admin"; private static final String DEFAULT_STATUS_ACTIVITY_SUBTITLE_EXPRESSION = "#{instance.registration.name} with id #{instance.id} changed status from #{lastStatus} to #{event.statusInfo.status}"; private final SpelExpressionParser parser = new SpelExpressionParser(); @Setter private RestTemplate restTemplate; /** * Webhook url for Microsoft Teams Channel Webhook connector (i.e. * ...{webhook-id}) */ @Nullable private URI webhookUrl; /** * Theme Color is the color of the accent on the message that appears in Microsoft * Teams. Default is Spring Green */ private Expression themeColor; /** * Message will be used as title of the Activity section of the Teams message when an * app de-registers. */ private Expression deregisterActivitySubtitle; /** * Message will be used as title of the Activity section of the Teams message when an * app registers */ private Expression registerActivitySubtitle; /** * Message will be used as title of the Activity section of the Teams message when an * app changes status */ private Expression statusActivitySubtitle; /** * Title of the Teams message when an app de-registers */ @Setter @Getter private String deRegisteredTitle = "De-Registered"; /** * Title of the Teams message when an app registers */ @Setter @Getter private String registeredTitle = "Registered"; /** * Title of the Teams message when an app changes status */ @Setter @Getter private String statusChangedTitle = "Status Changed"; /** * Summary section of every Teams message originating from Spring Boot Admin */ @Setter @Getter private String messageSummary = "Spring Boot Admin Notification"; public MicrosoftTeamsNotifier(InstanceRepository repository, RestTemplate restTemplate) { super(repository); this.restTemplate = restTemplate; this.themeColor = parser.parseExpression(DEFAULT_THEME_COLOR_EXPRESSION, ParserContext.TEMPLATE_EXPRESSION); this.deregisterActivitySubtitle = parser.parseExpression(DEFAULT_DEREGISTER_ACTIVITY_SUBTITLE_EXPRESSION, ParserContext.TEMPLATE_EXPRESSION); this.registerActivitySubtitle = parser.parseExpression(DEFAULT_REGISTER_ACTIVITY_SUBTITLE_EXPRESSION, ParserContext.TEMPLATE_EXPRESSION); this.statusActivitySubtitle = parser.parseExpression(DEFAULT_STATUS_ACTIVITY_SUBTITLE_EXPRESSION, ParserContext.TEMPLATE_EXPRESSION); } @Override protected Mono doNotify(InstanceEvent event, Instance instance) { Message message; EvaluationContext context = createEvaluationContext(event, instance); if (event instanceof InstanceRegisteredEvent) { message = getRegisteredMessage(instance, context); } else if (event instanceof InstanceDeregisteredEvent) { message = getDeregisteredMessage(instance, context); } else if (event instanceof InstanceStatusChangedEvent) { message = getStatusChangedMessage(instance, context); } else { return Mono.empty(); } HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_JSON); if (webhookUrl == null) { return Mono.error(new IllegalStateException("'webhookUrl' must not be null.")); } return Mono.fromRunnable(() -> this.restTemplate.postForEntity(webhookUrl, new HttpEntity(message, headers), Void.class)); } @Override protected boolean shouldNotify(InstanceEvent event, Instance instance) { return event instanceof InstanceRegisteredEvent || event instanceof InstanceDeregisteredEvent || super.shouldNotify(event, instance); } protected Message getDeregisteredMessage(Instance instance, EvaluationContext context) { String activitySubtitle = evaluateExpression(context, deregisterActivitySubtitle); return createMessage(instance, deRegisteredTitle, activitySubtitle, context); } protected Message getRegisteredMessage(Instance instance, EvaluationContext context) { String activitySubtitle = evaluateExpression(context, registerActivitySubtitle); return createMessage(instance, registeredTitle, activitySubtitle, context); } protected Message getStatusChangedMessage(Instance instance, EvaluationContext context) { String activitySubtitle = evaluateExpression(context, statusActivitySubtitle); return createMessage(instance, statusChangedTitle, activitySubtitle, context); } protected Message createMessage(Instance instance, String registeredTitle, String activitySubtitle, EvaluationContext context) { List facts = new ArrayList<>(); facts.add(new Fact(STATUS_KEY, instance.getStatusInfo().getStatus())); facts.add(new Fact(SERVICE_URL_KEY, instance.getRegistration().getServiceUrl())); facts.add(new Fact(HEALTH_URL_KEY, instance.getRegistration().getHealthUrl())); facts.add(new Fact(MANAGEMENT_URL_KEY, instance.getRegistration().getManagementUrl())); facts.add(new Fact(SOURCE_KEY, instance.getRegistration().getSource())); Section section = Section.builder() .activityTitle(instance.getRegistration().getName()) .activitySubtitle(activitySubtitle) .facts(facts) .build(); return Message.builder() .title(registeredTitle) .summary(messageSummary) .themeColor(evaluateExpression(context, themeColor)) .sections(singletonList(section)) .build(); } protected String evaluateExpression(EvaluationContext context, Expression expression) { return Objects.requireNonNull(expression.getValue(context, String.class)); } protected EvaluationContext createEvaluationContext(InstanceEvent event, Instance instance) { Map root = new HashMap<>(); root.put("event", event); root.put("instance", instance); root.put("lastStatus", getLastStatus(event.getInstance())); return SimpleEvaluationContext .forPropertyAccessors(DataBindingPropertyAccessor.forReadOnlyAccess(), new MapAccessor()) .withRootObject(root) .build(); } @Nullable public URI getWebhookUrl() { return webhookUrl; } public void setWebhookUrl(@Nullable URI webhookUrl) { this.webhookUrl = webhookUrl; } public String getThemeColor() { return themeColor.getExpressionString(); } public void setThemeColor(String themeColor) { this.themeColor = parser.parseExpression(themeColor, ParserContext.TEMPLATE_EXPRESSION); } public String getDeregisterActivitySubtitle() { return deregisterActivitySubtitle.getExpressionString(); } public void setDeregisterActivitySubtitle(String deregisterActivitySubtitle) { this.deregisterActivitySubtitle = parser.parseExpression(deregisterActivitySubtitle, ParserContext.TEMPLATE_EXPRESSION); } public String getRegisterActivitySubtitle() { return registerActivitySubtitle.getExpressionString(); } public void setRegisterActivitySubtitle(String registerActivitySubtitle) { this.registerActivitySubtitle = parser.parseExpression(registerActivitySubtitle, ParserContext.TEMPLATE_EXPRESSION); } public String getStatusActivitySubtitle() { return statusActivitySubtitle.getExpressionString(); } public void setStatusActivitySubtitle(String statusActivitySubtitle) { this.statusActivitySubtitle = parser.parseExpression(statusActivitySubtitle, ParserContext.TEMPLATE_EXPRESSION); } @Data @Builder public static class Message { private final String summary; private final String themeColor; private final String title; @Builder.Default private final List
sections = new ArrayList<>(); } @Data @Builder public static class Section { private final String activityTitle; private final String activitySubtitle; @Builder.Default private final List facts = new ArrayList<>(); } public record Fact(String name, @Nullable String value) { } } ================================================ FILE: spring-boot-admin-server/src/main/java/de/codecentric/boot/admin/server/notify/NotificationTrigger.java ================================================ /* * 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.server.notify; import org.reactivestreams.Publisher; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import de.codecentric.boot.admin.server.domain.events.InstanceEvent; import de.codecentric.boot.admin.server.services.AbstractEventHandler; public class NotificationTrigger extends AbstractEventHandler { private static final Logger log = LoggerFactory.getLogger(NotificationTrigger.class); private final Notifier notifier; public NotificationTrigger(Notifier notifier, Publisher publisher) { super(publisher, InstanceEvent.class); this.notifier = notifier; } @Override protected Publisher handle(Flux publisher) { return publisher.flatMap(this::sendNotifications); } protected Mono sendNotifications(InstanceEvent event) { return this.notifier.notify(event) .doOnError((e) -> log.warn("Couldn't notify for event {} ", event, e)) .onErrorResume((e) -> Mono.empty()); } } ================================================ FILE: spring-boot-admin-server/src/main/java/de/codecentric/boot/admin/server/notify/Notifier.java ================================================ /* * 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.server.notify; import reactor.core.publisher.Mono; import de.codecentric.boot.admin.server.domain.events.InstanceEvent; /** * Interface for components which emits notifications upon status changes in clients * * @author Johannes Edmeier */ public interface Notifier { Mono notify(InstanceEvent event); } ================================================ FILE: spring-boot-admin-server/src/main/java/de/codecentric/boot/admin/server/notify/NotifierProxyProperties.java ================================================ /* * 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.server.notify; import org.springframework.boot.context.properties.ConfigurationProperties; @lombok.Data @ConfigurationProperties("spring.boot.admin.notify.proxy") public class NotifierProxyProperties { /** * Proxy-Host for sending notifications */ private String host; /** * Proxy-Port for sending notifications */ private int port; /** * Proxy-User for sending notifications (if proxy requires authentication). */ private String username; /** * Proxy-Password for sending notifications (if proxy requires authentication). */ private String password; } ================================================ FILE: spring-boot-admin-server/src/main/java/de/codecentric/boot/admin/server/notify/OpsGenieNotifier.java ================================================ /* * 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.server.notify; import java.net.URI; import java.util.HashMap; import java.util.Map; import org.jspecify.annotations.Nullable; import org.springframework.expression.Expression; import org.springframework.expression.ParserContext; import org.springframework.expression.spel.standard.SpelExpressionParser; import org.springframework.expression.spel.support.DataBindingPropertyAccessor; import org.springframework.expression.spel.support.MapAccessor; import org.springframework.expression.spel.support.SimpleEvaluationContext; import org.springframework.http.HttpEntity; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; import org.springframework.http.MediaType; import org.springframework.web.client.RestTemplate; 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.events.InstanceEvent; import de.codecentric.boot.admin.server.domain.events.InstanceStatusChangedEvent; import de.codecentric.boot.admin.server.domain.values.StatusInfo; /** * Notifier submitting events to opsgenie.com. * * @author Fernando Sure */ public class OpsGenieNotifier extends AbstractStatusChangeNotifier { private static final URI DEFAULT_URI = URI.create("https://api.opsgenie.com/v2/alerts"); private static final String DEFAULT_MESSAGE = "#{instance.registration.name}/#{instance.id} is #{instance.statusInfo.status}"; private final SpelExpressionParser parser = new SpelExpressionParser(); private RestTemplate restTemplate; /** * BASE URL for OpsGenie API */ private URI url = DEFAULT_URI; /** * Integration ApiKey */ @Nullable private String apiKey; /** * Comma separated list of actions that can be executed. */ @Nullable private String actions; /** * Field to specify source of alert. By default, it will be assigned to IP address of * incoming request */ @Nullable private String source; /** * Comma separated list of labels attached to the alert */ @Nullable private String tags; /** * The entity the alert is related to. */ @Nullable private String entity; /** * Default owner of the execution. If user is not specified, the system becomes owner * of the execution. */ @Nullable private String user; /** * Trigger description. SpEL template using event as root; */ private Expression description; public OpsGenieNotifier(InstanceRepository repository, RestTemplate restTemplate) { super(repository); this.restTemplate = restTemplate; this.description = parser.parseExpression(DEFAULT_MESSAGE, ParserContext.TEMPLATE_EXPRESSION); } @Override protected Mono doNotify(InstanceEvent event, Instance instance) { return Mono.fromRunnable(() -> restTemplate.exchange(buildUrl(event, instance), HttpMethod.POST, createRequest(event, instance), Void.class)); } protected String buildUrl(InstanceEvent event, Instance instance) { if ((event instanceof InstanceStatusChangedEvent statusChangedEvent) && (StatusInfo.STATUS_UP.equals(statusChangedEvent.getStatusInfo().getStatus()))) { return String.format("%s/%s/close?identifierType=alias", url, generateAlias(instance)); } return url.toString(); } protected HttpEntity createRequest(InstanceEvent event, Instance instance) { Map body = new HashMap<>(); if (user != null) { body.put("user", user); } if (source != null) { body.put("source", source); } if (event instanceof InstanceStatusChangedEvent statusChangedEvent && !StatusInfo.STATUS_UP.equals(statusChangedEvent.getStatusInfo().getStatus())) { body.put("message", getMessage(event, instance)); body.put("alias", generateAlias(instance)); body.put("description", getDescription(event, instance)); if (actions != null) { body.put("actions", actions); } if (tags != null) { body.put("tags", tags); } if (entity != null) { body.put("entity", entity); } Map details = new HashMap<>(); details.put("type", "link"); details.put("href", instance.getRegistration().getHealthUrl()); details.put("text", "Instance health-endpoint"); body.put("details", details); } HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_JSON); headers.set(HttpHeaders.AUTHORIZATION, "GenieKey " + apiKey); return new HttpEntity<>(body, headers); } protected String generateAlias(Instance instance) { return instance.getRegistration().getName() + "_" + instance.getId(); } @Nullable protected String getMessage(InstanceEvent event, Instance instance) { Map root = new HashMap<>(); root.put("event", event); root.put("instance", instance); root.put("lastStatus", getLastStatus(event.getInstance())); SimpleEvaluationContext context = SimpleEvaluationContext .forPropertyAccessors(DataBindingPropertyAccessor.forReadOnlyAccess(), new MapAccessor()) .withRootObject(root) .build(); return description.getValue(context, String.class); } protected String getDescription(InstanceEvent event, Instance instance) { return String.format("Instance %s (%s) went from %s to %s", instance.getRegistration().getName(), instance.getId(), getLastStatus(instance.getId()), ((InstanceStatusChangedEvent) event).getStatusInfo().getStatus()); } @Nullable public String getApiKey() { return apiKey; } public void setApiKey(@Nullable String apiKey) { this.apiKey = apiKey; } public void setDescription(String description) { this.description = parser.parseExpression(description, ParserContext.TEMPLATE_EXPRESSION); } public String getMessage() { return description.getExpressionString(); } public void setRestTemplate(RestTemplate restTemplate) { this.restTemplate = restTemplate; } @Nullable public String getActions() { return actions; } public void setActions(@Nullable String actions) { this.actions = actions; } @Nullable public String getSource() { return source; } public void setSource(@Nullable String source) { this.source = source; } @Nullable public String getTags() { return tags; } public void setTags(@Nullable String tags) { this.tags = tags; } @Nullable public String getEntity() { return entity; } public void setEntity(@Nullable String entity) { this.entity = entity; } @Nullable public String getUser() { return user; } public void setUser(@Nullable String user) { this.user = user; } public URI getUrl() { return url; } public void setUrl(URI url) { this.url = url; } } ================================================ FILE: spring-boot-admin-server/src/main/java/de/codecentric/boot/admin/server/notify/PagerdutyNotifier.java ================================================ /* * 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.server.notify; import java.net.URI; import java.util.HashMap; import java.util.Map; import jakarta.annotation.Nullable; import org.springframework.expression.Expression; import org.springframework.expression.ParserContext; import org.springframework.expression.spel.standard.SpelExpressionParser; import org.springframework.expression.spel.support.DataBindingPropertyAccessor; import org.springframework.expression.spel.support.MapAccessor; import org.springframework.expression.spel.support.SimpleEvaluationContext; import org.springframework.web.client.RestTemplate; 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.events.InstanceEvent; import de.codecentric.boot.admin.server.domain.events.InstanceStatusChangedEvent; import static java.util.Collections.singletonList; /** * Notifier submitting events to Pagerduty. * * @author Johannes Edmeier */ public class PagerdutyNotifier extends AbstractStatusChangeNotifier { public static final URI DEFAULT_URI = URI .create("https://events.pagerduty.com/generic/2010-04-15/create_event.json"); private static final String DEFAULT_DESCRIPTION = "#{instance.registration.name}/#{instance.id} is #{instance.statusInfo.status}"; private final SpelExpressionParser parser = new SpelExpressionParser(); private RestTemplate restTemplate; /** * URI for pagerduty-REST-API */ private URI url = DEFAULT_URI; /** * Service-Key for pagerduty-REST-API */ @Nullable private String serviceKey; /** * Client for pagerduty-REST-API */ @Nullable private String client; /** * Client-url for pagerduty-REST-API */ @Nullable private URI clientUrl; /** * Trigger description. SpEL template using event as root; */ private Expression description; public PagerdutyNotifier(InstanceRepository repository, RestTemplate restTemplate) { super(repository); this.restTemplate = restTemplate; this.description = parser.parseExpression(DEFAULT_DESCRIPTION, ParserContext.TEMPLATE_EXPRESSION); } @Override protected Mono doNotify(InstanceEvent event, Instance instance) { return Mono .fromRunnable(() -> restTemplate.postForEntity(url, createPagerdutyEvent(event, instance), Void.class)); } protected Map createPagerdutyEvent(InstanceEvent event, Instance instance) { Map result = new HashMap<>(); result.put("service_key", serviceKey); result.put("incident_key", instance.getRegistration().getName() + "/" + event.getInstance()); result.put("description", getDescription(event, instance)); Map details = getDetails(event); result.put("details", details); if (event instanceof InstanceStatusChangedEvent statusChangedEvent) { if ("UP".equals(statusChangedEvent.getStatusInfo().getStatus())) { result.put("event_type", "resolve"); } else { result.put("event_type", "trigger"); if (client != null) { result.put("client", client); } if (clientUrl != null) { result.put("client_url", clientUrl); } Map context = new HashMap<>(); context.put("type", "link"); context.put("href", instance.getRegistration().getHealthUrl()); context.put("text", "Application health-endpoint"); result.put("contexts", singletonList(context)); } } return result; } @Nullable protected String getDescription(InstanceEvent event, Instance instance) { Map root = new HashMap<>(); root.put("event", event); root.put("instance", instance); root.put("lastStatus", getLastStatus(event.getInstance())); SimpleEvaluationContext context = SimpleEvaluationContext .forPropertyAccessors(DataBindingPropertyAccessor.forReadOnlyAccess(), new MapAccessor()) .withRootObject(root) .build(); return description.getValue(context, String.class); } protected Map getDetails(InstanceEvent event) { Map details = new HashMap<>(); if (event instanceof InstanceStatusChangedEvent statusChangedEvent) { details.put("from", this.getLastStatus(event.getInstance())); details.put("to", statusChangedEvent.getStatusInfo()); } return details; } public URI getUrl() { return url; } public void setUrl(URI url) { this.url = url; } @Nullable public String getClient() { return client; } public void setClient(@Nullable String client) { this.client = client; } @Nullable public URI getClientUrl() { return clientUrl; } public void setClientUrl(@Nullable URI clientUrl) { this.clientUrl = clientUrl; } @Nullable public String getServiceKey() { return serviceKey; } public void setServiceKey(@Nullable String serviceKey) { this.serviceKey = serviceKey; } public String getDescription() { return description.getExpressionString(); } public void setDescription(String description) { this.description = parser.parseExpression(description, ParserContext.TEMPLATE_EXPRESSION); } public void setRestTemplate(RestTemplate restTemplate) { this.restTemplate = restTemplate; } } ================================================ FILE: spring-boot-admin-server/src/main/java/de/codecentric/boot/admin/server/notify/RemindingNotifier.java ================================================ /* * 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.server.notify; import java.time.Duration; import java.time.Instant; import java.util.Arrays; import java.util.concurrent.ConcurrentHashMap; import java.util.logging.Level; import org.jspecify.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.util.Assert; import reactor.core.Disposable; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import reactor.core.scheduler.Scheduler; import reactor.core.scheduler.Schedulers; import reactor.util.retry.Retry; 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.InstanceDeregisteredEvent; import de.codecentric.boot.admin.server.domain.events.InstanceEvent; import de.codecentric.boot.admin.server.domain.events.InstanceStatusChangedEvent; import de.codecentric.boot.admin.server.domain.values.InstanceId; /** * Notifier that reminds certain statuses to send reminder notification using a delegate. * * @author Johannes Edmeier */ public class RemindingNotifier extends AbstractEventNotifier { private static final Logger log = LoggerFactory.getLogger(RemindingNotifier.class); private final ConcurrentHashMap reminders = new ConcurrentHashMap<>(); private final Notifier delegate; private Duration checkReminderInverval = Duration.ofSeconds(10); private Duration reminderPeriod = Duration.ofMinutes(10); private String[] reminderStatuses = { "DOWN", "OFFLINE" }; @Nullable private Disposable subscription; @Nullable private Scheduler reminderScheduler; public RemindingNotifier(Notifier delegate, InstanceRepository repository) { super(repository); Assert.notNull(delegate, "'delegate' must not be null!"); this.delegate = delegate; } @Override public Mono doNotify(InstanceEvent event, Instance instance) { return this.delegate.notify(event).doFinally((s) -> { if (shouldEndReminder(event)) { this.reminders.remove(event.getInstance()); } else if (shouldStartReminder(event)) { this.reminders.putIfAbsent(event.getInstance(), new Reminder(event)); } }).onErrorResume((e) -> Mono.empty()); } public void start() { this.reminderScheduler = Schedulers.newSingle("reminders"); this.subscription = Flux.interval(this.checkReminderInverval, this.reminderScheduler) .log(log.getName(), Level.FINEST) .doOnSubscribe((s) -> log.debug("Started reminders")) .flatMap((i) -> this.sendReminders()) .retryWhen(Retry.indefinitely() .doBeforeRetry((s) -> log.warn("Unexpected error when sending reminders", s.failure()))) .subscribe(); } public void stop() { if (this.subscription != null && !this.subscription.isDisposed()) { log.debug("stopped reminders"); this.subscription.dispose(); this.subscription = null; } if (this.reminderScheduler != null) { this.reminderScheduler.dispose(); this.reminderScheduler = null; } } protected Mono sendReminders() { Instant now = Instant.now(); return Flux.fromIterable(this.reminders.values()) .filter((reminder) -> reminder.getLastNotification().plus(this.reminderPeriod).isBefore(now)) .flatMap((reminder) -> this.delegate.notify(reminder.getEvent()) .doOnSuccess((signal) -> reminder.setLastNotification(now))) .then(); } protected boolean shouldStartReminder(InstanceEvent event) { if (event instanceof InstanceStatusChangedEvent statusChangedEvent) { return Arrays.binarySearch(this.reminderStatuses, statusChangedEvent.getStatusInfo().getStatus()) >= 0; } return false; } protected boolean shouldEndReminder(InstanceEvent event) { if (event instanceof InstanceDeregisteredEvent) { return true; } if (event instanceof InstanceStatusChangedEvent statusChangedEvent) { return Arrays.binarySearch(this.reminderStatuses, statusChangedEvent.getStatusInfo().getStatus()) < 0; } return false; } public void setReminderPeriod(Duration reminderPeriod) { this.reminderPeriod = reminderPeriod; } public void setReminderStatuses(String[] reminderStatuses) { String[] copy = Arrays.copyOf(reminderStatuses, reminderStatuses.length); Arrays.sort(copy); this.reminderStatuses = copy; } public void setCheckReminderInverval(Duration checkReminderInverval) { this.checkReminderInverval = checkReminderInverval; } protected static final class Reminder { private final InstanceEvent event; private Instant lastNotification; private Reminder(InstanceEvent event) { this.event = event; this.lastNotification = event.getTimestamp(); } public Instant getLastNotification() { return this.lastNotification; } public void setLastNotification(Instant lastNotification) { this.lastNotification = lastNotification; } public InstanceEvent getEvent() { return this.event; } } } ================================================ FILE: spring-boot-admin-server/src/main/java/de/codecentric/boot/admin/server/notify/RocketChatNotifier.java ================================================ /* * 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.server.notify; import java.net.URI; import java.util.HashMap; import java.util.Map; import org.jspecify.annotations.Nullable; import org.springframework.http.HttpEntity; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; import org.springframework.http.MediaType; import org.springframework.web.client.RestTemplate; 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.events.InstanceEvent; import de.codecentric.boot.admin.server.notify.filter.AbstractContentNotifier; /** * Notifier submitting events to RocketChat. * * @author Nicolas Badenne */ public class RocketChatNotifier extends AbstractContentNotifier { private static final String DEFAULT_MESSAGE = "*#{name}* (#{id}) is *#{status}*"; private RestTemplate restTemplate; /** * Host URL for RocketChat server */ @Nullable private String url; /** * Room Id to send message */ @Nullable private String roomId; /** * Token for RocketChat API */ @Nullable private String token; /** * User Id for RocketChat API */ private String userId; public RocketChatNotifier(InstanceRepository repository, RestTemplate restTemplate) { super(repository); this.restTemplate = restTemplate; } @Override protected Mono doNotify(InstanceEvent event, Instance instance) { HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_JSON); headers.add("X-Auth-Token", token); headers.add("X-User-Id", userId); return Mono.fromRunnable(() -> restTemplate.exchange(getUri(), HttpMethod.POST, new HttpEntity<>(createMessage(event, instance), headers), Void.class)); } private URI getUri() { if (url == null) { throw new IllegalStateException("'url' must not be null."); } return URI.create(String.format("%s/api/v1/chat.sendMessage", url)); } protected Object createMessage(InstanceEvent event, Instance instance) { Map messageJsonData = new HashMap<>(); messageJsonData.put("rid", roomId); messageJsonData.put("msg", createContent(event, instance)); Map messageJson = new HashMap<>(); messageJson.put("message", messageJsonData); return messageJson; } @Override protected Map buildContentModel(InstanceEvent event, Instance instance) { var content = super.buildContentModel(event, instance); content.put("roomId", roomId); return content; } @Override protected String getDefaultMessage() { return DEFAULT_MESSAGE; } public void setRestTemplate(RestTemplate restTemplate) { this.restTemplate = restTemplate; } @Nullable public String getUrl() { return url; } public void setUrl(String url) { this.url = url; } @Nullable public String getRoomId() { return roomId; } public void setRoomId(String roomId) { this.roomId = roomId; } @Nullable public String getToken() { return token; } public void setToken(String token) { this.token = token; } @Nullable public String getUserId() { return userId; } public void setUserId(String userId) { this.userId = userId; } } ================================================ FILE: spring-boot-admin-server/src/main/java/de/codecentric/boot/admin/server/notify/SlackNotifier.java ================================================ /* * 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.server.notify; import java.net.URI; import java.util.Collections; import java.util.HashMap; import java.util.Map; import org.jspecify.annotations.Nullable; import org.springframework.http.HttpEntity; import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; import org.springframework.web.client.RestTemplate; 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.events.InstanceEvent; import de.codecentric.boot.admin.server.domain.events.InstanceStatusChangedEvent; import de.codecentric.boot.admin.server.domain.values.StatusInfo; import de.codecentric.boot.admin.server.notify.filter.AbstractContentNotifier; /** * Notifier submitting events to Slack. * * @author Artur Dobosiewicz */ public class SlackNotifier extends AbstractContentNotifier { private static final String DEFAULT_MESSAGE = "*#{name}* (#{id}) is *#{status}*"; private RestTemplate restTemplate; /** * Webhook url for Slack API (i.e. https://hooks.slack.com/services/xxx) */ @Nullable private URI webhookUrl; /** * Optional channel name without # sign (i.e. somechannel) */ @Nullable private String channel; /** * Optional emoji icon without colons (i.e. my-emoji) */ @Nullable private String icon; /** * Optional username which sends notification */ @Nullable private String username = "Spring Boot Admin"; public SlackNotifier(InstanceRepository repository, RestTemplate restTemplate) { super(repository); this.restTemplate = restTemplate; } @Override protected Mono doNotify(InstanceEvent event, Instance instance) { if (webhookUrl == null) { return Mono.error(new IllegalStateException("'webhookUrl' must not be null.")); } return Mono .fromRunnable(() -> restTemplate.postForEntity(webhookUrl, createMessage(event, instance), Void.class)); } public void setRestTemplate(RestTemplate restTemplate) { this.restTemplate = restTemplate; } protected Object createMessage(InstanceEvent event, Instance instance) { Map messageJson = new HashMap<>(); messageJson.put("username", username); if (icon != null) { messageJson.put("icon_emoji", ":" + icon + ":"); } if (channel != null) { messageJson.put("channel", channel); } Map attachments = new HashMap<>(); attachments.put("text", createContent(event, instance)); attachments.put("color", getColor(event)); attachments.put("mrkdwn_in", Collections.singletonList("text")); messageJson.put("attachments", Collections.singletonList(attachments)); HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_JSON); return new HttpEntity<>(messageJson, headers); } protected String getColor(InstanceEvent event) { if (event instanceof InstanceStatusChangedEvent statusChangedEvent) { return StatusInfo.STATUS_UP.equals(statusChangedEvent.getStatusInfo().getStatus()) ? "good" : "danger"; } else { return "#439FE0"; } } @Nullable public URI getWebhookUrl() { return webhookUrl; } public void setWebhookUrl(@Nullable URI webhookUrl) { this.webhookUrl = webhookUrl; } @Nullable public String getChannel() { return channel; } public void setChannel(@Nullable String channel) { this.channel = channel; } @Nullable public String getIcon() { return icon; } public void setIcon(@Nullable String icon) { this.icon = icon; } @Nullable public String getUsername() { return username; } public void setUsername(@Nullable String username) { this.username = username; } @Override protected String getDefaultMessage() { return DEFAULT_MESSAGE; } } ================================================ FILE: spring-boot-admin-server/src/main/java/de/codecentric/boot/admin/server/notify/TelegramNotifier.java ================================================ /* * 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.server.notify; import java.util.HashMap; import java.util.Map; import org.jspecify.annotations.Nullable; import org.springframework.web.client.RestTemplate; 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.events.InstanceEvent; import de.codecentric.boot.admin.server.notify.filter.AbstractContentNotifier; /** * Notifier submitting events to Telegram. */ public class TelegramNotifier extends AbstractContentNotifier { private static final String DEFAULT_MESSAGE = "#{name}/#{id} is #{status}"; private RestTemplate restTemplate; /** * base url for telegram (i.e. https://api.telegram.org) */ private String apiUrl = "https://api.telegram.org"; /** * Unique identifier for the target chat or username of the target channel */ @Nullable private String chatId; /** * The token identifying und authorizing your Telegram bot (e.g. * `123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11`) */ @Nullable private String authToken; /** * Send Markdown or HTML, if you want Telegram apps to show bold, italic, fixed-width * text or inline URLs in your bot's message. */ private String parseMode = "HTML"; /** * If true users will receive a notification with no sound. */ private boolean disableNotify = false; public TelegramNotifier(InstanceRepository repository, RestTemplate restTemplate) { super(repository); this.restTemplate = restTemplate; } @Override protected Mono doNotify(InstanceEvent event, Instance instance) { return Mono .fromRunnable(() -> restTemplate.getForObject(buildUrl(), Void.class, createMessage(event, instance))); } protected String buildUrl() { return String.format("%s/bot%s/sendmessage?chat_id={chat_id}&text={text}&parse_mode={parse_mode}" + "&disable_notification={disable_notification}", this.apiUrl, this.authToken); } private Map createMessage(InstanceEvent event, Instance instance) { Map parameters = new HashMap<>(); parameters.put("chat_id", this.chatId); parameters.put("parse_mode", this.parseMode); parameters.put("disable_notification", this.disableNotify); parameters.put("text", createContent(event, instance)); return parameters; } @Override protected String getDefaultMessage() { return DEFAULT_MESSAGE; } public void setRestTemplate(RestTemplate restTemplate) { this.restTemplate = restTemplate; } public String getApiUrl() { return apiUrl; } public void setApiUrl(String apiUrl) { this.apiUrl = apiUrl; } @Nullable public String getChatId() { return chatId; } public void setChatId(@Nullable String chatId) { this.chatId = chatId; } @Nullable public String getAuthToken() { return authToken; } public void setAuthToken(@Nullable String authToken) { this.authToken = authToken; } public boolean isDisableNotify() { return disableNotify; } public void setDisableNotify(boolean disableNotify) { this.disableNotify = disableNotify; } public String getParseMode() { return parseMode; } public void setParseMode(String parseMode) { this.parseMode = parseMode; } } ================================================ FILE: spring-boot-admin-server/src/main/java/de/codecentric/boot/admin/server/notify/WebexNotifier.java ================================================ /* * Copyright 2014-2024 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.server.notify; import java.net.URI; import java.util.HashMap; import java.util.Map; import org.jspecify.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.http.HttpEntity; import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; import org.springframework.web.client.RestTemplate; 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.events.InstanceEvent; import de.codecentric.boot.admin.server.notify.filter.AbstractContentNotifier; // The following class, `WebexNotifier`, is responsible for sending notifications through the Webex API // whenever events related to the state of instances within the Spring Boot Admin server occur. /** * `WebexNotifier` sends notifications via Webex API when instance events occur. It is * part of the spring-boot-admin-server which is used for monitoring and managing Spring * Boot applications. */ public class WebexNotifier extends AbstractContentNotifier { private static final Logger LOGGER = LoggerFactory.getLogger(WebexNotifier.class); private static final URI DEFAULT_URL = URI.create("https://webexapis.com/v1/messages"); private static final String DEFAULT_MESSAGE = "#{name}/#{id} is #{status}"; private RestTemplate restTemplate; /** * base url for Webex API (i.e. https://webexapis.com/v1/messages) */ private URI url = DEFAULT_URL; /** * Bearer authentication token for Webex API */ @Nullable private String authToken; /** * Room identifier in Webex where the message will be sent */ @Nullable private String roomId; /** * Creates a new WebexNotifier with the given repository and restTemplate. * @param repository the instance repository responsible for storing instances * @param restTemplate the restTemplate used to make HTTP requests */ public WebexNotifier(InstanceRepository repository, RestTemplate restTemplate) { super(repository); this.restTemplate = restTemplate; } /** * Sends a notification with the given event and instance. * @param event the instance event to notify * @param instance the instance associated with the event * @return a Mono representing the completion of the notification * @throws IllegalStateException if 'authToken' is null */ @Override protected Mono doNotify(InstanceEvent event, Instance instance) { if (authToken == null) { return Mono.error(new IllegalStateException("'authToken' must not be null.")); } HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_JSON); headers.setBearerAuth(authToken); LOGGER.debug("Event: {}", event.getInstance()); return Mono.fromRunnable(() -> restTemplate.postForEntity(url, new HttpEntity<>(createMessage(event, instance), headers), Void.class)); } /** * Creates a message object containing the parameters required for sending a * notification. * @param event the instance event for which the message is being created * @param instance the instance associated with the event * @return a Map object containing the parameters for sending a notification */ protected Object createMessage(InstanceEvent event, Instance instance) { Map parameters = new HashMap<>(); parameters.put("roomId", this.roomId); parameters.put("markdown", createContent(event, instance)); return parameters; } @Override protected String getDefaultMessage() { return DEFAULT_MESSAGE; } public void setRestTemplate(RestTemplate restTemplate) { this.restTemplate = restTemplate; } public URI getUrl() { return url; } public void setUrl(URI url) { this.url = url; } @Nullable public String getAuthToken() { return authToken; } public void setAuthToken(@Nullable String authToken) { this.authToken = authToken; } @Nullable public String getRoomId() { return roomId; } public void setRoomId(@Nullable String roomId) { this.roomId = roomId; } } ================================================ FILE: spring-boot-admin-server/src/main/java/de/codecentric/boot/admin/server/notify/filter/AbstractContentNotifier.java ================================================ /* * 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 * * 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.server.notify.filter; import java.util.HashMap; import java.util.Map; import org.springframework.expression.Expression; import org.springframework.expression.ParserContext; import org.springframework.expression.spel.standard.SpelExpressionParser; import org.springframework.expression.spel.support.DataBindingPropertyAccessor; import org.springframework.expression.spel.support.MapAccessor; import org.springframework.expression.spel.support.SimpleEvaluationContext; 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.AbstractStatusChangeNotifier; /** * Base class for notifiers that generate message content from templates using Spring * Expression Language (SpEL). *

* This class provides a framework for creating custom notifiers that format notification * messages using SpEL templates with access to instance event data. Subclasses must * implement two methods: *

    *
  • {@link #buildContentModel(InstanceEvent, Instance)} - Provide the data model for * template evaluation
  • *
  • {@link #getDefaultMessage()} - Define the default SpEL template string
  • *
*

* Usage Example:

{@code
 * public class EmailNotifier extends AbstractContentNotifier {
 *     public EmailNotifier(InstanceRepository repository) {
 *         super(repository);
 *     }
 *
 *     @Override
 *     protected Map getContent(InstanceEvent event, Instance instance) {
 *         var content = super.getContent(event, instance);
 *         content.put("customContent", "Hello, World!");
 *         return content;
 *     }
 *
 *

@Override
 *     protected String getDefaultMessage() {
 *         return "#{name} is #{status} at #{url}";
 *     }
 * }
 * }
*

* The message template can be customized at runtime using {@link #setMessage(String)}. */ public abstract class AbstractContentNotifier extends AbstractStatusChangeNotifier { private Expression message; private final SpelExpressionParser parser = new SpelExpressionParser(); public AbstractContentNotifier(InstanceRepository repository) { super(repository); this.message = this.parser.parseExpression(getDefaultMessage(), ParserContext.TEMPLATE_EXPRESSION); } public String getMessage() { return this.message.getExpressionString(); } public void setMessage(String message) { this.message = this.parser.parseExpression(message, ParserContext.TEMPLATE_EXPRESSION); } /** * Generates the notification message content by evaluating the SpEL template with * event and instance data. *

* This method combines the configured message template with the data provided by * {@link #buildContentModel(InstanceEvent, Instance)} to produce the final * notification text. * @param event the instance event that triggered the notification * @param instance the instance associated with the event * @return the evaluated message content as a string */ protected String createContent(InstanceEvent event, Instance instance) { SimpleEvaluationContext context = SimpleEvaluationContext .forPropertyAccessors(DataBindingPropertyAccessor.forReadOnlyAccess(), new MapAccessor()) .withRootObject(buildContentModel(event, instance)) .build(); return this.message.getValue(context, String.class); } /** * Provides the data model used for evaluating the message template. *

* The returned map contains key-value pairs that can be referenced in the SpEL * template using #{key} syntax. For example, if the map contains {"name": "MyApp", * "status": "UP"}, the template "#{name} is #{status}" would evaluate to "MyApp is * UP". * @param event the instance event containing event-specific data * @param instance the instance containing registration and status information * @return a map of template variables and their values */ protected Map buildContentModel(InstanceEvent event, Instance instance) { Map content = new HashMap<>(); content.put("name", instance.getRegistration().getName()); content.put("id", instance.getId().getValue()); content.put("status", (event instanceof InstanceStatusChangedEvent statusChangedEvent) ? statusChangedEvent.getStatusInfo().getStatus() : "UNKNOWN"); content.put("lastStatus", getLastStatus(event.getInstance())); return content; } /** * Defines the default SpEL template string used for message generation. *

* The template should use #{key} syntax to reference variables provided by * {@link #buildContentModel(InstanceEvent, Instance)}. This default can be overridden * at runtime using {@link #setMessage(String)}. * @return the default SpEL template string (e.g., "#{name} is #{status}") */ protected abstract String getDefaultMessage(); } ================================================ FILE: spring-boot-admin-server/src/main/java/de/codecentric/boot/admin/server/notify/filter/AbstractNotificationFilter.java ================================================ /* * 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.server.notify.filter; import java.util.concurrent.atomic.AtomicLong; public abstract class AbstractNotificationFilter implements NotificationFilter { private static final AtomicLong instanceCounter = new AtomicLong(0L); private final String id; public AbstractNotificationFilter() { this.id = "F-" + instanceCounter.getAndIncrement(); } @Override public String getId() { return id; } } ================================================ FILE: spring-boot-admin-server/src/main/java/de/codecentric/boot/admin/server/notify/filter/ApplicationNameNotificationFilter.java ================================================ /* * 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.server.notify.filter; import java.time.Instant; import org.jspecify.annotations.Nullable; import de.codecentric.boot.admin.server.domain.entities.Instance; import de.codecentric.boot.admin.server.domain.events.InstanceEvent; public class ApplicationNameNotificationFilter extends ExpiringNotificationFilter { private final String applicationName; public ApplicationNameNotificationFilter(String applicationName, @Nullable Instant expiry) { super(expiry); this.applicationName = applicationName; } @Override protected boolean doFilter(InstanceEvent event, Instance instance) { return applicationName.equals(instance.getRegistration().getName()); } public String getApplicationName() { return applicationName; } @Override public String toString() { return "NotificationFilter [applicationName=" + applicationName + ", expiry=" + getExpiry() + "]"; } } ================================================ FILE: spring-boot-admin-server/src/main/java/de/codecentric/boot/admin/server/notify/filter/ExpiringNotificationFilter.java ================================================ /* * 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.server.notify.filter; import java.time.Instant; import org.jspecify.annotations.Nullable; import de.codecentric.boot.admin.server.domain.entities.Instance; import de.codecentric.boot.admin.server.domain.events.InstanceEvent; public abstract class ExpiringNotificationFilter extends AbstractNotificationFilter { @Nullable private final Instant expiry; public ExpiringNotificationFilter(@Nullable Instant expiry) { this.expiry = expiry; } public boolean isExpired() { return expiry != null && expiry.isBefore(Instant.now()); } @Override public boolean filter(InstanceEvent event, Instance instance) { return !isExpired() && doFilter(event, instance); } protected abstract boolean doFilter(InstanceEvent event, Instance instance); @Nullable public Instant getExpiry() { return expiry; } } ================================================ FILE: spring-boot-admin-server/src/main/java/de/codecentric/boot/admin/server/notify/filter/FilteringNotifier.java ================================================ /* * 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.server.notify.filter; import java.time.Duration; import java.time.Instant; import java.util.Collections; import java.util.HashMap; import java.util.Map; import java.util.Map.Entry; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import org.jspecify.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.util.Assert; 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.events.InstanceEvent; import de.codecentric.boot.admin.server.notify.AbstractEventNotifier; import de.codecentric.boot.admin.server.notify.Notifier; /** * Notifier that allows to filter certain events based on policies. * * @author Johannes Edmeier */ public class FilteringNotifier extends AbstractEventNotifier { private static final Logger LOGGER = LoggerFactory.getLogger(FilteringNotifier.class); private final ConcurrentMap filters = new ConcurrentHashMap<>(); private final Notifier delegate; private Instant lastCleanup = Instant.EPOCH; private Duration cleanupInterval = Duration.ofSeconds(10); public FilteringNotifier(Notifier delegate, InstanceRepository repository) { super(repository); Assert.notNull(delegate, "'delegate' must not be null!"); this.delegate = delegate; } @Override protected boolean shouldNotify(InstanceEvent event, Instance instance) { return !filter(event, instance); } @Override public Mono doNotify(InstanceEvent event, Instance instance) { return delegate.notify(event); } private boolean filter(InstanceEvent event, Instance instance) { cleanUp(); for (Entry entry : getNotificationFilters().entrySet()) { if (entry.getValue().filter(event, instance)) { LOGGER.debug("The event '{}' was suppressed by filter '{}'", event, entry); return true; } } return false; } private void cleanUp() { Instant now = Instant.now(); if (lastCleanup.plus(cleanupInterval).isAfter(now)) { return; } lastCleanup = now; for (Entry entry : getNotificationFilters().entrySet()) { if (entry.getValue() instanceof ExpiringNotificationFilter filter && filter.isExpired()) { LOGGER.debug("Expired filter '{}' removed", entry); filters.remove(entry.getKey()); } } } public void addFilter(NotificationFilter filter) { LOGGER.debug("Added filter '{}'", filter); filters.put(filter.getId(), filter); } @Nullable public NotificationFilter removeFilter(String id) { LOGGER.debug("Removed filter with id '{}'", id); return filters.remove(id); } public Map getNotificationFilters() { return Collections.unmodifiableMap(new HashMap<>(filters)); } public void setCleanupInterval(Duration cleanupInterval) { this.cleanupInterval = cleanupInterval; } } ================================================ FILE: spring-boot-admin-server/src/main/java/de/codecentric/boot/admin/server/notify/filter/InstanceIdNotificationFilter.java ================================================ /* * 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.server.notify.filter; import java.time.Instant; import org.jspecify.annotations.Nullable; import de.codecentric.boot.admin.server.domain.entities.Instance; import de.codecentric.boot.admin.server.domain.events.InstanceEvent; import de.codecentric.boot.admin.server.domain.values.InstanceId; public class InstanceIdNotificationFilter extends ExpiringNotificationFilter { private final InstanceId instanceId; public InstanceIdNotificationFilter(InstanceId instanceId, @Nullable Instant expiry) { super(expiry); this.instanceId = instanceId; } @Override protected boolean doFilter(InstanceEvent event, Instance instance) { return instanceId.equals(event.getInstance()); } public InstanceId getInstanceId() { return instanceId; } @Override public String toString() { return "NotificationFilter [instanceId=" + instanceId + ", expiry=" + getExpiry() + "]"; } } ================================================ FILE: spring-boot-admin-server/src/main/java/de/codecentric/boot/admin/server/notify/filter/NotificationFilter.java ================================================ /* * 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.server.notify.filter; import de.codecentric.boot.admin.server.domain.entities.Instance; import de.codecentric.boot.admin.server.domain.events.InstanceEvent; public interface NotificationFilter { String getId(); boolean filter(InstanceEvent event, Instance instance); } ================================================ FILE: spring-boot-admin-server/src/main/java/de/codecentric/boot/admin/server/notify/filter/package-info.java ================================================ /* * 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. */ // Spring Boot Admin Server - filter package. @NullMarked package de.codecentric.boot.admin.server.notify.filter; import org.jspecify.annotations.NullMarked; ================================================ FILE: spring-boot-admin-server/src/main/java/de/codecentric/boot/admin/server/notify/filter/web/NotificationFilterController.java ================================================ /* * 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.server.notify.filter.web; import java.time.Instant; import java.util.Collection; import org.jspecify.annotations.Nullable; import org.springframework.http.ResponseEntity; import org.springframework.util.MimeTypeUtils; import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.ResponseBody; import de.codecentric.boot.admin.server.domain.values.InstanceId; import de.codecentric.boot.admin.server.notify.filter.ApplicationNameNotificationFilter; import de.codecentric.boot.admin.server.notify.filter.FilteringNotifier; import de.codecentric.boot.admin.server.notify.filter.InstanceIdNotificationFilter; import de.codecentric.boot.admin.server.notify.filter.NotificationFilter; import de.codecentric.boot.admin.server.web.AdminController; import static org.springframework.util.StringUtils.hasText; /** * REST-Controller for managing notification filters * * @author Johannes Edmeier */ @AdminController @ResponseBody public class NotificationFilterController { private final FilteringNotifier filteringNotifier; public NotificationFilterController(FilteringNotifier filteringNotifier) { this.filteringNotifier = filteringNotifier; } @GetMapping(path = "/notifications/filters", produces = MimeTypeUtils.APPLICATION_JSON_VALUE) public Collection getFilters() { return filteringNotifier.getNotificationFilters().values(); } @PostMapping(path = "/notifications/filters", produces = MimeTypeUtils.APPLICATION_JSON_VALUE) public ResponseEntity addFilter(@RequestParam(name = "instanceId", required = false) String instanceId, @RequestParam(name = "applicationName", required = false) String name, @RequestParam(name = "ttl", required = false) Long ttl) { if (hasText(instanceId) || hasText(name)) { NotificationFilter filter = createFilter(hasText(instanceId) ? InstanceId.of(instanceId) : null, name, ttl); filteringNotifier.addFilter(filter); return ResponseEntity.ok(filter); } else { return ResponseEntity.badRequest().body("Either 'instanceId' or 'applicationName' must be set"); } } @DeleteMapping(path = "/notifications/filters/{id}") public ResponseEntity deleteFilter(@PathVariable("id") String id) { NotificationFilter deleted = filteringNotifier.removeFilter(id); if (deleted != null) { return ResponseEntity.ok().build(); } else { return ResponseEntity.notFound().build(); } } private NotificationFilter createFilter(@Nullable InstanceId id, String name, @Nullable Long ttl) { Instant expiry = ((ttl != null) && (ttl >= 0)) ? Instant.now().plusMillis(ttl) : null; if (id != null) { return new InstanceIdNotificationFilter(id, expiry); } else { return new ApplicationNameNotificationFilter(name, expiry); } } } ================================================ FILE: spring-boot-admin-server/src/main/java/de/codecentric/boot/admin/server/notify/filter/web/package-info.java ================================================ /* * 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. */ // Spring Boot Admin Server - notifications filter package. @NullMarked package de.codecentric.boot.admin.server.notify.filter.web; import org.jspecify.annotations.NullMarked; ================================================ FILE: spring-boot-admin-server/src/main/java/de/codecentric/boot/admin/server/notify/package-info.java ================================================ /* * 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. */ // Spring Boot Admin Server - notifier package. @NullMarked package de.codecentric.boot.admin.server.notify; import org.jspecify.annotations.NullMarked; ================================================ FILE: spring-boot-admin-server/src/main/java/de/codecentric/boot/admin/server/services/AbstractEventHandler.java ================================================ /* * 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.server.services; import java.util.logging.Level; import org.jspecify.annotations.Nullable; import org.reactivestreams.Publisher; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import reactor.core.Disposable; import reactor.core.publisher.Flux; import reactor.core.scheduler.Scheduler; import reactor.core.scheduler.Schedulers; import de.codecentric.boot.admin.server.domain.events.InstanceEvent; public abstract class AbstractEventHandler { private final Logger log = LoggerFactory.getLogger(this.getClass()); private final Publisher publisher; private final Class eventType; @Nullable private Disposable subscription; @Nullable private Scheduler scheduler; protected AbstractEventHandler(Publisher publisher, Class eventType) { this.publisher = publisher; this.eventType = eventType; } public void start() { this.scheduler = this.createScheduler(); this.subscription = Flux.from(this.publisher) .subscribeOn(this.scheduler) .log(this.log.getName(), Level.FINEST) .doOnSubscribe((s) -> this.log.debug("Subscribed to {} events", this.eventType)) .ofType(this.eventType) .cast(this.eventType) .transform(this::handle) .onErrorContinue((throwable, o) -> this.log.warn("Unexpected error", throwable)) .subscribe(); } protected abstract Publisher handle(Flux publisher); protected Scheduler createScheduler() { return Schedulers.newSingle(this.getClass().getSimpleName()); } public void stop() { if (this.subscription != null) { this.subscription.dispose(); this.subscription = null; } if (this.scheduler != null) { this.scheduler.dispose(); this.scheduler = null; } } } ================================================ FILE: spring-boot-admin-server/src/main/java/de/codecentric/boot/admin/server/services/ApiMediaTypeHandler.java ================================================ /* * 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.server.services; import java.util.stream.Stream; import org.springframework.boot.actuate.endpoint.ApiVersion; import org.springframework.http.MediaType; public class ApiMediaTypeHandler { public boolean isApiMediaType(MediaType mediaType) { return Stream.of(ApiVersion.values()) .map(ApiVersion::getProducedMimeType) .anyMatch((mimeType) -> mimeType.isCompatibleWith(mediaType)); } } ================================================ FILE: spring-boot-admin-server/src/main/java/de/codecentric/boot/admin/server/services/ApplicationRegistry.java ================================================ /* * 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.server.services; import java.time.Instant; import java.util.List; import java.util.Map; import java.util.Objects; import org.jspecify.annotations.Nullable; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import reactor.util.function.Tuple2; import reactor.util.function.Tuples; import de.codecentric.boot.admin.server.domain.entities.Application; import de.codecentric.boot.admin.server.domain.entities.Instance; import de.codecentric.boot.admin.server.domain.values.BuildVersion; import de.codecentric.boot.admin.server.domain.values.InstanceId; import de.codecentric.boot.admin.server.domain.values.StatusInfo; import de.codecentric.boot.admin.server.eventstore.InstanceEventPublisher; import static de.codecentric.boot.admin.server.domain.values.StatusInfo.STATUS_UNKNOWN; import static java.util.Comparator.naturalOrder; import static java.util.stream.Collectors.toMap; /** * Registry for all applications that should be managed/administrated by the Spring Boot * Admin server. Backed by an InstanceRegistry for persistence and an * InstanceEventPublisher for events * * @author Dean de Bree */ public class ApplicationRegistry { private final InstanceRegistry instanceRegistry; private final InstanceEventPublisher instanceEventPublisher; public ApplicationRegistry(InstanceRegistry instanceRegistry, InstanceEventPublisher instanceEventPublisher) { this.instanceRegistry = instanceRegistry; this.instanceEventPublisher = instanceEventPublisher; } /** * Get a list of all registered applications. * @return flux of all the applications. */ public Flux getApplications() { return this.instanceRegistry.getInstances() .filter(Instance::isRegistered) .groupBy((instance) -> instance.getRegistration().getName()) .flatMap((grouped) -> toApplication(grouped.key(), grouped), Integer.MAX_VALUE); } /** * Get a specific application instance. * @param name the name of the application to find. * @return a Mono with the application or an empty Mono if not found. */ public Mono getApplication(String name) { return this.toApplication(name, this.instanceRegistry.getInstances(name).filter(Instance::isRegistered)) .filter((a) -> !a.getInstances().isEmpty()); } public Flux getApplicationStream() { return Flux.from(this.instanceEventPublisher) .flatMap((event) -> this.instanceRegistry.getInstance(event.getInstance())) .map(this::getApplicationForInstance) .flatMap((group) -> toApplication(group.getT1(), group.getT2())); } public Flux deregister(String name) { return this.instanceRegistry.getInstances(name) .flatMap((instance) -> this.instanceRegistry.deregister(instance.getId())); } protected Tuple2> getApplicationForInstance(Instance instance) { String name = instance.getRegistration().getName(); return Tuples.of(name, this.instanceRegistry.getInstances(name).filter(Instance::isRegistered)); } protected Mono toApplication(String name, Flux instances) { return instances.collectList().map((instanceList) -> { Tuple2 status = getStatus(instanceList); return Application.create(name) .instances(instanceList) .buildVersion(getBuildVersion(instanceList)) .status(status.getT1()) .statusTimestamp(status.getT2()) .build(); }); } @Nullable protected BuildVersion getBuildVersion(List instances) { List versions = instances.stream() .map(Instance::getBuildVersion) .filter(Objects::nonNull) .distinct() .sorted() .toList(); if (versions.isEmpty()) { return null; } else if (versions.size() == 1) { return versions.get(0); } else { return BuildVersion.valueOf(versions.get(0) + " ... " + versions.get(versions.size() - 1)); } } protected Tuple2 getStatus(List instances) { // TODO: Correct is just a second readmodel for groups Map statusWithTime = instances.stream() .collect(toMap((instance) -> instance.getStatusInfo().getStatus(), Instance::getStatusTimestamp, this::getMax)); if (statusWithTime.size() == 1) { Map.Entry e = statusWithTime.entrySet().iterator().next(); return Tuples.of(e.getKey(), e.getValue()); } if (statusWithTime.containsKey(StatusInfo.STATUS_UP)) { Instant oldestNonUp = statusWithTime.entrySet() .stream() .filter((e) -> !StatusInfo.STATUS_UP.equals(e.getKey())) .map(Map.Entry::getValue) .min(naturalOrder()) .orElse(Instant.EPOCH); Instant latest = getMax(oldestNonUp, statusWithTime.getOrDefault(StatusInfo.STATUS_UP, Instant.EPOCH)); return Tuples.of(StatusInfo.STATUS_RESTRICTED, latest); } return statusWithTime.entrySet() .stream() .min(Map.Entry.comparingByKey(StatusInfo.severity())) .map((e) -> Tuples.of(e.getKey(), e.getValue())) .orElse(Tuples.of(STATUS_UNKNOWN, Instant.EPOCH)); } protected Instant getMax(Instant t1, Instant t2) { return (t1.compareTo(t2) >= 0) ? t1 : t2; } } ================================================ FILE: spring-boot-admin-server/src/main/java/de/codecentric/boot/admin/server/services/CloudFoundryInstanceIdGenerator.java ================================================ /* * 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.server.services; import org.springframework.util.StringUtils; import de.codecentric.boot.admin.server.domain.values.InstanceId; import de.codecentric.boot.admin.server.domain.values.Registration; /** * Generates CF instance uniqueId "applicationId:instanceId" for CloudFoundry instance. * Uses a fallback InstanceIdGenerator when the metadata isn't present. * * @author Tetsushi Awano */ public class CloudFoundryInstanceIdGenerator implements InstanceIdGenerator { private final InstanceIdGenerator fallbackIdGenerator; public CloudFoundryInstanceIdGenerator(InstanceIdGenerator fallbackIdGenerator) { this.fallbackIdGenerator = fallbackIdGenerator; } @Override public InstanceId generateId(Registration registration) { String applicationId = registration.getMetadata().get("applicationId"); String instanceId = registration.getMetadata().get("instanceId"); if (StringUtils.hasText(applicationId) && StringUtils.hasText(instanceId)) { return InstanceId.of(String.format("%s:%s", applicationId, instanceId)); } return fallbackIdGenerator.generateId(registration); } } ================================================ FILE: spring-boot-admin-server/src/main/java/de/codecentric/boot/admin/server/services/EndpointDetectionTrigger.java ================================================ /* * 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.server.services; import org.reactivestreams.Publisher; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import de.codecentric.boot.admin.server.domain.events.InstanceEvent; import de.codecentric.boot.admin.server.domain.events.InstanceRegistrationUpdatedEvent; import de.codecentric.boot.admin.server.domain.events.InstanceStatusChangedEvent; public class EndpointDetectionTrigger extends AbstractEventHandler { private static final Logger log = LoggerFactory.getLogger(EndpointDetectionTrigger.class); private final EndpointDetector endpointDetector; public EndpointDetectionTrigger(EndpointDetector endpointDetector, Publisher publisher) { super(publisher, InstanceEvent.class); this.endpointDetector = endpointDetector; } @Override protected Publisher handle(Flux publisher) { return publisher .filter((event) -> event instanceof InstanceStatusChangedEvent || event instanceof InstanceRegistrationUpdatedEvent) .flatMap(this::detectEndpoints); } protected Mono detectEndpoints(InstanceEvent event) { return this.endpointDetector.detectEndpoints(event.getInstance()).onErrorResume((e) -> { log.warn("Unexpected error while detecting endpoints for {}", event.getInstance(), e); return Mono.empty(); }); } } ================================================ FILE: spring-boot-admin-server/src/main/java/de/codecentric/boot/admin/server/services/EndpointDetector.java ================================================ /* * 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.server.services; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.util.StringUtils; 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.InstanceId; import de.codecentric.boot.admin.server.services.endpoints.EndpointDetectionStrategy; /** * @author Johannes Edmeier */ public class EndpointDetector { private static final Logger log = LoggerFactory.getLogger(EndpointDetector.class); private final InstanceRepository repository; private final EndpointDetectionStrategy strategy; public EndpointDetector(InstanceRepository repository, EndpointDetectionStrategy strategy) { this.repository = repository; this.strategy = strategy; } public Mono detectEndpoints(InstanceId id) { return repository.computeIfPresent(id, (key, instance) -> this.doDetectEndpoints(instance)).then(); } private Mono doDetectEndpoints(Instance instance) { if (!StringUtils.hasText(instance.getRegistration().getManagementUrl()) || instance.getStatusInfo().isOffline() || instance.getStatusInfo().isUnknown()) { return Mono.empty(); } log.debug("Detect endpoints for {}", instance); return strategy.detectEndpoints(instance).map(instance::withEndpoints); } } ================================================ FILE: spring-boot-admin-server/src/main/java/de/codecentric/boot/admin/server/services/HashingInstanceUrlIdGenerator.java ================================================ /* * 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.server.services; import java.nio.charset.StandardCharsets; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import de.codecentric.boot.admin.server.domain.values.InstanceId; import de.codecentric.boot.admin.server.domain.values.Registration; /** * Generates an SHA-1 Hash based on the instance health url. */ public class HashingInstanceUrlIdGenerator implements InstanceIdGenerator { private static final char[] HEX_CHARS = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f' }; @Override public InstanceId generateId(Registration registration) { try { MessageDigest digest = MessageDigest.getInstance("SHA-1"); byte[] bytes = digest.digest(registration.getHealthUrl().getBytes(StandardCharsets.UTF_8)); return InstanceId.of(new String(encodeHex(bytes, 0, 12))); } catch (NoSuchAlgorithmException ex) { throw new IllegalStateException(ex); } } private char[] encodeHex(byte[] bytes, int offset, int length) { char[] chars = new char[length]; for (int i = 0; i < length; i = i + 2) { byte b = bytes[offset + (i / 2)]; chars[i] = HEX_CHARS[(b >>> 0x4) & 0xf]; chars[i + 1] = HEX_CHARS[b & 0xf]; } return chars; } } ================================================ FILE: spring-boot-admin-server/src/main/java/de/codecentric/boot/admin/server/services/InfoUpdateTrigger.java ================================================ /* * 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.server.services; import java.time.Duration; import org.reactivestreams.Publisher; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import de.codecentric.boot.admin.server.domain.events.InstanceEndpointsDetectedEvent; import de.codecentric.boot.admin.server.domain.events.InstanceEvent; import de.codecentric.boot.admin.server.domain.events.InstanceRegistrationUpdatedEvent; import de.codecentric.boot.admin.server.domain.events.InstanceStatusChangedEvent; import de.codecentric.boot.admin.server.domain.values.InstanceId; public class InfoUpdateTrigger extends AbstractEventHandler { private static final Logger log = LoggerFactory.getLogger(InfoUpdateTrigger.class); private final InfoUpdater infoUpdater; private final IntervalCheck intervalCheck; public InfoUpdateTrigger(InfoUpdater infoUpdater, Publisher publisher, Duration updateInterval, Duration infoLifetime, Duration maxBackoff) { super(publisher, InstanceEvent.class); this.infoUpdater = infoUpdater; this.intervalCheck = new IntervalCheck("info", this::updateInfo, updateInterval, infoLifetime, maxBackoff); } @Override protected Publisher handle(Flux publisher) { return publisher .filter((event) -> event instanceof InstanceEndpointsDetectedEvent || event instanceof InstanceStatusChangedEvent || event instanceof InstanceRegistrationUpdatedEvent) .flatMap((event) -> this.updateInfo(event.getInstance())); } protected Mono updateInfo(InstanceId instanceId) { return this.infoUpdater.updateInfo(instanceId).onErrorResume((e) -> { log.warn("Unexpected error while updating info for {}", instanceId, e); return Mono.empty(); }).doFinally((s) -> this.intervalCheck.markAsChecked(instanceId)); } @Override public void start() { super.start(); this.intervalCheck.start(); } @Override public void stop() { super.stop(); this.intervalCheck.stop(); } public void setInterval(Duration updateInterval) { this.intervalCheck.setInterval(updateInterval); } public void setLifetime(Duration infoLifetime) { this.intervalCheck.setMinRetention(infoLifetime); } } ================================================ FILE: spring-boot-admin-server/src/main/java/de/codecentric/boot/admin/server/services/InfoUpdater.java ================================================ /* * 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.server.services; import java.util.Map; import java.util.logging.Level; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.core.ParameterizedTypeReference; import org.springframework.http.MediaType; import org.springframework.web.reactive.function.client.ClientResponse; 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.Endpoint; import de.codecentric.boot.admin.server.domain.values.Info; import de.codecentric.boot.admin.server.domain.values.InstanceId; import de.codecentric.boot.admin.server.web.client.InstanceWebClient; /** * The StatusUpdater is responsible for updating the status of all or a single application * querying the healthUrl. * * @author Johannes Edmeier */ public class InfoUpdater { private static final Logger log = LoggerFactory.getLogger(InfoUpdater.class); private static final ParameterizedTypeReference> RESPONSE_TYPE = new ParameterizedTypeReference<>() { }; private final InstanceRepository repository; private final InstanceWebClient instanceWebClient; private final ApiMediaTypeHandler apiMediaTypeHandler; public InfoUpdater(InstanceRepository repository, InstanceWebClient instanceWebClient, ApiMediaTypeHandler apiMediaTypeHandler) { this.repository = repository; this.instanceWebClient = instanceWebClient; this.apiMediaTypeHandler = apiMediaTypeHandler; } public Mono updateInfo(InstanceId id) { return this.repository.computeIfPresent(id, (key, instance) -> this.doUpdateInfo(instance)).then(); } protected Mono doUpdateInfo(Instance instance) { if (instance.getStatusInfo().isOffline() || instance.getStatusInfo().isUnknown()) { return Mono.empty(); } if (!instance.getEndpoints().isPresent(Endpoint.INFO)) { return Mono.empty(); } log.debug("Update info for {}", instance); return this.instanceWebClient.instance(instance) .get() .uri(Endpoint.INFO) .exchangeToMono((response) -> convertInfo(instance, response)) .log(log.getName(), Level.FINEST) .onErrorResume((ex) -> Mono.just(convertInfo(instance, ex))) .map(instance::withInfo); } protected Mono convertInfo(Instance instance, ClientResponse response) { if (response.statusCode().is2xxSuccessful() && response.headers() .contentType() .filter((mt) -> mt.isCompatibleWith(MediaType.APPLICATION_JSON) || this.apiMediaTypeHandler.isApiMediaType(mt)) .isPresent()) { return response.bodyToMono(RESPONSE_TYPE).map(Info::from).defaultIfEmpty(Info.empty()); } log.info("Couldn't retrieve info for {}: {}", instance, response.statusCode()); return response.releaseBody().then(Mono.just(Info.empty())); } protected Info convertInfo(Instance instance, Throwable ex) { log.warn("Couldn't retrieve info for {}", instance, ex); return Info.empty(); } } ================================================ FILE: spring-boot-admin-server/src/main/java/de/codecentric/boot/admin/server/services/InstanceFilter.java ================================================ /* * Copyright 2014-2024 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.server.services; import de.codecentric.boot.admin.server.domain.entities.Instance; /** * Interface that is applied to InstanceRegistry and returns Instances matching the * filter, only. Default implementation is to return all instances. * * @author dzahbarov * @see de.codecentric.boot.admin.server.config.AdminServerAutoConfiguration#instanceFilter() */ @FunctionalInterface public interface InstanceFilter { boolean filter(Instance instance); } ================================================ FILE: spring-boot-admin-server/src/main/java/de/codecentric/boot/admin/server/services/InstanceIdGenerator.java ================================================ /* * 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.server.services; import de.codecentric.boot.admin.server.domain.values.InstanceId; import de.codecentric.boot.admin.server.domain.values.Registration; public interface InstanceIdGenerator { /** * Generate an id based on the given Instance * @param registration the registration the id is computed for. * @return the instance id */ InstanceId generateId(Registration registration); } ================================================ FILE: spring-boot-admin-server/src/main/java/de/codecentric/boot/admin/server/services/InstanceRegistry.java ================================================ /* * 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.server.services; import org.springframework.util.Assert; import reactor.core.publisher.Flux; 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.InstanceId; import de.codecentric.boot.admin.server.domain.values.Registration; /** * Registry for all application instances that should be managed/administrated by the * Spring Boot Admin server. Backed by an InstanceRepository for persistence, an * InstanceIdGenerator for id generation and InstanceFilter for instance filtering. */ public class InstanceRegistry { private final InstanceRepository repository; private final InstanceIdGenerator generator; private final InstanceFilter filter; public InstanceRegistry(InstanceRepository repository, InstanceIdGenerator generator, InstanceFilter filter) { this.repository = repository; this.generator = generator; this.filter = filter; } /** * Register instance. * @param registration instance to be registered. * @return the id of the registered instance. */ public Mono register(Registration registration) { Assert.notNull(registration, "'registration' must not be null"); InstanceId id = generator.generateId(registration); Assert.notNull(id, "'id' must not be null"); return repository.compute(id, (key, instance) -> { if (instance == null) { instance = Instance.create(key); } return Mono.just(instance.register(registration)); }).map(Instance::getId); } /** * Get a list of all registered instances that satisfy the filter. * @return list of all instances satisfying the filter. */ public Flux getInstances() { return repository.findAll().filter(filter::filter); } /** * Get a list of all registered application instances that satisfy the filter. * @param name the name to search for. * @return list of instances for the given application that satisfy the filter. */ public Flux getInstances(String name) { return repository.findByName(name).filter(filter::filter); } /** * Get a specific instance * @param id the id * @return a Mono with the Instance. */ public Mono getInstance(InstanceId id) { return repository.find(id).filter(filter::filter); } /** * Remove a specific instance from services * @param id the instances id to unregister * @return the id of the unregistered instance */ public Mono deregister(InstanceId id) { return repository.computeIfPresent(id, (key, instance) -> Mono.just(instance.deregister())) .map(Instance::getId); } } ================================================ FILE: spring-boot-admin-server/src/main/java/de/codecentric/boot/admin/server/services/IntervalCheck.java ================================================ /* * 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.server.services; import java.time.Duration; import java.time.Instant; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.function.Consumer; import java.util.function.Function; import java.util.logging.Level; import lombok.Getter; import lombok.NonNull; import lombok.Setter; import lombok.extern.slf4j.Slf4j; import org.jspecify.annotations.Nullable; import org.reactivestreams.Publisher; import reactor.core.Disposable; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import reactor.core.scheduler.Scheduler; import reactor.core.scheduler.Schedulers; import reactor.util.retry.Retry; import de.codecentric.boot.admin.server.domain.values.InstanceId; /** * Calls the checkFn for all instances in the given time, but not before the given * retention time has passed. The instances which will be checked have to be registered * via `markAsChecked`. * * @author Johannes Edmeier */ @Slf4j public class IntervalCheck { private final String name; private final Map lastChecked = new ConcurrentHashMap<>(); private final Function> checkFn; @Setter private Duration maxBackoff; @Getter @Setter private Duration interval; @Setter private Duration minRetention; @Nullable private Disposable subscription; @Nullable private Scheduler scheduler; @Setter @NonNull private Consumer retryConsumer; public IntervalCheck(String name, Function> checkFn, Duration interval, Duration minRetention, Duration maxBackoff) { this.name = name; this.retryConsumer = (Throwable throwable) -> log.warn("Unexpected error in {}-check", this.name, throwable); this.checkFn = checkFn; this.interval = interval; this.minRetention = minRetention; this.maxBackoff = maxBackoff; } public void start() { this.scheduler = Schedulers.newSingle(this.name + "-check"); this.subscription = Flux.interval(this.interval) // ensure the most recent interval tick is always processed, preventing // lost checks under overload. .onBackpressureLatest() .doOnSubscribe((s) -> log.debug("Scheduled {}-check every {}", this.name, this.interval)) .log(log.getName(), Level.FINEST) // .subscribeOn(this.scheduler) // // Allow concurrent check cycles if previous is slow .flatMap((i) -> this.checkAllInstances(), Math.max(1, Runtime.getRuntime().availableProcessors() / 2)) .retryWhen(createRetrySpec()) .subscribe(null, (Throwable error) -> log.error("Unexpected error in {}-check", this.name, error)); } private Retry createRetrySpec() { return Retry.backoff(Long.MAX_VALUE, Duration.ofSeconds(1)) .maxBackoff(maxBackoff) .doBeforeRetry((s) -> this.retryConsumer.accept(s.failure())); } public void markAsChecked(InstanceId instanceId) { this.lastChecked.put(instanceId, Instant.now()); } protected Publisher checkAllInstances() { log.debug("check {} for all instances", this.name); Instant expiration = Instant.now().minus(this.minRetention); return Flux.fromIterable(this.lastChecked.entrySet()) .filter((entry) -> entry.getValue().isBefore(expiration)) .map(Map.Entry::getKey) .flatMap(this.checkFn) .then(); } public void stop() { if (this.subscription != null) { this.subscription.dispose(); this.subscription = null; } if (this.scheduler != null) { this.scheduler.dispose(); this.scheduler = null; } } } ================================================ FILE: spring-boot-admin-server/src/main/java/de/codecentric/boot/admin/server/services/StatusUpdateTrigger.java ================================================ /* * 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.server.services; import java.time.Duration; import org.reactivestreams.Publisher; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import de.codecentric.boot.admin.server.domain.events.InstanceEvent; import de.codecentric.boot.admin.server.domain.events.InstanceRegisteredEvent; import de.codecentric.boot.admin.server.domain.events.InstanceRegistrationUpdatedEvent; import de.codecentric.boot.admin.server.domain.values.InstanceId; public class StatusUpdateTrigger extends AbstractEventHandler { private static final Logger log = LoggerFactory.getLogger(StatusUpdateTrigger.class); private final StatusUpdater statusUpdater; private final IntervalCheck intervalCheck; public StatusUpdateTrigger(StatusUpdater statusUpdater, Publisher publisher, Duration updateInterval, Duration statusLifetime, Duration maxBackoff) { super(publisher, InstanceEvent.class); this.statusUpdater = statusUpdater; this.intervalCheck = new IntervalCheck("status", this::updateStatus, updateInterval, statusLifetime, maxBackoff); } @Override protected Publisher handle(Flux publisher) { return publisher .filter((event) -> event instanceof InstanceRegisteredEvent || event instanceof InstanceRegistrationUpdatedEvent) .flatMap((event) -> updateStatus(event.getInstance())); } protected Mono updateStatus(InstanceId instanceId) { return this.statusUpdater.timeout(this.intervalCheck.getInterval()) .updateStatus(instanceId) .onErrorResume((e) -> { log.warn("Unexpected error while updating status for {}", instanceId, e); return Mono.empty(); }) .doFinally((s) -> this.intervalCheck.markAsChecked(instanceId)); } @Override public void start() { super.start(); this.intervalCheck.start(); } @Override public void stop() { super.stop(); this.intervalCheck.stop(); } public void setInterval(Duration updateInterval) { this.intervalCheck.setInterval(updateInterval); } public void setLifetime(Duration statusLifetime) { this.intervalCheck.setMinRetention(statusLifetime); } } ================================================ FILE: spring-boot-admin-server/src/main/java/de/codecentric/boot/admin/server/services/StatusUpdater.java ================================================ /* * 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.server.services; import java.time.Duration; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.Map; import java.util.Objects; import java.util.logging.Level; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.core.ParameterizedTypeReference; import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatusCode; import org.springframework.http.MediaType; import org.springframework.web.reactive.function.client.ClientResponse; 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.Endpoint; import de.codecentric.boot.admin.server.domain.values.InstanceId; import de.codecentric.boot.admin.server.domain.values.StatusInfo; import de.codecentric.boot.admin.server.web.client.InstanceWebClient; import static java.util.Collections.emptyMap; /** * The StatusUpdater is responsible for updating the status of all or a single application * querying the healthUrl. * * @author Johannes Edmeier */ @Slf4j @RequiredArgsConstructor public class StatusUpdater { private static final ParameterizedTypeReference> RESPONSE_TYPE = new ParameterizedTypeReference<>() { }; private final InstanceRepository repository; private final InstanceWebClient instanceWebClient; private final ApiMediaTypeHandler apiMediaTypeHandler; private Duration timeout = Duration.ofSeconds(10); public StatusUpdater timeout(Duration timeout) { this.timeout = timeout; return this; } public Mono updateStatus(InstanceId id) { return this.repository.computeIfPresent(id, (key, instance) -> this.doUpdateStatus(instance)).then(); } protected Mono doUpdateStatus(Instance instance) { if (!instance.isRegistered()) { return Mono.empty(); } log.debug("Update status for {}", instance); return this.instanceWebClient.instance(instance) .get() .uri(Endpoint.HEALTH) .exchangeToMono(this::convertStatusInfo) .log(log.getName(), Level.FINEST) .timeout(getTimeoutWithMargin()) .doOnError((ex) -> logError(instance, ex)) .onErrorResume(this::handleError) .map(instance::withStatusInfo); } /* * return a timeout less than the given one to prevent backdrops in concurrent get * request. This prevents flakiness of health checks. */ private Duration getTimeoutWithMargin() { return this.timeout.minusSeconds(1).abs(); } protected Mono convertStatusInfo(ClientResponse response) { boolean hasCompatibleContentType = response.headers() .contentType() .filter((mt) -> mt.isCompatibleWith(MediaType.APPLICATION_JSON) || this.apiMediaTypeHandler.isApiMediaType(mt)) .isPresent(); StatusInfo statusInfoFromStatus = this.getStatusInfoFromStatus(response.statusCode(), emptyMap()); if (hasCompatibleContentType) { return response.bodyToMono(RESPONSE_TYPE).map((body) -> { if (body.get("status") instanceof String) { return StatusInfo.from(body); } return getStatusInfoFromStatus(response.statusCode(), body); }).defaultIfEmpty(statusInfoFromStatus); } return response.releaseBody().then(Mono.just(statusInfoFromStatus)); } @SuppressWarnings("unchecked") protected StatusInfo getStatusInfoFromStatus(HttpStatusCode httpStatus, Map body) { if (httpStatus.is2xxSuccessful()) { return StatusInfo.ofUp(); } Map details = new LinkedHashMap<>(); details.put("status", httpStatus.value()); details.put("error", Objects.requireNonNull(HttpStatus.resolve(httpStatus.value())).getReasonPhrase()); if (body.get("details") instanceof Map) { details.putAll((Map) body.get("details")); } else { details.putAll(body); } return StatusInfo.ofDown(details); } protected 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)); } protected void logError(Instance instance, Throwable ex) { if (instance.getStatusInfo().isOffline()) { log.debug("Couldn't retrieve status for {}", instance, ex); } else { log.info("Couldn't retrieve status for {}", instance, ex); } } } ================================================ FILE: spring-boot-admin-server/src/main/java/de/codecentric/boot/admin/server/services/endpoints/ChainingStrategy.java ================================================ /* * 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.server.services.endpoints; import org.springframework.util.Assert; import reactor.core.publisher.Mono; import de.codecentric.boot.admin.server.domain.entities.Instance; import de.codecentric.boot.admin.server.domain.values.Endpoints; public class ChainingStrategy implements EndpointDetectionStrategy { private final EndpointDetectionStrategy[] delegates; public ChainingStrategy(EndpointDetectionStrategy... delegates) { Assert.notNull(delegates, "'delegates' must not be null."); Assert.noNullElements(delegates, "'delegates' must not contain null."); this.delegates = delegates; } @Override public Mono detectEndpoints(Instance instance) { Mono result = Mono.empty(); for (EndpointDetectionStrategy delegate : delegates) { result = result.switchIfEmpty(delegate.detectEndpoints(instance)); } return result.switchIfEmpty(Mono.just(Endpoints.empty())); } } ================================================ FILE: spring-boot-admin-server/src/main/java/de/codecentric/boot/admin/server/services/endpoints/EndpointDetectionStrategy.java ================================================ /* * 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.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); } ================================================ FILE: spring-boot-admin-server/src/main/java/de/codecentric/boot/admin/server/services/endpoints/ProbeEndpointsStrategy.java ================================================ /* * 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.server.services.endpoints; import java.net.URI; import java.util.Arrays; import java.util.List; import java.util.Map; import java.util.function.Function; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.util.Assert; import org.springframework.web.reactive.function.client.ClientResponse; import org.springframework.web.util.UriComponentsBuilder; import reactor.core.publisher.Flux; 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.domain.values.InstanceId; import de.codecentric.boot.admin.server.web.client.InstanceWebClient; import static java.util.Comparator.comparingInt; import static java.util.stream.Collectors.groupingBy; public class ProbeEndpointsStrategy implements EndpointDetectionStrategy { private static final Logger log = LoggerFactory.getLogger(ProbeEndpointsStrategy.class); private final List endpoints; private final InstanceWebClient instanceWebClient; public ProbeEndpointsStrategy(InstanceWebClient instanceWebClient, String[] endpoints) { Assert.notNull(endpoints, "'endpoints' must not be null."); Assert.noNullElements(endpoints, "'endpoints' must not contain null."); this.endpoints = Arrays.stream(endpoints).map(EndpointDefinition::create).toList(); this.instanceWebClient = instanceWebClient; } @Override public Mono detectEndpoints(Instance instance) { if (instance.getRegistration().getManagementUrl() == null) { log.debug("Endpoint probe for instance {} omitted. No management-url registered.", instance.getId()); return Mono.empty(); } return Flux.fromIterable(this.endpoints) .flatMap((endpoint) -> detectEndpoint(instance, endpoint)) .collectList() .flatMap(this::convert); } protected Mono detectEndpoint(Instance instance, EndpointDefinition endpoint) { Assert.notNull(instance.getRegistration().getManagementUrl(), "managementUrl must not be null"); URI uri = UriComponentsBuilder.fromUriString(instance.getRegistration().getManagementUrl()) .path("/") .path(endpoint.path()) .build() .toUri(); return this.instanceWebClient.instance(instance) .options() .uri(uri) .exchangeToMono(this.convert(instance.getId(), endpoint, uri)) .onErrorResume((e) -> { log.warn("Endpoint probe for instance {} on endpoint '{}' failed: {}", instance.getId(), uri, e.getMessage()); log.debug("Endpoint probe for instance {} on endpoint '{}' failed.", instance.getId(), uri, e); return Mono.empty(); }); } protected Function> convert(InstanceId instanceId, EndpointDefinition endpointDefinition, URI uri) { return (response) -> { Mono endpoint = Mono.empty(); if (response.statusCode().is2xxSuccessful()) { endpoint = Mono.just(DetectedEndpoint.of(endpointDefinition, uri.toString())); log.debug("Endpoint probe for instance {} on endpoint '{}' successful.", instanceId, uri); } else { log.debug("Endpoint probe for instance {} on endpoint '{}' failed with status {}.", instanceId, uri, response.statusCode().value()); } return response.releaseBody().then(endpoint); }; } protected Mono convert(List endpoints) { if (endpoints.isEmpty()) { return Mono.empty(); } Map> endpointsById = endpoints.stream() .collect(groupingBy((e) -> e.definition().id())); List result = endpointsById.values().stream().map((endpointList) -> { endpointList.sort(comparingInt((e) -> this.endpoints.indexOf(e.definition()))); if (endpointList.size() > 1) { log.warn("Duplicate endpoints for id '{}' detected. Omitting: {}", endpointList.get(0).definition().id(), endpointList.subList(1, endpointList.size())); } return endpointList.get(0).endpoint(); }).toList(); return Mono.just(Endpoints.of(result)); } protected record DetectedEndpoint(EndpointDefinition definition, Endpoint endpoint) { private static DetectedEndpoint of(EndpointDefinition endpointDefinition, String url) { return new DetectedEndpoint(endpointDefinition, Endpoint.of(endpointDefinition.id(), url)); } } protected record EndpointDefinition(String id, String path) { private static EndpointDefinition create(String idWithPath) { int idxDelimiter = idWithPath.indexOf(':'); if (idxDelimiter < 0) { return new EndpointDefinition(idWithPath, idWithPath); } else { return new EndpointDefinition(idWithPath.substring(0, idxDelimiter), idWithPath.substring(idxDelimiter + 1)); } } } } ================================================ FILE: spring-boot-admin-server/src/main/java/de/codecentric/boot/admin/server/services/endpoints/QueryIndexEndpointStrategy.java ================================================ /* * 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.server.services.endpoints; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.function.Function; import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonProperty; import lombok.Data; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.web.reactive.function.client.ClientResponse; 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.domain.values.InstanceId; import de.codecentric.boot.admin.server.domain.values.Registration; import de.codecentric.boot.admin.server.services.ApiMediaTypeHandler; import de.codecentric.boot.admin.server.web.client.InstanceWebClient; public class QueryIndexEndpointStrategy implements EndpointDetectionStrategy { private static final Logger log = LoggerFactory.getLogger(QueryIndexEndpointStrategy.class); private final InstanceWebClient instanceWebClient; private final ApiMediaTypeHandler apiMediaTypeHandler; public QueryIndexEndpointStrategy(InstanceWebClient instanceWebClient, ApiMediaTypeHandler apiMediaTypeHandler) { this.instanceWebClient = instanceWebClient; this.apiMediaTypeHandler = apiMediaTypeHandler; } @Override public Mono detectEndpoints(Instance instance) { Registration registration = instance.getRegistration(); String managementUrl = registration.getManagementUrl(); if (managementUrl == null || Objects.equals(registration.getServiceUrl(), managementUrl)) { log.debug("Querying actuator-index for instance {} omitted.", instance.getId()); return Mono.empty(); } return this.instanceWebClient.instance(instance) .get() .uri(managementUrl) .exchangeToMono(this.convert(instance, managementUrl)) .onErrorResume((e) -> { log.warn("Querying actuator-index for instance {} on '{}' failed: {}", instance.getId(), managementUrl, e.getMessage()); log.debug("Querying actuator-index for instance {} on '{}' failed.", instance.getId(), managementUrl, e); return Mono.empty(); }); } protected Function> convert(Instance instance, String managementUrl) { return (response) -> { if (!response.statusCode().is2xxSuccessful()) { log.debug("Querying actuator-index for instance {} on '{}' failed with status {}.", instance.getId(), managementUrl, response.statusCode().value()); return response.releaseBody().then(Mono.empty()); } if (response.headers().contentType().filter(this.apiMediaTypeHandler::isApiMediaType).isEmpty()) { log.debug("Querying actuator-index for instance {} on '{}' failed with incompatible Content-Type '{}'.", instance.getId(), managementUrl, response.headers().contentType().map(Objects::toString).orElse("(missing)")); return response.releaseBody().then(Mono.empty()); } log.debug("Querying actuator-index for instance {} on '{}' successful.", instance.getId(), managementUrl); return response.bodyToMono(Response.class) .flatMap(this::convertResponse) .map(this.alignWithManagementUrl(instance.getId(), managementUrl)); }; } protected Function alignWithManagementUrl(InstanceId instanceId, String managementUrl) { return (endpoints) -> { if (!managementUrl.startsWith("https:")) { return endpoints; } if (endpoints.stream().noneMatch((e) -> e.getUrl().startsWith("http:"))) { return endpoints; } log.warn( "Endpoints for instance {} queried from {} are falsely using http. Rewritten to https. Consider configuring this instance to use 'server.forward-headers-strategy=native'.", instanceId, managementUrl); return Endpoints.of(endpoints.stream() .map((e) -> Endpoint.of(e.getId(), e.getUrl().replaceFirst("http:", "https:"))) .toList()); }; } protected Mono convertResponse(Response response) { List endpoints = response.getLinks() .entrySet() .stream() .filter((e) -> !e.getKey().equals("self") && !e.getValue().isTemplated()) .map((e) -> Endpoint.of(e.getKey(), e.getValue().getHref())) .toList(); return endpoints.isEmpty() ? Mono.empty() : Mono.just(Endpoints.of(endpoints)); } @Data protected static class Response { @JsonProperty("_links") private Map links = new HashMap<>(); @Data protected static class EndpointRef { private final String href; private final boolean templated; @JsonCreator EndpointRef(@JsonProperty("href") String href, @JsonProperty("templated") boolean templated) { this.href = href; this.templated = templated; } } } } ================================================ FILE: spring-boot-admin-server/src/main/java/de/codecentric/boot/admin/server/services/endpoints/package-info.java ================================================ /* * 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. */ // Spring Boot Admin Server - endpoints package. @NullMarked package de.codecentric.boot.admin.server.services.endpoints; import org.jspecify.annotations.NullMarked; ================================================ FILE: spring-boot-admin-server/src/main/java/de/codecentric/boot/admin/server/services/package-info.java ================================================ /* * 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. */ // Spring Boot Admin Server - services package. @NullMarked package de.codecentric.boot.admin.server.services; import org.jspecify.annotations.NullMarked; ================================================ FILE: spring-boot-admin-server/src/main/java/de/codecentric/boot/admin/server/utils/jackson/AdminServerModule.java ================================================ /* * 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.server.utils.jackson; import tools.jackson.databind.module.SimpleModule; import de.codecentric.boot.admin.server.domain.events.InstanceDeregisteredEvent; import de.codecentric.boot.admin.server.domain.events.InstanceEndpointsDetectedEvent; import de.codecentric.boot.admin.server.domain.events.InstanceEvent; import de.codecentric.boot.admin.server.domain.events.InstanceInfoChangedEvent; import de.codecentric.boot.admin.server.domain.events.InstanceRegisteredEvent; import de.codecentric.boot.admin.server.domain.events.InstanceRegistrationUpdatedEvent; import de.codecentric.boot.admin.server.domain.events.InstanceStatusChangedEvent; import de.codecentric.boot.admin.server.domain.values.BuildVersion; import de.codecentric.boot.admin.server.domain.values.Endpoint; import de.codecentric.boot.admin.server.domain.values.Endpoints; import de.codecentric.boot.admin.server.domain.values.InstanceId; import de.codecentric.boot.admin.server.domain.values.Registration; import de.codecentric.boot.admin.server.domain.values.StatusInfo; import de.codecentric.boot.admin.server.domain.values.Tags; /** * Jackson module for Spring Boot Admin Server.
* To use this module, add it to your JsonMapper builder:

 *     JsonMapper.Builder builder = JsonMapper.builder();
 *     builder.addModule(new AdminServerModule(...));
 *     return builder.build();
 * 
* * @author Stefan Rempfer */ public class AdminServerModule extends SimpleModule { /** * Construct the module with a pattern for registration metadata keys. The values of * the matched metadata keys will be sanitized before serializing to json. * @param metadataKeyPatterns pattern for metadata keys which should be sanitized */ public AdminServerModule(String[] metadataKeyPatterns) { super(AdminServerModule.class.getName()); addDeserializer(Registration.class, new RegistrationDeserializer()); setSerializerModifier(new RegistrationBeanSerializerModifier(new SanitizingMapSerializer(metadataKeyPatterns))); setMixInAnnotation(InstanceDeregisteredEvent.class, InstanceDeregisteredEventMixin.class); setMixInAnnotation(InstanceEndpointsDetectedEvent.class, InstanceEndpointsDetectedEventMixin.class); setMixInAnnotation(InstanceEvent.class, InstanceEventMixin.class); setMixInAnnotation(InstanceInfoChangedEvent.class, InstanceInfoChangedEventMixin.class); setMixInAnnotation(InstanceRegisteredEvent.class, InstanceRegisteredEventMixin.class); setMixInAnnotation(InstanceRegistrationUpdatedEvent.class, InstanceRegistrationUpdatedEventMixin.class); setMixInAnnotation(InstanceStatusChangedEvent.class, InstanceStatusChangedEventMixin.class); setMixInAnnotation(BuildVersion.class, BuildVersionMixin.class); setMixInAnnotation(Endpoint.class, EndpointMixin.class); setMixInAnnotation(Endpoints.class, EndpointsMixin.class); setMixInAnnotation(InstanceId.class, InstanceIdMixin.class); setMixInAnnotation(StatusInfo.class, StatusInfoMixin.class); setMixInAnnotation(Tags.class, TagsMixin.class); } } ================================================ FILE: spring-boot-admin-server/src/main/java/de/codecentric/boot/admin/server/utils/jackson/BuildVersionMixin.java ================================================ /* * 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.server.utils.jackson; import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonValue; import de.codecentric.boot.admin.server.domain.values.BuildVersion; /** * Jackson Mixin class helps in serialize/deserialize {@link BuildVersion}. * * @author Stefan Rempfer */ public abstract class BuildVersionMixin { @JsonCreator public static BuildVersion valueOf(String s) { return BuildVersion.valueOf(s); } @JsonValue public abstract String getValue(); } ================================================ FILE: spring-boot-admin-server/src/main/java/de/codecentric/boot/admin/server/utils/jackson/EndpointMixin.java ================================================ /* * 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.server.utils.jackson; import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonProperty; import de.codecentric.boot.admin.server.domain.values.Endpoint; /** * Jackson Mixin class helps in serialize/deserialize {@link Endpoint}. * * @author Stefan Rempfer */ public abstract class EndpointMixin { @JsonCreator public static Endpoint of(@JsonProperty("id") String id, @JsonProperty("url") String url) { return Endpoint.of(id, url); } } ================================================ FILE: spring-boot-admin-server/src/main/java/de/codecentric/boot/admin/server/utils/jackson/EndpointsMixin.java ================================================ /* * 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.server.utils.jackson; import java.util.Collection; import com.fasterxml.jackson.annotation.JsonCreator; import org.jspecify.annotations.Nullable; import de.codecentric.boot.admin.server.domain.values.Endpoint; import de.codecentric.boot.admin.server.domain.values.Endpoints; /** * Jackson Mixin class helps in serialize/deserialize {@link Endpoints}. * * @author Stefan Rempfer */ public abstract class EndpointsMixin { @JsonCreator public static Endpoints of(@Nullable Collection endpoints) { return Endpoints.of(endpoints); } } ================================================ FILE: spring-boot-admin-server/src/main/java/de/codecentric/boot/admin/server/utils/jackson/InfoMixin.java ================================================ /* * 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.server.utils.jackson; import java.util.LinkedHashMap; import java.util.Map; import com.fasterxml.jackson.annotation.JsonAnyGetter; import com.fasterxml.jackson.annotation.JsonAnySetter; import tools.jackson.databind.annotation.JsonDeserialize; import de.codecentric.boot.admin.server.domain.values.Info; /** * Jackson Mixin class helps in serialize/deserialize {@link Info}. * * @author Stefan Rempfer */ @JsonDeserialize(builder = InfoMixin.Builder.class) public abstract class InfoMixin { @JsonAnyGetter public abstract Map getValues(); public static class Builder { private final Map values = new LinkedHashMap<>(); @JsonAnySetter public Builder set(String key, Object value) { this.values.put(key, value); return this; } public Info build() { return Info.from(this.values); } } } ================================================ FILE: spring-boot-admin-server/src/main/java/de/codecentric/boot/admin/server/utils/jackson/InstanceDeregisteredEventMixin.java ================================================ /* * 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.server.utils.jackson; import java.time.Instant; import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonProperty; import de.codecentric.boot.admin.server.domain.events.InstanceDeregisteredEvent; import de.codecentric.boot.admin.server.domain.values.InstanceId; /** * Jackson Mixin class helps in serialize/deserialize {@link InstanceDeregisteredEvent}. * * @author Stefan Rempfer */ public abstract class InstanceDeregisteredEventMixin { @JsonCreator public InstanceDeregisteredEventMixin(@JsonProperty("instance") InstanceId instance, @JsonProperty("version") long version, @JsonProperty("timestamp") Instant timestamp) { } } ================================================ FILE: spring-boot-admin-server/src/main/java/de/codecentric/boot/admin/server/utils/jackson/InstanceEndpointsDetectedEventMixin.java ================================================ /* * 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.server.utils.jackson; import java.time.Instant; import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonProperty; import de.codecentric.boot.admin.server.domain.events.InstanceEndpointsDetectedEvent; import de.codecentric.boot.admin.server.domain.values.Endpoints; import de.codecentric.boot.admin.server.domain.values.InstanceId; /** * Jackson Mixin class helps in serialize/deserialize * {@link InstanceEndpointsDetectedEvent}. * * @author Stefan Rempfer */ public abstract class InstanceEndpointsDetectedEventMixin { @JsonCreator public InstanceEndpointsDetectedEventMixin(@JsonProperty("instance") InstanceId instance, @JsonProperty("version") long version, @JsonProperty("timestamp") Instant timestamp, @JsonProperty("endpoints") Endpoints endpoints) { } } ================================================ FILE: spring-boot-admin-server/src/main/java/de/codecentric/boot/admin/server/utils/jackson/InstanceEventMixin.java ================================================ /* * 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.server.utils.jackson; import com.fasterxml.jackson.annotation.JsonSubTypes; import com.fasterxml.jackson.annotation.JsonTypeInfo; import de.codecentric.boot.admin.server.domain.events.InstanceDeregisteredEvent; import de.codecentric.boot.admin.server.domain.events.InstanceEndpointsDetectedEvent; import de.codecentric.boot.admin.server.domain.events.InstanceEvent; import de.codecentric.boot.admin.server.domain.events.InstanceInfoChangedEvent; import de.codecentric.boot.admin.server.domain.events.InstanceRegisteredEvent; import de.codecentric.boot.admin.server.domain.events.InstanceRegistrationUpdatedEvent; import de.codecentric.boot.admin.server.domain.events.InstanceStatusChangedEvent; /** * Jackson Mixin class helps in serialize/deserialize {@link InstanceEvent}s. * * @author Stefan Rempfer */ @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.EXISTING_PROPERTY, property = "type") @JsonSubTypes({ @JsonSubTypes.Type(value = InstanceEndpointsDetectedEvent.class, name = InstanceEndpointsDetectedEvent.TYPE), @JsonSubTypes.Type(value = InstanceRegistrationUpdatedEvent.class, name = InstanceRegistrationUpdatedEvent.TYPE), @JsonSubTypes.Type(value = InstanceInfoChangedEvent.class, name = InstanceInfoChangedEvent.TYPE), @JsonSubTypes.Type(value = InstanceDeregisteredEvent.class, name = InstanceDeregisteredEvent.TYPE), @JsonSubTypes.Type(value = InstanceRegisteredEvent.class, name = InstanceRegisteredEvent.TYPE), @JsonSubTypes.Type(value = InstanceStatusChangedEvent.class, name = InstanceStatusChangedEvent.TYPE) }) public abstract class InstanceEventMixin { } ================================================ FILE: spring-boot-admin-server/src/main/java/de/codecentric/boot/admin/server/utils/jackson/InstanceIdMixin.java ================================================ /* * 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.server.utils.jackson; import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonValue; import de.codecentric.boot.admin.server.domain.values.InstanceId; /** * Jackson Mixin class helps in serialize/deserialize {@link InstanceId}. * * @author Stefan Rempfer */ public abstract class InstanceIdMixin { @JsonCreator public static InstanceId of(String value) { return InstanceId.of(value); } @JsonValue public abstract String getValue(); } ================================================ FILE: spring-boot-admin-server/src/main/java/de/codecentric/boot/admin/server/utils/jackson/InstanceInfoChangedEventMixin.java ================================================ /* * 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.server.utils.jackson; import java.time.Instant; import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonProperty; import de.codecentric.boot.admin.server.domain.events.InstanceInfoChangedEvent; import de.codecentric.boot.admin.server.domain.values.Info; import de.codecentric.boot.admin.server.domain.values.InstanceId; /** * Jackson Mixin class helps in serialize/deserialize {@link InstanceInfoChangedEvent}. * * @author Stefan Rempfer */ public abstract class InstanceInfoChangedEventMixin { @JsonCreator public InstanceInfoChangedEventMixin(@JsonProperty("instance") InstanceId instance, @JsonProperty("version") long version, @JsonProperty("timestamp") Instant timestamp, @JsonProperty("info") Info info) { } } ================================================ FILE: spring-boot-admin-server/src/main/java/de/codecentric/boot/admin/server/utils/jackson/InstanceRegisteredEventMixin.java ================================================ /* * 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.server.utils.jackson; import java.time.Instant; import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonProperty; import de.codecentric.boot.admin.server.domain.events.InstanceRegisteredEvent; import de.codecentric.boot.admin.server.domain.values.InstanceId; import de.codecentric.boot.admin.server.domain.values.Registration; /** * Jackson Mixin class helps in serialize/deserialize {@link InstanceRegisteredEvent}. * * @author Stefan Rempfer */ public abstract class InstanceRegisteredEventMixin { @JsonCreator public InstanceRegisteredEventMixin(@JsonProperty("instance") InstanceId instance, @JsonProperty("version") long version, @JsonProperty("timestamp") Instant timestamp, @JsonProperty("registration") Registration registration) { } } ================================================ FILE: spring-boot-admin-server/src/main/java/de/codecentric/boot/admin/server/utils/jackson/InstanceRegistrationUpdatedEventMixin.java ================================================ /* * 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.server.utils.jackson; import java.time.Instant; import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonProperty; import de.codecentric.boot.admin.server.domain.events.InstanceRegistrationUpdatedEvent; import de.codecentric.boot.admin.server.domain.values.InstanceId; import de.codecentric.boot.admin.server.domain.values.Registration; /** * Jackson Mixin class helps in serialize/deserialize * {@link InstanceRegistrationUpdatedEvent}. * * @author Stefan Rempfer */ public abstract class InstanceRegistrationUpdatedEventMixin { @JsonCreator public InstanceRegistrationUpdatedEventMixin(@JsonProperty("instance") InstanceId instance, @JsonProperty("version") long version, @JsonProperty("timestamp") Instant timestamp, @JsonProperty("registration") Registration registration) { } } ================================================ FILE: spring-boot-admin-server/src/main/java/de/codecentric/boot/admin/server/utils/jackson/InstanceStatusChangedEventMixin.java ================================================ /* * 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.server.utils.jackson; import java.time.Instant; import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonProperty; import de.codecentric.boot.admin.server.domain.events.InstanceStatusChangedEvent; import de.codecentric.boot.admin.server.domain.values.InstanceId; import de.codecentric.boot.admin.server.domain.values.StatusInfo; /** * Jackson Mixin class helps in serialize/deserialize {@link InstanceStatusChangedEvent}. * * @author Stefan Rempfer */ public abstract class InstanceStatusChangedEventMixin { @JsonCreator public InstanceStatusChangedEventMixin(@JsonProperty("instance") InstanceId instance, @JsonProperty("version") long version, @JsonProperty("timestamp") Instant timestamp, @JsonProperty("statusInfo") StatusInfo statusInfo) { } } ================================================ FILE: spring-boot-admin-server/src/main/java/de/codecentric/boot/admin/server/utils/jackson/RegistrationBeanSerializerModifier.java ================================================ /* * 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.server.utils.jackson; import java.util.List; import tools.jackson.databind.BeanDescription; import tools.jackson.databind.SerializationConfig; import tools.jackson.databind.ValueSerializer; import tools.jackson.databind.ser.BeanPropertyWriter; import tools.jackson.databind.ser.ValueSerializerModifier; import de.codecentric.boot.admin.server.domain.values.Registration; public class RegistrationBeanSerializerModifier extends ValueSerializerModifier { private final ValueSerializer metadataSerializer; @SuppressWarnings("unchecked") public RegistrationBeanSerializerModifier(SanitizingMapSerializer metadataSerializer) { this.metadataSerializer = (ValueSerializer) (ValueSerializer) metadataSerializer; } @Override public List changeProperties(SerializationConfig config, BeanDescription.Supplier beanDesc, List beanProperties) { if (!Registration.class.isAssignableFrom(beanDesc.getBeanClass())) { return beanProperties; } beanProperties.stream() .filter((beanProperty) -> "metadata".equals(beanProperty.getName())) .forEach((beanProperty) -> beanProperty.assignSerializer(metadataSerializer)); return beanProperties; } } ================================================ FILE: spring-boot-admin-server/src/main/java/de/codecentric/boot/admin/server/utils/jackson/RegistrationDeserializer.java ================================================ /* * 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.server.utils.jackson; import tools.jackson.core.JsonParser; import tools.jackson.databind.DeserializationContext; import tools.jackson.databind.JsonNode; import tools.jackson.databind.deser.std.StdDeserializer; import de.codecentric.boot.admin.server.domain.values.Registration; public class RegistrationDeserializer extends StdDeserializer { public RegistrationDeserializer() { super(Registration.class); } @Override public Registration deserialize(JsonParser p, DeserializationContext ctxt) { JsonNode node = p.readValueAsTree(); Registration.Builder builder = Registration.builder(); builder.name(firstNonNullAsString(node, "name")); if (node.hasNonNull("url")) { String url = firstNonNullAsString(node, "url"); builder.healthUrl(url.replaceFirst("/+$", "") + "/health").managementUrl(url); } else { builder.healthUrl(firstNonNullAsString(node, "healthUrl", "health_url")); builder.managementUrl(firstNonNullAsString(node, "managementUrl", "management_url")); builder.serviceUrl(firstNonNullAsString(node, "serviceUrl", "service_url")); } if (node.has("metadata")) { node.get("metadata") .properties() .forEach((entry) -> builder.metadata(entry.getKey(), entry.getValue().asString())); } builder.source(firstNonNullAsString(node, "source")); return builder.build(); } private String firstNonNullAsString(JsonNode node, String... fieldNames) { for (String fieldName : fieldNames) { if (node.hasNonNull(fieldName)) { return node.get(fieldName).asString(); } } return null; } } ================================================ FILE: spring-boot-admin-server/src/main/java/de/codecentric/boot/admin/server/utils/jackson/SanitizingMapSerializer.java ================================================ /* * 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.server.utils.jackson; import java.util.Arrays; import java.util.Map; import java.util.regex.Pattern; import org.jspecify.annotations.Nullable; import tools.jackson.core.JsonGenerator; import tools.jackson.databind.SerializationContext; import tools.jackson.databind.ser.std.StdSerializer; public class SanitizingMapSerializer extends StdSerializer> { private final Pattern[] keysToSanitize; @SuppressWarnings("unchecked") public SanitizingMapSerializer(String[] patterns) { super((Class>) (Class) Map.class); this.keysToSanitize = createPatterns(patterns); } private static Pattern[] createPatterns(String... keys) { return Arrays.stream(keys).map((key) -> Pattern.compile(key, Pattern.CASE_INSENSITIVE)).toArray(Pattern[]::new); } @Override public void serialize(Map value, JsonGenerator gen, SerializationContext provider) { gen.writeStartObject(); for (Map.Entry entry : value.entrySet()) { gen.writeStringProperty(entry.getKey(), sanitize(entry.getKey(), entry.getValue())); } gen.writeEndObject(); } @Nullable private String sanitize(String key, @Nullable String value) { if (value == null) { return null; } boolean matchesAnyPattern = Arrays.stream(this.keysToSanitize) .anyMatch((pattern) -> pattern.matcher(key).matches()); return matchesAnyPattern ? "******" : value; } } ================================================ FILE: spring-boot-admin-server/src/main/java/de/codecentric/boot/admin/server/utils/jackson/StatusInfoMixin.java ================================================ /* * 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.server.utils.jackson; import java.util.Map; import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonProperty; import org.jspecify.annotations.Nullable; import de.codecentric.boot.admin.server.domain.values.StatusInfo; /** * Jackson Mixin class helps in serialize/deserialize {@link StatusInfo}. * * @author Stefan Rempfer */ public abstract class StatusInfoMixin { @JsonCreator public static StatusInfo valueOf(@JsonProperty("status") String statusCode, @JsonProperty("details") @Nullable Map details) { return StatusInfo.valueOf(statusCode, details); } @JsonIgnore public abstract boolean isUp(); @JsonIgnore public abstract boolean isOffline(); @JsonIgnore public abstract boolean isDown(); @JsonIgnore public abstract boolean isUnknown(); } ================================================ FILE: spring-boot-admin-server/src/main/java/de/codecentric/boot/admin/server/utils/jackson/TagsMixin.java ================================================ /* * 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.server.utils.jackson; import java.util.Map; import com.fasterxml.jackson.annotation.JsonAnyGetter; import com.fasterxml.jackson.annotation.JsonCreator; import de.codecentric.boot.admin.server.domain.values.Tags; /** * Jackson Mixin class helps in serialize/deserialize {@link Tags}. * * @author Stefan Rempfer */ public abstract class TagsMixin { @JsonCreator public static Tags from(Map map) { return Tags.from(map); } @JsonAnyGetter public abstract Map getValues(); } ================================================ FILE: spring-boot-admin-server/src/main/java/de/codecentric/boot/admin/server/utils/jackson/package-info.java ================================================ /* * 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. */ // Spring Boot Admin Server - jackson utils package. @NullMarked package de.codecentric.boot.admin.server.utils.jackson; import org.jspecify.annotations.NullMarked; ================================================ FILE: spring-boot-admin-server/src/main/java/de/codecentric/boot/admin/server/web/AdminController.java ================================================ /* * 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.server.web; import java.lang.annotation.Documented; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; /** * Indicates that the annotated class is a mvc controller used within spring boot admin. * * @author Johannes Edmeier */ @Target({ ElementType.TYPE }) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface AdminController { } ================================================ FILE: spring-boot-admin-server/src/main/java/de/codecentric/boot/admin/server/web/ApplicationsController.java ================================================ /* * Copyright 2014-2024 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.server.web; import java.time.Duration; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.context.ApplicationEventPublisher; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.http.codec.ServerSentEvent; import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.ResponseBody; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import de.codecentric.boot.admin.server.domain.entities.Application; import de.codecentric.boot.admin.server.services.ApplicationRegistry; import de.codecentric.boot.admin.server.web.client.RefreshInstancesEvent; /** * REST controller for controlling registration of managed instances. */ @AdminController @ResponseBody public class ApplicationsController { private static final Logger log = LoggerFactory.getLogger(ApplicationsController.class); private static final ServerSentEvent PING = ServerSentEvent.builder().comment("ping").build(); private static final Flux> PING_FLUX = Flux.interval(Duration.ZERO, Duration.ofSeconds(10L)) .map((tick) -> PING); private final ApplicationRegistry registry; private final ApplicationEventPublisher publisher; public ApplicationsController(ApplicationRegistry registry, ApplicationEventPublisher publisher) { this.registry = registry; this.publisher = publisher; } @GetMapping(path = "/applications", produces = MediaType.APPLICATION_JSON_VALUE) public Flux applications() { return registry.getApplications(); } @PostMapping(path = "/applications", produces = MediaType.APPLICATION_JSON_VALUE) public void refreshApplications() { publisher.publishEvent(new RefreshInstancesEvent(this)); } @GetMapping(path = "/applications/{name}", produces = MediaType.APPLICATION_JSON_VALUE) public Mono> application(@PathVariable("name") String name) { return registry.getApplication(name).map(ResponseEntity::ok).defaultIfEmpty(ResponseEntity.notFound().build()); } @GetMapping(path = "/applications", produces = MediaType.TEXT_EVENT_STREAM_VALUE) public Flux> applicationsStream() { return registry.getApplicationStream() .map((application) -> ServerSentEvent.builder(application).build()) .mergeWith(ping()); } @DeleteMapping(path = "/applications/{name}") public Mono> unregister(@PathVariable("name") String name) { log.debug("Unregister application with name '{}'", name); return registry.deregister(name) .collectList() .map((deregistered) -> !deregistered.isEmpty() ? ResponseEntity.noContent().build() : ResponseEntity.notFound().build()); } @SuppressWarnings("unchecked") private static Flux> ping() { return (Flux>) (Flux) PING_FLUX; } } ================================================ FILE: spring-boot-admin-server/src/main/java/de/codecentric/boot/admin/server/web/HttpHeaderFilter.java ================================================ /* * 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.server.web; import java.util.Arrays; import java.util.Map; import java.util.Set; import java.util.stream.Collectors; import java.util.stream.Stream; import org.springframework.http.HttpHeaders; import static java.util.stream.Collectors.toMap; /** * Returns a new HttpHeaders from the given one but omits the hop-by-hop headers and * specified headers. * * @author Johannes Edmeier */ public class HttpHeaderFilter { private static final String[] HOP_BY_HOP_HEADERS = new String[] { "Host", "Connection", "Keep-Alive", "Proxy-Authenticate", "Proxy-Authorization", "TE", "Trailer", "Transfer-Encoding", "Upgrade", "X-Application-Context" }; private final Set ignoredHeaders; public HttpHeaderFilter(Set ignoredHeaders) { this.ignoredHeaders = Stream.concat(ignoredHeaders.stream(), Arrays.stream(HOP_BY_HOP_HEADERS)) .map(String::toLowerCase) .collect(Collectors.toSet()); } public HttpHeaders filterHeaders(HttpHeaders headers) { HttpHeaders filtered = new HttpHeaders(); filtered.putAll(headers.headerSet() .stream() .filter((e) -> this.includeHeader(e.getKey())) .collect(toMap(Map.Entry::getKey, Map.Entry::getValue))); return filtered; } private boolean includeHeader(String header) { return !this.ignoredHeaders.contains(header.toLowerCase()); } } ================================================ FILE: spring-boot-admin-server/src/main/java/de/codecentric/boot/admin/server/web/InstanceWebProxy.java ================================================ /* * 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.server.web; import java.io.IOException; import java.net.URI; import java.util.List; import java.util.concurrent.TimeoutException; import java.util.function.Function; import com.fasterxml.jackson.annotation.JsonInclude; import io.netty.handler.timeout.ReadTimeoutException; import org.jspecify.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; import org.springframework.http.HttpStatus; import org.springframework.http.client.reactive.ClientHttpRequest; import org.springframework.web.reactive.function.BodyInserter; import org.springframework.web.reactive.function.client.ClientResponse; import org.springframework.web.reactive.function.client.ExchangeStrategies; import org.springframework.web.reactive.function.client.WebClient; import org.springframework.web.reactive.function.client.WebClientRequestException; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import de.codecentric.boot.admin.server.domain.entities.Instance; import de.codecentric.boot.admin.server.domain.values.InstanceId; import de.codecentric.boot.admin.server.web.client.InstanceWebClient; import de.codecentric.boot.admin.server.web.client.exception.ResolveEndpointException; import static org.springframework.http.HttpMethod.PATCH; import static org.springframework.http.HttpMethod.POST; import static org.springframework.http.HttpMethod.PUT; /** * Forwards a request to a single instances endpoint and will respond with: - 502 (Bad * Gateway) when any error occurs during the request - 503 (Service unavailable) when the * instance is not found - 504 (Gateway timeout) when the request exceeds the timeout * * @author Johannes Edmeier */ public class InstanceWebProxy { private static final Logger log = LoggerFactory.getLogger(InstanceWebProxy.class); private static final Instance NULL_INSTANCE = Instance.create(InstanceId.of("null")); private final InstanceWebClient instanceWebClient; private final ExchangeStrategies strategies = ExchangeStrategies.withDefaults(); public InstanceWebProxy(InstanceWebClient instanceWebClient) { this.instanceWebClient = instanceWebClient; } public Mono forward(Mono instanceMono, ForwardRequest forwardRequest, Function> responseHandler) { return instanceMono.defaultIfEmpty(NULL_INSTANCE).flatMap((instance) -> { if (!instance.equals(NULL_INSTANCE)) { return this.forward(instance, forwardRequest, responseHandler); } else { return Mono.defer(() -> responseHandler .apply(ClientResponse.create(HttpStatus.SERVICE_UNAVAILABLE, this.strategies).build())); } }); } public Flux forward(Flux instances, ForwardRequest forwardRequest) { return instances.flatMap((instance) -> this.forward(instance, forwardRequest, (clientResponse) -> { InstanceResponse.Builder response = InstanceResponse.builder() .instanceId(instance.getId()) .status(clientResponse.statusCode().value()) .contentType(String.join(", ", clientResponse.headers().header(HttpHeaders.CONTENT_TYPE))); return clientResponse.bodyToMono(String.class) .map(response::body) .defaultIfEmpty(response) .map(InstanceResponse.Builder::build); })); } private Mono forward(Instance instance, ForwardRequest forwardRequest, Function> responseHandler) { log.trace("Proxy-Request for instance {} with URL '{}'", instance.getId(), forwardRequest.getUri()); WebClient.RequestBodySpec bodySpec = this.instanceWebClient.instance(instance) .method(forwardRequest.getMethod()) .uri(forwardRequest.getUri()) .headers((h) -> h.addAll(forwardRequest.getHeaders())); WebClient.RequestHeadersSpec headersSpec = bodySpec; if (requiresBody(forwardRequest.getMethod())) { headersSpec = bodySpec.body(forwardRequest.getBody()); } return headersSpec.exchangeToMono(responseHandler).onErrorResume(ResolveEndpointException.class, (ex) -> { log.trace("No Endpoint found for Proxy-Request for instance {} with URL '{}'", instance.getId(), forwardRequest.getUri()); return responseHandler.apply(ClientResponse.create(HttpStatus.NOT_FOUND, this.strategies).build()); }).onErrorResume((ex) -> { Throwable cause = ex; if (ex instanceof WebClientRequestException) { cause = ex.getCause(); } if (cause instanceof ReadTimeoutException || cause instanceof TimeoutException) { log.trace("Timeout for Proxy-Request for instance {} with URL '{}'", instance.getId(), forwardRequest.getUri()); return responseHandler .apply(ClientResponse.create(HttpStatus.GATEWAY_TIMEOUT, this.strategies).build()); } if (cause instanceof IOException) { log.trace("Proxy-Request for instance {} with URL '{}' errored", instance.getId(), forwardRequest.getUri(), cause); return responseHandler.apply(ClientResponse.create(HttpStatus.BAD_GATEWAY, this.strategies).build()); } return Mono.error(ex); }); } private boolean requiresBody(HttpMethod method) { return List.of(PUT, POST, PATCH).contains(method); } @lombok.Data @lombok.Builder(builderClassName = "Builder") public static class InstanceResponse { private final InstanceId instanceId; private final int status; @Nullable @JsonInclude(JsonInclude.Include.NON_EMPTY) private final String body; @Nullable @JsonInclude(JsonInclude.Include.NON_EMPTY) private final String contentType; } @lombok.Data @lombok.Builder(builderClassName = "Builder") public static class ForwardRequest { private final URI uri; private final HttpMethod method; private final HttpHeaders headers; private final BodyInserter body; } } ================================================ FILE: spring-boot-admin-server/src/main/java/de/codecentric/boot/admin/server/web/InstancesController.java ================================================ /* * 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.server.web; import java.net.URI; import java.time.Duration; import java.util.Collections; import java.util.Map; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.http.codec.ServerSentEvent; import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.util.UriComponentsBuilder; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import de.codecentric.boot.admin.server.domain.entities.Instance; import de.codecentric.boot.admin.server.domain.events.InstanceEvent; import de.codecentric.boot.admin.server.domain.values.InstanceId; import de.codecentric.boot.admin.server.domain.values.Registration; import de.codecentric.boot.admin.server.eventstore.InstanceEventStore; import de.codecentric.boot.admin.server.services.InstanceRegistry; /** * REST controller for controlling registration of managed instances. */ @AdminController @ResponseBody public class InstancesController { private static final Logger LOGGER = LoggerFactory.getLogger(InstancesController.class); private static final ServerSentEvent PING = ServerSentEvent.builder().comment("ping").build(); private static final Flux> PING_FLUX = Flux.interval(Duration.ZERO, Duration.ofSeconds(10L)) .map((tick) -> PING); private final InstanceRegistry registry; private final InstanceEventStore eventStore; public InstancesController(InstanceRegistry registry, InstanceEventStore eventStore) { this.registry = registry; this.eventStore = eventStore; } /** * Register an instance. * @param registration registration info * @param builder the UriComponentsBuilder * @return the registered instance id; */ @PostMapping(path = "/instances", consumes = MediaType.APPLICATION_JSON_VALUE) public Mono>> register(@RequestBody Registration registration, UriComponentsBuilder builder) { Registration withSource = Registration.copyOf(registration).source("http-api").build(); LOGGER.debug("Register instance {}", withSource); return registry.register(withSource).map((id) -> { URI location = builder.replacePath("/instances/{id}").buildAndExpand(id).toUri(); return ResponseEntity.created(location).body(Collections.singletonMap("id", id)); }); } /** * List all registered instances with name * @param name the name to search for * @return application list */ @GetMapping(path = "/instances", produces = MediaType.APPLICATION_JSON_VALUE, params = "name") public Flux instances(@RequestParam("name") String name) { return registry.getInstances(name).filter(Instance::isRegistered); } /** * List all registered instances with name * @return application list */ @GetMapping(path = "/instances", produces = MediaType.APPLICATION_JSON_VALUE) public Flux instances() { LOGGER.debug("Deliver all registered instances"); return registry.getInstances().filter(Instance::isRegistered); } /** * Get a single instance. * @param id the application identifier. * @return the registered application. */ @GetMapping(path = "/instances/{id}", produces = MediaType.APPLICATION_JSON_VALUE) public Mono> instance(@PathVariable String id) { LOGGER.debug("Deliver registered instance with ID '{}'", id); return registry.getInstance(InstanceId.of(id)) .filter(Instance::isRegistered) .map(ResponseEntity::ok) .defaultIfEmpty(ResponseEntity.notFound().build()); } /** * Unregister an instance * @param id the instance id. * @return response indicating the success */ @DeleteMapping(path = "/instances/{id}") public Mono> unregister(@PathVariable String id) { LOGGER.debug("Unregister instance with ID '{}'", id); return registry.deregister(InstanceId.of(id)) .map((v) -> ResponseEntity.noContent().build()) .defaultIfEmpty(ResponseEntity.notFound().build()); } /** * Retrieve all instance events as a JSON array. Returns all events for all registered * instances. Useful for reconstructing application state or initializing the UI. * @return flux of {@link InstanceEvent} objects */ @GetMapping(path = "/instances/events", produces = MediaType.APPLICATION_JSON_VALUE) public Flux events() { return eventStore.findAll(); } /** * Stream all instance events as Server-Sent Events (SSE). Returns a continuous stream * of instance events for real-time monitoring and UI updates. * @return flux of {@link ServerSentEvent} containing {@link InstanceEvent} */ @GetMapping(path = "/instances/events", produces = MediaType.TEXT_EVENT_STREAM_VALUE) public Flux> eventStream() { return Flux.from(eventStore).map((event) -> ServerSentEvent.builder(event).build()).mergeWith(ping()); } /** * Stream events for a specific instance as Server-Sent Events (SSE). Streams events * for the instance identified by its ID. Each event is delivered as an SSE message. * @param id the instance ID * @return flux of {@link ServerSentEvent} containing {@link Instance} */ @GetMapping(path = "/instances/{id}", produces = MediaType.TEXT_EVENT_STREAM_VALUE) public Flux> instanceStream(@PathVariable String id) { return Flux.from(eventStore) .filter((event) -> event.getInstance().equals(InstanceId.of(id))) .flatMap((event) -> registry.getInstance(event.getInstance())) .map((event) -> ServerSentEvent.builder(event).build()) .mergeWith(ping()); } /** * Returns a periodic Server-Sent Event (SSE) comment-only ping every 10 seconds. *

* This method is used to keep SSE connections alive for all event stream endpoints in * Spring Boot Admin. The ping event is sent as a comment (": ping") and does not * contain any data payload.
* Why? Many proxies, firewalls, and browsers may close idle HTTP connections. * The ping event provides regular activity on the stream, ensuring the connection * remains open even when no instance events are emitted.
* Technical details: *

    *
  • Interval: 10 seconds
  • *
  • Format: SSE comment-only event
  • *
  • Applies to: All event stream endpoints (e.g., /instances/events, * /instances/{id} with Accept: text/event-stream)
  • *
*

* @param the type of event data (unused for ping) * @return flux of ServerSentEvent representing periodic ping comments */ @SuppressWarnings("unchecked") private static Flux> ping() { return (Flux>) (Flux) PING_FLUX; } } ================================================ FILE: spring-boot-admin-server/src/main/java/de/codecentric/boot/admin/server/web/PathUtils.java ================================================ /* * 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.server.web; import org.springframework.util.StringUtils; public final class PathUtils { private PathUtils() { } public static String normalizePath(String path) { if (!StringUtils.hasText(path)) { return path; } String normalizedPath = path; if (!normalizedPath.startsWith("/")) { normalizedPath = "/" + normalizedPath; } if (normalizedPath.endsWith("/")) { normalizedPath = normalizedPath.substring(0, normalizedPath.length() - 1); } if (normalizedPath.startsWith("//")) { normalizedPath = normalizedPath.substring(1); } return normalizedPath; } } ================================================ FILE: spring-boot-admin-server/src/main/java/de/codecentric/boot/admin/server/web/client/BasicAuthHttpHeaderProvider.java ================================================ /* * 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.server.web.client; import java.nio.charset.StandardCharsets; import java.util.Base64; import java.util.Collections; import java.util.Map; import org.jspecify.annotations.Nullable; import org.springframework.http.HttpHeaders; import org.springframework.util.StringUtils; import de.codecentric.boot.admin.server.domain.entities.Instance; /** * Provides Basic Auth headers for the {@link Instance} using the metadata for "user.name" * and "user.password". * * Other allowed key names: - "user-name" / "user-password" - "username" / "userpassword" * * @author Johannes Edmeier */ public class BasicAuthHttpHeaderProvider implements HttpHeadersProvider { private static final String[] USERNAME_KEYS = { "user.name", "user-name", "username" }; private static final String[] PASSWORD_KEYS = { "user.password", "user-password", "userpassword" }; @Nullable private final String defaultUserName; @Nullable private final String defaultPassword; private final Map serviceMap; public BasicAuthHttpHeaderProvider(@Nullable String defaultUserName, @Nullable String defaultPassword, Map serviceMap) { this.defaultUserName = defaultUserName; this.defaultPassword = defaultPassword; this.serviceMap = serviceMap; } public BasicAuthHttpHeaderProvider() { this(null, null, Collections.emptyMap()); } private static @Nullable String getMetadataValue(Instance instance, String[] keys) { Map metadata = instance.getRegistration().getMetadata(); for (String key : keys) { String value = metadata.get(key); if (value != null) { return value; } } return null; } private static String base64Encode(byte[] src) { if (src.length == 0) { return ""; } byte[] dest = Base64.getEncoder().encode(src); return new String(dest, StandardCharsets.UTF_8); } @Override public HttpHeaders getHeaders(Instance instance) { String username = getMetadataValue(instance, USERNAME_KEYS); String password = getMetadataValue(instance, PASSWORD_KEYS); if (!(StringUtils.hasText(username) && StringUtils.hasText(password))) { String registeredName = instance.getRegistration().getName(); InstanceCredentials credentials = this.serviceMap.get(registeredName); if (credentials != null) { username = credentials.getUserName(); password = credentials.getUserPassword(); } else { username = this.defaultUserName; password = this.defaultPassword; } } HttpHeaders headers = new HttpHeaders(); if (StringUtils.hasText(username) && StringUtils.hasText(password)) { headers.set(HttpHeaders.AUTHORIZATION, encode(username, password)); } return headers; } protected String encode(String username, String password) { String token = base64Encode((username + ":" + password).getBytes(StandardCharsets.UTF_8)); return "Basic " + token; } @lombok.Data @lombok.NoArgsConstructor @lombok.AllArgsConstructor public static class InstanceCredentials { /** * user name for this instance */ @lombok.NonNull private String userName; /** * user password for this instance */ @lombok.NonNull private String userPassword; } } ================================================ FILE: spring-boot-admin-server/src/main/java/de/codecentric/boot/admin/server/web/client/CloudFoundryHttpHeaderProvider.java ================================================ /* * 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.server.web.client; import org.springframework.http.HttpHeaders; import org.springframework.util.StringUtils; import de.codecentric.boot.admin.server.domain.entities.Instance; /** * Provides CloudFoundry related X-CF-APP-INSTANCE header for the {@link Instance} using * the metadata for "applicationId" and "instanceId". * * @author Tetsushi Awano */ public class CloudFoundryHttpHeaderProvider implements HttpHeadersProvider { @Override public HttpHeaders getHeaders(Instance instance) { String applicationId = instance.getRegistration().getMetadata().get("applicationId"); String instanceId = instance.getRegistration().getMetadata().get("instanceId"); if (StringUtils.hasText(applicationId) && StringUtils.hasText(instanceId)) { HttpHeaders headers = new HttpHeaders(); headers.set("X-CF-APP-INSTANCE", applicationId + ":" + instanceId); return headers; } return HttpHeaders.EMPTY; } } ================================================ FILE: spring-boot-admin-server/src/main/java/de/codecentric/boot/admin/server/web/client/CompositeHttpHeadersProvider.java ================================================ /* * 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.server.web.client; import java.util.Collection; import org.springframework.http.HttpHeaders; import de.codecentric.boot.admin.server.domain.entities.Instance; public class CompositeHttpHeadersProvider implements HttpHeadersProvider { private final Collection delegates; public CompositeHttpHeadersProvider(Collection delegates) { this.delegates = delegates; } @Override public HttpHeaders getHeaders(Instance instance) { HttpHeaders headers = new HttpHeaders(); delegates.forEach((delegate) -> headers.addAll(delegate.getHeaders(instance))); return headers; } } ================================================ FILE: spring-boot-admin-server/src/main/java/de/codecentric/boot/admin/server/web/client/HttpHeadersProvider.java ================================================ /* * 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.server.web.client; import org.springframework.http.HttpHeaders; import de.codecentric.boot.admin.server.domain.entities.Instance; /** * Is responsible to provide the {@link HttpHeaders} used to interact with the given * {@link Instance}. * * @author Johannes Edmeier */ public interface HttpHeadersProvider { HttpHeaders getHeaders(Instance instance); } ================================================ FILE: spring-boot-admin-server/src/main/java/de/codecentric/boot/admin/server/web/client/InstanceExchangeFilterFunction.java ================================================ /* * 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.server.web.client; import org.springframework.web.reactive.function.client.ClientRequest; import org.springframework.web.reactive.function.client.ClientResponse; import org.springframework.web.reactive.function.client.ExchangeFunction; import reactor.core.publisher.Mono; import de.codecentric.boot.admin.server.domain.entities.Instance; /** * Represents a function that filters an{@linkplain ExchangeFunction exchange function} * issued on a registered instance. * * @author Johannes Edmeier */ @FunctionalInterface public interface InstanceExchangeFilterFunction { Mono filter(Instance instance, ClientRequest request, ExchangeFunction next); } ================================================ FILE: spring-boot-admin-server/src/main/java/de/codecentric/boot/admin/server/web/client/InstanceExchangeFilterFunctions.java ================================================ /* * 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.server.web.client; import java.net.URI; import java.time.Duration; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.stream.Stream; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.boot.actuate.endpoint.ApiVersion; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; import org.springframework.http.MediaType; import org.springframework.util.CollectionUtils; import org.springframework.util.MultiValueMap; import org.springframework.web.reactive.function.client.ClientRequest; import org.springframework.web.reactive.function.client.ClientResponse; import org.springframework.web.util.UriComponents; import org.springframework.web.util.UriComponentsBuilder; import reactor.core.publisher.Mono; import de.codecentric.boot.admin.server.domain.values.Endpoint; import de.codecentric.boot.admin.server.domain.values.InstanceId; import de.codecentric.boot.admin.server.web.client.cookies.PerInstanceCookieStore; import de.codecentric.boot.admin.server.web.client.exception.ResolveEndpointException; import de.codecentric.boot.admin.server.web.client.reactive.ReactiveHttpHeadersProvider; import static java.util.Arrays.asList; import static java.util.Collections.singletonList; public final class InstanceExchangeFilterFunctions { public static final String ATTRIBUTE_ENDPOINT = "endpointId"; private static final Logger log = LoggerFactory.getLogger(InstanceExchangeFilterFunctions.class); private static final List DEFAULT_LOGFILE_ACCEPT_MEDIA_TYPES = singletonList(MediaType.TEXT_PLAIN); static final MediaType V1_ACTUATOR_JSON = MediaType.valueOf("application/vnd.spring-boot.actuator.v1+json"); private static final List DEFAULT_ACCEPT_MEDIA_TYPES = asList( new MediaType(ApiVersion.V3.getProducedMimeType()), new MediaType(ApiVersion.V2.getProducedMimeType()), V1_ACTUATOR_JSON, MediaType.APPLICATION_JSON); private InstanceExchangeFilterFunctions() { } public static InstanceExchangeFilterFunction addHeaders(HttpHeadersProvider httpHeadersProvider) { return (instance, request, next) -> { request = ClientRequest.from(request) .headers((headers) -> headers.addAll(httpHeadersProvider.getHeaders(instance))) .build(); return next.exchange(request); }; } public static InstanceExchangeFilterFunction addHeadersReactive(ReactiveHttpHeadersProvider httpHeadersProvider) { return (instance, request, next) -> httpHeadersProvider.getHeaders(instance).flatMap((httpHeaders) -> { ClientRequest requestWithAdditionalHeaders = ClientRequest.from(request) .headers((headers) -> headers.addAll(httpHeaders)) .build(); return next.exchange(requestWithAdditionalHeaders); }).switchIfEmpty(Mono.defer(() -> next.exchange(request))); } public static InstanceExchangeFilterFunction rewriteEndpointUrl() { return (instance, request, next) -> { if (request.url().isAbsolute()) { log.trace("Absolute URL '{}' for instance {} not rewritten", request.url(), instance.getId()); if (request.url().toString().equals(instance.getRegistration().getManagementUrl())) { request = ClientRequest.from(request) .attribute(ATTRIBUTE_ENDPOINT, Endpoint.ACTUATOR_INDEX) .build(); } return next.exchange(request); } UriComponents requestUrl = UriComponentsBuilder.fromUri(request.url()).build(); if (requestUrl.getPathSegments().isEmpty()) { return Mono.error(new ResolveEndpointException("No endpoint specified")); } String endpointId = requestUrl.getPathSegments().get(0); Optional endpoint = instance.getEndpoints().get(endpointId); if (endpoint.isEmpty()) { return Mono.error(new ResolveEndpointException("Endpoint '" + endpointId + "' not found")); } URI rewrittenUrl = rewriteUrl(requestUrl, endpoint.get().getUrl()); log.trace("URL '{}' for Endpoint {} of instance {} rewritten to {}", requestUrl, endpoint.get().getId(), instance.getId(), rewrittenUrl); request = ClientRequest.from(request) .attribute(ATTRIBUTE_ENDPOINT, endpoint.get().getId()) .url(rewrittenUrl) .build(); return next.exchange(request); }; } private static URI rewriteUrl(UriComponents oldUrl, String targetUrl) { String[] newPathSegments = oldUrl.getPathSegments() .subList(1, oldUrl.getPathSegments().size()) .toArray(new String[] {}); return UriComponentsBuilder.fromUriString(targetUrl) .pathSegment(newPathSegments) .query(oldUrl.getQuery()) .build(true) .toUri(); } public static InstanceExchangeFilterFunction convertLegacyEndpoints(List converters) { return (instance, request, next) -> { Mono clientResponse = next.exchange(request); Optional endpoint = request.attribute(ATTRIBUTE_ENDPOINT); if (endpoint.isEmpty()) { return clientResponse; } for (LegacyEndpointConverter converter : converters) { if (converter.canConvert(endpoint.get())) { return clientResponse.map((response) -> { if (isLegacyResponse(response)) { return convertLegacyResponse(converter, response); } return response; }); } } return clientResponse; }; } private static Boolean isLegacyResponse(ClientResponse response) { return response.headers() .contentType() .filter((t) -> V1_ACTUATOR_JSON.isCompatibleWith(t) || MediaType.APPLICATION_JSON.isCompatibleWith(t)) .isPresent(); } private static ClientResponse convertLegacyResponse(LegacyEndpointConverter converter, ClientResponse response) { return response.mutate().headers((headers) -> { headers.setContentType(MediaType.asMediaType(ApiVersion.LATEST.getProducedMimeType())); headers.remove(HttpHeaders.CONTENT_LENGTH); }).body(converter::convert).build(); } public static InstanceExchangeFilterFunction setDefaultAcceptHeader() { return (instance, request, next) -> { if (request.headers().getAccept().isEmpty()) { Boolean isRequestForLogfile = request.attribute(ATTRIBUTE_ENDPOINT) .map(Endpoint.LOGFILE::equals) .orElse(false); List acceptedHeaders = isRequestForLogfile ? DEFAULT_LOGFILE_ACCEPT_MEDIA_TYPES : DEFAULT_ACCEPT_MEDIA_TYPES; request = ClientRequest.from(request).headers((headers) -> headers.setAccept(acceptedHeaders)).build(); } return next.exchange(request); }; } public static InstanceExchangeFilterFunction retry(int defaultRetries, Map retriesPerEndpoint) { return (instance, request, next) -> { int retries = 0; if (!request.method().equals(HttpMethod.DELETE) && !request.method().equals(HttpMethod.PATCH) && !request.method().equals(HttpMethod.POST) && !request.method().equals(HttpMethod.PUT)) { retries = request.attribute(ATTRIBUTE_ENDPOINT).map(retriesPerEndpoint::get).orElse(defaultRetries); } return next.exchange(request).retry(retries); }; } public static InstanceExchangeFilterFunction timeout(Duration defaultTimeout, Map timeoutPerEndpoint) { return (instance, request, next) -> { Duration timeout = request.attribute(ATTRIBUTE_ENDPOINT) .map(timeoutPerEndpoint::get) .orElse(defaultTimeout); return next.exchange(request).timeout(timeout); }; } // Accept header is broken on /logfile. We need to add "*/*" for old clients // see https://github.com/spring-projects/spring-boot/issues/16188 public static InstanceExchangeFilterFunction logfileAcceptWorkaround() { return (instance, request, next) -> { if (request.attribute(ATTRIBUTE_ENDPOINT).map(Endpoint.LOGFILE::equals).orElse(false)) { List newAcceptHeaders = Stream .concat(request.headers().getAccept().stream(), Stream.of(MediaType.ALL)) .toList(); request = ClientRequest.from(request).headers((h) -> h.setAccept(newAcceptHeaders)).build(); } return next.exchange(request); }; } /** * Creates the {@link InstanceExchangeFilterFunction} that could handle cookies during * requests and responses to/from applications. * @param store the cookie store to use * @return the new filter function */ public static InstanceExchangeFilterFunction handleCookies(final PerInstanceCookieStore store) { return (instance, request, next) -> { // we need an absolute URL to be able to deal with cookies if (request.url().isAbsolute()) { return next.exchange(enrichRequestWithStoredCookies(instance.getId(), request, store)) .map((response) -> storeCookiesFromResponse(instance.getId(), request, response, store)); } return next.exchange(request); }; } private static ClientRequest enrichRequestWithStoredCookies(final InstanceId instId, final ClientRequest request, final PerInstanceCookieStore store) { final MultiValueMap storedCookies = store.get(instId, request.url(), request.headers().asMultiValueMap()); if (CollectionUtils.isEmpty(storedCookies)) { log.trace("No cookies found for request [url={}]", request.url()); return request; } log.trace("Cookies found for request [url={}]", request.url()); return ClientRequest.from(request).cookies((cm) -> cm.addAll(storedCookies)).build(); } private static ClientResponse storeCookiesFromResponse(final InstanceId instId, final ClientRequest request, final ClientResponse response, final PerInstanceCookieStore store) { final HttpHeaders headers = response.headers().asHttpHeaders(); log.trace("Searching for cookies in header values of response [url={},headerValues={}]", request.url(), headers); store.put(instId, request.url(), headers.asMultiValueMap()); return response; } } ================================================ FILE: spring-boot-admin-server/src/main/java/de/codecentric/boot/admin/server/web/client/InstanceWebClient.java ================================================ /* * 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.server.web.client; import java.util.ArrayList; import java.util.List; import java.util.function.Consumer; import org.springframework.web.reactive.function.client.ClientRequest; import org.springframework.web.reactive.function.client.ExchangeFilterFunction; 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.web.client.exception.ResolveInstanceException; public class InstanceWebClient { public static final String ATTRIBUTE_INSTANCE = "instance"; private final WebClient webClient; protected InstanceWebClient(WebClient webClient) { this.webClient = webClient; } public WebClient instance(Mono instance) { return this.webClient.mutate().filters((filters) -> filters.add(0, setInstance(instance))).build(); } public WebClient instance(Instance instance) { return this.instance(Mono.justOrEmpty(instance)); } public static InstanceWebClient.Builder builder() { return new InstanceWebClient.Builder(); } public static InstanceWebClient.Builder builder(WebClient.Builder webClient) { return new InstanceWebClient.Builder(webClient); } private static ExchangeFilterFunction setInstance(Mono instance) { return (request, next) -> instance .map((i) -> ClientRequest.from(request).attribute(ATTRIBUTE_INSTANCE, i).build()) .switchIfEmpty(Mono.error(() -> new ResolveInstanceException("Could not resolve Instance"))) .flatMap(next::exchange); } private static ExchangeFilterFunction toExchangeFilterFunction(InstanceExchangeFilterFunction filter) { return (request, next) -> request.attribute(ATTRIBUTE_INSTANCE) .filter(Instance.class::isInstance) .map(Instance.class::cast) .map((instance) -> filter.filter(instance, request, next)) .orElseGet(() -> next.exchange(request)); } public static class Builder { private List filters = new ArrayList<>(); private WebClient.Builder webClientBuilder; public Builder() { this(WebClient.builder()); } public Builder(WebClient.Builder webClientBuilder) { this.webClientBuilder = webClientBuilder; } protected Builder(Builder other) { this.filters = new ArrayList<>(other.filters); this.webClientBuilder = other.webClientBuilder.clone(); } public Builder filter(InstanceExchangeFilterFunction filter) { this.filters.add(filter); return this; } public Builder filters(Consumer> filtersConsumer) { filtersConsumer.accept(this.filters); return this; } public Builder webClient(WebClient.Builder builder) { this.webClientBuilder = builder; return this; } public Builder clone() { return new Builder(this); } public InstanceWebClient build() { this.filters.stream() .map(InstanceWebClient::toExchangeFilterFunction) .forEach(this.webClientBuilder::filter); return new InstanceWebClient(this.webClientBuilder.build()); } } } ================================================ FILE: spring-boot-admin-server/src/main/java/de/codecentric/boot/admin/server/web/client/InstanceWebClientCustomizer.java ================================================ /* * 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.server.web.client; /** * Callback interface that can be used to customize a {@link InstanceWebClient.Builder * InstanceWebClient.Builder} * * @author Johannes Edmeier */ @FunctionalInterface public interface InstanceWebClientCustomizer { void customize(InstanceWebClient.Builder instanceWebClientBuilder); } ================================================ FILE: spring-boot-admin-server/src/main/java/de/codecentric/boot/admin/server/web/client/LegacyEndpointConverter.java ================================================ /* * 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.server.web.client; import java.util.function.Function; import org.springframework.core.io.buffer.DataBuffer; import reactor.core.publisher.Flux; /** * @author Johannes Edmeier */ public class LegacyEndpointConverter { private final String endpointId; private final Function, Flux> converterFn; protected LegacyEndpointConverter(String endpointId, Function, Flux> converterFn) { this.endpointId = endpointId; this.converterFn = converterFn; } public boolean canConvert(Object endpointId) { return this.endpointId.equals(endpointId); } public Flux convert(Flux body) { return converterFn.apply(body); } } ================================================ FILE: spring-boot-admin-server/src/main/java/de/codecentric/boot/admin/server/web/client/LegacyEndpointConverters.java ================================================ /* * 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.server.web.client; import java.time.DateTimeException; import java.time.Instant; import java.time.OffsetDateTime; import java.time.format.DateTimeFormatter; import java.util.ArrayList; import java.util.Arrays; import java.util.Date; import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.function.Function; import java.util.stream.Collectors; import org.jspecify.annotations.Nullable; import org.springframework.core.ParameterizedTypeReference; import org.springframework.core.ResolvableType; import org.springframework.core.io.buffer.DataBuffer; import org.springframework.core.io.buffer.DefaultDataBufferFactory; import org.springframework.http.codec.json.JacksonJsonDecoder; import org.springframework.http.codec.json.JacksonJsonEncoder; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import tools.jackson.databind.DeserializationFeature; import tools.jackson.databind.cfg.DateTimeFeature; import tools.jackson.databind.json.JsonMapper; import de.codecentric.boot.admin.server.domain.values.Endpoint; import static java.util.Arrays.asList; import static java.util.Collections.emptyList; import static java.util.Collections.emptySet; import static java.util.Collections.singletonList; import static java.util.Collections.singletonMap; import static java.util.function.Function.identity; import static java.util.stream.Collectors.joining; import static java.util.stream.Collectors.toMap; public final class LegacyEndpointConverters { private static final ParameterizedTypeReference> RESPONSE_TYPE_MAP = new ParameterizedTypeReference<>() { }; private static final ParameterizedTypeReference> RESPONSE_TYPE_LIST = new ParameterizedTypeReference<>() { }; private static final ParameterizedTypeReference>> RESPONSE_TYPE_LIST_MAP = new ParameterizedTypeReference<>() { }; private static final JacksonJsonDecoder DECODER; private static final JacksonJsonEncoder ENCODER; private static final DateTimeFormatter TIMESTAMP_PATTERN = DateTimeFormatter .ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSSZ"); static { var om = JsonMapper.builder() .disable(DeserializationFeature.FAIL_ON_NULL_FOR_PRIMITIVES) .disable(DateTimeFeature.WRITE_DATES_AS_TIMESTAMPS); DECODER = new JacksonJsonDecoder(om); ENCODER = new JacksonJsonEncoder(om); } private LegacyEndpointConverters() { } public static LegacyEndpointConverter health() { return new LegacyEndpointConverter(Endpoint.HEALTH, convertUsing(RESPONSE_TYPE_MAP, RESPONSE_TYPE_MAP, LegacyEndpointConverters::convertHealth)); } public static LegacyEndpointConverter env() { return new LegacyEndpointConverter(Endpoint.ENV, convertUsing(RESPONSE_TYPE_MAP, RESPONSE_TYPE_MAP, LegacyEndpointConverters::convertEnv)); } public static LegacyEndpointConverter httptrace() { return new LegacyEndpointConverter(Endpoint.HTTPTRACE, convertUsing(RESPONSE_TYPE_LIST_MAP, RESPONSE_TYPE_MAP, LegacyEndpointConverters::convertHttptrace)); } public static LegacyEndpointConverter threaddump() { return new LegacyEndpointConverter(Endpoint.THREADDUMP, convertUsing(RESPONSE_TYPE_LIST, RESPONSE_TYPE_MAP, LegacyEndpointConverters::convertThreaddump)); } public static LegacyEndpointConverter liquibase() { return new LegacyEndpointConverter(Endpoint.LIQUIBASE, convertUsing(RESPONSE_TYPE_LIST_MAP, RESPONSE_TYPE_MAP, LegacyEndpointConverters::convertLiquibase)); } public static LegacyEndpointConverter flyway() { return new LegacyEndpointConverter(Endpoint.FLYWAY, convertUsing(RESPONSE_TYPE_LIST_MAP, RESPONSE_TYPE_MAP, LegacyEndpointConverters::convertFlyway)); } public static LegacyEndpointConverter info() { return new LegacyEndpointConverter(Endpoint.INFO, (flux) -> flux); } public static LegacyEndpointConverter beans() { return new LegacyEndpointConverter(Endpoint.BEANS, convertUsing(RESPONSE_TYPE_LIST_MAP, RESPONSE_TYPE_MAP, LegacyEndpointConverters::convertBeans)); } public static LegacyEndpointConverter configprops() { return new LegacyEndpointConverter(Endpoint.CONFIGPROPS, convertUsing(RESPONSE_TYPE_MAP, RESPONSE_TYPE_MAP, LegacyEndpointConverters::convertConfigprops)); } public static LegacyEndpointConverter mappings() { return new LegacyEndpointConverter(Endpoint.MAPPINGS, convertUsing(RESPONSE_TYPE_MAP, RESPONSE_TYPE_MAP, LegacyEndpointConverters::convertMappings)); } public static LegacyEndpointConverter startup() { return new LegacyEndpointConverter(Endpoint.STARTUP, (flux) -> flux); } static Map convertMappingHandlerMethod(String methodDeclaration) { // In order to keep parsing logic sane, parameterized types are dropped early // and then we split the method declaration parts on space // Example: // public java.lang.Object bar.Handler.handle(java.util.List) // -> "public","java.lang.Object","bar.Handler.handle(java.util.List)" String[] declarationParts = methodDeclaration // remove parameterized types using the regex below .replaceAll("<[a-zA-Z1-9$_\\.,<> ]*>", "") .replace(" synchronized ", " ") // and split on single space to get the decl parts .split(" "); // method -> "bar.Handler.handle(java.util.List)" String method = declarationParts[2]; // methodRef -> "bar.Handler.handle" String methodRef = method.substring(0, method.indexOf('(')); // className -> "bar.Handler" String className = methodRef.substring(0, methodRef.lastIndexOf('.')); // methodName -> "handle" String methodName = methodRef.substring(methodRef.lastIndexOf('.') + 1); // In order to simulate descriptors' output we need to read the return type and // the arguments of the handler method. Parameterized types were stripped early // in the parsing process to maintain parsing simplicity. // We also assume object types (see L) which is highly likely for spring-web // @RequestMapping methods but there will be false positives (e.g. on void). // // returnType -> Ljava/lang/Object; String returnType = declarationParts[1].replaceFirst("^", "L").replace(".", "/").concat(";"); // In order to simulate descriptors' output we need to read the return type and // the arguments of the handler method. Parameterized types were stripped early // in the parsing process to maintain parsing simplicity. // We also assume object types (see L) which is highly likely for spring-web // @RequestMapping methods but there will be false positives. // // methodArgs -> (Ljava/util/List;) String methodArgs = Arrays.stream(method // get what's included in parentheses .substring(method.indexOf('('), method.length() - 1) // now remove the parenthesis .replaceAll("[()]", "") // and split on comma .split(",") // then for each argument ) .map((arg) -> arg // prepend L char - indicated ObjectType .replaceFirst("^", "L") // replace dots with slashes .replace(".", "/") // and append ; .concat(";") // then join back to simulate MethodDescriptors output ) .collect(joining("", "(", ")")); Map handlerMethod = new LinkedHashMap<>(); handlerMethod.put("className", className); handlerMethod.put("descriptor", methodArgs + returnType); handlerMethod.put("name", methodName); return handlerMethod; } @SuppressWarnings("unchecked") private static Function, Flux> convertUsing( ParameterizedTypeReference sourceType, ParameterizedTypeReference targetType, Function converterFn) { return (input) -> DECODER.decodeToMono(input, ResolvableType.forType(sourceType), null, null) .map((body) -> converterFn.apply((S) body)) .flatMapMany((output) -> ENCODER.encode(Mono.just(output), new DefaultDataBufferFactory(), ResolvableType.forType(targetType), null, null)); } @SuppressWarnings("unchecked") private static Map convertHealth(Map body) { Map converted = new LinkedHashMap<>(); Map details = new LinkedHashMap<>(); body.forEach((key, value) -> { if ("status".equals(key)) { converted.put(key, value); } else if (value instanceof Map) { details.put(key, convertHealth((Map) value)); } else { details.put(key, value); } }); if (!details.isEmpty()) { converted.put("details", details); } return converted; } @SuppressWarnings("unchecked") private static Map convertEnv(Map body) { Map converted = new LinkedHashMap<>(); List> propertySources = new ArrayList<>(body.size()); body.forEach((key, value) -> { if ("profiles".equals(key)) { converted.put("activeProfiles", value); } else if (value instanceof Map) { Map values = (Map) value; Map properties = new LinkedHashMap<>(); values.forEach((propKey, propValue) -> properties.put(propKey, singletonMap("value", propValue))); Map propertySource = new LinkedHashMap<>(); propertySource.put("name", key); propertySource.put("properties", properties); propertySources.add(propertySource); } }); converted.put("propertySources", propertySources); return converted; } private static Map convertHttptrace(List> traces) { return singletonMap("traces", traces.stream().map(LegacyEndpointConverters::convertHttptrace).toList()); } @SuppressWarnings("unchecked") private static Map convertHttptrace(Map in) { Map out = new LinkedHashMap<>(); out.put("timestamp", getInstant(in.get("timestamp"))); Map in_info = (Map) in.get("info"); if (in_info != null) { Map request = new LinkedHashMap<>(); request.put("method", in_info.get("method")); request.put("uri", in_info.get("path")); out.put("request", request); Map response = new LinkedHashMap<>(); Map in_headers = (Map) in_info.get("headers"); if (in_headers != null) { Map in_request_headers = (Map) in_headers.get("request"); if (in_request_headers != null) { Map requestHeaders = new LinkedHashMap<>(); in_request_headers.forEach((k, v) -> requestHeaders.put(k, singletonList(v))); request.put("headers", requestHeaders); } Map in_response_headers = (Map) in_headers.get("response"); if (in_response_headers != null) { if (in_response_headers.get("status") instanceof String status) { response.put("status", Long.valueOf(status)); } Map responseHeaders = new LinkedHashMap<>(); in_response_headers.forEach((k, v) -> responseHeaders.put(k, singletonList(v))); responseHeaders.remove("status"); response.put("headers", responseHeaders); } } out.put("response", response); if (in_info.get("timeTaken") instanceof String timeTaken) { out.put("timeTaken", Long.valueOf(timeTaken)); } } return out; } private static Map convertThreaddump(List threads) { return singletonMap("threads", threads); } @SuppressWarnings("unchecked") private static Map convertLiquibase(List> reports) { Map liquibaseBeans = reports.stream() .collect(toMap((r) -> (String) r.get("name"), (r) -> singletonMap("changeSets", LegacyEndpointConverters .convertLiquibaseChangesets((List>) r.get("changeLogs"))))); return singletonMap("contexts", singletonMap("application", singletonMap("liquibaseBeans", liquibaseBeans))); } private static List> convertLiquibaseChangesets(List> changeSets) { return changeSets.stream().map((changeset) -> { Map converted = new LinkedHashMap<>(); converted.put("id", changeset.get("ID")); converted.put("author", changeset.get("AUTHOR")); converted.put("changeLog", changeset.get("FILENAME")); if (changeset.get("DATEEXECUTED") instanceof Long dateExecuted) { converted.put("dateExecuted", new Date(dateExecuted)); } converted.put("orderExecuted", changeset.get("ORDEREXECUTED")); converted.put("execType", changeset.get("EXECTYPE")); converted.put("checksum", changeset.get("MD5SUM")); converted.put("description", changeset.get("DESCRIPTION")); converted.put("comments", changeset.get("COMMENTS")); converted.put("tag", changeset.get("TAG")); converted.put("contexts", (changeset.get("CONTEXTS") instanceof String contexts) ? new LinkedHashSet<>(asList((contexts).split(",\\s*"))) : emptySet()); converted.put("labels", (changeset.get("LABELS") instanceof String labels) ? new LinkedHashSet<>(asList((labels).split(",\\s*"))) : emptySet()); converted.put("deploymentId", changeset.get("DEPLOYMENT_ID")); return converted; }).toList(); } @SuppressWarnings("unchecked") private static Map convertFlyway(List> reports) { Map flywayBeans = reports.stream() .collect(toMap((r) -> (String) r.get("name"), (r) -> singletonMap("migrations", LegacyEndpointConverters .convertFlywayMigrations((List>) r.get("migrations"))))); return singletonMap("contexts", singletonMap("application", singletonMap("flywayBeans", flywayBeans))); } private static List> convertFlywayMigrations(List> migrations) { return migrations.stream().map((migration) -> { Map converted = new LinkedHashMap<>(migration); if (migration.get("installedOn") instanceof Long installedOn) { converted.put("installedOn", new Date(installedOn)); } return converted; }).toList(); } private static Map convertBeans(List> contextBeans) { Map convertedContexts = contextBeans.stream().map((context) -> { String contextName = (String) context.get("context"); String parentId = (String) context.get("parent"); // SB 1.x /beans child application context has // itself as parent as well. In order to avoid contexts // with same name we simply append .child in that case. if (contextName.equals(parentId)) { contextName = contextName + ".child"; } List> legacyBeans = (List>) context.get("beans"); Map convertedBeans = legacyBeans.stream() .collect(toMap((bean) -> (String) bean.get("bean"), identity())); Map convertedContext = new LinkedHashMap<>(); convertedContext.put("contextName", contextName); convertedContext.put("parentId", parentId); convertedContext.put("beans", convertedBeans); return convertedContext; }).collect(toMap((context) -> (String) context.get("contextName"), identity())); return singletonMap("contexts", convertedContexts); } private static Map convertConfigprops(Map configProps) { Map contexts = new LinkedHashMap<>(); // SB 1.x /configprops contains a parent entry which // contains the configprops of the parent context. // We put this on a parentContext entry in the // converted response. Object parentConfigProps = configProps.get("parent"); if (parentConfigProps != null) { configProps.remove("parent"); contexts.put("parentContext", singletonMap("beans", parentConfigProps)); } contexts.put("application", singletonMap("beans", configProps)); return singletonMap("contexts", contexts); } private static Map convertMappings(Map mappings) { List> convertedMappings = mappings.entrySet().stream().map((entry) -> { Map convertedMapping = new LinkedHashMap<>(); Map convertedMappingDetails = new LinkedHashMap<>(); convertedMapping.put("details", convertedMappingDetails); String predicate = entry.getKey(); convertedMapping.put("predicate", predicate); String method = (String) ((Map) entry.getValue()).get("method"); if (method != null) { convertedMapping.put("handler", method); convertedMappingDetails.put("handlerMethod", convertMappingHandlerMethod(method)); } convertedMappingDetails.put("requestMappingConditions", convertMappingConditions(predicate)); return convertedMapping; }).toList(); return singletonMap("contexts", // singletonMap("application", // singletonMap("mappings", // singletonMap("dispatcherServlets", // singletonMap("dispatcherServlet", convertedMappings))))); } private static Map convertMappingConditions(String predicate) { // Before further processing we need to remove the following occurrences // {[, ]}, [, ] and split on comma to get condition pairs from the // predicate string. // Example: // {[/scratch/{ticketId}/selectPrize/{prizeId}],methods=[POST]} // -> "/scratch/{ticketId}/selectPrize/{prizeId}","methods=POST" String[] conditionPairs = predicate // remove all {[ and ]} pairs .replaceAll("\\{\\[|\\]\\}", "") // remove all single brackets [ and ] .replaceAll("[\\[\\]]", "") // split on comma .split(","); Map conditionsMap = new LinkedHashMap<>(); conditionsMap.put("consumes", emptyList()); conditionsMap.put("headers", emptyList()); conditionsMap.put("methods", emptyList()); conditionsMap.put("params", emptyList()); conditionsMap.put("patterns", emptyList()); conditionsMap.put("produces", emptyList()); Arrays.stream(conditionPairs).forEach((condition) -> { String[] conditionParts = condition.split("="); // URI path patterns part of the details doesn't follow the detail=value // semantics it's just the value so splits to a single item array boolean isPattern = conditionParts.length == 2; String conditionKey = isPattern ? conditionParts[0] : "patterns"; String conditionValueStr = isPattern ? conditionParts[1] : conditionParts[0]; // All detail values in SB1.x are in form of 'str1 || str2' so we split // them on ' || ' String[] split = conditionValueStr.split(" \\|\\| "); List conditionValue = Arrays.asList(split); // Based on conditionKey we may need to apply some transformations, // mostly wrapping, of the input values switch (conditionKey) { case "consumes": case "produces": conditionValue = conditionValue.stream() .map((v) -> singletonMap("mediaType", v)) .collect(Collectors.toList()); break; case "headers": case "params": case "method": case "patterns": default: break; } conditionsMap.put(conditionKey, conditionValue); }); return conditionsMap; } @Nullable private static Instant getInstant(Object o) { try { if (o instanceof String stringObj) { return OffsetDateTime.parse(stringObj, TIMESTAMP_PATTERN).toInstant(); } else if (o instanceof Long longObj) { return Instant.ofEpochMilli(longObj); } } catch (DateTimeException | ClassCastException ex) { return null; } return null; } } ================================================ FILE: spring-boot-admin-server/src/main/java/de/codecentric/boot/admin/server/web/client/RefreshInstancesEvent.java ================================================ /* * Copyright 2014-2024 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.server.web.client; import org.springframework.context.ApplicationEvent; public class RefreshInstancesEvent extends ApplicationEvent { public RefreshInstancesEvent(Object source) { super(source); } } ================================================ FILE: spring-boot-admin-server/src/main/java/de/codecentric/boot/admin/server/web/client/cookies/CookieStoreCleanupTrigger.java ================================================ /* * 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.server.web.client.cookies; import org.reactivestreams.Publisher; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import de.codecentric.boot.admin.server.domain.events.InstanceDeregisteredEvent; import de.codecentric.boot.admin.server.domain.events.InstanceEvent; import de.codecentric.boot.admin.server.services.AbstractEventHandler; /** * Triggers cleanup of {@link de.codecentric.boot.admin.server.domain.entities.Instance} * specific data in {@link PerInstanceCookieStore} on receiving an * {@link InstanceDeregisteredEvent}. */ public class CookieStoreCleanupTrigger extends AbstractEventHandler { private final PerInstanceCookieStore cookieStore; /** * Creates a trigger to cleanup the cookie store on deregistering of an * {@link de.codecentric.boot.admin.server.domain.entities.Instance}. * @param publisher publisher of {@link InstanceEvent}s events * @param cookieStore the store to inform about deregistration of an * {@link de.codecentric.boot.admin.server.domain.entities.Instance} */ public CookieStoreCleanupTrigger(final Publisher publisher, final PerInstanceCookieStore cookieStore) { super(publisher, InstanceDeregisteredEvent.class); this.cookieStore = cookieStore; } @Override protected Publisher handle(final Flux publisher) { return publisher.flatMap((event) -> { cleanupCookieStore(event); return Mono.empty(); }); } private void cleanupCookieStore(final InstanceDeregisteredEvent event) { cookieStore.cleanupInstance(event.getInstance()); } } ================================================ FILE: spring-boot-admin-server/src/main/java/de/codecentric/boot/admin/server/web/client/cookies/JdkPerInstanceCookieStore.java ================================================ /* * 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.server.web.client.cookies; import java.io.IOException; import java.net.CookieHandler; import java.net.CookieManager; import java.net.CookiePolicy; import java.net.CookieStore; import java.net.URI; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.concurrent.ConcurrentHashMap; import org.springframework.util.Assert; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; import org.springframework.util.MultiValueMapAdapter; import de.codecentric.boot.admin.server.domain.values.InstanceId; import de.codecentric.boot.admin.server.web.client.exception.InstanceWebClientException; /** * A {@link PerInstanceCookieStore} that is using per * {@link de.codecentric.boot.admin.server.domain.entities.Instance} a {@link CookieStore} * from JDK as back end store. * * As Cookie2 cookies are * not * recommended any more only * Cookie * cookies are supported. */ public class JdkPerInstanceCookieStore implements PerInstanceCookieStore { private static final String REQ_COOKIE_HEADER_KEY = "Cookie"; /** * Holds a cookie store per * {@link de.codecentric.boot.admin.server.domain.entities.Instance}. */ private final Map cookieHandlerRegistry = new ConcurrentHashMap<>(); private final CookiePolicy cookiePolicy; /** * Creates a new {@link JdkPerInstanceCookieStore}. * * Same as * *
	 * new JdkPerInstanceCookieStore(CookiePolicy.ACCEPT_ORIGINAL_SERVER);
	 * 
*/ public JdkPerInstanceCookieStore() { this(CookiePolicy.ACCEPT_ORIGINAL_SERVER); } /** * Creates a new {@link JdkPerInstanceCookieStore} using the given * {@link CookiePolicy}. * @param cookiePolicy policy used by created {@link CookieStore}s */ public JdkPerInstanceCookieStore(final CookiePolicy cookiePolicy) { Assert.notNull(cookiePolicy, "'cookiePolicy' must not be null"); this.cookiePolicy = cookiePolicy; } @Override public MultiValueMap get(final InstanceId instanceId, final URI requestUri, final MultiValueMap requestHeaders) { try { final List rawCookies = getCookieHandler(instanceId).get(requestUri, requestHeaders) .get(REQ_COOKIE_HEADER_KEY); // split each rawCookie at first '=' into name/cookieValue and // return as MultiValueMap return Optional.ofNullable(rawCookies) .map((rcList) -> rcList.stream() .map((rc) -> rc.split("=", 2)) .collect(LinkedMultiValueMap::new, (map, nv) -> map.add(nv[0], nv[1]), MultiValueMapAdapter::addAll)) .orElseGet(LinkedMultiValueMap::new); } catch (IOException ioe) { throw new InstanceWebClientException("Could not get cookies from store.", ioe); } } @Override public void put(final InstanceId instanceId, final URI requestUrl, final MultiValueMap headers) { try { getCookieHandler(instanceId).put(requestUrl, headers); } catch (IOException ioe) { throw new InstanceWebClientException("Could not set cookies to store.", ioe); } } @Override public void cleanupInstance(final InstanceId instanceId) { cookieHandlerRegistry.computeIfPresent(instanceId, (id, ch) -> null); } /** * Returns the stored {@link CookieHandler} for the identified * {@link de.codecentric.boot.admin.server.domain.entities.Instance} or creates a new * one, stores and returns it. * @param instanceId identifies the * {@link de.codecentric.boot.admin.server.domain.entities.Instance} * @return {@link CookieHandler} responsible for the given instanceId */ protected CookieHandler getCookieHandler(final InstanceId instanceId) { return cookieHandlerRegistry.computeIfAbsent(instanceId, this::createCookieHandler); } protected CookieHandler createCookieHandler(final InstanceId instanceId) { return new CookieManager(null, cookiePolicy); } } ================================================ FILE: spring-boot-admin-server/src/main/java/de/codecentric/boot/admin/server/web/client/cookies/PerInstanceCookieStore.java ================================================ /* * 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.server.web.client.cookies; import java.net.URI; import org.springframework.util.MultiValueMap; import de.codecentric.boot.admin.server.domain.entities.Instance; import de.codecentric.boot.admin.server.domain.values.InstanceId; /** * A cookie store that stores cookies per {@link Instance}. */ public interface PerInstanceCookieStore { /** * Gets all the applicable cookies (cookie name => string representation of cookie) * for the given instanceId and the specified uri in the request header. * * The URI passed as an argument specifies the intended use for the cookies. * @param instanceId identifies the web client instance * @param requestUri a URI representing the intended use for the cookies * @param requestHeaders a Map from request header field names to lists of field * values representing the current request * @return an immutable map from cookie names to text representations of cookies to be * included into a request header */ MultiValueMap get(InstanceId instanceId, URI requestUri, MultiValueMap requestHeaders); /** * Stores all the applicable cookies (examples are response header fields that are * named Set-Cookie) present in the response headers. * @param instanceId identifies the web client instance * @param requestUri an URI where the cookies come from * @param responseHeaders a map from field names to lists of field values representing * the response header fields */ void put(InstanceId instanceId, URI requestUri, MultiValueMap responseHeaders); /** * Informs the store that the cookies of the given instanceId could be * removed. * @param instanceId identifies the {@link Instance} */ void cleanupInstance(InstanceId instanceId); } ================================================ FILE: spring-boot-admin-server/src/main/java/de/codecentric/boot/admin/server/web/client/cookies/package-info.java ================================================ /* * 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. */ @NullMarked package de.codecentric.boot.admin.server.web.client.cookies; import org.jspecify.annotations.NullMarked; ================================================ FILE: spring-boot-admin-server/src/main/java/de/codecentric/boot/admin/server/web/client/exception/InstanceWebClientException.java ================================================ /* * 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.server.web.client.exception; public class InstanceWebClientException extends RuntimeException { public InstanceWebClientException(String message) { super(message); } public InstanceWebClientException(String message, Throwable cause) { super(message, cause); } } ================================================ FILE: spring-boot-admin-server/src/main/java/de/codecentric/boot/admin/server/web/client/exception/ResolveEndpointException.java ================================================ /* * 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.server.web.client.exception; public class ResolveEndpointException extends InstanceWebClientException { public ResolveEndpointException(String message) { super(message); } public ResolveEndpointException(String message, Throwable cause) { super(message, cause); } } ================================================ FILE: spring-boot-admin-server/src/main/java/de/codecentric/boot/admin/server/web/client/exception/ResolveInstanceException.java ================================================ /* * 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.server.web.client.exception; public class ResolveInstanceException extends InstanceWebClientException { public ResolveInstanceException(String message) { super(message); } public ResolveInstanceException(String message, Throwable cause) { super(message, cause); } } ================================================ FILE: spring-boot-admin-server/src/main/java/de/codecentric/boot/admin/server/web/client/exception/package-info.java ================================================ /* * 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. */ // Spring Boot Admin Server - client exceptions package. @NullMarked package de.codecentric.boot.admin.server.web.client.exception; import org.jspecify.annotations.NullMarked; ================================================ FILE: spring-boot-admin-server/src/main/java/de/codecentric/boot/admin/server/web/client/package-info.java ================================================ /* * 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. */ // Spring Boot Admin Server - instance client package. @NullMarked package de.codecentric.boot.admin.server.web.client; import org.jspecify.annotations.NullMarked; ================================================ FILE: spring-boot-admin-server/src/main/java/de/codecentric/boot/admin/server/web/client/reactive/CompositeReactiveHttpHeadersProvider.java ================================================ /* * 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.server.web.client.reactive; import java.util.Arrays; import java.util.Collection; import java.util.List; import org.springframework.http.HttpHeaders; import reactor.core.publisher.Mono; import de.codecentric.boot.admin.server.domain.entities.Instance; public class CompositeReactiveHttpHeadersProvider implements ReactiveHttpHeadersProvider { private final Collection delegates; public CompositeReactiveHttpHeadersProvider(Collection delegates) { this.delegates = delegates; } @Override public Mono getHeaders(Instance instance) { List> headers = delegates.stream() .map((reactiveHttpHeadersProvider) -> reactiveHttpHeadersProvider.getHeaders(instance)) .toList(); return Mono.zip(headers, this::mergeMonosToHeaders); } private HttpHeaders mergeMonosToHeaders(Object[] e) { return Arrays.stream(e).map(HttpHeaders.class::cast).reduce(new HttpHeaders(), (h1, h2) -> { h1.addAll(h2); return h1; }); } } ================================================ FILE: spring-boot-admin-server/src/main/java/de/codecentric/boot/admin/server/web/client/reactive/ReactiveHttpHeadersProvider.java ================================================ /* * 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.server.web.client.reactive; import org.springframework.http.HttpHeaders; import reactor.core.publisher.Mono; import de.codecentric.boot.admin.server.domain.entities.Instance; /** * Is responsible to provide the {@link HttpHeaders} used to interact with the given * {@link Instance}. */ public interface ReactiveHttpHeadersProvider { Mono getHeaders(Instance instance); } ================================================ FILE: spring-boot-admin-server/src/main/java/de/codecentric/boot/admin/server/web/package-info.java ================================================ /* * 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. */ // Spring Boot Admin Server - web package. @NullMarked package de.codecentric.boot.admin.server.web; import org.jspecify.annotations.NullMarked; ================================================ FILE: spring-boot-admin-server/src/main/java/de/codecentric/boot/admin/server/web/reactive/AdminControllerHandlerMapping.java ================================================ /* * 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.server.web.reactive; import java.lang.reflect.Method; import org.springframework.core.annotation.AnnotatedElementUtils; import org.springframework.util.StringUtils; import org.springframework.web.reactive.result.condition.PatternsRequestCondition; import org.springframework.web.reactive.result.method.RequestMappingInfo; import org.springframework.web.reactive.result.method.annotation.RequestMappingHandlerMapping; import de.codecentric.boot.admin.server.web.AdminController; import de.codecentric.boot.admin.server.web.PathUtils; public class AdminControllerHandlerMapping extends RequestMappingHandlerMapping { private final String adminContextPath; public AdminControllerHandlerMapping(String adminContextPath) { this.adminContextPath = adminContextPath; } @Override protected boolean isHandler(Class beanType) { return AnnotatedElementUtils.hasAnnotation(beanType, AdminController.class); } @Override protected void registerHandlerMethod(Object handler, Method method, RequestMappingInfo mapping) { super.registerHandlerMethod(handler, method, withPrefix(mapping)); } private RequestMappingInfo withPrefix(RequestMappingInfo mapping) { if (!StringUtils.hasText(adminContextPath)) { return mapping; } return mapping.mutate().paths(withNewPatterns(mapping.getPatternsCondition())).build(); } private String[] withNewPatterns(PatternsRequestCondition patternsRequestCondition) { return patternsRequestCondition.getPatterns() .stream() .map((pattern) -> PathUtils.normalizePath(adminContextPath + pattern)) .toArray(String[]::new); } } ================================================ FILE: spring-boot-admin-server/src/main/java/de/codecentric/boot/admin/server/web/reactive/InstancesProxyController.java ================================================ /* * 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.server.web.reactive; import java.net.URI; import java.util.Set; import org.springframework.core.io.buffer.DataBuffer; import org.springframework.core.io.buffer.DataBufferFactory; import org.springframework.core.io.buffer.DataBufferUtils; import org.springframework.core.io.buffer.DefaultDataBufferFactory; import org.springframework.http.server.reactive.ServerHttpRequest; import org.springframework.http.server.reactive.ServerHttpResponse; import org.springframework.util.AntPathMatcher; import org.springframework.util.PathMatcher; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.reactive.function.BodyExtractors; import org.springframework.web.reactive.function.BodyInserters; import org.springframework.web.util.UriComponentsBuilder; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import de.codecentric.boot.admin.server.domain.values.InstanceId; import de.codecentric.boot.admin.server.services.InstanceRegistry; import de.codecentric.boot.admin.server.web.AdminController; import de.codecentric.boot.admin.server.web.HttpHeaderFilter; import de.codecentric.boot.admin.server.web.InstanceWebProxy; import de.codecentric.boot.admin.server.web.client.InstanceWebClient; /** * Http Handler for proxied requests */ @AdminController public class InstancesProxyController { private static final String INSTANCE_MAPPED_PATH = "/instances/{instanceId}/actuator/**"; private static final String APPLICATION_MAPPED_PATH = "/applications/{applicationName}/actuator/**"; private final PathMatcher pathMatcher = new AntPathMatcher(); private final DataBufferFactory bufferFactory = new DefaultDataBufferFactory(); private final InstanceRegistry registry; private final InstanceWebProxy instanceWebProxy; private final String adminContextPath; private final HttpHeaderFilter httpHeadersFilter; public InstancesProxyController(String adminContextPath, Set ignoredHeaders, InstanceRegistry registry, InstanceWebClient instanceWebClient) { this.adminContextPath = adminContextPath; this.registry = registry; this.httpHeadersFilter = new HttpHeaderFilter(ignoredHeaders); this.instanceWebProxy = new InstanceWebProxy(instanceWebClient); } @RequestMapping(path = INSTANCE_MAPPED_PATH, method = { RequestMethod.GET, RequestMethod.HEAD, RequestMethod.POST, RequestMethod.PUT, RequestMethod.PATCH, RequestMethod.DELETE, RequestMethod.OPTIONS }) public Mono endpointProxy(@PathVariable("instanceId") String instanceId, ServerHttpRequest request, ServerHttpResponse response) { InstanceWebProxy.ForwardRequest fwdRequest = createForwardRequest(request, request.getBody(), this.adminContextPath + INSTANCE_MAPPED_PATH); return this.instanceWebProxy.forward(this.registry.getInstance(InstanceId.of(instanceId)), fwdRequest, (clientResponse) -> { response.setStatusCode(clientResponse.statusCode()); response.getHeaders() .addAll(this.httpHeadersFilter.filterHeaders(clientResponse.headers().asHttpHeaders())); return response.writeAndFlushWith(clientResponse.body(BodyExtractors.toDataBuffers()).window(1)); }); } @ResponseBody @RequestMapping(path = APPLICATION_MAPPED_PATH, method = { RequestMethod.GET, RequestMethod.HEAD, RequestMethod.POST, RequestMethod.PUT, RequestMethod.PATCH, RequestMethod.DELETE, RequestMethod.OPTIONS }) public Flux endpointProxy( @PathVariable("applicationName") String applicationName, ServerHttpRequest request) { Flux cachedBody = request.getBody().map((b) -> { DataBuffer dataBuffer = this.bufferFactory.allocateBuffer(b.readableByteCount()); try (var iterator = b.readableByteBuffers()) { iterator.forEachRemaining(dataBuffer::write); } DataBufferUtils.release(b); return dataBuffer; }).cache(); InstanceWebProxy.ForwardRequest fwdRequest = createForwardRequest(request, cachedBody, this.adminContextPath + APPLICATION_MAPPED_PATH); return this.instanceWebProxy.forward(this.registry.getInstances(applicationName), fwdRequest); } private InstanceWebProxy.ForwardRequest createForwardRequest(ServerHttpRequest request, Flux cachedBody, String pathPattern) { String localPath = this.getLocalPath(pathPattern, request); URI uri = UriComponentsBuilder.fromPath(localPath).query(request.getURI().getRawQuery()).build(true).toUri(); return InstanceWebProxy.ForwardRequest.builder() .uri(uri) .method(request.getMethod()) .headers(this.httpHeadersFilter.filterHeaders(request.getHeaders())) .body(BodyInserters.fromDataBuffers(cachedBody)) .build(); } private String getLocalPath(String pathPattern, ServerHttpRequest request) { String pathWithinApplication = request.getPath().pathWithinApplication().value(); return this.pathMatcher.extractPathWithinPattern(pathPattern, pathWithinApplication); } } ================================================ FILE: spring-boot-admin-server/src/main/java/de/codecentric/boot/admin/server/web/reactive/package-info.java ================================================ /* * 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. */ // Spring Boot Admin Server - react native package. @NullMarked package de.codecentric.boot.admin.server.web.reactive; import org.jspecify.annotations.NullMarked; ================================================ FILE: spring-boot-admin-server/src/main/java/de/codecentric/boot/admin/server/web/servlet/AdminControllerHandlerMapping.java ================================================ /* * 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.server.web.servlet; import java.lang.reflect.Method; import java.util.Set; import org.springframework.core.annotation.AnnotatedElementUtils; import org.springframework.util.StringUtils; import org.springframework.web.servlet.mvc.method.RequestMappingInfo; import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping; import org.springframework.web.util.pattern.PathPattern; import de.codecentric.boot.admin.server.web.AdminController; import de.codecentric.boot.admin.server.web.PathUtils; public class AdminControllerHandlerMapping extends RequestMappingHandlerMapping { private final String adminContextPath; public AdminControllerHandlerMapping(String adminContextPath) { this.adminContextPath = adminContextPath; } @Override protected boolean isHandler(Class beanType) { return AnnotatedElementUtils.hasAnnotation(beanType, AdminController.class); } @Override protected void registerHandlerMethod(Object handler, Method method, RequestMappingInfo mapping) { super.registerHandlerMethod(handler, method, withPrefix(mapping)); } private RequestMappingInfo withPrefix(RequestMappingInfo mapping) { if (!StringUtils.hasText(this.adminContextPath)) { return mapping; } RequestMappingInfo.Builder mutate = mapping.mutate(); return mutate.paths(withNewPatterns(mapping.getPathPatternsCondition().getPatterns())).build(); } private String[] withNewPatterns(Set patterns) { return patterns.stream() .map((pattern) -> PathUtils.normalizePath(this.adminContextPath + pattern.getPatternString())) .toArray(String[]::new); } } ================================================ FILE: spring-boot-admin-server/src/main/java/de/codecentric/boot/admin/server/web/servlet/InstancesProxyController.java ================================================ /* * 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.server.web.servlet; import java.io.IOException; import java.io.OutputStream; import java.net.URI; import java.util.Set; import jakarta.servlet.AsyncContext; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import org.springframework.core.io.buffer.DataBuffer; import org.springframework.core.io.buffer.DataBufferFactory; import org.springframework.core.io.buffer.DataBufferUtils; import org.springframework.core.io.buffer.DefaultDataBufferFactory; import org.springframework.http.server.ServerHttpResponse; import org.springframework.http.server.ServletServerHttpRequest; import org.springframework.http.server.ServletServerHttpResponse; import org.springframework.util.AntPathMatcher; import org.springframework.util.PathMatcher; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.reactive.function.BodyExtractors; import org.springframework.web.reactive.function.BodyInserters; import org.springframework.web.servlet.HandlerMapping; import org.springframework.web.util.UriComponentsBuilder; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import de.codecentric.boot.admin.server.domain.values.InstanceId; import de.codecentric.boot.admin.server.services.InstanceRegistry; import de.codecentric.boot.admin.server.web.AdminController; import de.codecentric.boot.admin.server.web.HttpHeaderFilter; import de.codecentric.boot.admin.server.web.InstanceWebProxy; import de.codecentric.boot.admin.server.web.client.InstanceWebClient; /** * Http Handler for proxied requests */ @AdminController public class InstancesProxyController { private static final String INSTANCE_MAPPED_PATH = "/instances/{instanceId}/actuator/**"; private static final String APPLICATION_MAPPED_PATH = "/applications/{applicationName}/actuator/**"; private final DataBufferFactory bufferFactory = new DefaultDataBufferFactory(); private final PathMatcher pathMatcher = new AntPathMatcher(); private final InstanceWebProxy instanceWebProxy; private final HttpHeaderFilter httpHeadersFilter; private final InstanceRegistry registry; private final String adminContextPath; public InstancesProxyController(String adminContextPath, Set ignoredHeaders, InstanceRegistry registry, InstanceWebClient instanceWebClient) { this.adminContextPath = adminContextPath; this.registry = registry; this.httpHeadersFilter = new HttpHeaderFilter(ignoredHeaders); this.instanceWebProxy = new InstanceWebProxy(instanceWebClient); } @ResponseBody @RequestMapping(path = INSTANCE_MAPPED_PATH, method = { RequestMethod.GET, RequestMethod.HEAD, RequestMethod.POST, RequestMethod.PUT, RequestMethod.PATCH, RequestMethod.DELETE, RequestMethod.OPTIONS }) public void instanceProxy(@PathVariable("instanceId") String instanceId, HttpServletRequest servletRequest) { // start async because we will commit from different thread. // otherwise incorrect thread local objects (session and security context) will be // stored. // check for example // org.springframework.security.web.context.HttpSessionSecurityContextRepository.SaveToSessionRequestWrapper.startAsync() AsyncContext asyncContext = servletRequest.startAsync(); asyncContext.setTimeout(-1); // no timeout because instanceWebProxy will handle it // for us try { ServletServerHttpRequest request = new ServletServerHttpRequest( (HttpServletRequest) asyncContext.getRequest()); Flux requestBody = DataBufferUtils.readInputStream(request::getBody, this.bufferFactory, 4096); InstanceWebProxy.ForwardRequest fwdRequest = createForwardRequest(request, requestBody, this.adminContextPath + INSTANCE_MAPPED_PATH); this.instanceWebProxy .forward(this.registry.getInstance(InstanceId.of(instanceId)), fwdRequest, (clientResponse) -> { ServerHttpResponse response = new ServletServerHttpResponse( (HttpServletResponse) asyncContext.getResponse()); response.setStatusCode(clientResponse.statusCode()); response.getHeaders() .addAll(this.httpHeadersFilter.filterHeaders(clientResponse.headers().asHttpHeaders())); try { OutputStream responseBody = response.getBody(); response.flush(); return clientResponse.body(BodyExtractors.toDataBuffers()) .window(1) .concatMap((body) -> writeAndFlush(body, responseBody)) .then(); } catch (IOException ex) { return Mono.error(ex); } }) // We need to explicitly block so the headers are received and written // before any async dispatch otherwise the FrameworkServlet will add // wrong // Allow header for OPTIONS request .block(); } finally { asyncContext.complete(); } } @ResponseBody @RequestMapping(path = APPLICATION_MAPPED_PATH, method = { RequestMethod.GET, RequestMethod.HEAD, RequestMethod.POST, RequestMethod.PUT, RequestMethod.PATCH, RequestMethod.DELETE, RequestMethod.OPTIONS }) public Flux endpointProxy( @PathVariable("applicationName") String applicationName, HttpServletRequest servletRequest) { ServletServerHttpRequest request = new ServletServerHttpRequest(servletRequest); Flux cachedBody = DataBufferUtils.readInputStream(request::getBody, this.bufferFactory, 4096) .cache(); InstanceWebProxy.ForwardRequest fwdRequest = createForwardRequest(request, cachedBody, this.adminContextPath + APPLICATION_MAPPED_PATH); return this.instanceWebProxy.forward(this.registry.getInstances(applicationName), fwdRequest); } private InstanceWebProxy.ForwardRequest createForwardRequest(ServletServerHttpRequest request, Flux body, String pathPattern) { String endpointLocalPath = this.getLocalPath(pathPattern, request); URI uri = UriComponentsBuilder.fromPath(endpointLocalPath) .query(request.getURI().getRawQuery()) .build(true) .toUri(); return InstanceWebProxy.ForwardRequest.builder() .uri(uri) .method(request.getMethod()) .headers(this.httpHeadersFilter.filterHeaders(request.getHeaders())) .body(BodyInserters.fromDataBuffers(body)) .build(); } private String getLocalPath(String pathPattern, ServletServerHttpRequest request) { String pathWithinApplication = request.getServletRequest() .getAttribute(HandlerMapping.PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE) .toString(); return this.pathMatcher.extractPathWithinPattern(pathPattern, pathWithinApplication); } private Mono writeAndFlush(Flux body, OutputStream responseBody) { return DataBufferUtils.write(body, responseBody).map(DataBufferUtils::release).then(Mono.create((sink) -> { try { responseBody.flush(); sink.success(); } catch (IOException ex) { sink.error(ex); } })); } } ================================================ FILE: spring-boot-admin-server/src/main/java/de/codecentric/boot/admin/server/web/servlet/package-info.java ================================================ /* * 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. */ // Spring Boot Admin Server - servlet package. @NullMarked package de.codecentric.boot.admin.server.web.servlet; import org.jspecify.annotations.NullMarked; ================================================ FILE: spring-boot-admin-server/src/main/resources/META-INF/additional-spring-configuration-metadata.json ================================================ { "groups": [ ], "properties": [ { "name": "spring.boot.admin.hazelcast.enabled", "type": "java.lang.Boolean", "description": "Enable Hazelcast support.", "defaultValue": "true" }, { "name": "spring.boot.admin.hazelcast.event-store", "type": "java.lang.String", "description": "Name of backing Hazelcast-Map for storing the instance events.", "defaultValue": "spring-boot-admin-application-store" }, { "name": "spring.boot.admin.hazelcast.sent-notifications", "type": "java.lang.String", "description": "Name of backing Hazelcast-Map for storing the sent notifications.", "defaultValue": "spring-boot-admin-sent-notifications" }, { "name": "spring.boot.admin.monitor.period", "type": "java.lang.Long", "deprecation": { "level": "warning", "replacement": "spring.boot.admin.monitor.status-interval" } }, { "name": "spring.boot.admin.monitor.read-timeout", "type": "java.lang.Long", "deprecation": { "replacement": "spring.boot.admin.monitor.default-timeout", "level": "error" } }, { "name": "spring.boot.admin.monitor.connect-timeout", "type": "java.lang.Long", "deprecation": { "replacement": "spring.boot.admin.monitor.default-timeout", "level": "error" } } ] } ================================================ FILE: spring-boot-admin-server/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports ================================================ de.codecentric.boot.admin.server.config.AdminServerAutoConfiguration de.codecentric.boot.admin.server.config.AdminServerNotifierAutoConfiguration de.codecentric.boot.admin.server.config.AdminServerHazelcastAutoConfiguration de.codecentric.boot.admin.server.config.AdminServerCloudFoundryAutoConfiguration ================================================ FILE: spring-boot-admin-server/src/main/resources/META-INF/spring-boot-admin-server/mail/status-changed.html ================================================ [[${instance.registration.name}]] ([[(${instance.id})]]) is [[${event.statusInfo.status}]]

() is

Instance changed status from to

Status Details

Registration

Service Url
Health Url
Management Url
================================================ FILE: spring-boot-admin-server/src/test/java/de/codecentric/boot/admin/server/AbstractAdminApplicationTest.java ================================================ /* * 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.server; import java.net.URI; import java.time.Duration; import java.util.concurrent.atomic.AtomicReference; import lombok.Getter; import org.json.JSONObject; import org.junit.jupiter.api.Test; import org.springframework.http.MediaType; import org.springframework.http.codec.json.JacksonJsonDecoder; import org.springframework.http.codec.json.JacksonJsonEncoder; 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.domain.values.Registration; import static org.assertj.core.api.Assertions.assertThat; @Getter public abstract class AbstractAdminApplicationTest { private WebTestClient webClient; private int port; public void setUp(int port) { this.port = port; this.webClient = createWebClient(port); } @Test public void lifecycle() { AtomicReference location = new AtomicReference<>(); StepVerifier.create(getEventStream().log()).expectSubscription().then(() -> { StepVerifier.create(listEmptyInstances()).expectNext(true).verifyComplete(); location.set(registerInstance()); }) .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(() -> { StepVerifier.create(getInstance(location.get())).expectNext(true).verifyComplete(); StepVerifier.create(listInstances()).expectNext(true).verifyComplete(); StepVerifier.create(deregisterInstance(location.get())).expectNext(true).verifyComplete(); }) .assertNext((event) -> assertThat(event.opt("type")).isEqualTo("DEREGISTERED")) .then(() -> StepVerifier.create(listEmptyInstances()).expectNext(true).verifyComplete()) .thenCancel() .verify(Duration.ofSeconds(120)); } protected Flux getEventStream() { //@formatter:off return this.webClient.get().uri("/instances/events") .accept(MediaType.TEXT_EVENT_STREAM) .exchange() .expectStatus().isOk() .expectHeader().contentTypeCompatibleWith(MediaType.TEXT_EVENT_STREAM) .returnResult(JSONObject.class).getResponseBody(); //@formatter:on } protected URI registerInstance() { //@formatter:off return this.webClient.post().uri("/instances") .contentType(MediaType.APPLICATION_JSON) .bodyValue(createRegistration()) .exchange() .expectStatus().isCreated() .expectHeader().valueMatches("location", "^http://localhost:" + this.port + "/instances/[a-f0-9]+$") .returnResult(Void.class).getResponseHeaders().getLocation(); //@formatter:on } protected Mono getInstance(URI uri) { //@formatter:off return this.webClient.get().uri(uri) .accept(MediaType.APPLICATION_JSON) .exchange() .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 } protected Mono listInstances() { //@formatter:off return this.webClient.get().uri("/instances") .accept(MediaType.APPLICATION_JSON) .exchange() .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 } protected Mono listEmptyInstances() { //@formatter:off return this.webClient.get().uri("/instances") .accept(MediaType.APPLICATION_JSON) .exchange() .returnResult(String.class).getResponseBody() .collectList() .map((list) -> { assertThat(list).hasSize(1); assertThat(list.get(0)).isEqualTo("[]"); return true; }); //@formatter:on } protected Mono deregisterInstance(URI uri) { //@formatter:off return this.webClient.delete().uri(uri) .exchange() .returnResult(Void.class).getResponseBody() .then(Mono.just(true)); //@formatter:on } private Registration createRegistration() { return Registration.builder() .name("Test-Instance") .healthUrl("http://localhost:" + this.port + "/mgmt/health") .managementUrl("http://localhost:" + this.port + "/mgmt") .serviceUrl("http://localhost:" + this.port) .build(); } protected WebTestClient createWebClient(int port) { JsonMapper mapper = JsonMapper.builder().addModule(new JsonOrgModule()).build(); return WebTestClient.bindToServer() .baseUrl("http://localhost:" + port) .exchangeStrategies(ExchangeStrategies.builder().codecs((configurer) -> { configurer.defaultCodecs().jacksonJsonDecoder(new JacksonJsonDecoder(mapper)); configurer.defaultCodecs().jacksonJsonEncoder(new JacksonJsonEncoder(mapper)); }).build()) .build(); } } ================================================ FILE: spring-boot-admin-server/src/test/java/de/codecentric/boot/admin/server/AdminApplicationHazelcastTest.java ================================================ /* * 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.server; import java.util.stream.Collectors; import com.hazelcast.config.Config; import com.hazelcast.config.EvictionConfig; import com.hazelcast.config.EvictionPolicy; import com.hazelcast.config.InMemoryFormat; import com.hazelcast.config.MapConfig; import com.hazelcast.config.MergePolicyConfig; import com.hazelcast.config.TcpIpConfig; import com.hazelcast.spi.merge.PutIfAbsentMergePolicy; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.boot.SpringBootConfiguration; import org.springframework.boot.WebApplicationType; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.builder.SpringApplicationBuilder; import org.springframework.context.ConfigurableApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.http.MediaType; 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 reactor.core.publisher.Mono; import reactor.test.StepVerifier; import de.codecentric.boot.admin.server.config.EnableAdminServer; import static de.codecentric.boot.admin.server.config.AdminServerHazelcastAutoConfiguration.DEFAULT_NAME_EVENT_STORE_MAP; import static de.codecentric.boot.admin.server.config.AdminServerHazelcastAutoConfiguration.DEFAULT_NAME_SENT_NOTIFICATIONS_MAP; import static java.util.Collections.singletonList; import static org.assertj.core.api.Assertions.assertThat; /** * Integration test to verify the correct functionality of the REST API with Hazelcast * * @author Dennis Schulte */ class AdminApplicationHazelcastTest extends AbstractAdminApplicationTest { private ConfigurableApplicationContext instance1; private ConfigurableApplicationContext instance2; private WebTestClient webClient2; @BeforeEach void setUp() { System.setProperty("hazelcast.wait.seconds.before.join", "0"); this.instance1 = new SpringApplicationBuilder().sources(TestAdminApplication.class) .web(WebApplicationType.REACTIVE) .run("--server.port=0", "--management.endpoints.web.base-path=/mgmt", "--management.endpoints.web.exposure.include=info,health", "--info.test=foobar", "--spring.jmx.enabled=false"); this.instance2 = new SpringApplicationBuilder().sources(TestAdminApplication.class) .web(WebApplicationType.REACTIVE) .run("--server.port=0", "--management.endpoints.web.base-path=/mgmt", "--management.endpoints.web.exposure.include=info,health", "--info.test=foobar", "--spring.jmx.enabled=false"); super.setUp(this.instance1.getEnvironment().getProperty("local.server.port", Integer.class, 0)); this.webClient2 = createWebClient( this.instance2.getEnvironment().getProperty("local.server.port", Integer.class, 0)); } @Test @Override public void lifecycle() { super.lifecycle(); Mono events1 = getWebClient().get() .uri("/instances/events") .accept(MediaType.APPLICATION_JSON) .exchange() .expectStatus() .isOk() .returnResult(String.class) .getResponseBody() .collect(Collectors.joining()); Mono events2 = this.webClient2.get() .uri("/instances/events") .accept(MediaType.APPLICATION_JSON) .exchange() .expectStatus() .isOk() .returnResult(String.class) .getResponseBody() .collect(Collectors.joining()); StepVerifier.create(events1.zipWith(events2)) .assertNext((t) -> assertThat(t.getT1()).isEqualTo(t.getT2())) .verifyComplete(); } @AfterEach void shutdown() { this.instance1.close(); this.instance2.close(); } @SpringBootConfiguration @EnableAutoConfiguration @EnableAdminServer @EnableWebFluxSecurity public static class TestAdminApplication { @Bean SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) { return http.authorizeExchange((authorizeExchange) -> authorizeExchange.anyExchange().permitAll()) .csrf(ServerHttpSecurity.CsrfSpec::disable) .build(); } @Bean public Config hazelcastConfig() { MapConfig eventStoreMap = new MapConfig(DEFAULT_NAME_EVENT_STORE_MAP) .setInMemoryFormat(InMemoryFormat.OBJECT) .setBackupCount(1) .setMergePolicyConfig(new MergePolicyConfig(PutIfAbsentMergePolicy.class.getName(), 100)); MapConfig sentNotificationsMap = new MapConfig(DEFAULT_NAME_SENT_NOTIFICATIONS_MAP) .setInMemoryFormat(InMemoryFormat.OBJECT) .setBackupCount(1) .setEvictionConfig(new EvictionConfig().setEvictionPolicy(EvictionPolicy.LRU)) .setMergePolicyConfig(new MergePolicyConfig(PutIfAbsentMergePolicy.class.getName(), 100)); Config config = new Config(); config.addMapConfig(eventStoreMap); config.addMapConfig(sentNotificationsMap); config.getNetworkConfig().getJoin().getMulticastConfig().setEnabled(false); TcpIpConfig tcpIpConfig = config.getNetworkConfig().getJoin().getTcpIpConfig(); tcpIpConfig.setEnabled(true); tcpIpConfig.setMembers(singletonList("127.0.0.1")); return config; } } } ================================================ FILE: spring-boot-admin-server/src/test/java/de/codecentric/boot/admin/server/AdminReactiveApplicationTest.java ================================================ /* * 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.server; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.springframework.boot.SpringBootConfiguration; import org.springframework.boot.WebApplicationType; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.builder.SpringApplicationBuilder; import org.springframework.context.ConfigurableApplicationContext; import org.springframework.context.annotation.Bean; 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 de.codecentric.boot.admin.server.config.EnableAdminServer; public class AdminReactiveApplicationTest extends AbstractAdminApplicationTest { private ConfigurableApplicationContext instance; @BeforeEach void setUp() { this.instance = new SpringApplicationBuilder().sources(TestAdminApplication.class) .web(WebApplicationType.REACTIVE) .run("--server.port=0", "--management.endpoints.web.base-path=/mgmt", "--management.endpoints.web.exposure.include=info,health", "--info.test=foobar"); super.setUp(this.instance.getEnvironment().getProperty("local.server.port", Integer.class, 0)); } @AfterEach void shutdown() { this.instance.close(); } @EnableAdminServer @EnableAutoConfiguration @SpringBootConfiguration @EnableWebFluxSecurity public static class TestAdminApplication { @Bean public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) { return http.authorizeExchange((authorizeExchange) -> authorizeExchange.anyExchange().permitAll()) .csrf(ServerHttpSecurity.CsrfSpec::disable) .build(); } } } ================================================ FILE: spring-boot-admin-server/src/test/java/de/codecentric/boot/admin/server/AdminServletApplicationTest.java ================================================ /* * 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.server; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.springframework.boot.SpringBootConfiguration; import org.springframework.boot.WebApplicationType; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.builder.SpringApplicationBuilder; import org.springframework.context.ConfigurableApplicationContext; 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.config.annotation.web.configurers.AbstractHttpConfigurer; import org.springframework.security.web.SecurityFilterChain; import de.codecentric.boot.admin.server.config.EnableAdminServer; public class AdminServletApplicationTest extends AbstractAdminApplicationTest { private ConfigurableApplicationContext instance; @BeforeEach void setUp() { this.instance = new SpringApplicationBuilder().sources(TestAdminApplication.class) .web(WebApplicationType.SERVLET) .run("--server.port=0", "--management.endpoints.web.base-path=/mgmt", "--management.endpoints.web.exposure.include=info,health", "--info.test=foobar"); super.setUp(this.instance.getEnvironment().getProperty("local.server.port", Integer.class, 0)); } @AfterEach void shutdown() { this.instance.close(); } @EnableAdminServer @EnableAutoConfiguration @SpringBootConfiguration public static class TestAdminApplication { @Configuration(proxyBeanMethods = false) public static class SecurityConfiguration { @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http.csrf(AbstractHttpConfigurer::disable) .authorizeHttpRequests((authz) -> authz.anyRequest().permitAll()); return http.build(); } } } } ================================================ FILE: spring-boot-admin-server/src/test/java/de/codecentric/boot/admin/server/config/AdminServerAutoConfigurationTest.java ================================================ /* * 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.server.config; import com.hazelcast.config.Config; import org.junit.jupiter.api.Test; import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.hazelcast.autoconfigure.HazelcastAutoConfiguration; import org.springframework.boot.http.client.autoconfigure.reactive.ReactiveHttpClientAutoConfiguration; import org.springframework.boot.test.context.runner.WebApplicationContextRunner; import org.springframework.boot.webclient.autoconfigure.WebClientAutoConfiguration; import org.springframework.boot.webmvc.autoconfigure.WebMvcAutoConfiguration; import org.springframework.context.annotation.Bean; import reactor.core.publisher.Mono; import de.codecentric.boot.admin.server.domain.entities.InstanceRepository; import de.codecentric.boot.admin.server.domain.entities.SnapshottingInstanceRepository; import de.codecentric.boot.admin.server.eventstore.ConcurrentMapEventStore; import de.codecentric.boot.admin.server.eventstore.HazelcastEventStore; import de.codecentric.boot.admin.server.eventstore.InstanceEventStore; import de.codecentric.boot.admin.server.notify.HazelcastNotificationTrigger; import de.codecentric.boot.admin.server.notify.MailNotifier; import de.codecentric.boot.admin.server.notify.NotificationTrigger; import de.codecentric.boot.admin.server.notify.Notifier; import static org.assertj.core.api.Assertions.assertThat; class AdminServerAutoConfigurationTest { private final WebApplicationContextRunner contextRunner = new WebApplicationContextRunner() .withConfiguration(AutoConfigurations.of(ReactiveHttpClientAutoConfiguration.class, WebClientAutoConfiguration.class, HazelcastAutoConfiguration.class, WebMvcAutoConfiguration.class, AdminServerHazelcastAutoConfiguration.class, AdminServerAutoConfiguration.class)) .withUserConfiguration(AdminServerMarkerConfiguration.class); @Test void simpleConfig() { this.contextRunner.run((context) -> { assertThat(context).getBean(InstanceRepository.class).isInstanceOf(SnapshottingInstanceRepository.class); assertThat(context).doesNotHaveBean(MailNotifier.class); assertThat(context).getBean(InstanceEventStore.class).isInstanceOf(ConcurrentMapEventStore.class); }); } @Test void hazelcastConfig() { this.contextRunner.withUserConfiguration(TestHazelcastConfig.class).run((context) -> { assertThat(context).getBean(InstanceEventStore.class).isInstanceOf(HazelcastEventStore.class); assertThat(context).getBean(NotificationTrigger.class).isInstanceOf(HazelcastNotificationTrigger.class); }); } public static class TestHazelcastConfig { @Bean public Config config() { return new Config(); } @Bean public Notifier notifier() { return (e) -> Mono.empty(); } } } ================================================ FILE: spring-boot-admin-server/src/test/java/de/codecentric/boot/admin/server/config/AdminServerCloudFoundryAutoConfigurationTest.java ================================================ /* * 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.server.config; import org.junit.jupiter.api.Test; import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.hazelcast.autoconfigure.HazelcastAutoConfiguration; import org.springframework.boot.http.client.autoconfigure.reactive.ReactiveHttpClientAutoConfiguration; import org.springframework.boot.test.context.runner.WebApplicationContextRunner; import org.springframework.boot.webclient.autoconfigure.WebClientAutoConfiguration; import org.springframework.boot.webmvc.autoconfigure.WebMvcAutoConfiguration; import de.codecentric.boot.admin.server.services.CloudFoundryInstanceIdGenerator; import de.codecentric.boot.admin.server.services.HashingInstanceUrlIdGenerator; import de.codecentric.boot.admin.server.services.InstanceIdGenerator; import de.codecentric.boot.admin.server.web.client.CloudFoundryHttpHeaderProvider; import static org.assertj.core.api.Assertions.assertThat; class AdminServerCloudFoundryAutoConfigurationTest { private final WebApplicationContextRunner contextRunner = new WebApplicationContextRunner() .withConfiguration(AutoConfigurations.of(ReactiveHttpClientAutoConfiguration.class, WebClientAutoConfiguration.class, HazelcastAutoConfiguration.class, WebMvcAutoConfiguration.class, AdminServerAutoConfiguration.class, AdminServerCloudFoundryAutoConfiguration.class)) .withUserConfiguration(AdminServerMarkerConfiguration.class); @Test void non_cloud_platform() { this.contextRunner.run((context) -> { assertThat(context).doesNotHaveBean(CloudFoundryHttpHeaderProvider.class); assertThat(context).getBean(InstanceIdGenerator.class).isInstanceOf(HashingInstanceUrlIdGenerator.class); }); } @Test void cloudfoundry() { this.contextRunner.withPropertyValues("VCAP_APPLICATION:{}").run((context) -> { assertThat(context).hasSingleBean(CloudFoundryHttpHeaderProvider.class); assertThat(context).getBean(InstanceIdGenerator.class).isInstanceOf(CloudFoundryInstanceIdGenerator.class); }); } } ================================================ FILE: spring-boot-admin-server/src/test/java/de/codecentric/boot/admin/server/config/AdminServerInstanceWebClientConfigurationTest.java ================================================ /* * 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.server.config; import org.junit.jupiter.api.Test; import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.http.client.autoconfigure.reactive.ReactiveHttpClientAutoConfiguration; import org.springframework.boot.test.context.runner.WebApplicationContextRunner; import org.springframework.boot.webclient.autoconfigure.WebClientAutoConfiguration; import org.springframework.boot.webmvc.autoconfigure.WebMvcAutoConfiguration; import de.codecentric.boot.admin.server.web.client.BasicAuthHttpHeaderProvider; import de.codecentric.boot.admin.server.web.client.InstanceExchangeFilterFunction; import de.codecentric.boot.admin.server.web.client.InstanceWebClient; import de.codecentric.boot.admin.server.web.client.LegacyEndpointConverter; import static org.assertj.core.api.Assertions.assertThat; class AdminServerInstanceWebClientConfigurationTest { private final WebApplicationContextRunner contextRunner = new WebApplicationContextRunner() .withConfiguration(AutoConfigurations.of(ReactiveHttpClientAutoConfiguration.class, WebClientAutoConfiguration.class, WebMvcAutoConfiguration.class, AdminServerAutoConfiguration.class, AdminServerInstanceWebClientConfiguration.class)) .withUserConfiguration(AdminServerMarkerConfiguration.class); @Test void simpleConfig() { this.contextRunner.run((context) -> { assertThat(context).hasSingleBean(InstanceWebClient.Builder.class); assertThat(context).hasBean("filterInstanceWebClientCustomizer"); assertThat(context).hasSingleBean(BasicAuthHttpHeaderProvider.class); assertThat(context).getBeanNames(InstanceExchangeFilterFunction.class) .containsExactly("addHeadersInstanceExchangeFilter", "rewriteEndpointUrlInstanceExchangeFilter", "setDefaultAcceptHeaderInstanceExchangeFilter", "legacyEndpointConverterInstanceExchangeFilter", "logfileAcceptWorkaround", "cookieHandlingInstanceExchangeFilter", "retryInstanceExchangeFilter", "timeoutInstanceExchangeFilter"); assertThat(context).getBeanNames(LegacyEndpointConverter.class) .containsExactly("healthLegacyEndpointConverter", "infoLegacyEndpointConverter", "envLegacyEndpointConverter", "httptraceLegacyEndpointConverter", "threaddumpLegacyEndpointConverter", "liquibaseLegacyEndpointConverter", "flywayLegacyEndpointConverter", "beansLegacyEndpointConverter", "configpropsLegacyEndpointConverter", "mappingsLegacyEndpointConverter", "startupLegacyEndpointConverter"); }); } } ================================================ FILE: spring-boot-admin-server/src/test/java/de/codecentric/boot/admin/server/config/AdminServerNotifierAutoConfigurationTest.java ================================================ /* * Copyright 2014-2024 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.server.config; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.autoconfigure.AutoConfigureAfter; import org.springframework.boot.hazelcast.autoconfigure.HazelcastAutoConfiguration; import org.springframework.boot.http.client.autoconfigure.reactive.ReactiveHttpClientAutoConfiguration; import org.springframework.boot.test.context.runner.WebApplicationContextRunner; import org.springframework.boot.webclient.autoconfigure.WebClientAutoConfiguration; import org.springframework.boot.webmvc.autoconfigure.WebMvcAutoConfiguration; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Primary; import org.springframework.mail.javamail.JavaMailSenderImpl; import de.codecentric.boot.admin.server.notify.CompositeNotifier; import de.codecentric.boot.admin.server.notify.DiscordNotifier; import de.codecentric.boot.admin.server.notify.HipchatNotifier; import de.codecentric.boot.admin.server.notify.LetsChatNotifier; import de.codecentric.boot.admin.server.notify.MailNotifier; import de.codecentric.boot.admin.server.notify.MattermostNotifier; import de.codecentric.boot.admin.server.notify.MicrosoftTeamsNotifier; import de.codecentric.boot.admin.server.notify.NotificationTrigger; import de.codecentric.boot.admin.server.notify.Notifier; import de.codecentric.boot.admin.server.notify.NotifierProxyProperties; import de.codecentric.boot.admin.server.notify.OpsGenieNotifier; import de.codecentric.boot.admin.server.notify.PagerdutyNotifier; import de.codecentric.boot.admin.server.notify.RocketChatNotifier; import de.codecentric.boot.admin.server.notify.SlackNotifier; import de.codecentric.boot.admin.server.notify.TelegramNotifier; import de.codecentric.boot.admin.server.notify.TestNotifier; import de.codecentric.boot.admin.server.notify.WebexNotifier; import static org.assertj.core.api.Assertions.assertThat; class AdminServerNotifierAutoConfigurationTest { private final WebApplicationContextRunner contextRunner = new WebApplicationContextRunner() .withConfiguration(AutoConfigurations.of(ReactiveHttpClientAutoConfiguration.class, WebClientAutoConfiguration.class, HazelcastAutoConfiguration.class, WebMvcAutoConfiguration.class, AdminServerAutoConfiguration.class, AdminServerNotifierAutoConfiguration.class)) .withUserConfiguration(AdminServerMarkerConfiguration.class); @Test void test_notifierListener() { this.contextRunner.withUserConfiguration(TestSingleNotifierConfig.class).run((context) -> { assertThat(context).getBean(Notifier.class).isInstanceOf(TestNotifier.class); assertThat(context).getBeans(Notifier.class).hasSize(1); }); } @Test void test_no_notifierListener() { this.contextRunner.run((context) -> assertThat(context).doesNotHaveBean(NotificationTrigger.class)); } @Test void test_mail() { this.contextRunner.withUserConfiguration(MailSenderConfig.class) .run((context) -> assertThat(context).getBean(MailNotifier.class).isInstanceOf(MailNotifier.class)); } @Test void test_hipchat() { this.contextRunner.withPropertyValues("spring.boot.admin.notify.hipchat.url:https://example.com") .run((context) -> assertThat(context).hasSingleBean(HipchatNotifier.class)); } @Test void test_letschat() { this.contextRunner.withPropertyValues("spring.boot.admin.notify.letschat.url:https://example.com") .run((context) -> assertThat(context).hasSingleBean(LetsChatNotifier.class)); } @Test void test_slack() { this.contextRunner.withPropertyValues("spring.boot.admin.notify.slack.webhook-url:https://example.com") .run((context) -> assertThat(context).hasSingleBean(SlackNotifier.class)); } @Test void test_mattermost() { this.contextRunner.withPropertyValues("spring.boot.admin.notify.mattermost.api-url:https://example.com") .run((context) -> assertThat(context).hasSingleBean(MattermostNotifier.class)); } @Test void test_pagerduty() { this.contextRunner.withPropertyValues("spring.boot.admin.notify.pagerduty.service-key:foo") .run((context) -> assertThat(context).hasSingleBean(PagerdutyNotifier.class)); } @Test void test_opsgenie() { this.contextRunner.withPropertyValues("spring.boot.admin.notify.opsgenie.api-key:foo") .run((context) -> assertThat(context).hasSingleBean(OpsGenieNotifier.class)); } @Test void test_ms_teams() { this.contextRunner.withPropertyValues("spring.boot.admin.notify.ms-teams.webhook-url:https://example.com") .run((context) -> assertThat(context).hasSingleBean(MicrosoftTeamsNotifier.class)); } @Test void test_telegram() { this.contextRunner .withPropertyValues( "spring.boot.admin.notify.telegram.auth-token:123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11") .run((context) -> assertThat(context).hasSingleBean(TelegramNotifier.class)); } @Test void test_discord() { this.contextRunner.withPropertyValues("spring.boot.admin.notify.discord.webhook-url:https://example.com") .run((context) -> assertThat(context).hasSingleBean(DiscordNotifier.class)); } @Test void test_rocketchat() { this.contextRunner.withPropertyValues("spring.boot.admin.notify.rocketchat.url:https://example.com") .run((context) -> assertThat(context).hasSingleBean(RocketChatNotifier.class)); } @Test void test_webex() { this.contextRunner .withPropertyValues("spring.boot.admin.notify.webex.auth-token:123456:abtshubzztk-abtabhixta-788654") .run((context) -> assertThat(context).hasSingleBean(WebexNotifier.class)); } @Test void test_multipleNotifiers() { this.contextRunner.withUserConfiguration(TestMultipleNotifierConfig.class).run((context) -> { assertThat(context.getBean(Notifier.class)).isInstanceOf(CompositeNotifier.class); assertThat(context).getBeans(Notifier.class).hasSize(3); }); } @Test void test_multipleNotifiersWithPrimary() { this.contextRunner.withUserConfiguration(TestMultipleWithPrimaryNotifierConfig.class).run((context) -> { assertThat(context.getBean(Notifier.class)).isInstanceOf(TestNotifier.class); assertThat(context).getBeans(Notifier.class).hasSize(2); }); } @Test void test_notifierProxyProperties() { this.contextRunner.withPropertyValues("spring.boot.admin.notify.proxy.host") .run((context) -> assertThat(context).hasSingleBean(NotifierProxyProperties.class)); } @Test void test_autoConfigureAfterAnnotationReferencesExistingClass() { // Get the @AutoConfigureAfter annotation from // AdminServerNotifierAutoConfiguration AutoConfigureAfter autoConfigureAfter = AdminServerNotifierAutoConfiguration.class .getAnnotation(org.springframework.boot.autoconfigure.AutoConfigureAfter.class); assertThat(autoConfigureAfter).isNotNull(); // Get the class names from the annotation String[] classNames = autoConfigureAfter.name(); assertThat(classNames).isNotEmpty(); // Verify that the class can be loaded for (String className : classNames) { try { Class.forName(className); } catch (ClassNotFoundException ex) { throw new AssertionError( "Class referenced in @AutoConfigureAfter annotation does not exist: " + className, ex); } } } public static class TestSingleNotifierConfig { @Bean @Qualifier("testNotifier") public TestNotifier testNotifier() { return new TestNotifier(); } } public static class MailSenderConfig { @Bean public JavaMailSenderImpl mailSender() { return new JavaMailSenderImpl(); } } public static class TestMultipleNotifierConfig { @Bean @Qualifier("testNotifier1") public TestNotifier testNotifier1() { return new TestNotifier(); } @Bean @Qualifier("testNotifier2") public TestNotifier testNotifier2() { return new TestNotifier(); } } public static class TestMultipleWithPrimaryNotifierConfig { @Bean @Primary @Qualifier("testNotifier") public TestNotifier testNotifierPrimary() { return new TestNotifier(); } @Bean @Qualifier("testNotifier3") public TestNotifier testNotifier2() { return new TestNotifier(); } } } ================================================ FILE: spring-boot-admin-server/src/test/java/de/codecentric/boot/admin/server/config/AdminServerPropertiesTest.java ================================================ /* * 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.server.config; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.test.context.TestPropertySource; import org.springframework.test.context.junit.jupiter.SpringExtension; import static org.assertj.core.api.Assertions.assertThat; @ExtendWith(SpringExtension.class) @EnableConfigurationProperties(AdminServerProperties.class) @TestPropertySource("classpath:server-config-test.properties") class AdminServerPropertiesTest { @Autowired private AdminServerProperties serverConfig; @Test void testLoadConfigurationProperties() { assertThat(serverConfig.getContextPath()).isEqualTo("/admin"); assertThat(serverConfig.getInstanceAuth().getDefaultUserName()).isEqualTo("admin"); assertThat(serverConfig.getInstanceAuth().getDefaultPassword()).isEqualTo("topsecret"); assertThat(serverConfig.getInstanceAuth().getServiceMap().get("my-service").getUserName()).isEqualTo("me"); assertThat(serverConfig.getInstanceAuth().getServiceMap().get("my-service").getUserPassword()) .isEqualTo("secret"); } } ================================================ FILE: spring-boot-admin-server/src/test/java/de/codecentric/boot/admin/server/config/SpringBootAdminServerEnabledConditionTest.java ================================================ /* * 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.server.config; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.mockito.BDDMockito; import org.springframework.context.annotation.ConditionContext; import org.springframework.core.type.AnnotatedTypeMetadata; import org.springframework.mock.env.MockEnvironment; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.mock; class SpringBootAdminServerEnabledConditionTest { private SpringBootAdminServerEnabledCondition condition; private AnnotatedTypeMetadata annotatedTypeMetadata; private ConditionContext conditionContext; @BeforeEach void setUp() { condition = new SpringBootAdminServerEnabledCondition(); annotatedTypeMetadata = mock(AnnotatedTypeMetadata.class); conditionContext = mock(ConditionContext.class); } @Test void test_server_enabled() { MockEnvironment environment = new MockEnvironment(); BDDMockito.given(conditionContext.getEnvironment()).willReturn(environment); assertThat(condition.getMatchOutcome(conditionContext, annotatedTypeMetadata).isMatch()).isTrue(); } @Test void test_server_disabled() { MockEnvironment environment = new MockEnvironment(); environment.setProperty("spring.boot.admin.server.enabled", "false"); BDDMockito.given(conditionContext.getEnvironment()).willReturn(environment); assertThat(condition.getMatchOutcome(conditionContext, annotatedTypeMetadata).isMatch()).isFalse(); } } ================================================ FILE: spring-boot-admin-server/src/test/java/de/codecentric/boot/admin/server/domain/entities/AbstractInstanceRepositoryTest.java ================================================ /* * 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.server.domain.entities; import java.util.concurrent.atomic.AtomicLong; import org.junit.jupiter.api.Test; import org.opentest4j.AssertionFailedError; import reactor.core.publisher.Mono; import reactor.test.StepVerifier; import de.codecentric.boot.admin.server.domain.values.Endpoints; import de.codecentric.boot.admin.server.domain.values.InstanceId; import de.codecentric.boot.admin.server.domain.values.Registration; import static org.assertj.core.api.Assertions.assertThat; public abstract class AbstractInstanceRepositoryTest { private final Instance instance1 = Instance.create(InstanceId.of("app-1")) .register(Registration.create("app", "https://health").build()); private final Instance instance2 = Instance.create(InstanceId.of("app-2")) .register(Registration.create("app", "https://health").build()); private final Instance instance3 = Instance.create(InstanceId.of("other-1")) .register(Registration.create("other", "https://health").build()); private InstanceRepository repository; public void setUp(InstanceRepository repository) { this.repository = repository; } @Test public void should_save() { // when StepVerifier.create(this.repository.save(this.instance1)).expectNext(this.instance1).verifyComplete(); // then StepVerifier.create(this.repository.find(this.instance1.getId())).expectNext(this.instance1).verifyComplete(); } @Test public void should_find_instances() { // given StepVerifier.create(this.repository.save(this.instance1)).expectNextCount(1).verifyComplete(); StepVerifier.create(this.repository.save(this.instance2)).expectNextCount(1).verifyComplete(); StepVerifier.create(this.repository.save(this.instance3)).expectNextCount(1).verifyComplete(); // when/then StepVerifier.create(this.repository.find(this.instance2.getId())).expectNext(this.instance2).verifyComplete(); StepVerifier.create(this.repository.findByName("app").collectList()) .assertNext((v) -> assertThat(v).containsExactlyInAnyOrder(this.instance1, this.instance2)) .verifyComplete(); StepVerifier.create(this.repository.findAll().collectList()) .assertNext((v) -> assertThat(v).containsExactlyInAnyOrder(this.instance1, this.instance2, this.instance3)) .verifyComplete(); } @Test public void should_computeIfPresent() { AtomicLong counter = new AtomicLong(3L); Endpoints infoEndpoint = Endpoints.single("info", "info"); // given StepVerifier.create(this.repository.save(this.instance1)).expectNextCount(1).verifyComplete(); // when StepVerifier.create(this.repository.computeIfPresent(this.instance1.getId(), (key, value) -> { if (counter.getAndDecrement() > 0L) { return Mono.just(this.instance1); // causes OptimisticLockingException } else { return Mono.just(value.withEndpoints(infoEndpoint)); } })).expectNext(this.instance1.withEndpoints(infoEndpoint)).verifyComplete(); // then StepVerifier.create(this.repository.find(this.instance1.getId())) .expectNext(this.instance1.withEndpoints(infoEndpoint)) .verifyComplete(); } @Test public void should_not_compute_if_not_present() { // given InstanceId instanceId = InstanceId.of("not-existent"); // when StepVerifier .create(this.repository.computeIfPresent(instanceId, (key, application) -> Mono.error(new AssertionFailedError("Should not call any computation")))) .verifyComplete(); // then StepVerifier.create(this.repository.find(instanceId)).verifyComplete(); } @Test public void should_run_compute_with_null() { InstanceId instanceId = InstanceId.of("app-1"); Registration registration = Registration.create("app", "https://health").build(); // when StepVerifier.create(this.repository.compute(this.instance1.getId(), (key, application) -> { assertThat(application).isNull(); return Mono.just(Instance.create(key).register(registration)); })).assertNext((v) -> { assertThat(v.getId()).isEqualTo(instanceId); assertThat(v.getRegistration()).isEqualTo(registration); }).verifyComplete(); // then StepVerifier.create(this.repository.find(instanceId)).assertNext((v) -> { assertThat(v.getId()).isEqualTo(instanceId); assertThat(v.getRegistration()).isEqualTo(registration); }).verifyComplete(); } @Test public void should_retry_compute() { AtomicLong counter = new AtomicLong(3L); Endpoints infoEndpoint = Endpoints.single("info", "info"); // given StepVerifier.create(this.repository.save(this.instance1)).expectNextCount(1).verifyComplete(); // when StepVerifier.create(this.repository.compute(this.instance1.getId(), (key, value) -> { if (counter.getAndDecrement() > 0L) { return Mono.just(this.instance1); // causes OptimisticLockingException } else { return Mono.just(value.withEndpoints(infoEndpoint)); } })).expectNext(this.instance1.withEndpoints(infoEndpoint)).verifyComplete(); // then StepVerifier.create(this.repository.find(this.instance1.getId())) .expectNext(this.instance1.withEndpoints(infoEndpoint)) .verifyComplete(); } } ================================================ FILE: spring-boot-admin-server/src/test/java/de/codecentric/boot/admin/server/domain/entities/EventsourcingInstanceRepositoryTest.java ================================================ /* * 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.server.domain.entities; import org.junit.jupiter.api.BeforeEach; import de.codecentric.boot.admin.server.eventstore.InMemoryEventStore; class EventsourcingInstanceRepositoryTest extends AbstractInstanceRepositoryTest { @BeforeEach void setUp() { super.setUp(new EventsourcingInstanceRepository(new InMemoryEventStore())); } } ================================================ FILE: spring-boot-admin-server/src/test/java/de/codecentric/boot/admin/server/domain/entities/InstanceTest.java ================================================ /* * 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.server.domain.entities; import java.util.List; import org.junit.jupiter.api.Test; import de.codecentric.boot.admin.server.domain.events.InstanceDeregisteredEvent; import de.codecentric.boot.admin.server.domain.events.InstanceEvent; import de.codecentric.boot.admin.server.domain.events.InstanceInfoChangedEvent; import de.codecentric.boot.admin.server.domain.values.BuildVersion; import de.codecentric.boot.admin.server.domain.values.Endpoints; import de.codecentric.boot.admin.server.domain.values.Info; import de.codecentric.boot.admin.server.domain.values.InstanceId; import de.codecentric.boot.admin.server.domain.values.Registration; import de.codecentric.boot.admin.server.domain.values.StatusInfo; import static java.util.Collections.singletonMap; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.assertj.core.api.Assertions.entry; class InstanceTest { @Test void invariants() { assertThatThrownBy(() -> Instance.create(null)).isInstanceOf(IllegalArgumentException.class) .hasMessage("'id' must not be null"); assertThatThrownBy(() -> Instance.create(InstanceId.of("id")).register(null)) .isInstanceOf(IllegalArgumentException.class) .hasMessage("'registration' must not be null"); assertThatThrownBy(() -> Instance.create(InstanceId.of("id")).withInfo(null)) .isInstanceOf(IllegalArgumentException.class) .hasMessage("'info' must not be null"); assertThatThrownBy(() -> Instance.create(InstanceId.of("id")).withStatusInfo(null)) .isInstanceOf(IllegalArgumentException.class) .hasMessage("'statusInfo' must not be null"); assertThatThrownBy(() -> Instance.create(InstanceId.of("id")).withEndpoints(null)) .isInstanceOf(IllegalArgumentException.class) .hasMessage("'endpoints' must not be null"); } @Test void should_track_unsaved_events() { Registration registration = Registration.create("foo", "https://health").build(); Info info = Info.from(singletonMap("foo", "bar")); Instance newInstance = Instance.create(InstanceId.of("id")); assertThat(newInstance.isRegistered()).isFalse(); assertThatThrownBy(newInstance::getRegistration).isInstanceOf(IllegalStateException.class); assertThat(newInstance.getInfo()).isEqualTo(Info.empty()); assertThat(newInstance.getStatusInfo()).isEqualTo(StatusInfo.ofUnknown()); assertThat(newInstance.getUnsavedEvents()).isEmpty(); Instance instance = newInstance.register(registration).register(registration); assertThat(instance.getRegistration()).isEqualTo(registration); assertThat(instance.isRegistered()).isTrue(); assertThat(instance.getVersion()).isZero(); Registration registration2 = Registration.create("foo2", "https://health").build(); instance = instance.register(registration2); assertThat(instance.getRegistration()).isEqualTo(registration2); assertThat(instance.isRegistered()).isTrue(); assertThat(instance.getVersion()).isEqualTo(1L); instance = instance.withStatusInfo(StatusInfo.ofUp()).withStatusInfo(StatusInfo.ofUp()); assertThat(instance.getStatusInfo()).isEqualTo(StatusInfo.ofUp()); assertThat(instance.getVersion()).isEqualTo(2L); instance = instance.withInfo(info).withInfo(info); assertThat(instance.getInfo()).isEqualTo(info); assertThat(instance.getVersion()).isEqualTo(3L); instance = instance.deregister().deregister(); assertThat(instance.isRegistered()).isFalse(); assertThat(instance.getRegistration()).isEqualTo(registration2); assertThat(instance.getInfo()).isEqualTo(Info.empty()); assertThat(instance.getStatusInfo()).isEqualTo(StatusInfo.ofUnknown()); assertThat(instance.getVersion()).isEqualTo(4L); assertThat(instance.getUnsavedEvents().stream().map(InstanceEvent::getType)).containsExactly("REGISTERED", "REGISTRATION_UPDATED", "STATUS_CHANGED", "INFO_CHANGED", "DEREGISTERED"); } @Test void should_yield_same_status_from_replaying() { Registration registration = Registration.create("foo-instance", "https://health") .metadata("version", "1.0.0") .build(); Instance instance = Instance.create(InstanceId.of("id")) .register(registration.toBuilder().clearMetadata().build()) .register(registration) .withEndpoints(Endpoints.single("info", "info")) .withStatusInfo(StatusInfo.ofUp()) .withInfo(Info.from(singletonMap("foo", "bar"))); Instance loaded = Instance.create(InstanceId.of("id")).apply(instance.getUnsavedEvents()); assertThat(loaded.getUnsavedEvents()).isEmpty(); assertThat(loaded.getRegistration()).isEqualTo(registration); assertThat(loaded.isRegistered()).isTrue(); assertThat(loaded.getStatusInfo()).isEqualTo(StatusInfo.ofUp()); assertThat(loaded.getStatusTimestamp()).isEqualTo(instance.getStatusTimestamp()); assertThat(loaded.getInfo()).isEqualTo(Info.from(singletonMap("foo", "bar"))); assertThat(loaded.getEndpoints()) .isEqualTo(Endpoints.single("info", "info").withEndpoint("health", "https://health")); assertThat(loaded.getVersion()).isEqualTo(4L); assertThat(loaded.getBuildVersion()).isEqualTo(BuildVersion.valueOf("1.0.0")); Instance deregisteredInstance = instance.deregister(); loaded = Instance.create(InstanceId.of("id")).apply(deregisteredInstance.getUnsavedEvents()); assertThat(loaded.getUnsavedEvents()).isEmpty(); assertThat(loaded.isRegistered()).isFalse(); assertThat(loaded.getInfo()).isEqualTo(Info.empty()); assertThat(loaded.getStatusInfo()).isEqualTo(StatusInfo.ofUnknown()); assertThat(loaded.getStatusTimestamp()).isEqualTo(deregisteredInstance.getStatusTimestamp()); assertThat(loaded.getEndpoints()).isEqualTo(Endpoints.empty()); assertThat(loaded.getVersion()).isEqualTo(5L); assertThat(loaded.getBuildVersion()).isNull(); } @Test void should_throw_when_applied_wrong_event() { Instance instance = Instance.create(InstanceId.of("id")); assertThatThrownBy(() -> instance.apply((InstanceEvent) null)).isInstanceOf(IllegalArgumentException.class) .hasMessage("'event' must not be null"); assertThatThrownBy(() -> instance.apply(new InstanceDeregisteredEvent(InstanceId.of("wrong"), 0L))) .isInstanceOf(IllegalArgumentException.class) .hasMessage("'event' must refer the same instance"); assertThatThrownBy(() -> instance.apply(new InstanceDeregisteredEvent(InstanceId.of("id"), 1L)) .apply(new InstanceDeregisteredEvent(InstanceId.of("id"), 1L))).isInstanceOf(IllegalArgumentException.class) .hasMessage("Event 1 must be greater or equal to 2"); } @Test void should_update_buildVersion() { Instance instance = Instance.create(InstanceId.of("id")); assertThat(instance.getBuildVersion()).isNull(); Registration registration = Registration.create("foo-instance", "https://health") .metadata("version", "1.0.0") .build(); instance = instance.register(registration).withInfo(Info.empty()); assertThat(instance.getBuildVersion()).isEqualTo(BuildVersion.valueOf("1.0.0")); instance = instance.register(registration.toBuilder().clearMetadata().build()); assertThat(instance.getBuildVersion()).isNull(); instance = instance.withInfo(Info.from(singletonMap("build", singletonMap("version", "2.1.1")))); assertThat(instance.getBuildVersion()).isEqualTo(BuildVersion.valueOf("2.1.1")); instance = instance.deregister(); assertThat(instance.getBuildVersion()).isNull(); } @Test void should_extract_tags() { Instance instance = Instance.create(InstanceId.of("id")); assertThat(instance.getTags().getValues()).isEmpty(); Registration registration = Registration.create("foo-instance", "https://health") .metadata("tags.environment", "test") .metadata("tags.region", "EU") .build(); instance = instance.register(registration); assertThat(instance.getTags().getValues()).containsExactly(entry("environment", "test"), entry("region", "EU")); instance = instance.withInfo(Info.from(singletonMap("tags", singletonMap("region", "US-East")))); assertThat(instance.getTags().getValues()).containsExactly(entry("environment", "test"), entry("region", "US-East")); instance = instance.deregister(); assertThat(instance.getTags().getValues()).isEmpty(); instance = instance.register(registration.toBuilder().clearMetadata().build()); assertThat(instance.getTags().getValues()).isEmpty(); } @Test void should_rebuild_instance() { Instance instance = Instance.create(InstanceId.of("id")) .register(Registration.create("test", "http://test").build()) .withInfo(Info.from(singletonMap("info", "remove"))) .withInfo(Info.from(singletonMap("info", "test2"))); List relevantEvents = instance.getUnsavedEvents() .stream() .filter((e) -> !(e instanceof InstanceInfoChangedEvent infoChangedEvent && infoChangedEvent.getInfo().getValues().get("info").equals("remove"))) .toList(); Instance rebuilt = Instance.create(InstanceId.of("id")).apply(relevantEvents); assertThat(rebuilt).isEqualTo(instance); } } ================================================ FILE: spring-boot-admin-server/src/test/java/de/codecentric/boot/admin/server/domain/entities/SnapshottingInstanceRepositoryTest.java ================================================ /* * 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.server.domain.entities; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import reactor.test.StepVerifier; import de.codecentric.boot.admin.server.domain.events.InstanceRegisteredEvent; import de.codecentric.boot.admin.server.domain.values.InstanceId; import de.codecentric.boot.admin.server.domain.values.Registration; import de.codecentric.boot.admin.server.domain.values.StatusInfo; import de.codecentric.boot.admin.server.eventstore.InMemoryEventStore; import de.codecentric.boot.admin.server.eventstore.OptimisticLockingException; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.never; import static org.mockito.Mockito.reset; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; class SnapshottingInstanceRepositoryTest extends AbstractInstanceRepositoryTest { private final Instance instance = Instance.create(InstanceId.of("app-1")) .register(Registration.create("app", "https://health").build()); private final InMemoryEventStore eventStore = spy(new InMemoryEventStore()); private SnapshottingInstanceRepository repository; @BeforeEach void setUp() { this.repository = new SnapshottingInstanceRepository(this.eventStore); this.repository.start(); super.setUp(this.repository); } @AfterEach void tearDown() { this.repository.stop(); } @Test void should_return_instance_from_cache() { // given StepVerifier.create(this.repository.save(this.instance)).expectNext(this.instance).verifyComplete(); // when reset(this.eventStore); StepVerifier.create(this.repository.find(this.instance.getId())).expectNext(this.instance).verifyComplete(); // then verify(this.eventStore, never()).find(any()); } @Test void should_return_all_instances_from_cache() { // given StepVerifier.create(this.repository.save(this.instance)).expectNext(this.instance).verifyComplete(); // when reset(this.eventStore); StepVerifier.create(this.repository.findAll()).expectNext(this.instance).verifyComplete(); // then verify(this.eventStore, never()).findAll(); } @Test void should_update_cache_after_error() { // given this.repository.stop(); when(this.eventStore.findAll()).thenReturn( Flux.just(new InstanceRegisteredEvent(InstanceId.of("broken"), 0L, this.instance.getRegistration()), new InstanceRegisteredEvent(InstanceId.of("broken"), 0L, this.instance.getRegistration()), new InstanceRegisteredEvent(this.instance.getId(), 0L, this.instance.getRegistration()), new InstanceRegisteredEvent(InstanceId.of("broken"), 1L, this.instance.getRegistration()))); // when this.repository.start(); // then reset(this.eventStore); StepVerifier.create(this.repository.find(this.instance.getId())).expectNext(this.instance).verifyComplete(); StepVerifier.create(this.repository.find(InstanceId.of("broken"))) .assertNext((i) -> assertThat(i.getVersion()).isEqualTo(1L)) .verifyComplete(); } @Test void should_return_outdated_instance_not_present_in_cache() { this.repository.stop(); // given StepVerifier.create(this.repository.save(this.instance)).expectNext(this.instance).verifyComplete(); StepVerifier.create(this.repository.save(this.instance)).verifyError(OptimisticLockingException.class); // when StepVerifier.create(this.repository.find(this.instance.getId())).expectNext(this.instance).verifyComplete(); } @Test void should_refresh_snapshots_eagerly_on_optimistic_locking_exception() { // given StepVerifier.create(this.repository.save(this.instance)).expectNextCount(1L).verifyComplete(); this.repository.stop(); StepVerifier .create(this.repository.save(this.instance.clearUnsavedEvents().withStatusInfo(StatusInfo.ofDown()))) .expectNextCount(1L) .verifyComplete(); // when StepVerifier .create(this.repository.computeIfPresent(this.instance.getId(), (id, i) -> Mono.just(i.withStatusInfo(StatusInfo.ofUp())))) .expectNextCount(1L) .verifyComplete(); } } ================================================ FILE: spring-boot-admin-server/src/test/java/de/codecentric/boot/admin/server/domain/values/BuildVersionTest.java ================================================ /* * 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.server.domain.values; import org.junit.jupiter.api.Test; import static java.util.Collections.emptyMap; import static java.util.Collections.singletonMap; import static org.assertj.core.api.Assertions.assertThat; class BuildVersionTest { @Test void should_return_version() { assertThat(BuildVersion.valueOf(null).getValue()).isEqualTo("UNKNOWN"); assertThat(BuildVersion.from(emptyMap())).isNull(); assertThat(BuildVersion.from(singletonMap("version", "1.0.0"))).isEqualTo(BuildVersion.valueOf("1.0.0")); assertThat(BuildVersion.from(singletonMap("build.version", "1.0.0"))).isEqualTo(BuildVersion.valueOf("1.0.0")); assertThat(BuildVersion.from(singletonMap("build", singletonMap("version", "1.0.0")))) .isEqualTo(BuildVersion.valueOf("1.0.0")); } @Test void should_return_simple_string() { assertThat(BuildVersion.valueOf("1.0.0")).hasToString("1.0.0"); } @Test void compare() { assertThat(doCompare("1.0.0", "1.0.0")).isZero(); assertThat(doCompare("1.0.1", "1.0.0")).isEqualTo(1); assertThat(doCompare("1.0.0", "1.0.1")).isEqualTo(-1); assertThat(doCompare("1.1.0", "1.0.0")).isEqualTo(1); assertThat(doCompare("1.0.0", "1.1.0")).isEqualTo(-1); assertThat(doCompare("2.0.0", "1.0.0")).isEqualTo(1); assertThat(doCompare("1.0.0", "2.0.0")).isEqualTo(-1); assertThat(doCompare("1.0.0.0", "1.0.0")).isEqualTo(1); assertThat(doCompare("1.0.0", "1.0.0.0")).isEqualTo(-1); assertThat(doCompare("1.11.0", "1.2.0")).isEqualTo(1); assertThat(doCompare("1.2.0", "1.11.0")).isEqualTo(-1); assertThat(doCompare("1.0.0.RC1", "1.0.0.RC1")).isZero(); assertThat(doCompare("1.0.0.RC2", "1.0.0.RC1")).isEqualTo(1); assertThat(doCompare("1.0.0.RC1", "1.0.0.RC2")).isEqualTo(-1); assertThat(doCompare("1.0.1.RC1", "1.0.0.RC1")).isEqualTo(1); assertThat(doCompare("1.0.0.RC1", "1.0.1.RC1")).isEqualTo(-1); assertThat(doCompare("1.0.0-beta1", "1.0.0-beta1")).isZero(); assertThat(doCompare("1.0.0-beta2", "1.0.0-beta1")).isEqualTo(1); assertThat(doCompare("1.0.0-beta1", "1.0.0-beta2")).isEqualTo(-1); } private int doCompare(String v1, String v2) { return BuildVersion.valueOf(v1).compareTo(BuildVersion.valueOf(v2)); } } ================================================ FILE: spring-boot-admin-server/src/test/java/de/codecentric/boot/admin/server/domain/values/EndpointTest.java ================================================ /* * 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.server.domain.values; import org.junit.jupiter.api.Test; import static org.assertj.core.api.Assertions.assertThatThrownBy; class EndpointTest { @Test void invariants() { assertThatThrownBy(() -> Endpoint.of("", "")).isInstanceOf(IllegalArgumentException.class) .hasMessage("'id' must not be empty."); assertThatThrownBy(() -> Endpoint.of("id", "")).isInstanceOf(IllegalArgumentException.class) .hasMessage("'url' must not be empty."); } } ================================================ FILE: spring-boot-admin-server/src/test/java/de/codecentric/boot/admin/server/domain/values/EndpointsTest.java ================================================ /* * 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.server.domain.values; import java.util.Collections; import org.junit.jupiter.api.Test; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; class EndpointsTest { @Test void should_return_endpoint_or_empty() { Endpoints endpoints = Endpoints.single("id", "path"); assertThat(endpoints.isPresent("id")).isTrue(); assertThat(endpoints.get("id")).contains(Endpoint.of("id", "path")); assertThat(endpoints.get("none!")).isEmpty(); } @Test void factory_methods() { assertThat(Endpoints.empty()).isEqualTo(Endpoints.of(Collections.emptyList())).isEqualTo(Endpoints.of(null)); assertThat(Endpoints.of(Collections.singletonList(Endpoint.of("id", "path")))) .isEqualTo(Endpoints.empty().withEndpoint("id", "path")) .isEqualTo(Endpoints.single("id", "path")); } @Test void should_throw_on_iterator_modification() { Endpoints endpoints = Endpoints.single("id", "path"); assertThatThrownBy(() -> endpoints.iterator().remove()).isInstanceOf(UnsupportedOperationException.class); } } ================================================ FILE: spring-boot-admin-server/src/test/java/de/codecentric/boot/admin/server/domain/values/InfoTest.java ================================================ /* * 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.server.domain.values; import java.util.Iterator; import java.util.LinkedHashMap; import java.util.Map; import org.assertj.core.data.MapEntry; import org.junit.jupiter.api.Test; import static org.assertj.core.api.Assertions.assertThat; class InfoTest { @Test void should_keep_order() { Map map = new LinkedHashMap<>(); map.put("z", "1"); map.put("x", "2"); Iterator> iterator = Info.from(map).getValues().entrySet().iterator(); assertThat(iterator.next()).isEqualTo(MapEntry.entry("z", "1")); assertThat(iterator.next()).isEqualTo(MapEntry.entry("x", "2")); } } ================================================ FILE: spring-boot-admin-server/src/test/java/de/codecentric/boot/admin/server/domain/values/InstanceIdTest.java ================================================ /* * 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.server.domain.values; import org.junit.jupiter.api.Test; import static org.assertj.core.api.Assertions.assertThatThrownBy; class InstanceIdTest { @Test void invariants() { assertThatThrownBy(() -> InstanceId.of(null)).isInstanceOf(IllegalArgumentException.class) .hasMessage("'value' must have text"); assertThatThrownBy(() -> InstanceId.of("")).isInstanceOf(IllegalArgumentException.class) .hasMessage("'value' must have text"); } } ================================================ FILE: spring-boot-admin-server/src/test/java/de/codecentric/boot/admin/server/domain/values/RegistrationTest.java ================================================ /* * 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.server.domain.values; import org.assertj.core.api.WithAssertions; import org.junit.jupiter.api.Test; import static org.assertj.core.api.Assertions.assertThatThrownBy; class RegistrationTest implements WithAssertions { @Test void invariants() { assertThatThrownBy(() -> Registration.create(null, null).build()).isInstanceOf(IllegalArgumentException.class) .hasMessage("'name' must not be empty."); assertThatThrownBy(() -> Registration.create("test", null).build()).isInstanceOf(IllegalArgumentException.class) .hasMessage("'healthUrl' must not be empty."); assertThatThrownBy(() -> Registration.create("test", "invalid").build()) .isInstanceOf(IllegalArgumentException.class) .hasMessage("'healthUrl' is not valid: invalid"); assertThatThrownBy(() -> Registration.create("test", "https://example.com").managementUrl("invalid").build()) .isInstanceOf(IllegalArgumentException.class) .hasMessage("'managementUrl' is not valid: invalid"); assertThatThrownBy(() -> Registration.create("test", "https://example.com").serviceUrl("invalid").build()) .isInstanceOf(IllegalArgumentException.class) .hasMessage("'serviceUrl' is not valid: invalid"); } @Test void returnsNull_whenServiceUrlIsNull_evenIfMetadataHasOverride() { Registration reg = Registration.builder() .name("app") .healthUrl("https://example.com/actuator/health") .metadata("service-url", "https://override.example.com") .build(); assertThat(reg.getServiceUrl()).isNull(); } @Test void usesMetadataOverride_whenValidAbsoluteUrlProvided() { Registration reg = Registration.create("app", "https://example.com/actuator/health") .serviceUrl("https://base.example.com") .metadata("service-url", "https://override.example.com") .build(); assertThat(reg.getServiceUrl()).isEqualTo("https://override.example.com"); } @Test void fallsBackToOriginal_whenMetadataOverrideIsInvalidSyntax() { Registration reg = Registration.create("app", "https://example.com/actuator/health") .serviceUrl("https://base.example.com") .metadata("service-url", "http://exa mple.com") // invalide URI (Leerzeichen) .build(); assertThat(reg.getServiceUrl()).isEqualTo("https://base.example.com"); } @Test void keepsOriginal_whenNoMetadataOverridePresent() { Registration reg = Registration.create("app", "https://example.com/actuator/health") .serviceUrl("https://base.example.com") .metadata("other", "value") .build(); assertThat(reg.getServiceUrl()).isEqualTo("https://base.example.com"); } @Test void acceptsEmptyStringFromMetadata_evenThoughItIsRelative() { Registration reg = Registration.create("app", "https://example.com/actuator/health") .serviceUrl("https://base.example.com") .metadata("service-url", "") .build(); // new URI("") erzeugt eine relative, aber syntaktisch valide URI -> Methode gibt // "" zurück assertThat(reg.getServiceUrl()).isEqualTo(""); } } ================================================ FILE: spring-boot-admin-server/src/test/java/de/codecentric/boot/admin/server/domain/values/StatusInfoTest.java ================================================ /* * 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.server.domain.values; import java.util.HashMap; import java.util.List; import java.util.Map; import org.junit.jupiter.api.Test; import static de.codecentric.boot.admin.server.domain.values.StatusInfo.STATUS_DOWN; import static de.codecentric.boot.admin.server.domain.values.StatusInfo.STATUS_OFFLINE; import static de.codecentric.boot.admin.server.domain.values.StatusInfo.STATUS_OUT_OF_SERVICE; import static de.codecentric.boot.admin.server.domain.values.StatusInfo.STATUS_RESTRICTED; import static de.codecentric.boot.admin.server.domain.values.StatusInfo.STATUS_UNKNOWN; import static de.codecentric.boot.admin.server.domain.values.StatusInfo.STATUS_UP; import static java.util.Arrays.asList; import static java.util.Collections.singletonMap; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; class StatusInfoTest { @Test void invariants() { assertThatThrownBy(() -> StatusInfo.valueOf("")).isInstanceOf(IllegalArgumentException.class) .hasMessage("'status' must not be empty."); } @Test void test_isMethods() { assertThat(StatusInfo.valueOf("FOO").isUp()).isFalse(); assertThat(StatusInfo.valueOf("FOO").isDown()).isFalse(); assertThat(StatusInfo.valueOf("FOO").isUnknown()).isFalse(); assertThat(StatusInfo.valueOf("FOO").isOffline()).isFalse(); assertThat(StatusInfo.valueOf("FOO").isOutOfService()).isFalse(); assertThat(StatusInfo.valueOf("FOO").isRestricted()).isFalse(); assertThat(StatusInfo.ofUp().isUp()).isTrue(); assertThat(StatusInfo.ofUp().isDown()).isFalse(); assertThat(StatusInfo.ofUp().isUnknown()).isFalse(); assertThat(StatusInfo.ofUp().isOffline()).isFalse(); assertThat(StatusInfo.ofUp().isOutOfService()).isFalse(); assertThat(StatusInfo.ofUp().isRestricted()).isFalse(); assertThat(StatusInfo.ofDown().isUp()).isFalse(); assertThat(StatusInfo.ofDown().isDown()).isTrue(); assertThat(StatusInfo.ofDown().isUnknown()).isFalse(); assertThat(StatusInfo.ofDown().isOffline()).isFalse(); assertThat(StatusInfo.ofDown().isOutOfService()).isFalse(); assertThat(StatusInfo.ofDown().isRestricted()).isFalse(); assertThat(StatusInfo.ofUnknown().isUp()).isFalse(); assertThat(StatusInfo.ofUnknown().isDown()).isFalse(); assertThat(StatusInfo.ofUnknown().isUnknown()).isTrue(); assertThat(StatusInfo.ofUnknown().isOffline()).isFalse(); assertThat(StatusInfo.ofUnknown().isOutOfService()).isFalse(); assertThat(StatusInfo.ofUnknown().isRestricted()).isFalse(); assertThat(StatusInfo.ofOffline().isUp()).isFalse(); assertThat(StatusInfo.ofOffline().isDown()).isFalse(); assertThat(StatusInfo.ofOffline().isUnknown()).isFalse(); assertThat(StatusInfo.ofOffline().isOffline()).isTrue(); assertThat(StatusInfo.ofOffline().isOutOfService()).isFalse(); assertThat(StatusInfo.ofOffline().isRestricted()).isFalse(); assertThat(StatusInfo.ofOutOfService().isUp()).isFalse(); assertThat(StatusInfo.ofOutOfService().isDown()).isFalse(); assertThat(StatusInfo.ofOutOfService().isUnknown()).isFalse(); assertThat(StatusInfo.ofOutOfService().isOffline()).isFalse(); assertThat(StatusInfo.ofOutOfService().isOutOfService()).isTrue(); assertThat(StatusInfo.ofOutOfService().isRestricted()).isFalse(); assertThat(StatusInfo.ofRestricted().isUp()).isFalse(); assertThat(StatusInfo.ofRestricted().isDown()).isFalse(); assertThat(StatusInfo.ofRestricted().isUnknown()).isFalse(); assertThat(StatusInfo.ofRestricted().isOffline()).isFalse(); assertThat(StatusInfo.ofRestricted().isOutOfService()).isFalse(); assertThat(StatusInfo.ofRestricted().isRestricted()).isTrue(); } @Test void from_map_should_return_same_result() { Map map = new HashMap<>(); map.put("status", "UP"); map.put("details", singletonMap("foo", "bar")); assertThat(StatusInfo.from(map)).isEqualTo(StatusInfo.ofUp(singletonMap("foo", "bar"))); } @Test void factory_methods_with_details() { Map details = singletonMap("reason", "maintenance"); StatusInfo outOfService = StatusInfo.ofOutOfService(details); assertThat(outOfService.getStatus()).isEqualTo(STATUS_OUT_OF_SERVICE); assertThat(outOfService.getDetails()).containsEntry("reason", "maintenance"); assertThat(outOfService.isOutOfService()).isTrue(); StatusInfo restricted = StatusInfo.ofRestricted(details); assertThat(restricted.getStatus()).isEqualTo(STATUS_RESTRICTED); assertThat(restricted.getDetails()).containsEntry("reason", "maintenance"); assertThat(restricted.isRestricted()).isTrue(); } @Test void when_first_level_key_is_components() { Map map = new HashMap<>(); map.put("status", "UP"); map.put("components", singletonMap("foo", "bar")); assertThat(StatusInfo.from(map)).isEqualTo(StatusInfo.ofUp(singletonMap("foo", "bar"))); } @Test void should_sort_by_status_order() { List unordered = asList(STATUS_OUT_OF_SERVICE, STATUS_UNKNOWN, STATUS_OFFLINE, STATUS_DOWN, STATUS_UP, STATUS_RESTRICTED); List ordered = unordered.stream().sorted(StatusInfo.severity()).toList(); assertThat(ordered).containsExactly(STATUS_DOWN, STATUS_OUT_OF_SERVICE, STATUS_OFFLINE, STATUS_UNKNOWN, STATUS_RESTRICTED, STATUS_UP); } } ================================================ FILE: spring-boot-admin-server/src/test/java/de/codecentric/boot/admin/server/domain/values/TagsTest.java ================================================ /* * 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.server.domain.values; import java.util.Collections; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.Map; import org.junit.jupiter.api.Test; import static java.util.Collections.singletonMap; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.entry; class TagsTest { @Test void should_return_empty_from_factory_method() { assertThat(Tags.empty().getValues()).isEmpty(); assertThat(Tags.from(Collections.emptyMap())).isSameAs(Tags.empty()); } @Test void should_return_tags_from_flat_map() { Map flatTags = new LinkedHashMap<>(); flatTags.put("tags.env", "test"); flatTags.put("tags.foo", "bar"); flatTags.put("ignore", "ignored"); flatTags.put("tagsi", "ignored"); assertThat(Tags.from(flatTags, "tags").getValues()).containsExactly(entry("env", "test"), entry("foo", "bar")); } @Test void should_return_tags_from_nested_map() { Map tags = new LinkedHashMap<>(); tags.put("env", "test"); tags.put("foo", "bar"); Map nestedTags = new HashMap<>(); nestedTags.put("tags", tags); nestedTags.put("tagsi", singletonMap("ignore", "ignored")); assertThat(Tags.from(nestedTags, "tags").getValues()).containsExactly(entry("env", "test"), entry("foo", "bar")); } @Test void should_append_tags() { Tags tags = Tags.empty() .append(Tags.from(singletonMap("tags.env", "test"), "tags")) .append(Tags.from(singletonMap("env", "test2"))) .append(Tags.from(singletonMap("foo", "bar"))); assertThat(tags.getValues()).containsExactly(entry("env", "test2"), entry("foo", "bar")); } } ================================================ FILE: spring-boot-admin-server/src/test/java/de/codecentric/boot/admin/server/eventstore/AbstractEventStoreTest.java ================================================ /* * 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.server.eventstore; import java.time.Duration; import java.time.Instant; import java.util.List; import java.util.function.Function; import java.util.stream.LongStream; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Test; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import reactor.core.scheduler.Schedulers; import reactor.test.StepVerifier; import de.codecentric.boot.admin.server.domain.events.InstanceDeregisteredEvent; import de.codecentric.boot.admin.server.domain.events.InstanceEvent; import de.codecentric.boot.admin.server.domain.events.InstanceRegisteredEvent; import de.codecentric.boot.admin.server.domain.events.InstanceStatusChangedEvent; import de.codecentric.boot.admin.server.domain.values.InstanceId; import de.codecentric.boot.admin.server.domain.values.Registration; import de.codecentric.boot.admin.server.domain.values.StatusInfo; import static java.util.Arrays.asList; import static java.util.Collections.singletonList; import static org.assertj.core.api.Assertions.assertThat; public abstract class AbstractEventStoreTest { private static final Logger log = LoggerFactory.getLogger(AbstractEventStoreTest.class); private final InstanceId id = InstanceId.of("id"); private final Registration registration = Registration.create("foo", "https://health") .metadata("test", "dummy") .build(); protected abstract InstanceEventStore createStore(int maxLogSizePerAggregate); protected abstract void shutdownStore(); @AfterEach void tearDown() { this.shutdownStore(); } @Test public void should_store_events() { InstanceEventStore store = createStore(100); StepVerifier.create(store.findAll()).verifyComplete(); Instant now = Instant.now(); InstanceEvent event1 = new InstanceRegisteredEvent(id, 0L, now, registration); InstanceEvent eventOther = new InstanceRegisteredEvent(InstanceId.of("other"), 0L, now.plusMillis(10), registration); InstanceEvent event2 = new InstanceDeregisteredEvent(id, 1L, now.plusMillis(20)); StepVerifier.create(store) .expectSubscription() .then(() -> StepVerifier.create(store.append(singletonList(event1))).verifyComplete()) .expectNext(event1) .then(() -> StepVerifier.create(store.append(singletonList(eventOther))).verifyComplete()) .expectNext(eventOther) .then(() -> StepVerifier.create(store.append(singletonList(event2))).verifyComplete()) .expectNext(event2) .thenCancel() .verify(); StepVerifier.create(store.find(id)).expectNext(event1, event2).verifyComplete(); StepVerifier.create(store.find(InstanceId.of("-"))).verifyComplete(); StepVerifier.create(store.findAll()).expectNext(event1, eventOther, event2).verifyComplete(); } @Test public void should_shorten_log_on_exceeded_capacity() { InstanceEventStore store = createStore(2); InstanceEvent event1 = new InstanceRegisteredEvent(id, 0L, registration); InstanceEvent event2 = new InstanceStatusChangedEvent(id, 1L, StatusInfo.ofDown()); InstanceEvent event3 = new InstanceStatusChangedEvent(id, 2L, StatusInfo.ofUp()); StepVerifier.create(store.append(asList(event1, event2, event3))).verifyComplete(); StepVerifier.create(store.findAll()).expectNext(event1, event3).verifyComplete(); } @Test public void should_throw_optimistic_locking_exception() { InstanceEvent event0 = new InstanceRegisteredEvent(id, 0L, registration); InstanceEvent event1 = new InstanceStatusChangedEvent(id, 1L, StatusInfo.ofDown()); InstanceEvent event1b = new InstanceDeregisteredEvent(id, 1L); InstanceEventStore store = createStore(100); StepVerifier.create(store.append(asList(event0, event1))).verifyComplete(); StepVerifier.create(store.append(singletonList(event1b))).verifyError(OptimisticLockingException.class); } @Test public void concurrent_read_writes() { InstanceId instanceId = InstanceId.of("a"); InstanceEventStore store = createStore(500); Function eventFactory = (i) -> new InstanceDeregisteredEvent(instanceId, i); Flux eventGenerator = Flux.range(0, 500) .map(eventFactory) .buffer(2) .flatMap((events) -> store.append(events).onErrorResume(OptimisticLockingException.class, (ex) -> { log.info("skipped {}", ex.getMessage()); return Mono.empty(); }).delayElement(Duration.ofMillis(5L))); StepVerifier .create(eventGenerator.subscribeOn(Schedulers.newSingle("a")) .mergeWith(eventGenerator.subscribeOn(Schedulers.newSingle("a"))) .mergeWith(eventGenerator.subscribeOn(Schedulers.newSingle("a"))) .mergeWith(eventGenerator.subscribeOn(Schedulers.newSingle("a"))) .then()) .verifyComplete(); List versions = store.find(instanceId).map(InstanceEvent::getVersion).collectList().block(); List expected = LongStream.range(0, 500).boxed().toList(); assertThat(versions).containsExactlyElementsOf(expected); } } ================================================ FILE: spring-boot-admin-server/src/test/java/de/codecentric/boot/admin/server/eventstore/HazelcastEventStoreTest.java ================================================ /* * 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.server.eventstore; import com.hazelcast.config.Config; import com.hazelcast.core.Hazelcast; import com.hazelcast.core.HazelcastInstance; public class HazelcastEventStoreTest extends AbstractEventStoreTest { HazelcastInstance hazelcast; @Override protected InstanceEventStore createStore(int maxLogSizePerAggregate) { Config config = new Config(); config.getNetworkConfig().getJoin().getMulticastConfig().setEnabled(false); config.getNetworkConfig().getJoin().getAutoDetectionConfig().setEnabled(false); hazelcast = Hazelcast.newHazelcastInstance(config); return new HazelcastEventStore(maxLogSizePerAggregate, hazelcast.getMap("testList" + System.currentTimeMillis())); } @Override protected void shutdownStore() { if (this.hazelcast != null) { this.hazelcast.shutdown(); } } } ================================================ FILE: spring-boot-admin-server/src/test/java/de/codecentric/boot/admin/server/eventstore/HazelcastEventStoreWithClientConfigTest.java ================================================ /* * 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.server.eventstore; import java.util.List; import com.hazelcast.client.HazelcastClient; import com.hazelcast.client.config.ClientConfig; import com.hazelcast.core.HazelcastInstance; import com.hazelcast.map.IMap; import org.junit.jupiter.api.Tag; import org.testcontainers.containers.GenericContainer; import org.testcontainers.junit.jupiter.Container; import org.testcontainers.junit.jupiter.Testcontainers; import de.codecentric.boot.admin.server.domain.events.InstanceEvent; import de.codecentric.boot.admin.server.domain.values.InstanceId; @Testcontainers(disabledWithoutDocker = true) @Tag("docker") public class HazelcastEventStoreWithClientConfigTest extends AbstractEventStoreTest { @Container private static final GenericContainer hazelcastServer = new GenericContainer<>("hazelcast/hazelcast:4.2.2") .withExposedPorts(5701); private final HazelcastInstance hazelcast; public HazelcastEventStoreWithClientConfigTest() { this.hazelcast = createHazelcastInstance(); } @Override protected InstanceEventStore createStore(int maxLogSizePerAggregate) { IMap> eventLog = this.hazelcast.getMap("testList" + System.currentTimeMillis()); return new HazelcastEventStore(maxLogSizePerAggregate, eventLog); } @Override protected void shutdownStore() { this.hazelcast.shutdown(); } private HazelcastInstance createHazelcastInstance() { String address = hazelcastServer.getHost() + ":" + hazelcastServer.getMappedPort(5701); ClientConfig clientConfig = new ClientConfig(); clientConfig.getNetworkConfig().addAddress(address); return HazelcastClient.newHazelcastClient(clientConfig); } } ================================================ FILE: spring-boot-admin-server/src/test/java/de/codecentric/boot/admin/server/eventstore/HazelcastEventStoreWithServerConfigTest.java ================================================ /* * 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.server.eventstore; import java.util.List; import com.hazelcast.config.Config; import com.hazelcast.config.EvictionConfig; import com.hazelcast.config.EvictionPolicy; import com.hazelcast.config.InMemoryFormat; import com.hazelcast.config.MapConfig; import com.hazelcast.config.MergePolicyConfig; import com.hazelcast.config.TcpIpConfig; 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 static de.codecentric.boot.admin.server.config.AdminServerHazelcastAutoConfiguration.DEFAULT_NAME_EVENT_STORE_MAP; import static de.codecentric.boot.admin.server.config.AdminServerHazelcastAutoConfiguration.DEFAULT_NAME_SENT_NOTIFICATIONS_MAP; import static java.util.Collections.singletonList; public class HazelcastEventStoreWithServerConfigTest extends AbstractEventStoreTest { private final HazelcastInstance hazelcast; public HazelcastEventStoreWithServerConfigTest() { this.hazelcast = createHazelcastInstance(); } @Override protected InstanceEventStore createStore(int maxLogSizePerAggregate) { IMap> eventLogs = this.hazelcast .getMap("testList" + System.currentTimeMillis()); return new HazelcastEventStore(maxLogSizePerAggregate, eventLogs); } @Override protected void shutdownStore() { hazelcast.shutdown(); } private HazelcastInstance createHazelcastInstance() { Config config = createHazelcastConfig(); return Hazelcast.newHazelcastInstance(config); } // config from sample project private Config createHazelcastConfig() { // This map is used to store the events. // It should be configured to reliably hold all the data, // Spring Boot Admin will compact the events, if there are too many MapConfig eventStoreMap = new MapConfig(DEFAULT_NAME_EVENT_STORE_MAP).setInMemoryFormat(InMemoryFormat.OBJECT) .setBackupCount(1) .setMergePolicyConfig(new MergePolicyConfig(PutIfAbsentMergePolicy.class.getName(), 100)); // This map is used to deduplicate the notifications. // If data in this map gets lost it should not be a big issue as it will at most // lead to the same notification to be sent by multiple instances MapConfig sentNotificationsMap = new MapConfig(DEFAULT_NAME_SENT_NOTIFICATIONS_MAP) .setInMemoryFormat(InMemoryFormat.OBJECT) .setBackupCount(1) .setEvictionConfig(new EvictionConfig().setEvictionPolicy(EvictionPolicy.LRU)) .setMergePolicyConfig(new MergePolicyConfig(PutIfAbsentMergePolicy.class.getName(), 100)); Config config = new Config(); config.addMapConfig(eventStoreMap); config.addMapConfig(sentNotificationsMap); config.setProperty("hazelcast.jmx", "true"); // WARNING: This code setups a local cluster, you change it to fit your needs. config.getNetworkConfig().getJoin().getMulticastConfig().setEnabled(false); TcpIpConfig tcpIpConfig = config.getNetworkConfig().getJoin().getTcpIpConfig(); tcpIpConfig.setEnabled(true); tcpIpConfig.setMembers(singletonList("127.0.0.1")); return config; } } ================================================ FILE: spring-boot-admin-server/src/test/java/de/codecentric/boot/admin/server/eventstore/InMemoryEventStoreTest.java ================================================ /* * 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.server.eventstore; public class InMemoryEventStoreTest extends AbstractEventStoreTest { @Override protected InstanceEventStore createStore(int maxLogSizePerAggregate) { return new InMemoryEventStore(maxLogSizePerAggregate); } @Override protected void shutdownStore() { // NOOP; } } ================================================ FILE: spring-boot-admin-server/src/test/java/de/codecentric/boot/admin/server/notify/CompositeNotifierTest.java ================================================ /* * 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.server.notify; import java.util.Arrays; import org.junit.jupiter.api.Test; import reactor.core.publisher.Mono; import reactor.test.StepVerifier; import de.codecentric.boot.admin.server.domain.events.InstanceEvent; import de.codecentric.boot.admin.server.domain.events.InstanceStatusChangedEvent; import de.codecentric.boot.admin.server.domain.values.InstanceId; import de.codecentric.boot.admin.server.domain.values.StatusInfo; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; class CompositeNotifierTest { private static final InstanceEvent APP_DOWN = new InstanceStatusChangedEvent(InstanceId.of("-"), 0L, StatusInfo.ofDown()); @Test void should_throw_for_invariants() { assertThatThrownBy(() -> new CompositeNotifier(null)).isInstanceOf(IllegalArgumentException.class); } @Test void should_trigger_all_notifiers() { TestNotifier notifier1 = new TestNotifier(); TestNotifier notifier2 = new TestNotifier(); CompositeNotifier compositeNotifier = new CompositeNotifier(Arrays.asList(notifier1, notifier2)); StepVerifier.create(compositeNotifier.notify(APP_DOWN)).verifyComplete(); assertThat(notifier1.getEvents()).containsOnly(APP_DOWN); assertThat(notifier2.getEvents()).containsOnly(APP_DOWN); } @Test void should_continue_on_exception() { Notifier notifier1 = (ev) -> Mono.error(new IllegalStateException("Test")); TestNotifier notifier2 = new TestNotifier(); CompositeNotifier compositeNotifier = new CompositeNotifier(Arrays.asList(notifier1, notifier2)); StepVerifier.create(compositeNotifier.notify(APP_DOWN)).verifyComplete(); assertThat(notifier2.getEvents()).containsOnly(APP_DOWN); } } ================================================ FILE: spring-boot-admin-server/src/test/java/de/codecentric/boot/admin/server/notify/DingTalkNotifierTest.java ================================================ /* * 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.server.notify; import java.util.HashMap; import java.util.Map; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.http.HttpEntity; import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; import org.springframework.web.client.RestTemplate; import reactor.core.publisher.Mono; import reactor.test.StepVerifier; 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.InstanceStatusChangedEvent; import de.codecentric.boot.admin.server.domain.values.InstanceId; import de.codecentric.boot.admin.server.domain.values.Registration; import de.codecentric.boot.admin.server.domain.values.StatusInfo; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.clearInvocations; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; class DingTalkNotifierTest { private final Instance instance = Instance.create(InstanceId.of("-id-")) .register(Registration.create("DingTalk", "https://health").build()); private DingTalkNotifier notifier; private RestTemplate restTemplate; @BeforeEach void setUp() { InstanceRepository repository = mock(InstanceRepository.class); when(repository.find(instance.getId())).thenReturn(Mono.just(instance)); restTemplate = mock(RestTemplate.class); notifier = new DingTalkNotifier(repository, restTemplate); notifier.setWebhookUrl("https://dingtalk.com/"); notifier.setSecret("-secret-"); } @Test void test_onApplicationEvent_resolve() { StepVerifier .create(notifier .notify(new InstanceStatusChangedEvent(instance.getId(), instance.getVersion(), StatusInfo.ofDown()))) .verifyComplete(); clearInvocations(restTemplate); StepVerifier .create(notifier .notify(new InstanceStatusChangedEvent(instance.getId(), instance.getVersion(), StatusInfo.ofUp()))) .verifyComplete(); Object expected = expectedMessage(standardMessage("UP")); verify(restTemplate).postForEntity(any(String.class), eq(expected), eq(Void.class)); } @Test void test_onApplicationEvent_trigger() { StepVerifier .create(notifier .notify(new InstanceStatusChangedEvent(instance.getId(), instance.getVersion(), StatusInfo.ofUp()))) .verifyComplete(); StepVerifier .create(notifier .notify(new InstanceStatusChangedEvent(instance.getId(), instance.getVersion(), StatusInfo.ofDown()))) .verifyComplete(); Object expected = expectedMessage(standardMessage("DOWN")); verify(restTemplate).postForEntity(any(String.class), eq(expected), eq(Void.class)); } private HttpEntity> expectedMessage(String message) { Map messageJson = new HashMap<>(); messageJson.put("msgtype", "text"); Map content = new HashMap<>(); content.put("content", message); messageJson.put("text", content); HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_JSON); return new HttpEntity<>(messageJson, headers); } private String standardMessage(String status) { return instance.getRegistration().getName() + " " + instance.getId() + " is " + status; } } ================================================ FILE: spring-boot-admin-server/src/test/java/de/codecentric/boot/admin/server/notify/DiscordNotifierTest.java ================================================ /* * 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.server.notify; import java.net.URI; import java.util.HashMap; import java.util.Map; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.http.HttpEntity; import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; import org.springframework.web.client.RestTemplate; import reactor.core.publisher.Mono; import reactor.test.StepVerifier; 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.InstanceStatusChangedEvent; import de.codecentric.boot.admin.server.domain.values.InstanceId; import de.codecentric.boot.admin.server.domain.values.Registration; import de.codecentric.boot.admin.server.domain.values.StatusInfo; import static org.mockito.Mockito.clearInvocations; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; class DiscordNotifierTest { private static final String AVATAR_URL = "http://avatarUrl"; private static final String USER_NAME = "user"; private static final String APP_NAME = "App"; private static final URI WEBHOOK_URI = URI.create("http://localhost/"); private static final Instance INSTANCE = Instance.create(InstanceId.of("-id-")) .register(Registration.create(APP_NAME, "https://health").build()); private DiscordNotifier notifier; private RestTemplate restTemplate; @BeforeEach void setUp() { InstanceRepository repository = mock(InstanceRepository.class); when(repository.find(INSTANCE.getId())).thenReturn(Mono.just(INSTANCE)); restTemplate = mock(RestTemplate.class); notifier = new DiscordNotifier(repository, restTemplate); notifier.setWebhookUrl(WEBHOOK_URI); } @Test void test_onApplicationEvent_resolve() { notifier.setUsername(USER_NAME); notifier.setAvatarUrl(AVATAR_URL); notifier.setTts(true); StepVerifier .create(notifier .notify(new InstanceStatusChangedEvent(INSTANCE.getId(), INSTANCE.getVersion(), StatusInfo.ofDown()))) .verifyComplete(); clearInvocations(restTemplate); StepVerifier .create(notifier .notify(new InstanceStatusChangedEvent(INSTANCE.getId(), INSTANCE.getVersion(), StatusInfo.ofUp()))) .verifyComplete(); Object expected = expectedMessage(USER_NAME, true, AVATAR_URL, standardMessage("UP")); verify(restTemplate).postForEntity(WEBHOOK_URI, expected, Void.class); } @Test void test_onApplicationEvent_resolve_minimum_configuration() { StepVerifier .create(notifier .notify(new InstanceStatusChangedEvent(INSTANCE.getId(), INSTANCE.getVersion(), StatusInfo.ofDown()))) .verifyComplete(); clearInvocations(restTemplate); StepVerifier .create(notifier .notify(new InstanceStatusChangedEvent(INSTANCE.getId(), INSTANCE.getVersion(), StatusInfo.ofUp()))) .verifyComplete(); Object expected = expectedMessage(null, false, null, standardMessage("UP")); verify(restTemplate).postForEntity(WEBHOOK_URI, expected, Void.class); } private HttpEntity> expectedMessage(String username, boolean tts, String avatarUrl, String message) { Map body = new HashMap<>(); body.put("content", message); body.put("tts", tts); if (avatarUrl != null) { body.put("avatar_url", avatarUrl); } if (username != null) { body.put("username", username); } HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_JSON); headers.add(HttpHeaders.USER_AGENT, "RestTemplate"); return new HttpEntity<>(body, headers); } private String standardMessage(String status) { return "*" + INSTANCE.getRegistration().getName() + "* (" + INSTANCE.getId() + ") is *" + status + "*"; } } ================================================ FILE: spring-boot-admin-server/src/test/java/de/codecentric/boot/admin/server/notify/FeiShuNotifierTest.java ================================================ /* * 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.server.notify; import java.net.URI; import java.util.Map; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.mockito.ArgumentCaptor; import org.springframework.http.HttpEntity; import org.springframework.http.ResponseEntity; import org.springframework.web.client.RestTemplate; import reactor.core.publisher.Mono; import reactor.test.StepVerifier; 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.InstanceStatusChangedEvent; import de.codecentric.boot.admin.server.domain.values.InstanceId; import de.codecentric.boot.admin.server.domain.values.Registration; import de.codecentric.boot.admin.server.domain.values.StatusInfo; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; public class FeiShuNotifierTest { public static final String WEBHOOK_URL = "http://localhost/v2"; private final Instance instance = Instance.create(InstanceId.of("-id-")) .register(Registration.create("App", "https://health").build()); private FeiShuNotifier notifier; private RestTemplate restTemplate; @BeforeEach void setUp() { InstanceRepository instanceRepository = mock(InstanceRepository.class); when(instanceRepository.find(instance.getId())).thenReturn(Mono.just(instance)); restTemplate = mock(RestTemplate.class); notifier = new FeiShuNotifier(instanceRepository, restTemplate); notifier.setWebhookUrl(URI.create(WEBHOOK_URL)); } @Test void test_onApplicationEvent_resolve() { @SuppressWarnings("unchecked") ArgumentCaptor>> httpRequest = ArgumentCaptor .forClass((Class>>) (Class) HttpEntity.class); when(restTemplate.postForEntity(any(), httpRequest.capture(), eq(String.class))) .thenReturn(ResponseEntity.ok().build()); StepVerifier .create(notifier .notify(new InstanceStatusChangedEvent(instance.getId(), instance.getVersion(), StatusInfo.ofDown()))) .verifyComplete(); StepVerifier .create(notifier .notify(new InstanceStatusChangedEvent(instance.getId(), instance.getVersion(), StatusInfo.ofUp()))) .verifyComplete(); assertThat(httpRequest.getValue().getHeaders().toSingleValueMap()).containsEntry("Content-Type", "application/json"); Map body = httpRequest.getValue().getBody(); assertThat(body).containsEntry("card", "{\"elements\":[{\"tag\":\"div\",\"text\":{\"tag\":\"plain_text\",\"content\":\"ServiceName: App(-id-) \\nServiceUrl: \\nStatus: changed status from [DOWN] to [UP]\"}},{\"tag\":\"div\",\"text\":{\"tag\":\"lark_md\",\"content\":\"\"}}],\"header\":{\"template\":\"red\",\"title\":{\"tag\":\"plain_text\",\"content\":\"Codecentric's Spring Boot Admin notice\"}}}"); assertThat(body).containsEntry("msg_type", FeiShuNotifier.MessageType.interactive); } @Test void test_onApplicationEvent_trigger() { StatusInfo infoDown = StatusInfo.ofDown(); @SuppressWarnings("unchecked") ArgumentCaptor>> httpRequest = ArgumentCaptor .forClass((Class>>) (Class) HttpEntity.class); when(restTemplate.postForEntity(any(), httpRequest.capture(), eq(String.class))) .thenReturn(ResponseEntity.ok().build()); StepVerifier .create(notifier .notify(new InstanceStatusChangedEvent(instance.getId(), instance.getVersion(), StatusInfo.ofUp()))) .verifyComplete(); StepVerifier .create(notifier.notify(new InstanceStatusChangedEvent(instance.getId(), instance.getVersion(), infoDown))) .verifyComplete(); assertThat(httpRequest.getValue().getHeaders().toSingleValueMap()).containsEntry("Content-Type", "application/json"); Map body = httpRequest.getValue().getBody(); assertThat(body).containsEntry("card", "{\"elements\":[{\"tag\":\"div\",\"text\":{\"tag\":\"plain_text\",\"content\":\"ServiceName: App(-id-) \\nServiceUrl: \\nStatus: changed status from [UP] to [DOWN]\"}},{\"tag\":\"div\",\"text\":{\"tag\":\"lark_md\",\"content\":\"\"}}],\"header\":{\"template\":\"red\",\"title\":{\"tag\":\"plain_text\",\"content\":\"Codecentric's Spring Boot Admin notice\"}}}"); assertThat(body).containsEntry("msg_type", FeiShuNotifier.MessageType.interactive); } } ================================================ FILE: spring-boot-admin-server/src/test/java/de/codecentric/boot/admin/server/notify/HazelcastNotificationTriggerTest.java ================================================ /* * 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.server.notify; import java.util.concurrent.ConcurrentHashMap; import org.junit.jupiter.api.Test; import reactor.test.publisher.TestPublisher; import de.codecentric.boot.admin.server.domain.entities.Instance; import de.codecentric.boot.admin.server.domain.events.InstanceEvent; import de.codecentric.boot.admin.server.domain.events.InstanceStatusChangedEvent; import de.codecentric.boot.admin.server.domain.values.InstanceId; import de.codecentric.boot.admin.server.domain.values.Registration; import de.codecentric.boot.admin.server.domain.values.StatusInfo; import static org.awaitility.Awaitility.await; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; class HazelcastNotificationTriggerTest { private final Instance instance = Instance.create(InstanceId.of("id-1")) .register(Registration.create("foo", "http://health-1").build()); private final Notifier notifier = mock(Notifier.class); private final TestPublisher events = TestPublisher.create(); private final ConcurrentHashMap sentNotifications = new ConcurrentHashMap<>(); private final HazelcastNotificationTrigger trigger = new HazelcastNotificationTrigger(this.notifier, this.events, this.sentNotifications); @Test void should_trigger_notifications() { // given then notifier has subscribed to the events and no notification was sent // before this.sentNotifications.clear(); this.trigger.start(); await().until(this.events::wasSubscribed); // when registered event is emitted InstanceStatusChangedEvent event = new InstanceStatusChangedEvent(this.instance.getId(), this.instance.getVersion(), StatusInfo.ofDown()); this.events.next(event); // then should notify verify(this.notifier, times(1)).notify(event); } @Test void should_not_trigger_notifications() { // given the event is in the already sent notifications. InstanceStatusChangedEvent event = new InstanceStatusChangedEvent(this.instance.getId(), this.instance.getVersion(), StatusInfo.ofDown()); this.sentNotifications.put(event.getInstance(), event.getVersion()); this.trigger.start(); await().until(this.events::wasSubscribed); // when registered event is emitted this.events.next(event); // then should not notify verify(this.notifier, never()).notify(event); } } ================================================ FILE: spring-boot-admin-server/src/test/java/de/codecentric/boot/admin/server/notify/HipchatNotifierTest.java ================================================ /* * 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.server.notify; import java.net.URI; import java.util.Collections; import java.util.Map; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.mockito.ArgumentCaptor; import org.springframework.http.HttpEntity; import org.springframework.http.ResponseEntity; import org.springframework.web.client.RestTemplate; import reactor.core.publisher.Mono; import reactor.test.StepVerifier; 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.InstanceStatusChangedEvent; import de.codecentric.boot.admin.server.domain.values.InstanceId; import de.codecentric.boot.admin.server.domain.values.Registration; import de.codecentric.boot.admin.server.domain.values.StatusInfo; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.ArgumentMatchers.isA; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; /** * @author Jamie Brown */ class HipchatNotifierTest { private final Instance instance = Instance.create(InstanceId.of("-id-")) .register(Registration.create("App", "https://health").build()); private HipchatNotifier notifier; private RestTemplate restTemplate; @BeforeEach void setUp() { InstanceRepository repository = mock(InstanceRepository.class); when(repository.find(instance.getId())).thenReturn(Mono.just(instance)); restTemplate = mock(RestTemplate.class); notifier = new HipchatNotifier(repository, restTemplate); notifier.setNotify(true); notifier.setAuthToken("--token-"); notifier.setRoomId("-room-"); notifier.setUrl(URI.create("http://localhost/v2")); } @Test void test_onApplicationEvent_resolve() { @SuppressWarnings("unchecked") ArgumentCaptor>> httpRequest = ArgumentCaptor .forClass((Class>>) (Class) HttpEntity.class); when(restTemplate.postForEntity(isA(String.class), httpRequest.capture(), eq(Void.class))) .thenReturn(ResponseEntity.ok().build()); StepVerifier .create(notifier .notify(new InstanceStatusChangedEvent(instance.getId(), instance.getVersion(), StatusInfo.ofDown()))) .verifyComplete(); StepVerifier .create(notifier .notify(new InstanceStatusChangedEvent(instance.getId(), instance.getVersion(), StatusInfo.ofUp()))) .verifyComplete(); assertThat(httpRequest.getValue().getHeaders().asMultiValueMap()).containsEntry("Content-Type", Collections.singletonList("application/json")); Map body = httpRequest.getValue().getBody(); assertThat(body).containsEntry("color", "green"); assertThat(body).containsEntry("message", "App/-id- is UP"); assertThat(body).containsEntry("notify", Boolean.TRUE); assertThat(body).containsEntry("message_format", "html"); } @Test void test_onApplicationEvent_trigger() { StatusInfo infoDown = StatusInfo.ofDown(); @SuppressWarnings("unchecked") ArgumentCaptor>> httpRequest = ArgumentCaptor .forClass((Class>>) (Class) HttpEntity.class); when(restTemplate.postForEntity(isA(String.class), httpRequest.capture(), eq(Void.class))) .thenReturn(ResponseEntity.ok().build()); StepVerifier .create(notifier .notify(new InstanceStatusChangedEvent(instance.getId(), instance.getVersion(), StatusInfo.ofUp()))) .verifyComplete(); StepVerifier .create(notifier.notify(new InstanceStatusChangedEvent(instance.getId(), instance.getVersion(), infoDown))) .verifyComplete(); assertThat(httpRequest.getValue().getHeaders().toSingleValueMap()).containsEntry("Content-Type", "application/json"); Map body = httpRequest.getValue().getBody(); assertThat(body).containsEntry("color", "red"); assertThat(body).containsEntry("message", "App/-id- is DOWN"); assertThat(body).containsEntry("notify", Boolean.TRUE); assertThat(body).containsEntry("message_format", "html"); } } ================================================ FILE: spring-boot-admin-server/src/test/java/de/codecentric/boot/admin/server/notify/LetsChatNotifierTest.java ================================================ /* * 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.server.notify; import java.net.URI; import java.nio.charset.StandardCharsets; import java.util.Base64; import java.util.HashMap; import java.util.Map; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.http.HttpEntity; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; import org.springframework.http.MediaType; import org.springframework.web.client.RestTemplate; import reactor.core.publisher.Mono; import reactor.test.StepVerifier; 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.InstanceStatusChangedEvent; import de.codecentric.boot.admin.server.domain.values.InstanceId; import de.codecentric.boot.admin.server.domain.values.Registration; import de.codecentric.boot.admin.server.domain.values.StatusInfo; import static org.mockito.Mockito.clearInvocations; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; class LetsChatNotifierTest { private static final String ROOM = "text_room"; private static final String TOKEN = "text_token"; private static final String USER = "api_user"; private static final String HOST = "http://localhost"; private static final Instance instance = Instance.create(InstanceId.of("-id-")) .register(Registration.create("App", "https://health").build()); private LetsChatNotifier notifier; private RestTemplate restTemplate; @BeforeEach void setUp() { InstanceRepository repository = mock(InstanceRepository.class); when(repository.find(instance.getId())).thenReturn(Mono.just(instance)); restTemplate = mock(RestTemplate.class); notifier = new LetsChatNotifier(repository, restTemplate); notifier.setUsername(USER); notifier.setUrl(URI.create(HOST)); notifier.setRoom(ROOM); notifier.setToken(TOKEN); } @Test void test_onApplicationEvent_resolve() { StepVerifier .create(notifier .notify(new InstanceStatusChangedEvent(instance.getId(), instance.getVersion(), StatusInfo.ofDown()))) .verifyComplete(); clearInvocations(restTemplate); StepVerifier .create(notifier .notify(new InstanceStatusChangedEvent(instance.getId(), instance.getVersion(), StatusInfo.ofUp()))) .verifyComplete(); HttpEntity expected = expectedMessage(standardMessage("UP")); verify(restTemplate).exchange(URI.create(String.format("%s/rooms/%s/messages", HOST, ROOM)), HttpMethod.POST, expected, Void.class); } @Test void test_onApplicationEvent_resolve_with_custom_message() { notifier.setMessage("TEST"); StepVerifier .create(notifier .notify(new InstanceStatusChangedEvent(instance.getId(), instance.getVersion(), StatusInfo.ofDown()))) .verifyComplete(); clearInvocations(restTemplate); StepVerifier .create(notifier .notify(new InstanceStatusChangedEvent(instance.getId(), instance.getVersion(), StatusInfo.ofUp()))) .verifyComplete(); HttpEntity expected = expectedMessage("TEST"); verify(restTemplate).exchange(URI.create(String.format("%s/rooms/%s/messages", HOST, ROOM)), HttpMethod.POST, expected, Void.class); } private HttpEntity expectedMessage(String message) { HttpHeaders httpHeaders = new HttpHeaders(); httpHeaders.setContentType(MediaType.APPLICATION_JSON); String auth = Base64.getEncoder() .encodeToString(String.format("%s:%s", TOKEN, USER).getBytes(StandardCharsets.UTF_8)); httpHeaders.add(HttpHeaders.AUTHORIZATION, String.format("Basic %s", auth)); Map messageJson = new HashMap<>(); messageJson.put("text", message); return new HttpEntity<>(messageJson, httpHeaders); } private String standardMessage(String status) { return "*" + instance.getRegistration().getName() + "* (" + instance.getId() + ") is *" + status + "*"; } } ================================================ FILE: spring-boot-admin-server/src/test/java/de/codecentric/boot/admin/server/notify/MailNotifierIntegrationTest.java ================================================ /* * 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.server.notify; import java.io.FileNotFoundException; import java.net.URL; import org.assertj.core.api.WithAssertions; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.SpringBootConfiguration; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.test.context.SpringBootTest; import org.thymeleaf.context.Context; import de.codecentric.boot.admin.server.config.EnableAdminServer; @SpringBootTest(properties = { "spring.mail.host=localhost", "spring.boot.admin.notify.mail=true" }) class MailNotifierIntegrationTest implements WithAssertions { @Autowired MailNotifier mailNotifier; @Test void fileProtocolIsNotAllowed() { assertThatThrownBy(() -> { URL resource = getClass().getClassLoader().getResource("."); mailNotifier.setTemplate( "file://" + resource.getFile() + "de/codecentric/boot/admin/server/notify/vulnerable-file.html"); mailNotifier.getBody(new Context()); }).hasCauseInstanceOf(FileNotFoundException.class); } @Test void httpProtocolIsNotAllowed() { assertThatThrownBy(() -> { mailNotifier.setTemplate( "https://raw.githubusercontent.com/codecentric/spring-boot-admin/gh-pages/vulnerable-file.html"); mailNotifier.getBody(new Context()); }).hasCauseInstanceOf(FileNotFoundException.class); } @Test void classpathProtocolIsAllowed() { assertThatNoException().isThrownBy(() -> { mailNotifier.setTemplate("/de/codecentric/boot/admin/server/notify/allowed-file.html"); mailNotifier.getBody(new Context()); }); } @Test void callToReflectionUtilsAreNotAllowed() { assertThatThrownBy(() -> { mailNotifier.setTemplate("/de/codecentric/boot/admin/server/notify/vulnerable-file.html"); mailNotifier.getBody(new Context()); }).rootCause() .hasMessageContaining( "Access is forbidden for type 'org.springframework.util.ReflectionUtils' in this expression context."); } @EnableAdminServer @EnableAutoConfiguration @SpringBootConfiguration public static class TestAdminApplication { } } ================================================ FILE: spring-boot-admin-server/src/test/java/de/codecentric/boot/admin/server/notify/MailNotifierTest.java ================================================ /* * 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.server.notify; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.nio.charset.StandardCharsets; import java.util.HashMap; import java.util.Map; import java.util.Properties; import jakarta.activation.DataHandler; import jakarta.mail.Message; import jakarta.mail.MessagingException; import jakarta.mail.Session; import jakarta.mail.internet.InternetAddress; import jakarta.mail.internet.MimeMessage; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.mockito.ArgumentCaptor; import org.springframework.mail.javamail.JavaMailSender; import org.springframework.util.StreamUtils; import org.thymeleaf.spring6.SpringTemplateEngine; import org.thymeleaf.templatemode.TemplateMode; import org.thymeleaf.templateresolver.ClassLoaderTemplateResolver; import reactor.core.publisher.Mono; import reactor.test.StepVerifier; 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.domain.values.InstanceId; import de.codecentric.boot.admin.server.domain.values.Registration; import de.codecentric.boot.admin.server.domain.values.StatusInfo; import static java.util.Collections.singletonMap; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoMoreInteractions; import static org.mockito.Mockito.when; class MailNotifierTest { private final Instance instance = Instance.create(InstanceId.of("cafebabe")) .register(Registration.create("application-name", "http://localhost:8081/actuator/health") .managementUrl("http://localhost:8081/actuator") .serviceUrl("http://localhost:8081/") .build()); private JavaMailSender sender; private MailNotifier notifier; private InstanceRepository repository; @BeforeEach void setup() { repository = mock(InstanceRepository.class); when(repository.find(instance.getId())).thenReturn(Mono.just(instance)); sender = mock(JavaMailSender.class); when(sender.createMimeMessage()).thenAnswer((args) -> new MimeMessage(Session.getInstance(new Properties()))); SpringTemplateEngine templateEngine = new SpringTemplateEngine(); ClassLoaderTemplateResolver resolver = new ClassLoaderTemplateResolver(); resolver.setTemplateMode(TemplateMode.HTML); resolver.setCharacterEncoding(StandardCharsets.UTF_8.name()); templateEngine.addTemplateResolver(resolver); notifier = new MailNotifier(sender, repository, templateEngine); notifier.setTo(new String[] { "foo@bar.com" }); notifier.setCc(new String[] { "bar@foo.com" }); notifier.setFrom("SBA "); notifier.setBaseUrl("http://localhost:8080"); notifier.setTemplate("/META-INF/spring-boot-admin-server/mail/status-changed.html"); } @Test void should_send_mail_using_default_template() throws IOException, MessagingException { Map details = new HashMap<>(); details.put("Simple Value", 1234); details.put("Complex Value", singletonMap("Nested Simple Value", "99!")); StepVerifier.create(notifier.notify( new InstanceStatusChangedEvent(instance.getId(), instance.getVersion(), StatusInfo.ofDown(details)))) .verifyComplete(); ArgumentCaptor mailCaptor = ArgumentCaptor.forClass(MimeMessage.class); verify(sender).send(mailCaptor.capture()); MimeMessage mail = mailCaptor.getValue(); assertThat(mail.getSubject()).isEqualTo("application-name (cafebabe) is DOWN"); assertThat(mail.getRecipients(Message.RecipientType.TO)).containsExactly(new InternetAddress("foo@bar.com")); assertThat(mail.getRecipients(Message.RecipientType.CC)).containsExactly(new InternetAddress("bar@foo.com")); assertThat(mail.getFrom()).containsExactly(new InternetAddress("SBA ")); assertThat(mail.getDataHandler().getContentType()).isEqualTo("text/html;charset=UTF-8"); String body = extractBody(mail.getDataHandler()); assertThat(body).isEqualTo(loadExpectedBody("expected-default-mail")); } @Test void should_send_mail_using_custom_template_with_additional_properties() throws IOException, MessagingException { notifier.setTemplate("/de/codecentric/boot/admin/server/notify/custom-mail.html"); notifier.getAdditionalProperties().put("customProperty", "HELLO WORLD!"); StepVerifier .create(notifier .notify(new InstanceStatusChangedEvent(instance.getId(), instance.getVersion(), StatusInfo.ofDown()))) .verifyComplete(); ArgumentCaptor mailCaptor = ArgumentCaptor.forClass(MimeMessage.class); verify(sender).send(mailCaptor.capture()); MimeMessage mail = mailCaptor.getValue(); String body = extractBody(mail.getDataHandler()); assertThat(body).isEqualTo(loadExpectedBody("expected-custom-mail")); } // The following tests are rather for AbstractNotifier @Test void should_not_send_mail_when_disabled() { notifier.setEnabled(false); StepVerifier .create(notifier .notify(new InstanceStatusChangedEvent(instance.getId(), instance.getVersion(), StatusInfo.ofUp()))) .verifyComplete(); verifyNoMoreInteractions(sender); } @Test void should_not_send_when_unknown_to_up() { StepVerifier .create(notifier .notify(new InstanceStatusChangedEvent(instance.getId(), instance.getVersion(), StatusInfo.ofUp()))) .verifyComplete(); verifyNoMoreInteractions(sender); } @Test void should_not_send_on_wildcard_ignore() { notifier.setIgnoreChanges(new String[] { "*:UP" }); StepVerifier .create(notifier .notify(new InstanceStatusChangedEvent(instance.getId(), instance.getVersion(), StatusInfo.ofUp()))) .verifyComplete(); verifyNoMoreInteractions(sender); } @Test void should_not_propagate_error() { Notifier statusChangeNotifier = new AbstractStatusChangeNotifier(repository) { @Override protected Mono doNotify(InstanceEvent event, Instance application) { return Mono.error(new IllegalStateException("test")); } }; StepVerifier .create(statusChangeNotifier .notify(new InstanceStatusChangedEvent(instance.getId(), instance.getVersion(), StatusInfo.ofUp()))) .verifyComplete(); } private String loadExpectedBody(String resource) throws IOException { return StreamUtils.copyToString(this.getClass().getResourceAsStream(resource), StandardCharsets.UTF_8); } private String extractBody(DataHandler dataHandler) throws IOException { ByteArrayOutputStream os = new ByteArrayOutputStream(4096); dataHandler.writeTo(os); return os.toString(StandardCharsets.UTF_8).replaceAll("\\r?\\n", "\n"); } } ================================================ FILE: spring-boot-admin-server/src/test/java/de/codecentric/boot/admin/server/notify/MattermostNotifierTest.java ================================================ /* * 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.server.notify; import java.net.URI; import java.util.Collections; import java.util.HashMap; import java.util.Map; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.http.HttpEntity; import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; import org.springframework.web.client.RestTemplate; import reactor.core.publisher.Mono; import reactor.test.StepVerifier; 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.InstanceStatusChangedEvent; import de.codecentric.boot.admin.server.domain.values.InstanceId; import de.codecentric.boot.admin.server.domain.values.Registration; import de.codecentric.boot.admin.server.domain.values.StatusInfo; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.clearInvocations; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; class MattermostNotifierTest { private static final String CHANNEL_ID = "channel"; private static final String BOT_ACCESS_TOKEN = "bot_access_token"; private static final String APP_NAME = "App"; private static final Instance INSTANCE = Instance.create(InstanceId.of("-id-")) .register(Registration.create(APP_NAME, "https://health").build()); private static final String MESSAGE = "test"; private MattermostNotifier notifier; private RestTemplate restTemplate; @BeforeEach void setUp() { InstanceRepository repository = mock(InstanceRepository.class); when(repository.find(INSTANCE.getId())).thenReturn(Mono.just(INSTANCE)); restTemplate = mock(RestTemplate.class); notifier = new MattermostNotifier(repository, restTemplate); notifier.setBotAccessToken(BOT_ACCESS_TOKEN); notifier.setApiUrl(URI.create("http://localhost/")); } @Test void test_onApplicationEvent_resolve() { notifier.setChannelId(CHANNEL_ID); StepVerifier .create(notifier .notify(new InstanceStatusChangedEvent(INSTANCE.getId(), INSTANCE.getVersion(), StatusInfo.ofDown()))) .verifyComplete(); clearInvocations(restTemplate); StepVerifier .create(notifier .notify(new InstanceStatusChangedEvent(INSTANCE.getId(), INSTANCE.getVersion(), StatusInfo.ofUp()))) .verifyComplete(); Object expected = expectedMessage("#2eb885", CHANNEL_ID, BOT_ACCESS_TOKEN, standardMessage("UP")); verify(restTemplate).postForEntity(any(URI.class), eq(expected), eq(Void.class)); } @Test void test_onApplicationEvent_resolve_with_given_message() { notifier.setMessage(MESSAGE); notifier.setChannelId(CHANNEL_ID); StepVerifier .create(notifier .notify(new InstanceStatusChangedEvent(INSTANCE.getId(), INSTANCE.getVersion(), StatusInfo.ofDown()))) .verifyComplete(); clearInvocations(restTemplate); StepVerifier .create(notifier .notify(new InstanceStatusChangedEvent(INSTANCE.getId(), INSTANCE.getVersion(), StatusInfo.ofUp()))) .verifyComplete(); Object expected = expectedMessage("#2eb885", CHANNEL_ID, BOT_ACCESS_TOKEN, MESSAGE); verify(restTemplate).postForEntity(any(URI.class), eq(expected), eq(Void.class)); } @Test void test_onApplicationEvent_trigger() { notifier.setChannelId(CHANNEL_ID); StepVerifier .create(notifier .notify(new InstanceStatusChangedEvent(INSTANCE.getId(), INSTANCE.getVersion(), StatusInfo.ofUp()))) .verifyComplete(); clearInvocations(restTemplate); StepVerifier .create(notifier .notify(new InstanceStatusChangedEvent(INSTANCE.getId(), INSTANCE.getVersion(), StatusInfo.ofDown()))) .verifyComplete(); Object expected = expectedMessage("#a30100", CHANNEL_ID, BOT_ACCESS_TOKEN, standardMessage("DOWN")); verify(restTemplate).postForEntity(any(URI.class), eq(expected), eq(Void.class)); } private HttpEntity> expectedMessage(String color, String channelId, String botAccessToken, String message) { Map messageJson = new HashMap<>(); messageJson.put("channel_id", channelId); Map attachments = new HashMap<>(); attachments.put("text", message); attachments.put("fallback", message); attachments.put("color", color); Map props = new HashMap<>(); props.put("attachments", Collections.singletonList(attachments)); messageJson.put("props", props); HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_JSON); headers.setBearerAuth(botAccessToken); return new HttpEntity<>(messageJson, headers); } private String standardMessage(String status) { return "**" + INSTANCE.getRegistration().getName() + "** (" + INSTANCE.getId() + ") is **" + status + "**"; } } ================================================ FILE: spring-boot-admin-server/src/test/java/de/codecentric/boot/admin/server/notify/MicrosoftTeamsNotifierTest.java ================================================ /* * 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.server.notify; import java.net.URI; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.mockito.ArgumentCaptor; import org.springframework.http.HttpEntity; import org.springframework.http.MediaType; import org.springframework.web.client.RestTemplate; import reactor.core.publisher.Mono; import reactor.test.StepVerifier; 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.InstanceDeregisteredEvent; import de.codecentric.boot.admin.server.domain.events.InstanceRegisteredEvent; import de.codecentric.boot.admin.server.domain.events.InstanceStatusChangedEvent; import de.codecentric.boot.admin.server.domain.values.InstanceId; import de.codecentric.boot.admin.server.domain.values.Registration; import de.codecentric.boot.admin.server.domain.values.StatusInfo; import de.codecentric.boot.admin.server.notify.MicrosoftTeamsNotifier.Message; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; class MicrosoftTeamsNotifierTest { private static final String BLUE = "439fe0"; private static final String RED = "b32d36"; private static final String GREEN = "6db33f"; private static final String APP_NAME = "Test App"; private static final String APP_ID = "TestAppId"; private static final String HEALTH_URL = "https://health"; private static final String MANAGEMENT_URL = "https://management"; private static final String SERVICE_URL = "https://service"; private MicrosoftTeamsNotifier notifier; private RestTemplate mockRestTemplate; private Instance instance; @BeforeEach void setUp() { instance = Instance.create(InstanceId.of(APP_ID)) .register(Registration.create(APP_NAME, HEALTH_URL) .managementUrl(MANAGEMENT_URL) .serviceUrl(SERVICE_URL) .build()); InstanceRepository repository = mock(InstanceRepository.class); when(repository.find(instance.getId())).thenReturn(Mono.just(instance)); mockRestTemplate = mock(RestTemplate.class); notifier = new MicrosoftTeamsNotifier(repository, mockRestTemplate); notifier.setWebhookUrl(URI.create("https://example.com")); } @Test @SuppressWarnings("unchecked") void test_onClientApplicationDeRegisteredEvent_resolve() { InstanceDeregisteredEvent event = new InstanceDeregisteredEvent(instance.getId(), 1L); StepVerifier.create(notifier.doNotify(event, instance)).verifyComplete(); ArgumentCaptor> entity = ArgumentCaptor.forClass(HttpEntity.class); verify(mockRestTemplate).postForEntity(eq(URI.create("https://example.com")), entity.capture(), eq(Void.class)); assertThat(entity.getValue().getHeaders().getContentType()).isEqualTo(MediaType.APPLICATION_JSON); assertThat(entity.getValue().getBody()).isNotNull(); assertMessage(entity.getValue().getBody(), notifier.getDeRegisteredTitle(), notifier.getMessageSummary(), "Test App with id TestAppId has de-registered from Spring Boot Admin", BLUE); } @Test @SuppressWarnings("unchecked") void test_onApplicationRegisteredEvent_resolve() { InstanceRegisteredEvent event = new InstanceRegisteredEvent(instance.getId(), 1L, instance.getRegistration()); StepVerifier.create(notifier.doNotify(event, instance)).verifyComplete(); ArgumentCaptor> entity = ArgumentCaptor.forClass(HttpEntity.class); verify(mockRestTemplate).postForEntity(eq(URI.create("https://example.com")), entity.capture(), eq(Void.class)); assertThat(entity.getValue().getHeaders().getContentType()).isEqualTo(MediaType.APPLICATION_JSON); assertThat(entity.getValue().getBody()).isNotNull(); assertMessage(entity.getValue().getBody(), notifier.getRegisteredTitle(), notifier.getMessageSummary(), "Test App with id TestAppId has registered with Spring Boot Admin", BLUE); } @Test @SuppressWarnings("unchecked") void test_onApplicationStatusChangedEvent_resolve() { InstanceStatusChangedEvent event = new InstanceStatusChangedEvent(instance.getId(), 1L, StatusInfo.ofUp()); StepVerifier.create(notifier.doNotify(event, instance)).verifyComplete(); ArgumentCaptor> entity = ArgumentCaptor.forClass(HttpEntity.class); verify(mockRestTemplate).postForEntity(eq(URI.create("https://example.com")), entity.capture(), eq(Void.class)); assertThat(entity.getValue().getHeaders().getContentType()).isEqualTo(MediaType.APPLICATION_JSON); assertThat(entity.getValue().getBody()).isNotNull(); assertMessage(entity.getValue().getBody(), notifier.getStatusChangedTitle(), notifier.getMessageSummary(), "Test App with id TestAppId changed status from UNKNOWN to UP", GREEN); } @Test void test_shouldNotifyWithRegisteredEventReturns_true() { InstanceRegisteredEvent event = new InstanceRegisteredEvent(instance.getId(), 1L, instance.getRegistration()); assertThat(notifier.shouldNotify(event, instance)).isTrue(); } @Test void test_shouldNotifyWithDeRegisteredEventReturns_true() { InstanceDeregisteredEvent event = new InstanceDeregisteredEvent(instance.getId(), 1L); assertThat(notifier.shouldNotify(event, instance)).isTrue(); } @Test void test_getDeregisteredMessageForAppReturns_correctContent() { Message message = notifier.getDeregisteredMessage(instance, notifier.createEvaluationContext(new InstanceDeregisteredEvent(instance.getId(), 1L), instance)); assertMessage(message, notifier.getDeRegisteredTitle(), notifier.getMessageSummary(), "Test App with id TestAppId has de-registered from Spring Boot Admin", BLUE); } @Test void test_getRegisteredMessageForAppReturns_correctContent() { Message message = notifier.getRegisteredMessage(instance, notifier.createEvaluationContext(new InstanceDeregisteredEvent(instance.getId(), 1L), instance)); assertMessage(message, notifier.getRegisteredTitle(), notifier.getMessageSummary(), "Test App with id TestAppId has registered with Spring Boot Admin", BLUE); } @Test void test_getStatusChangedMessageForAppReturns_correctContent() { Message message = notifier.getStatusChangedMessage(instance, notifier.createEvaluationContext( new InstanceStatusChangedEvent(instance.getId(), 1L, StatusInfo.ofDown()), instance)); assertMessage(message, notifier.getStatusChangedTitle(), notifier.getMessageSummary(), "Test App with id TestAppId changed status from UNKNOWN to DOWN", RED); } @Test void test_getStatusChangedMessageForAppReturns_UP_to_DOWN() { notifier.updateLastStatus(new InstanceStatusChangedEvent(instance.getId(), 1L, StatusInfo.ofUp())); Message message = notifier.getStatusChangedMessage(instance, notifier.createEvaluationContext( new InstanceStatusChangedEvent(instance.getId(), 1L, StatusInfo.ofDown()), instance)); assertMessage(message, notifier.getStatusChangedTitle(), notifier.getMessageSummary(), "Test App with id TestAppId changed status from UP to DOWN", RED); } @Test void test_getStatusChangedMessageWithExtraFormatArgumentReturns_activitySubtitlePatternWithAppName() { notifier.setStatusActivitySubtitle("STATUS_ACTIVITY_PATTERN_#{instance.registration.name}"); Message message = notifier.getStatusChangedMessage(instance, notifier.createEvaluationContext(new InstanceDeregisteredEvent(instance.getId(), 1L), instance)); assertThat(message.getSections().get(0).getActivitySubtitle()).isEqualTo("STATUS_ACTIVITY_PATTERN_" + APP_NAME); } @Test void test_getRegisterMessageWithExtraFormatArgumentReturns_activitySubtitlePatternWithAppName() { notifier.setRegisterActivitySubtitle("REGISTER_ACTIVITY_PATTERN_#{instance.registration.name}"); Message message = notifier.getRegisteredMessage(instance, notifier.createEvaluationContext(new InstanceDeregisteredEvent(instance.getId(), 1L), instance)); assertThat(message.getSections().get(0).getActivitySubtitle()) .isEqualTo("REGISTER_ACTIVITY_PATTERN_" + APP_NAME); } @Test void test_getDeRegisterMessageWithExtraFormatArgumentReturns_activitySubtitlePatternWithAppName() { notifier.setDeregisterActivitySubtitle("DEREGISTER_ACTIVITY_PATTERN_#{instance.registration.name}"); Message message = notifier.getDeregisteredMessage(instance, notifier.createEvaluationContext(new InstanceDeregisteredEvent(instance.getId(), 1L), instance)); assertThat(message.getSections().get(0).getActivitySubtitle()) .isEqualTo("DEREGISTER_ACTIVITY_PATTERN_" + APP_NAME); } @Test void test_getStatusChangedMessage_parsesThemeColorFromSpelExpression() { notifier.setThemeColor( "#{event.type == 'STATUS_CHANGED' ? (event.statusInfo.status=='UP' ? 'green' : 'red') : 'blue'}"); Message message = notifier.getStatusChangedMessage(instance, notifier.createEvaluationContext( new InstanceStatusChangedEvent(instance.getId(), 1L, StatusInfo.ofUp()), instance)); assertThat(message.getThemeColor()).isEqualTo("green"); } private void assertMessage(Message message, String expectedTitle, String expectedSummary, String expectedSubTitle, String expectedColor) { assertThat(message.getTitle()).isEqualTo(expectedTitle); assertThat(message.getSummary()).isEqualTo(expectedSummary); assertThat(message.getThemeColor()).isEqualTo(expectedColor); assertThat(message.getSections()).hasSize(1).anySatisfy((section) -> { assertThat(section.getActivityTitle()).isEqualTo(instance.getRegistration().getName()); assertThat(section.getActivitySubtitle()).isEqualTo(expectedSubTitle); assertThat(section.getFacts()).hasSize(5).anySatisfy((fact) -> { assertThat(fact.name()).isEqualTo("Status"); assertThat(fact.value()).isEqualTo("UNKNOWN"); }).anySatisfy((fact) -> { assertThat(fact.name()).isEqualTo("Service URL"); assertThat(fact.value()).isEqualTo(SERVICE_URL); }).anySatisfy((fact) -> { assertThat(fact.name()).isEqualTo("Health URL"); assertThat(fact.value()).isEqualTo(HEALTH_URL); }).anySatisfy((fact) -> { assertThat(fact.name()).isEqualTo("Management URL"); assertThat(fact.value()).isEqualTo(MANAGEMENT_URL); }).anySatisfy((fact) -> { assertThat(fact.name()).isEqualTo("Source"); assertThat(fact.value()).isNull(); }); }); } } ================================================ FILE: spring-boot-admin-server/src/test/java/de/codecentric/boot/admin/server/notify/NotificationTriggerTest.java ================================================ /* * 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.server.notify; import org.junit.jupiter.api.Test; import reactor.core.publisher.Mono; import reactor.test.publisher.TestPublisher; import de.codecentric.boot.admin.server.domain.entities.Instance; import de.codecentric.boot.admin.server.domain.events.InstanceEvent; import de.codecentric.boot.admin.server.domain.events.InstanceRegisteredEvent; import de.codecentric.boot.admin.server.domain.events.InstanceStatusChangedEvent; import de.codecentric.boot.admin.server.domain.values.InstanceId; import de.codecentric.boot.admin.server.domain.values.Registration; import de.codecentric.boot.admin.server.domain.values.StatusInfo; import static org.awaitility.Awaitility.await; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.clearInvocations; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; class NotificationTriggerTest { private final Instance instance = Instance.create(InstanceId.of("id-1")) .register(Registration.create("foo", "http://health-1").build()); private final Notifier notifier = mock(Notifier.class); private final TestPublisher events = TestPublisher.create(); private final NotificationTrigger trigger = new NotificationTrigger(this.notifier, this.events); NotificationTriggerTest() { when(this.notifier.notify(any())).thenReturn(Mono.empty()); } @Test void should_notify_on_event() { // given the notifier subscribed to the events this.trigger.start(); await().until(this.events::wasSubscribed); // when registered event is emitted InstanceStatusChangedEvent event = new InstanceStatusChangedEvent(this.instance.getId(), this.instance.getVersion(), StatusInfo.ofDown()); this.events.next(event); // then should notify verify(this.notifier, times(1)).notify(event); // when registered event is emitted but the trigger has been stopped this.trigger.stop(); clearInvocations(this.notifier); this.events.next(new InstanceRegisteredEvent(this.instance.getId(), this.instance.getVersion(), this.instance.getRegistration())); // then should not notify verify(this.notifier, never()).notify(event); } @Test void should_resume_on_exception() { // given this.trigger.start(); await().until(this.events::wasSubscribed); when(this.notifier.notify(any())).thenReturn(Mono.error(new IllegalStateException("Test"))) .thenReturn(Mono.empty()); // when exception for the first event is thrown and a subsequent event is fired InstanceStatusChangedEvent event = new InstanceStatusChangedEvent(this.instance.getId(), this.instance.getVersion(), StatusInfo.ofDown()); this.events.next(event); this.events.next(event); // the notifier was after the exception verify(this.notifier, times(2)).notify(event); } } ================================================ FILE: spring-boot-admin-server/src/test/java/de/codecentric/boot/admin/server/notify/OpsGenieNotifierTest.java ================================================ /* * 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.server.notify; import java.util.HashMap; import java.util.Map; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.http.HttpEntity; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; import org.springframework.http.MediaType; import org.springframework.web.client.RestTemplate; import reactor.core.publisher.Mono; import reactor.test.StepVerifier; 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.InstanceStatusChangedEvent; import de.codecentric.boot.admin.server.domain.values.InstanceId; import de.codecentric.boot.admin.server.domain.values.Registration; import de.codecentric.boot.admin.server.domain.values.StatusInfo; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.reset; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; class OpsGenieNotifierTest { private OpsGenieNotifier notifier; private RestTemplate restTemplate; private InstanceRepository repository; private static final Instance INSTANCE = Instance.create(InstanceId.of("-id-")) .register(Registration.create("App", "https://health").build()); @BeforeEach void setUp() { repository = mock(InstanceRepository.class); when(repository.find(INSTANCE.getId())).thenReturn(Mono.just(INSTANCE)); restTemplate = mock(RestTemplate.class); notifier = new OpsGenieNotifier(repository, restTemplate); notifier.setApiKey("--service--"); notifier.setUser("--user--"); notifier.setSource("--source--"); notifier.setEntity("--entity--"); notifier.setTags("--tag1--,--tag2--"); notifier.setActions("--action1--,--action2--"); } @Test void test_onApplicationEvent_resolve() { StepVerifier .create(notifier.notify( new InstanceStatusChangedEvent(INSTANCE.getId(), INSTANCE.getVersion() + 1, StatusInfo.ofDown()))) .verifyComplete(); reset(restTemplate); when(repository.find(INSTANCE.getId())).thenReturn(Mono.just(INSTANCE.withStatusInfo(StatusInfo.ofUp()))); StepVerifier .create(notifier .notify(new InstanceStatusChangedEvent(INSTANCE.getId(), INSTANCE.getVersion() + 2, StatusInfo.ofUp()))) .verifyComplete(); verify(restTemplate).exchange("https://api.opsgenie.com/v2/alerts/App_-id-/close?identifierType=alias", HttpMethod.POST, expectedRequest("DOWN", "UP"), Void.class); } @Test void test_onApplicationEvent_trigger() { StepVerifier .create(notifier .notify(new InstanceStatusChangedEvent(INSTANCE.getId(), INSTANCE.getVersion() + 1, StatusInfo.ofUp()))) .verifyComplete(); reset(restTemplate); when(repository.find(INSTANCE.getId())).thenReturn(Mono.just(INSTANCE.withStatusInfo(StatusInfo.ofDown()))); StepVerifier .create(notifier.notify( new InstanceStatusChangedEvent(INSTANCE.getId(), INSTANCE.getVersion() + 2, StatusInfo.ofDown()))) .verifyComplete(); verify(restTemplate).exchange("https://api.opsgenie.com/v2/alerts", HttpMethod.POST, expectedRequest("UP", "DOWN"), Void.class); } private String getMessage(String expectedStatus) { return String.format("App/-id- is %s", expectedStatus); } private String getDescription(String expectedOldStatus, String expectedNewStatus) { return String.format("Instance App (-id-) went from %s to %s", expectedOldStatus, expectedNewStatus); } private HttpEntity> expectedRequest(String expectedOldStatus, String expectedNewStatus) { Map expected = new HashMap<>(); expected.put("user", "--user--"); expected.put("source", "--source--"); if (!"UP".equals(expectedNewStatus)) { expected.put("message", getMessage(expectedNewStatus)); expected.put("alias", "App_-id-"); expected.put("description", getDescription(expectedOldStatus, expectedNewStatus)); expected.put("entity", "--entity--"); expected.put("tags", "--tag1--,--tag2--"); expected.put("actions", "--action1--,--action2--"); Map details = new HashMap<>(); details.put("type", "link"); details.put("href", "https://health"); details.put("text", "Instance health-endpoint"); expected.put("details", details); } HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_JSON); headers.set(HttpHeaders.AUTHORIZATION, "GenieKey --service--"); return new HttpEntity<>(expected, headers); } } ================================================ FILE: spring-boot-admin-server/src/test/java/de/codecentric/boot/admin/server/notify/PagerdutyNotifierTest.java ================================================ /* * 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.server.notify; import java.net.URI; import java.util.HashMap; import java.util.List; import java.util.Map; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.web.client.RestTemplate; import reactor.core.publisher.Mono; import reactor.test.StepVerifier; 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.InstanceStatusChangedEvent; import de.codecentric.boot.admin.server.domain.values.InstanceId; import de.codecentric.boot.admin.server.domain.values.Registration; import de.codecentric.boot.admin.server.domain.values.StatusInfo; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.reset; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; class PagerdutyNotifierTest { private PagerdutyNotifier notifier; private RestTemplate restTemplate; private InstanceRepository repository; private static final String APP_NAME = "App"; private static final Instance INSTANCE = Instance.create(InstanceId.of("-id-")) .register(Registration.create(APP_NAME, "https://health").build()); @BeforeEach void setUp() { repository = mock(InstanceRepository.class); when(repository.find(INSTANCE.getId())).thenReturn(Mono.just(INSTANCE)); restTemplate = mock(RestTemplate.class); notifier = new PagerdutyNotifier(repository, restTemplate); notifier.setServiceKey("--service--"); notifier.setClient("TestClient"); notifier.setClientUrl(URI.create("http://localhost")); } @Test void test_onApplicationEvent_resolve() { StepVerifier .create(notifier.notify( new InstanceStatusChangedEvent(INSTANCE.getId(), INSTANCE.getVersion() + 1, StatusInfo.ofDown()))) .verifyComplete(); reset(restTemplate); StatusInfo up = StatusInfo.ofUp(); when(repository.find(INSTANCE.getId())).thenReturn(Mono.just(INSTANCE.withStatusInfo(up))); StepVerifier .create(notifier.notify(new InstanceStatusChangedEvent(INSTANCE.getId(), INSTANCE.getVersion() + 2, up))) .verifyComplete(); Map expected = new HashMap<>(); expected.put("service_key", "--service--"); expected.put("incident_key", "App/-id-"); expected.put("event_type", "resolve"); expected.put("description", "App/-id- is UP"); Map details = new HashMap<>(); details.put("from", "DOWN"); details.put("to", up); expected.put("details", details); verify(restTemplate).postForEntity(PagerdutyNotifier.DEFAULT_URI, expected, Void.class); } @Test void test_onApplicationEvent_trigger() { StepVerifier .create(notifier .notify(new InstanceStatusChangedEvent(INSTANCE.getId(), INSTANCE.getVersion() + 1, StatusInfo.ofUp()))) .verifyComplete(); reset(restTemplate); StatusInfo down = StatusInfo.ofDown(); when(repository.find(INSTANCE.getId())).thenReturn(Mono.just(INSTANCE.withStatusInfo(down))); StepVerifier .create(notifier.notify(new InstanceStatusChangedEvent(INSTANCE.getId(), INSTANCE.getVersion() + 2, down))) .verifyComplete(); Map expected = new HashMap<>(); expected.put("service_key", "--service--"); expected.put("incident_key", "App/-id-"); expected.put("event_type", "trigger"); expected.put("description", "App/-id- is DOWN"); expected.put("client", "TestClient"); expected.put("client_url", URI.create("http://localhost")); Map details = new HashMap<>(); details.put("from", "UP"); details.put("to", down); expected.put("details", details); Map context = new HashMap<>(); context.put("type", "link"); context.put("href", "https://health"); context.put("text", "Application health-endpoint"); expected.put("contexts", List.of(context)); verify(restTemplate).postForEntity(PagerdutyNotifier.DEFAULT_URI, expected, Void.class); } } ================================================ FILE: spring-boot-admin-server/src/test/java/de/codecentric/boot/admin/server/notify/RemindingNotifierTest.java ================================================ /* * 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.server.notify; import java.time.Duration; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.NullSource; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import reactor.test.StepVerifier; import reactor.test.publisher.TestPublisher; 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.InstanceDeregisteredEvent; import de.codecentric.boot.admin.server.domain.events.InstanceEndpointsDetectedEvent; import de.codecentric.boot.admin.server.domain.events.InstanceEvent; import de.codecentric.boot.admin.server.domain.events.InstanceStatusChangedEvent; import de.codecentric.boot.admin.server.domain.values.Endpoints; import de.codecentric.boot.admin.server.domain.values.InstanceId; import de.codecentric.boot.admin.server.domain.values.Registration; import de.codecentric.boot.admin.server.domain.values.StatusInfo; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.awaitility.Awaitility.await; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; class RemindingNotifierTest { private static final Instance instance1 = Instance.create(InstanceId.of("id-1")) .register(Registration.create("App", "https://health").build()) .withStatusInfo(StatusInfo.ofDown()); private static final Instance instance2 = Instance.create(InstanceId.of("id-2")) .register(Registration.create("App", "https://health").build()) .withStatusInfo(StatusInfo.ofDown()); private static final InstanceEvent appDown = new InstanceStatusChangedEvent(instance1.getId(), 0L, StatusInfo.ofDown()); private static final InstanceEvent appUp = new InstanceStatusChangedEvent(instance1.getId(), 0L, StatusInfo.ofUp()); private static final InstanceEvent appEndpointsDiscovered = new InstanceEndpointsDetectedEvent(instance1.getId(), 0L, Endpoints.empty()); private static final InstanceEvent appDeregister = new InstanceDeregisteredEvent(instance1.getId(), 0L); private static final InstanceEvent otherAppUp = new InstanceStatusChangedEvent(instance2.getId(), 0L, StatusInfo.ofUp()); private static final InstanceEndpointsDetectedEvent errorTriggeringEvent = new InstanceEndpointsDetectedEvent( instance1.getId(), 999L, Endpoints.empty()); private InstanceRepository repository; @BeforeEach void setUp() { this.repository = mock(InstanceRepository.class); when(this.repository.find(any())).thenReturn(Mono.empty()); when(this.repository.find(instance1.getId())).thenReturn(Mono.just(instance1)); when(this.repository.find(instance2.getId())).thenReturn(Mono.just(instance2)); } @ParameterizedTest @NullSource void should_throw_on_invalid_ctor(Iterable delegates) { assertThatThrownBy(() -> new CompositeNotifier(delegates)).isInstanceOf(IllegalArgumentException.class); } @Test void should_remind_only_down_events() { TestNotifier notifier = new TestNotifier(); RemindingNotifier reminder = new RemindingNotifier(notifier, this.repository); reminder.setReminderPeriod(Duration.ZERO); StepVerifier.create(reminder.notify(appDown)).verifyComplete(); StepVerifier.create(reminder.notify(appEndpointsDiscovered)).verifyComplete(); StepVerifier.create(reminder.notify(otherAppUp)).verifyComplete(); await().pollDelay(Duration.ofMillis(10)) .untilAsserted(() -> StepVerifier.create(reminder.sendReminders()).verifyComplete()); await().pollDelay(Duration.ofMillis(10)) .untilAsserted(() -> StepVerifier.create(reminder.sendReminders()).verifyComplete()); assertThat(notifier.getEvents()).containsExactlyInAnyOrder(appDown, appEndpointsDiscovered, otherAppUp, appDown, appDown); } @Test void should_not_remind_remind_after_up() { TestNotifier notifier = new TestNotifier(); RemindingNotifier reminder = new RemindingNotifier(notifier, this.repository); reminder.setReminderPeriod(Duration.ZERO); StepVerifier.create(reminder.notify(appDown)).verifyComplete(); StepVerifier.create(reminder.notify(appUp)).verifyComplete(); StepVerifier.create(reminder.sendReminders()).verifyComplete(); assertThat(notifier.getEvents()).containsExactlyInAnyOrder(appDown, appUp); } @Test void should_not_remind_remind_after_deregister() { TestNotifier notifier = new TestNotifier(); RemindingNotifier reminder = new RemindingNotifier(notifier, this.repository); reminder.setReminderPeriod(Duration.ZERO); StepVerifier.create(reminder.notify(appDown)).verifyComplete(); StepVerifier.create(reminder.notify(appDeregister)).verifyComplete(); StepVerifier.create(reminder.sendReminders()).verifyComplete(); assertThat(notifier.getEvents()).containsExactlyInAnyOrder(appDown, appDeregister); } @Test void should_not_remind_remind_before_period_ends() { TestNotifier notifier = new TestNotifier(); RemindingNotifier reminder = new RemindingNotifier(notifier, this.repository); reminder.setReminderPeriod(Duration.ofHours(24)); StepVerifier.create(reminder.notify(appDown)).verifyComplete(); StepVerifier.create(reminder.sendReminders()).verifyComplete(); assertThat(notifier.getEvents()).containsExactlyInAnyOrder(appDown); } @Test void should_resubscribe_after_error() { TestPublisher eventPublisher = TestPublisher.create(); Flux emittedNotifications = Flux.create((emitter) -> { Notifier notifier = (event) -> { emitter.next(event); if (event.equals(errorTriggeringEvent)) { return Mono.error(new IllegalArgumentException("TEST-ERROR")); } return Mono.empty(); }; RemindingNotifier reminder = new RemindingNotifier(notifier, this.repository); eventPublisher.flux().flatMap(reminder::notify).subscribe(); reminder.setCheckReminderInverval(Duration.ofMillis(10)); reminder.setReminderPeriod(Duration.ofMillis(10)); reminder.start(); }); StepVerifier.create(emittedNotifications) .expectSubscription() .then(() -> eventPublisher.next(appDown)) .expectNext(appDown, appDown) .then(() -> eventPublisher.next(errorTriggeringEvent)) .thenConsumeWhile((e) -> !e.equals(errorTriggeringEvent)) .expectNext(errorTriggeringEvent, appDown, appDown) .thenCancel() .verify(); } } ================================================ FILE: spring-boot-admin-server/src/test/java/de/codecentric/boot/admin/server/notify/RocketChatNotifierTest.java ================================================ /* * 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.server.notify; import java.net.URI; import java.util.HashMap; import java.util.Map; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.http.HttpEntity; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; import org.springframework.http.MediaType; import org.springframework.web.client.RestTemplate; import reactor.core.publisher.Mono; import reactor.test.StepVerifier; 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.InstanceStatusChangedEvent; import de.codecentric.boot.admin.server.domain.values.InstanceId; import de.codecentric.boot.admin.server.domain.values.Registration; import de.codecentric.boot.admin.server.domain.values.StatusInfo; import static org.mockito.Mockito.clearInvocations; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; class RocketChatNotifierTest { private static final String ROOM_ID = "roomId"; private static final String TOKEN = "tokenApi"; private static final String USER_ID = "userId"; private static final String HOST = "http://localhost"; private static final String MESSAGE = "test"; private static final Instance instance = Instance.create(InstanceId.of("-id-")) .register(Registration.create("App", "https://health").build()); private RocketChatNotifier notifier; private RestTemplate restTemplate; @BeforeEach void setUp() { InstanceRepository repository = mock(InstanceRepository.class); when(repository.find(instance.getId())).thenReturn(Mono.just(instance)); restTemplate = mock(RestTemplate.class); notifier = new RocketChatNotifier(repository, restTemplate); notifier.setUrl(HOST); notifier.setUserId(USER_ID); notifier.setToken(TOKEN); notifier.setRoomId(ROOM_ID); } @Test void test_onApplicationEvent_resolve() { StepVerifier .create(notifier .notify(new InstanceStatusChangedEvent(instance.getId(), instance.getVersion(), StatusInfo.ofDown()))) .verifyComplete(); clearInvocations(restTemplate); StepVerifier .create(notifier .notify(new InstanceStatusChangedEvent(instance.getId(), instance.getVersion(), StatusInfo.ofUp()))) .verifyComplete(); HttpEntity expected = expectedMessage(standardMessage(StatusInfo.ofUp().getStatus())); verify(restTemplate).exchange(URI.create(String.format("%s/api/v1/chat.sendMessage", HOST)), HttpMethod.POST, expected, Void.class); } @Test void test_onApplicationEvent_resolve_with_given_message() { notifier.setMessage(MESSAGE); StepVerifier .create(notifier .notify(new InstanceStatusChangedEvent(instance.getId(), instance.getVersion(), StatusInfo.ofDown()))) .verifyComplete(); clearInvocations(restTemplate); StepVerifier .create(notifier .notify(new InstanceStatusChangedEvent(instance.getId(), instance.getVersion(), StatusInfo.ofUp()))) .verifyComplete(); HttpEntity expected = expectedMessage(MESSAGE); verify(restTemplate).exchange(URI.create(String.format("%s/api/v1/chat.sendMessage", HOST)), HttpMethod.POST, expected, Void.class); } private HttpEntity expectedMessage(String message) { HttpHeaders httpHeaders = new HttpHeaders(); httpHeaders.setContentType(MediaType.APPLICATION_JSON); httpHeaders.add("X-Auth-Token", TOKEN); httpHeaders.add("X-User-Id", USER_ID); Map messageJsonData = new HashMap<>(); messageJsonData.put("rid", ROOM_ID); messageJsonData.put("msg", message); Map messageJson = new HashMap<>(); messageJson.put("message", messageJsonData); return new HttpEntity<>(messageJson, httpHeaders); } private String standardMessage(String status) { return "*" + instance.getRegistration().getName() + "* (" + instance.getId() + ") is *" + status + "*"; } } ================================================ FILE: spring-boot-admin-server/src/test/java/de/codecentric/boot/admin/server/notify/SlackNotifierTest.java ================================================ /* * 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.server.notify; import java.net.URI; import java.util.Collections; import java.util.HashMap; import java.util.Map; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.http.HttpEntity; import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; import org.springframework.web.client.RestTemplate; import reactor.core.publisher.Mono; import reactor.test.StepVerifier; 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.InstanceStatusChangedEvent; import de.codecentric.boot.admin.server.domain.values.InstanceId; import de.codecentric.boot.admin.server.domain.values.Registration; import de.codecentric.boot.admin.server.domain.values.StatusInfo; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.clearInvocations; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; class SlackNotifierTest { private static final String CHANNEL = "channel"; private static final String ICON = "icon"; private static final String USER = "user"; private static final String APP_NAME = "App"; private static final Instance INSTANCE = Instance.create(InstanceId.of("-id-")) .register(Registration.create(APP_NAME, "https://health").build()); private static final String MESSAGE = "test"; private SlackNotifier notifier; private RestTemplate restTemplate; @BeforeEach void setUp() { InstanceRepository repository = mock(InstanceRepository.class); when(repository.find(INSTANCE.getId())).thenReturn(Mono.just(INSTANCE)); restTemplate = mock(RestTemplate.class); notifier = new SlackNotifier(repository, restTemplate); notifier.setUsername(USER); notifier.setWebhookUrl(URI.create("http://localhost/")); } @Test void test_onApplicationEvent_resolve() { notifier.setChannel(CHANNEL); notifier.setIcon(ICON); StepVerifier .create(notifier .notify(new InstanceStatusChangedEvent(INSTANCE.getId(), INSTANCE.getVersion(), StatusInfo.ofDown()))) .verifyComplete(); clearInvocations(restTemplate); StepVerifier .create(notifier .notify(new InstanceStatusChangedEvent(INSTANCE.getId(), INSTANCE.getVersion(), StatusInfo.ofUp()))) .verifyComplete(); Object expected = expectedMessage("good", USER, ICON, CHANNEL, standardMessage("UP")); verify(restTemplate).postForEntity(any(URI.class), eq(expected), eq(Void.class)); } @Test void test_onApplicationEvent_resolve_without_channel_and_icon() { StepVerifier .create(notifier .notify(new InstanceStatusChangedEvent(INSTANCE.getId(), INSTANCE.getVersion(), StatusInfo.ofDown()))) .verifyComplete(); clearInvocations(restTemplate); StepVerifier .create(notifier .notify(new InstanceStatusChangedEvent(INSTANCE.getId(), INSTANCE.getVersion(), StatusInfo.ofUp()))) .verifyComplete(); Object expected = expectedMessage("good", USER, null, null, standardMessage("UP")); verify(restTemplate).postForEntity(any(URI.class), eq(expected), eq(Void.class)); } @Test void test_onApplicationEvent_resolve_with_given_user() { String anotherUser = "another user"; notifier.setUsername(anotherUser); notifier.setChannel(CHANNEL); notifier.setIcon(ICON); StepVerifier .create(notifier .notify(new InstanceStatusChangedEvent(INSTANCE.getId(), INSTANCE.getVersion(), StatusInfo.ofDown()))) .verifyComplete(); clearInvocations(restTemplate); StepVerifier .create(notifier .notify(new InstanceStatusChangedEvent(INSTANCE.getId(), INSTANCE.getVersion(), StatusInfo.ofUp()))) .verifyComplete(); Object expected = expectedMessage("good", anotherUser, ICON, CHANNEL, standardMessage("UP")); verify(restTemplate).postForEntity(any(URI.class), eq(expected), eq(Void.class)); } @Test void test_onApplicationEvent_resolve_with_given_message() { notifier.setMessage(MESSAGE); notifier.setChannel(CHANNEL); notifier.setIcon(ICON); StepVerifier .create(notifier .notify(new InstanceStatusChangedEvent(INSTANCE.getId(), INSTANCE.getVersion(), StatusInfo.ofDown()))) .verifyComplete(); clearInvocations(restTemplate); StepVerifier .create(notifier .notify(new InstanceStatusChangedEvent(INSTANCE.getId(), INSTANCE.getVersion(), StatusInfo.ofUp()))) .verifyComplete(); Object expected = expectedMessage("good", USER, ICON, CHANNEL, MESSAGE); verify(restTemplate).postForEntity(any(URI.class), eq(expected), eq(Void.class)); } @Test void test_onApplicationEvent_trigger() { notifier.setChannel(CHANNEL); notifier.setIcon(ICON); StepVerifier .create(notifier .notify(new InstanceStatusChangedEvent(INSTANCE.getId(), INSTANCE.getVersion(), StatusInfo.ofUp()))) .verifyComplete(); clearInvocations(restTemplate); StepVerifier .create(notifier .notify(new InstanceStatusChangedEvent(INSTANCE.getId(), INSTANCE.getVersion(), StatusInfo.ofDown()))) .verifyComplete(); Object expected = expectedMessage("danger", USER, ICON, CHANNEL, standardMessage("DOWN")); verify(restTemplate).postForEntity(any(URI.class), eq(expected), eq(Void.class)); } private HttpEntity> expectedMessage(String color, String user, String icon, String channel, String message) { Map messageJson = new HashMap<>(); messageJson.put("username", user); if (icon != null) { messageJson.put("icon_emoji", ":" + icon + ":"); } if (channel != null) { messageJson.put("channel", channel); } Map attachments = new HashMap<>(); attachments.put("text", message); attachments.put("color", color); attachments.put("mrkdwn_in", Collections.singletonList("text")); messageJson.put("attachments", Collections.singletonList(attachments)); HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_JSON); return new HttpEntity<>(messageJson, headers); } private String standardMessage(String status) { return "*" + INSTANCE.getRegistration().getName() + "* (" + INSTANCE.getId() + ") is *" + status + "*"; } } ================================================ FILE: spring-boot-admin-server/src/test/java/de/codecentric/boot/admin/server/notify/TelegramNotifierTest.java ================================================ /* * 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.server.notify; import java.util.HashMap; import java.util.Map; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.mockito.ArgumentCaptor; import org.springframework.http.HttpEntity; import org.springframework.http.ResponseEntity; import org.springframework.web.client.RestTemplate; import reactor.core.publisher.Mono; import reactor.test.StepVerifier; 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.InstanceStatusChangedEvent; import de.codecentric.boot.admin.server.domain.values.InstanceId; import de.codecentric.boot.admin.server.domain.values.Registration; import de.codecentric.boot.admin.server.domain.values.StatusInfo; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.ArgumentMatchers.isA; import static org.mockito.Mockito.clearInvocations; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; class TelegramNotifierTest { private final Instance instance = Instance.create(InstanceId.of("-id-")) .register(Registration.create("Telegram", "https://health").build()); private TelegramNotifier notifier; private RestTemplate restTemplate; @BeforeEach void setUp() { InstanceRepository repository = mock(InstanceRepository.class); when(repository.find(instance.getId())).thenReturn(Mono.just(instance)); restTemplate = mock(RestTemplate.class); notifier = new TelegramNotifier(repository, restTemplate); notifier.setDisableNotify(false); notifier.setAuthToken("--token-"); notifier.setChatId("-room-"); notifier.setParseMode("HTML"); notifier.setApiUrl("https://telegram.com"); } @Test void test_onApplicationEvent_resolve() { StepVerifier .create(notifier .notify(new InstanceStatusChangedEvent(instance.getId(), instance.getVersion(), StatusInfo.ofDown()))) .verifyComplete(); clearInvocations(restTemplate); StepVerifier .create(notifier .notify(new InstanceStatusChangedEvent(instance.getId(), instance.getVersion(), StatusInfo.ofUp()))) .verifyComplete(); verify(restTemplate).getForObject( "https://telegram.com/bot--token-/sendmessage?chat_id={chat_id}&text={text}" + "&parse_mode={parse_mode}&disable_notification={disable_notification}", Void.class, getParameters("UP")); } @Test void test_onApplicationEvent_trigger() { StatusInfo infoDown = StatusInfo.ofDown(); @SuppressWarnings("unchecked") ArgumentCaptor>> httpRequest = ArgumentCaptor .forClass((Class>>) (Class) HttpEntity.class); when(restTemplate.postForEntity(isA(String.class), httpRequest.capture(), eq(Void.class))) .thenReturn(ResponseEntity.ok().build()); StepVerifier .create(notifier .notify(new InstanceStatusChangedEvent(instance.getId(), instance.getVersion(), StatusInfo.ofUp()))) .verifyComplete(); StepVerifier .create(notifier.notify(new InstanceStatusChangedEvent(instance.getId(), instance.getVersion(), infoDown))) .verifyComplete(); verify(restTemplate).getForObject( "https://telegram.com/bot--token-/sendmessage?chat_id={chat_id}&text={text}" + "&parse_mode={parse_mode}&disable_notification={disable_notification}", Void.class, getParameters("DOWN")); } private Map getParameters(String status) { Map parameters = new HashMap<>(); parameters.put("chat_id", "-room-"); parameters.put("text", getMessage("Telegram", "-id-", status)); parameters.put("parse_mode", "HTML"); parameters.put("disable_notification", false); return parameters; } private String getMessage(String name, String id, String status) { return "" + name + "/" + id + " is " + status + ""; } } ================================================ FILE: spring-boot-admin-server/src/test/java/de/codecentric/boot/admin/server/notify/TestNotifier.java ================================================ /* * 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.server.notify; import java.util.ArrayList; import java.util.List; import lombok.Getter; import reactor.core.publisher.Mono; import de.codecentric.boot.admin.server.domain.events.InstanceEvent; @Getter public class TestNotifier implements Notifier { private final List events = new ArrayList<>(); @Override public Mono notify(InstanceEvent event) { this.events.add(event); return Mono.empty(); } } ================================================ FILE: spring-boot-admin-server/src/test/java/de/codecentric/boot/admin/server/notify/WebexNotifierTest.java ================================================ /* * Copyright 2014-2024 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.server.notify; import java.net.URI; import java.util.HashMap; import java.util.Map; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.mockito.ArgumentCaptor; import org.springframework.http.HttpEntity; import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.client.RestTemplate; import reactor.core.publisher.Mono; import reactor.test.StepVerifier; 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.InstanceStatusChangedEvent; import de.codecentric.boot.admin.server.domain.values.InstanceId; import de.codecentric.boot.admin.server.domain.values.Registration; import de.codecentric.boot.admin.server.domain.values.StatusInfo; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.ArgumentMatchers.isA; import static org.mockito.Mockito.clearInvocations; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; class WebexNotifierTest { private final Instance instance = Instance.create(InstanceId.of("-id-")) .register(Registration.create("webex", "https://health").build()); private WebexNotifier notifier; private RestTemplate restTemplate; @BeforeEach void setUp() { InstanceRepository repository = mock(InstanceRepository.class); when(repository.find(instance.getId())).thenReturn(Mono.just(instance)); restTemplate = mock(RestTemplate.class); notifier = new WebexNotifier(repository, restTemplate); notifier.setAuthToken("--token-"); notifier.setRoomId("--room--"); notifier.setUrl(URI.create("https://webexapis.com/v1/messages")); } @Test void test_onApplicationEvent_resolve() { StepVerifier .create(notifier .notify(new InstanceStatusChangedEvent(instance.getId(), instance.getVersion(), StatusInfo.ofDown()))) .verifyComplete(); clearInvocations(restTemplate); StepVerifier .create(notifier .notify(new InstanceStatusChangedEvent(instance.getId(), instance.getVersion(), StatusInfo.ofUp()))) .verifyComplete(); URI defaultUrl = URI.create("https://webexapis.com/v1/messages"); HttpEntity> entity = new HttpEntity<>(createMessage("UP"), createHeaders()); verify(restTemplate).postForEntity(defaultUrl, entity, Void.class); } @Test void test_onApplicationEvent_trigger() { StatusInfo infoDown = StatusInfo.ofDown(); @SuppressWarnings("unchecked") ArgumentCaptor>> httpRequest = ArgumentCaptor .forClass((Class>>) (Class) HttpEntity.class); when(restTemplate.postForEntity(isA(String.class), httpRequest.capture(), eq(Void.class))) .thenReturn(ResponseEntity.ok().build()); StepVerifier .create(notifier .notify(new InstanceStatusChangedEvent(instance.getId(), instance.getVersion(), StatusInfo.ofUp()))) .verifyComplete(); StepVerifier .create(notifier.notify(new InstanceStatusChangedEvent(instance.getId(), instance.getVersion(), infoDown))) .verifyComplete(); URI defaultUrl = URI.create("https://webexapis.com/v1/messages"); HttpEntity> entity = new HttpEntity<>(createMessage("DOWN"), createHeaders()); verify(restTemplate).postForEntity(defaultUrl, entity, Void.class); } private HttpHeaders createHeaders() { HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_JSON); headers.setBearerAuth("--token-"); return headers; } private Map createMessage(String status) { Map parameters = new HashMap<>(); parameters.put("roomId", "--room--"); parameters.put("markdown", getMessage("webex", "-id-", status)); return parameters; } private String getMessage(String name, String id, String status) { return "" + name + "/" + id + " is " + status + ""; } } ================================================ FILE: spring-boot-admin-server/src/test/java/de/codecentric/boot/admin/server/notify/filter/FilteringNotifierTest.java ================================================ /* * 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.server.notify.filter; import java.time.Duration; import java.time.Instant; import org.assertj.core.api.Assertions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import reactor.core.publisher.Mono; import reactor.test.StepVerifier; 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.InstanceRegisteredEvent; import de.codecentric.boot.admin.server.domain.values.InstanceId; import de.codecentric.boot.admin.server.domain.values.Registration; import de.codecentric.boot.admin.server.notify.TestNotifier; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; class FilteringNotifierTest { private final Instance instance = Instance.create(InstanceId.of("-")) .register(Registration.create("foo", "https://health").build()); private final InstanceRegisteredEvent event = new InstanceRegisteredEvent(instance.getId(), instance.getVersion(), instance.getRegistration()); private InstanceRepository repository; @BeforeEach void setUp() { repository = mock(InstanceRepository.class); when(repository.find(instance.getId())).thenReturn(Mono.just(instance)); } @Test void test_ctor_assert() { Assertions.assertThatThrownBy(() -> new FilteringNotifier(null, repository)) .isInstanceOf(IllegalArgumentException.class); } @Test void test_expired_removal() { FilteringNotifier notifier = new FilteringNotifier(new TestNotifier(), repository); notifier.setCleanupInterval(Duration.ZERO); ApplicationNameNotificationFilter filter1 = new ApplicationNameNotificationFilter("foo", Instant.now().minus(Duration.ofSeconds(1))); notifier.addFilter(filter1); ApplicationNameNotificationFilter filter2 = new ApplicationNameNotificationFilter("bar", null); notifier.addFilter(filter2); assertThat(notifier.getNotificationFilters()).containsKey(filter1.getId()).containsKey(filter2.getId()); StepVerifier.create(notifier.notify(event)).verifyComplete(); assertThat(notifier.getNotificationFilters()).doesNotContainKey(filter1.getId()).containsKey(filter2.getId()); notifier.removeFilter(filter2.getId()); assertThat(notifier.getNotificationFilters()).doesNotContainKey(filter2.getId()); } @Test void test_filter() { TestNotifier delegate = new TestNotifier(); FilteringNotifier notifier = new FilteringNotifier(delegate, repository); AbstractNotificationFilter trueFilter = new AbstractNotificationFilter() { @Override public boolean filter(InstanceEvent event, Instance instance) { return true; } }; notifier.addFilter(trueFilter); StepVerifier.create(notifier.notify(event)).verifyComplete(); assertThat(delegate.getEvents()).doesNotContain(event); notifier.removeFilter(trueFilter.getId()); StepVerifier.create(notifier.notify(event)).verifyComplete(); assertThat(delegate.getEvents()).contains(event); } } ================================================ FILE: spring-boot-admin-server/src/test/java/de/codecentric/boot/admin/server/notify/filter/InstanceIdNotificationFilterTest.java ================================================ /* * 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.server.notify.filter; import org.junit.jupiter.api.Test; import de.codecentric.boot.admin.server.domain.entities.Instance; import de.codecentric.boot.admin.server.domain.events.InstanceRegisteredEvent; import de.codecentric.boot.admin.server.domain.values.InstanceId; import de.codecentric.boot.admin.server.domain.values.Registration; import static org.assertj.core.api.Assertions.assertThat; class InstanceIdNotificationFilterTest { @Test void test_filterByName() { NotificationFilter filter = new InstanceIdNotificationFilter(InstanceId.of("cafebabe"), null); Instance filteredInstance = Instance.create(InstanceId.of("cafebabe")) .register(Registration.create("foo", "https://health").build()); InstanceRegisteredEvent filteredEvent = new InstanceRegisteredEvent(filteredInstance.getId(), filteredInstance.getVersion(), filteredInstance.getRegistration()); assertThat(filter.filter(filteredEvent, filteredInstance)).isTrue(); Instance ignoredInstance = Instance.create(InstanceId.of("-")) .register(Registration.create("foo", "https://health").build()); InstanceRegisteredEvent ignoredEvent = new InstanceRegisteredEvent(ignoredInstance.getId(), ignoredInstance.getVersion(), ignoredInstance.getRegistration()); assertThat(filter.filter(ignoredEvent, ignoredInstance)).isFalse(); } } ================================================ FILE: spring-boot-admin-server/src/test/java/de/codecentric/boot/admin/server/notify/filter/InstanceNameNotificationFilterTest.java ================================================ /* * 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.server.notify.filter; import java.time.Duration; import java.time.Instant; import org.junit.jupiter.api.Test; import de.codecentric.boot.admin.server.domain.entities.Instance; import de.codecentric.boot.admin.server.domain.events.InstanceRegisteredEvent; import de.codecentric.boot.admin.server.domain.values.InstanceId; import de.codecentric.boot.admin.server.domain.values.Registration; import static org.assertj.core.api.Assertions.assertThat; import static org.awaitility.Awaitility.await; class InstanceNameNotificationFilterTest { @Test void test_filterByName() { NotificationFilter filter = new ApplicationNameNotificationFilter("foo", null); Instance filteredInstance = Instance.create(InstanceId.of("-")) .register(Registration.create("foo", "https://health").build()); InstanceRegisteredEvent filteredEvent = new InstanceRegisteredEvent(filteredInstance.getId(), filteredInstance.getVersion(), filteredInstance.getRegistration()); assertThat(filter.filter(filteredEvent, filteredInstance)).isTrue(); Instance ignoredInstance = Instance.create(InstanceId.of("-")) .register(Registration.create("bar", "https://health").build()); InstanceRegisteredEvent ignoredEvent = new InstanceRegisteredEvent(ignoredInstance.getId(), ignoredInstance.getVersion(), ignoredInstance.getRegistration()); assertThat(filter.filter(ignoredEvent, ignoredInstance)).isFalse(); } @Test void test_expiry() { ExpiringNotificationFilter filterForever = new ApplicationNameNotificationFilter("foo", null); ExpiringNotificationFilter filterExpired = new ApplicationNameNotificationFilter("foo", Instant.now().minus(Duration.ofSeconds(1))); ExpiringNotificationFilter filterLong = new ApplicationNameNotificationFilter("foo", Instant.now().plus(Duration.ofMillis(100))); assertThat(filterForever.isExpired()).isFalse(); assertThat(filterLong.isExpired()).isFalse(); assertThat(filterExpired.isExpired()).isTrue(); await().atMost(Duration.ofMillis(200)) .pollInterval(Duration.ofMillis(10)) .untilAsserted(() -> assertThat(filterLong.isExpired()).isTrue()); } } ================================================ FILE: spring-boot-admin-server/src/test/java/de/codecentric/boot/admin/server/notify/filter/web/NotificationFilterControllerTest.java ================================================ /* * 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.server.notify.filter.web; import java.io.IOException; import java.util.Map; import org.junit.jupiter.api.Test; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.setup.MockMvcBuilders; import tools.jackson.databind.json.JsonMapper; import de.codecentric.boot.admin.server.domain.entities.EventsourcingInstanceRepository; import de.codecentric.boot.admin.server.domain.entities.InstanceRepository; import de.codecentric.boot.admin.server.eventstore.InMemoryEventStore; import de.codecentric.boot.admin.server.notify.LoggingNotifier; import de.codecentric.boot.admin.server.notify.filter.FilteringNotifier; import static org.hamcrest.Matchers.emptyString; import static org.hamcrest.Matchers.not; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; class NotificationFilterControllerTest { private final InstanceRepository repository = new EventsourcingInstanceRepository(new InMemoryEventStore()); private final NotificationFilterController controller = new NotificationFilterController( new FilteringNotifier(new LoggingNotifier(this.repository), this.repository)); private final MockMvc mvc = MockMvcBuilders.standaloneSetup(this.controller) .setCustomHandlerMapping( () -> new de.codecentric.boot.admin.server.web.servlet.AdminControllerHandlerMapping("/")) .build(); @Test void test_missing_parameters() throws Exception { this.mvc.perform(post("/notifications/filters")).andExpect(status().isBadRequest()); } @Test void test_delete_notfound() throws Exception { this.mvc.perform(delete("/notifications/filters/abcdef")).andExpect(status().isNotFound()); } @Test void test_post_delete() throws Exception { String response = this.mvc.perform(post("/notifications/filters?instanceId=1337&ttl=10000")) .andExpect(status().isOk()) .andExpect(content().string(not(emptyString()))) .andReturn() .getResponse() .getContentAsString(); String id = extractId(response); this.mvc.perform(get("/notifications/filters")).andExpect(status().isOk()); this.mvc.perform(delete("/notifications/filters/{id}", id)).andExpect(status().isOk()); this.mvc.perform(get("/notifications/filters")).andExpect(status().isOk()).andExpect(jsonPath("$").isEmpty()); } private String extractId(String response) throws IOException { Map map = new JsonMapper().readerFor(Map.class).readValue(response); return map.get("id").toString(); } } ================================================ FILE: spring-boot-admin-server/src/test/java/de/codecentric/boot/admin/server/services/AbstractEventHandlerTest.java ================================================ /* * 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.server.services; import java.time.Duration; import lombok.Getter; import org.junit.jupiter.api.Test; import org.reactivestreams.Publisher; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import reactor.core.publisher.Sinks; import reactor.test.StepVerifier; import reactor.test.publisher.TestPublisher; import de.codecentric.boot.admin.server.domain.events.InstanceDeregisteredEvent; import de.codecentric.boot.admin.server.domain.events.InstanceEvent; import de.codecentric.boot.admin.server.domain.events.InstanceRegisteredEvent; import de.codecentric.boot.admin.server.domain.values.InstanceId; import de.codecentric.boot.admin.server.domain.values.Registration; class AbstractEventHandlerTest { private static final Logger log = LoggerFactory.getLogger(AbstractEventHandlerTest.class); private static final Registration registration = Registration.create("foo", "https://health").build(); private static final InstanceRegisteredEvent firstEvent = new InstanceRegisteredEvent(InstanceId.of("id"), 0L, registration); private static final InstanceRegisteredEvent secondEvent = new InstanceRegisteredEvent(InstanceId.of("id"), 1L, registration); private static final InstanceRegisteredEvent errorEvent = new InstanceRegisteredEvent(InstanceId.of("err"), 2L, registration); private static final InstanceDeregisteredEvent ignoredEvent = new InstanceDeregisteredEvent(InstanceId.of("id"), 2L); @Test void should_resubscribe_after_error() { TestPublisher testPublisher = TestPublisher.create(); TestEventHandler eventHandler = new TestEventHandler(testPublisher.flux()); eventHandler.start(); StepVerifier.create(eventHandler.getFlux()) .expectSubscription() .then(() -> testPublisher.next(firstEvent, errorEvent, secondEvent)) .expectNext(firstEvent, secondEvent) .thenCancel() .verify(Duration.ofSeconds(2)); } @Test void should_filter() { TestPublisher testPublisher = TestPublisher.create(); TestEventHandler eventHandler = new TestEventHandler(testPublisher.flux()); eventHandler.start(); StepVerifier.create(eventHandler.getFlux()) .expectSubscription() .then(() -> testPublisher.next(firstEvent, ignoredEvent, secondEvent)) .expectNext(firstEvent, secondEvent) .thenCancel() .verify(Duration.ofSeconds(1)); } public static final class TestEventHandler extends AbstractEventHandler { private final Sinks.Many unicast; @Getter private final Flux flux; private TestEventHandler(Publisher publisher) { super(publisher, InstanceRegisteredEvent.class); this.unicast = Sinks.many().unicast().onBackpressureBuffer(); this.flux = this.unicast.asFlux(); } @Override protected Publisher handle(Flux publisher) { return publisher.flatMap((event) -> { if (event.equals(errorEvent)) { return Mono.error(new IllegalStateException("TestError")); } else { log.info("Event {}", event); this.unicast.tryEmitNext(event); return Mono.empty(); } }).then(); } } } ================================================ FILE: spring-boot-admin-server/src/test/java/de/codecentric/boot/admin/server/services/ApplicationRegistryTest.java ================================================ /* * 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.server.services; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.CsvSource; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import reactor.test.StepVerifier; import de.codecentric.boot.admin.server.domain.entities.Application; import de.codecentric.boot.admin.server.domain.entities.Instance; import de.codecentric.boot.admin.server.domain.values.BuildVersion; import de.codecentric.boot.admin.server.domain.values.InstanceId; import de.codecentric.boot.admin.server.domain.values.Registration; import de.codecentric.boot.admin.server.domain.values.StatusInfo; import de.codecentric.boot.admin.server.eventstore.InstanceEventPublisher; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; class ApplicationRegistryTest { private InstanceRegistry instanceRegistry; private ApplicationRegistry applicationRegistry; @BeforeEach void setUp() { this.instanceRegistry = mock(InstanceRegistry.class); InstanceEventPublisher instanceEventPublisher = mock(InstanceEventPublisher.class); this.applicationRegistry = new ApplicationRegistry(this.instanceRegistry, instanceEventPublisher); } @Test void getApplications_noRegisteredApplications() { when(this.instanceRegistry.getInstances()).thenReturn(Flux.just()); StepVerifier.create(this.applicationRegistry.getApplications()).verifyComplete(); } @Test void getApplications_oneRegisteredAndOneUnregisteredApplication() { Instance instance1 = getInstance("App1"); Instance instance2 = getInstance("App2").deregister(); when(this.instanceRegistry.getInstances()).thenReturn(Flux.just(instance1, instance2)); StepVerifier.create(this.applicationRegistry.getApplications()) .assertNext((app) -> assertThat(app.getName()).isEqualTo("App1")) .verifyComplete(); } @Test void getApplications_allRegisteredApplications() { Instance instance1 = getInstance("App1"); Instance instance2 = getInstance("App2"); when(this.instanceRegistry.getInstances()).thenReturn(Flux.just(instance1, instance2)); StepVerifier.create(this.applicationRegistry.getApplications()) .recordWith(ArrayList::new) .thenConsumeWhile((a) -> true) .consumeRecordedWith((applications) -> assertThat(applications.stream().map(Application::getName)) .containsExactlyInAnyOrder("App1", "App2")) .verifyComplete(); } @Test void getApplication_noRegisteredApplications() { when(this.instanceRegistry.getInstances(any(String.class))).thenReturn(Flux.just()); StepVerifier.create(this.applicationRegistry.getApplication("App1")).verifyComplete(); } @Test void getApplication_noMatchingRegisteredApplications() { when(this.instanceRegistry.getInstances("App2")).thenReturn(Flux.just(getInstance("App2"))); when(this.instanceRegistry.getInstances(any(String.class))).thenReturn(Flux.just()); StepVerifier.create(this.applicationRegistry.getApplication("App1")).verifyComplete(); } @Test void getApplication_matchingUnregisteredApplications() { Instance instance = getInstance("App1").deregister(); when(this.instanceRegistry.getInstances("App1")).thenReturn(Flux.just(instance)); StepVerifier.create(this.applicationRegistry.getApplication("App1")).verifyComplete(); } @Test void getApplication_matchingRegisteredApplications() { Instance instance = getInstance("App1"); when(this.instanceRegistry.getInstances("App1")).thenReturn(Flux.just(instance)); StepVerifier.create(this.applicationRegistry.getApplication("App1")) .assertNext((app) -> assertThat(app.getName()).isEqualTo("App1")) .verifyComplete(); } @Test void deregister() { Instance instance1 = getInstance("App1"); InstanceId instance1Id = instance1.getId(); when(this.instanceRegistry.getInstances("App1")).thenReturn(Flux.just(instance1)); when(this.instanceRegistry.deregister(instance1Id)).thenReturn(Mono.just(instance1Id)); StepVerifier.create(this.applicationRegistry.deregister("App1")) .assertNext((instanceId) -> assertThat(instanceId).isEqualTo(instance1Id)) .verifyComplete(); verify(this.instanceRegistry).deregister(instance1Id); } @Test void getBuildVersion() { Instance instance1 = getInstance("App1", "0.1"); Instance instance2 = getInstance("App2", "0.2"); // Empty list should return null: assertThat(this.applicationRegistry.getBuildVersion(Collections.emptyList())).isNull(); // Single instance should return the version number: assertThat(this.applicationRegistry.getBuildVersion(Collections.singletonList(instance1))) .isEqualTo(BuildVersion.valueOf("0.1")); // Multiple instances should return the version number range: assertThat(this.applicationRegistry.getBuildVersion(Arrays.asList(instance1, instance2))) .isEqualTo(BuildVersion.valueOf("0.1 ... 0.2")); } @ParameterizedTest @CsvSource({ "UP, UP, UP", "DOWN, DOWN, DOWN", "UNKNOWN, UNKNOWN, UNKNOWN", "UP, DOWN, RESTRICTED", "UP, UNKNOWN, RESTRICTED", "UP, OUT_OF_SERVICE, RESTRICTED", "UP, OFFLINE, RESTRICTED", "UP, RESTRICTED, RESTRICTED", "DOWN, UP, RESTRICTED" }) void getStatus(String instance1Status, String instance2Status, String expectedApplicationStatus) { Instance instance1 = getInstance("App1").withStatusInfo(StatusInfo.valueOf(instance1Status)); Instance instance2 = getInstance("App1").withStatusInfo(StatusInfo.valueOf(instance2Status)); when(this.instanceRegistry.getInstances()).thenReturn(Flux.just(instance1, instance2)); StepVerifier.create(this.applicationRegistry.getApplications()) .recordWith(ArrayList::new) .thenConsumeWhile((a) -> true) .consumeRecordedWith((applications) -> assertThat(applications.stream().map(Application::getStatus)) .containsExactly(expectedApplicationStatus)) .verifyComplete(); } private Instance getInstance(String applicationName, String version) { Registration registration = Registration.create(applicationName, "http://localhost:8080/health") .metadata("version", version) .build(); InstanceId id = InstanceId.of("TEST" + applicationName); return Instance.create(id).register(registration); } private Instance getInstance(String applicationName) { return getInstance(applicationName, "FooBarVersion"); } } ================================================ FILE: spring-boot-admin-server/src/test/java/de/codecentric/boot/admin/server/services/CloudFoundryInstanceIdGeneratorTest.java ================================================ /* * 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.server.services; import org.junit.jupiter.api.Test; import de.codecentric.boot.admin.server.domain.values.InstanceId; import de.codecentric.boot.admin.server.domain.values.Registration; import static org.assertj.core.api.Assertions.assertThat; class CloudFoundryInstanceIdGeneratorTest { private final CloudFoundryInstanceIdGenerator instance = new CloudFoundryInstanceIdGenerator( new HashingInstanceUrlIdGenerator()); @Test void test_cloud_foundry_instance_id() { Registration registration = Registration.create("foo", "https://health") .metadata("applicationId", "549e64cf-a478-423d-9d6d-02d803a028a8") .metadata("instanceId", "0") .build(); assertThat(instance.generateId(registration)) .isEqualTo(InstanceId.of("549e64cf-a478-423d-9d6d-02d803a028a8:0")); } @Test void test_health_url_instance_id() { Registration registration = Registration.create("foo", "https://health").build(); assertThat(instance.generateId(registration)).isEqualTo(InstanceId.of("cff917ccf90e")); } } ================================================ FILE: spring-boot-admin-server/src/test/java/de/codecentric/boot/admin/server/services/EndpointDetectionTriggerTest.java ================================================ /* * 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.server.services; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import reactor.core.publisher.Mono; import reactor.test.publisher.TestPublisher; import de.codecentric.boot.admin.server.domain.entities.Instance; import de.codecentric.boot.admin.server.domain.events.InstanceEvent; import de.codecentric.boot.admin.server.domain.events.InstanceRegisteredEvent; import de.codecentric.boot.admin.server.domain.events.InstanceRegistrationUpdatedEvent; import de.codecentric.boot.admin.server.domain.events.InstanceStatusChangedEvent; import de.codecentric.boot.admin.server.domain.values.InstanceId; import de.codecentric.boot.admin.server.domain.values.Registration; import de.codecentric.boot.admin.server.domain.values.StatusInfo; import static org.awaitility.Awaitility.await; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.clearInvocations; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; class EndpointDetectionTriggerTest { private final Instance instance = Instance.create(InstanceId.of("id-1")) .register(Registration.create("foo", "http://health-1").build()); private final TestPublisher events = TestPublisher.create(); private final EndpointDetector detector = mock(EndpointDetector.class); private EndpointDetectionTrigger trigger; @BeforeEach void setUp() { when(this.detector.detectEndpoints(any(InstanceId.class))).thenReturn(Mono.empty()); this.trigger = new EndpointDetectionTrigger(this.detector, this.events.flux()); this.trigger.start(); await().until(this.events::wasSubscribed); } @Test void should_detect_on_status_changed() { // when status-change event is emitted this.events.next( new InstanceStatusChangedEvent(this.instance.getId(), this.instance.getVersion(), StatusInfo.ofDown())); // then should update verify(this.detector, times(1)).detectEndpoints(this.instance.getId()); } @Test void should_detect_on_registration_updated() { // when status-change event is emitted this.events.next(new InstanceRegistrationUpdatedEvent(this.instance.getId(), this.instance.getVersion(), this.instance.getRegistration())); // then should update verify(this.detector, times(1)).detectEndpoints(this.instance.getId()); } @Test void should_not_detect_on_non_relevant_event() { // when some non-status-change event is emitted this.events.next(new InstanceRegisteredEvent(this.instance.getId(), this.instance.getVersion(), this.instance.getRegistration())); // then should not update verify(this.detector, never()).detectEndpoints(this.instance.getId()); } @Test void should_not_detect_on_trigger_stopped() { // when registered event is emitted but the trigger has been stopped this.trigger.stop(); clearInvocations(this.detector); this.events.next(new InstanceRegisteredEvent(this.instance.getId(), this.instance.getVersion(), this.instance.getRegistration())); // then should not update verify(this.detector, never()).detectEndpoints(this.instance.getId()); } @Test void should_continue_detection_after_error() { // when status-change event is emitted and an error is emitted when(this.detector.detectEndpoints(any())).thenReturn(Mono.error(IllegalStateException::new)) .thenReturn(Mono.empty()); this.events.next( new InstanceStatusChangedEvent(this.instance.getId(), this.instance.getVersion(), StatusInfo.ofDown())); this.events .next(new InstanceStatusChangedEvent(this.instance.getId(), this.instance.getVersion(), StatusInfo.ofUp())); // then should update verify(this.detector, times(2)).detectEndpoints(this.instance.getId()); } } ================================================ FILE: spring-boot-admin-server/src/test/java/de/codecentric/boot/admin/server/services/EndpointDetectorTest.java ================================================ /* * 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.server.services; import java.time.Duration; import java.util.logging.Level; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import reactor.test.StepVerifier; import de.codecentric.boot.admin.server.domain.entities.EventsourcingInstanceRepository; 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.InstanceEndpointsDetectedEvent; import de.codecentric.boot.admin.server.domain.values.Endpoints; import de.codecentric.boot.admin.server.domain.values.InstanceId; import de.codecentric.boot.admin.server.domain.values.Registration; import de.codecentric.boot.admin.server.domain.values.StatusInfo; import de.codecentric.boot.admin.server.eventstore.ConcurrentMapEventStore; import de.codecentric.boot.admin.server.eventstore.InMemoryEventStore; import de.codecentric.boot.admin.server.services.endpoints.EndpointDetectionStrategy; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; class EndpointDetectorTest { private EndpointDetector detector; private InstanceRepository repository; private ConcurrentMapEventStore eventStore; private EndpointDetectionStrategy strategy; @BeforeEach void setup() { eventStore = new InMemoryEventStore(); repository = new EventsourcingInstanceRepository(eventStore); strategy = mock(EndpointDetectionStrategy.class); detector = new EndpointDetector(repository, strategy); } @Test void should_update_endpoints() { // given Registration registration = Registration.create("foo", "https://health").managementUrl("https://mgmt").build(); Instance instance = Instance.create(InstanceId.of("onl")) .register(registration) .withStatusInfo(StatusInfo.ofUp()); StepVerifier.create(repository.save(instance)).expectNextCount(1).verifyComplete(); Instance noActuator = Instance.create(InstanceId.of("noActuator")) .register(Registration.create("foo", "https://health").build()) .withStatusInfo(StatusInfo.ofUp()); StepVerifier.create(repository.save(noActuator)).expectNextCount(1).verifyComplete(); Instance offline = Instance.create(InstanceId.of("off")) .register(registration) .withStatusInfo(StatusInfo.ofOffline()); StepVerifier.create(repository.save(offline)).expectNextCount(1).verifyComplete(); Instance unknown = Instance.create(InstanceId.of("unk")) .register(registration) .withStatusInfo(StatusInfo.ofUnknown()); StepVerifier.create(repository.save(unknown)).expectNextCount(1).verifyComplete(); when(strategy.detectEndpoints(any(Instance.class))).thenReturn(Mono.just(Endpoints.single("id", "url"))); // when/then StepVerifier.create(Flux.from(eventStore).log("FOO", Level.SEVERE)) .expectSubscription() .then(() -> StepVerifier.create(detector.detectEndpoints(offline.getId())).verifyComplete()) .then(() -> StepVerifier.create(detector.detectEndpoints(unknown.getId())).verifyComplete()) .then(() -> StepVerifier.create(detector.detectEndpoints(noActuator.getId())).verifyComplete()) .expectNoEvent(Duration.ofMillis(100L)) .then(() -> StepVerifier.create(detector.detectEndpoints(instance.getId())).verifyComplete()) .assertNext((event) -> assertThat(event).isInstanceOf(InstanceEndpointsDetectedEvent.class)) .thenCancel() .verify(); StepVerifier.create(repository.find(instance.getId())) .assertNext((app) -> assertThat(app.getEndpoints()) .isEqualTo(Endpoints.single("id", "url").withEndpoint("health", "https://health"))) .verifyComplete(); } } ================================================ FILE: spring-boot-admin-server/src/test/java/de/codecentric/boot/admin/server/services/InfoUpdateTriggerTest.java ================================================ /* * 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.server.services; import java.time.Duration; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import reactor.core.publisher.Mono; import reactor.test.publisher.TestPublisher; import de.codecentric.boot.admin.server.domain.entities.Instance; import de.codecentric.boot.admin.server.domain.events.InstanceEndpointsDetectedEvent; import de.codecentric.boot.admin.server.domain.events.InstanceEvent; import de.codecentric.boot.admin.server.domain.events.InstanceInfoChangedEvent; import de.codecentric.boot.admin.server.domain.events.InstanceRegisteredEvent; import de.codecentric.boot.admin.server.domain.events.InstanceRegistrationUpdatedEvent; import de.codecentric.boot.admin.server.domain.events.InstanceStatusChangedEvent; import de.codecentric.boot.admin.server.domain.values.Info; import de.codecentric.boot.admin.server.domain.values.InstanceId; import de.codecentric.boot.admin.server.domain.values.Registration; import de.codecentric.boot.admin.server.domain.values.StatusInfo; import static org.awaitility.Awaitility.await; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.atLeast; import static org.mockito.Mockito.clearInvocations; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; class InfoUpdateTriggerTest { private final Instance instance = Instance.create(InstanceId.of("id-1")) .register(Registration.create("foo", "http://health-1").build()); private final InfoUpdater updater = mock(InfoUpdater.class); private final TestPublisher events = TestPublisher.create(); private InfoUpdateTrigger trigger; @BeforeEach void setUp() { when(this.updater.updateInfo(any(InstanceId.class))).thenReturn(Mono.empty()); this.trigger = new InfoUpdateTrigger(this.updater, this.events.flux(), Duration.ofMinutes(5), Duration.ofMinutes(1), Duration.ofMinutes(10)); this.trigger.start(); await().until(this.events::wasSubscribed); } @Test void should_start_and_stop_monitor() { // given this.trigger.stop(); this.trigger.setInterval(Duration.ofMillis(100)); this.trigger.setLifetime(Duration.ofMillis(50)); this.trigger.start(); await().until(this.events::wasSubscribed); // when an event is emitted this.events.next( new InstanceStatusChangedEvent(this.instance.getId(), this.instance.getVersion(), StatusInfo.ofDown())); // then it should update at least once for the event await().atMost(Duration.ofMillis(200)) .untilAsserted(() -> verify(this.updater, atLeast(1)).updateInfo(this.instance.getId())); // and then at least one more time due to monitoring interval (after lifetime // expires) await().atMost(Duration.ofMillis(400)) .pollInterval(Duration.ofMillis(30)) .untilAsserted(() -> verify(this.updater, atLeast(2)).updateInfo(this.instance.getId())); // given long lifetime this.trigger.setLifetime(Duration.ofSeconds(10)); clearInvocations(this.updater); // when the lifetime is not expired should not update via interval monitoring await().pollDelay(Duration.ofMillis(150)) .atMost(Duration.ofMillis(200)) .untilAsserted(() -> verify(this.updater, never()).updateInfo(any(InstanceId.class))); // when trigger is stopped this.trigger.setLifetime(Duration.ofMillis(50)); this.trigger.stop(); clearInvocations(this.updater); // then it should stop updating await().pollDelay(Duration.ofMillis(150)) .atMost(Duration.ofMillis(200)) .untilAsserted(() -> verify(this.updater, never()).updateInfo(any(InstanceId.class))); } @Test void should_not_update_when_stopped() { // when registered event is emitted but the trigger has been stopped this.trigger.stop(); clearInvocations(this.updater); this.events.next(new InstanceRegisteredEvent(this.instance.getId(), this.instance.getVersion(), this.instance.getRegistration())); // then should not update verify(this.updater, never()).updateInfo(this.instance.getId()); } @Test void should_update_on_endpoints_detects_event() { // when registered event is emitted this.events.next(new InstanceEndpointsDetectedEvent(this.instance.getId(), this.instance.getVersion(), this.instance.getEndpoints())); // then should update verify(this.updater, times(1)).updateInfo(this.instance.getId()); } @Test void should_update_on_status_changed_event() { // when registered event is emitted this.events.next( new InstanceStatusChangedEvent(this.instance.getId(), this.instance.getVersion(), StatusInfo.ofDown())); // then should update verify(this.updater, times(1)).updateInfo(this.instance.getId()); } @Test void should_update_on_instance_registration_update_event() { // when registered event is emitted this.events.next(new InstanceRegistrationUpdatedEvent(this.instance.getId(), this.instance.getVersion(), this.instance.getRegistration())); // then should update verify(this.updater, times(1)).updateInfo(this.instance.getId()); } @Test void should_not_update_on_non_relevant_event() { // when some non-registered event is emitted this.events.next(new InstanceInfoChangedEvent(this.instance.getId(), this.instance.getVersion(), Info.empty())); // then should not update verify(this.updater, never()).updateInfo(this.instance.getId()); } @Test void should_continue_update_after_error() { // when status-change event is emitted and an error is emitted when(this.updater.updateInfo(any())).thenReturn(Mono.error(IllegalStateException::new)) .thenReturn(Mono.empty()); this.events.next( new InstanceStatusChangedEvent(this.instance.getId(), this.instance.getVersion(), StatusInfo.ofDown())); this.events .next(new InstanceStatusChangedEvent(this.instance.getId(), this.instance.getVersion(), StatusInfo.ofUp())); // then should update verify(this.updater, times(2)).updateInfo(this.instance.getId()); } } ================================================ FILE: spring-boot-admin-server/src/test/java/de/codecentric/boot/admin/server/services/InfoUpdaterTest.java ================================================ /* * 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.server.services; import java.time.Duration; import com.github.tomakehurst.wiremock.WireMockServer; import com.github.tomakehurst.wiremock.core.Options; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import reactor.test.StepVerifier; import de.codecentric.boot.admin.server.domain.entities.EventsourcingInstanceRepository; 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.InstanceInfoChangedEvent; import de.codecentric.boot.admin.server.domain.values.Endpoint; import de.codecentric.boot.admin.server.domain.values.Endpoints; import de.codecentric.boot.admin.server.domain.values.Info; import de.codecentric.boot.admin.server.domain.values.InstanceId; import de.codecentric.boot.admin.server.domain.values.Registration; import de.codecentric.boot.admin.server.domain.values.StatusInfo; import de.codecentric.boot.admin.server.eventstore.InMemoryEventStore; import de.codecentric.boot.admin.server.web.client.InstanceWebClient; import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; import static com.github.tomakehurst.wiremock.client.WireMock.get; import static com.github.tomakehurst.wiremock.client.WireMock.okJson; import static com.github.tomakehurst.wiremock.client.WireMock.serverError; import static com.github.tomakehurst.wiremock.stubbing.Scenario.STARTED; import static de.codecentric.boot.admin.server.web.client.InstanceExchangeFilterFunctions.retry; import static de.codecentric.boot.admin.server.web.client.InstanceExchangeFilterFunctions.rewriteEndpointUrl; import static de.codecentric.boot.admin.server.web.client.InstanceExchangeFilterFunctions.timeout; import static java.util.Collections.emptyMap; import static java.util.Collections.singletonMap; import static org.assertj.core.api.Assertions.assertThat; class InfoUpdaterTest { public final WireMockServer wireMock = new WireMockServer(Options.DYNAMIC_PORT); private InfoUpdater updater; private InMemoryEventStore eventStore; private InstanceRepository repository; private final ApiMediaTypeHandler apiMediaTypeHandler = new ApiMediaTypeHandler(); @BeforeEach void setup() { this.eventStore = new InMemoryEventStore(); this.repository = new EventsourcingInstanceRepository(this.eventStore); this.updater = new InfoUpdater(this.repository, InstanceWebClient.builder() .filter(rewriteEndpointUrl()) .filter(retry(0, singletonMap(Endpoint.INFO, 1))) .filter(timeout(Duration.ofSeconds(2), emptyMap())) .build(), this.apiMediaTypeHandler); this.wireMock.start(); } @AfterEach void teardown() { this.wireMock.stop(); } @BeforeAll static void setUp() { StepVerifier.setDefaultTimeout(Duration.ofSeconds(5)); } @AfterAll static void tearDown() { StepVerifier.resetDefaultTimeout(); } @Test void should_update_info_for_online_with_info_endpoint_only() { // given Registration registration = Registration.create("foo", this.wireMock.url("/health")).build(); Instance instance = Instance.create(InstanceId.of("onl")) .register(registration) .withEndpoints(Endpoints.single("info", this.wireMock.url("/info"))) .withStatusInfo(StatusInfo.ofUp()); StepVerifier.create(this.repository.save(instance)).expectNextCount(1).verifyComplete(); String body = "{ \"foo\": \"bar\" }"; this.wireMock.stubFor( get("/info").willReturn(okJson(body).withHeader("Content-Length", Integer.toString(body.length())))); Instance noInfo = Instance.create(InstanceId.of("noinfo")) .register(registration) .withEndpoints(Endpoints.single("beans", this.wireMock.url("/beans"))) .withStatusInfo(StatusInfo.ofUp()); StepVerifier.create(this.repository.save(noInfo)).expectNextCount(1).verifyComplete(); Instance offline = Instance.create(InstanceId.of("off")) .register(registration) .withStatusInfo(StatusInfo.ofOffline()); StepVerifier.create(this.repository.save(offline)).expectNextCount(1).verifyComplete(); Instance unknown = Instance.create(InstanceId.of("unk")) .register(registration) .withStatusInfo(StatusInfo.ofUnknown()); StepVerifier.create(this.repository.save(unknown)).expectNextCount(1).verifyComplete(); // when StepVerifier.create(this.eventStore) .expectSubscription() .then(() -> StepVerifier.create(this.updater.updateInfo(offline.getId())).verifyComplete()) .then(() -> StepVerifier.create(this.updater.updateInfo(unknown.getId())).verifyComplete()) .then(() -> StepVerifier.create(this.updater.updateInfo(noInfo.getId())).verifyComplete()) .expectNoEvent(Duration.ofMillis(100L)) .then(() -> StepVerifier.create(this.updater.updateInfo(instance.getId())).verifyComplete()) // then .assertNext((event) -> assertThat(event).isInstanceOf(InstanceInfoChangedEvent.class)) .thenCancel() .verify(); StepVerifier.create(this.repository.find(instance.getId())) .assertNext((app) -> assertThat(app.getInfo()).isEqualTo(Info.from(singletonMap("foo", "bar")))) .verifyComplete(); } @Test void should_clear_info_on_http_error() { // given Instance instance = Instance.create(InstanceId.of("onl")) .register(Registration.create("foo", this.wireMock.url("/health")).build()) .withEndpoints(Endpoints.single("info", this.wireMock.url("/info"))) .withStatusInfo(StatusInfo.ofUp()) .withInfo(Info.from(singletonMap("foo", "bar"))); StepVerifier.create(this.repository.save(instance)).expectNextCount(1).verifyComplete(); this.wireMock.stubFor(get("/info").willReturn(serverError())); // when StepVerifier.create(this.eventStore) .expectSubscription() .then(() -> StepVerifier.create(this.updater.updateInfo(instance.getId())).verifyComplete()) // then .assertNext((event) -> assertThat(event).isInstanceOf(InstanceInfoChangedEvent.class)) .thenCancel() .verify(); StepVerifier.create(this.repository.find(instance.getId())) .assertNext((app) -> assertThat(app.getInfo()).isEqualTo(Info.empty())) .verifyComplete(); } @Test void should_clear_info_on_exception() { // given Instance instance = Instance.create(InstanceId.of("onl")) .register(Registration.create("foo", this.wireMock.url("/health")).build()) .withEndpoints(Endpoints.single("info", this.wireMock.url("/info"))) .withStatusInfo(StatusInfo.ofUp()) .withInfo(Info.from(singletonMap("foo", "bar"))); StepVerifier.create(this.repository.save(instance)).expectNextCount(1).verifyComplete(); this.wireMock.stubFor(get("/info").willReturn(okJson("{ \"foo\": \"bar\" }").withFixedDelay(2100))); // when StepVerifier.create(this.eventStore) .expectSubscription() .then(() -> StepVerifier.create(this.updater.updateInfo(instance.getId())).verifyComplete()) // then .assertNext((event) -> assertThat(event).isInstanceOf(InstanceInfoChangedEvent.class)) .thenCancel() .verify(); StepVerifier.create(this.repository.find(instance.getId())) .assertNext((app) -> assertThat(app.getInfo()).isEqualTo(Info.empty())) .verifyComplete(); } @Test void should_retry() { // given Registration registration = Registration.create("foo", this.wireMock.url("/health")).build(); Instance instance = Instance.create(InstanceId.of("onl")) .register(registration) .withEndpoints(Endpoints.single("info", this.wireMock.url("/info"))) .withStatusInfo(StatusInfo.ofUp()); StepVerifier.create(this.repository.save(instance)).expectNextCount(1).verifyComplete(); this.wireMock.stubFor(get("/info").inScenario("retry") .whenScenarioStateIs(STARTED) .willReturn(aResponse().withFixedDelay(5000)) .willSetStateTo("recovered")); String body = "{ \"foo\": \"bar\" }"; this.wireMock.stubFor(get("/info").inScenario("retry") .whenScenarioStateIs("recovered") .willReturn(okJson(body).withHeader("Content-Length", Integer.toString(body.length())))); // when StepVerifier.create(this.eventStore) .expectSubscription() .then(() -> StepVerifier.create(this.updater.updateInfo(instance.getId())).verifyComplete()) // then .assertNext((event) -> assertThat(event).isInstanceOf(InstanceInfoChangedEvent.class)) .thenCancel() .verify(); StepVerifier.create(this.repository.find(instance.getId())) .assertNext((app) -> assertThat(app.getInfo()).isEqualTo(Info.from(singletonMap("foo", "bar")))) .verifyComplete(); } } ================================================ FILE: spring-boot-admin-server/src/test/java/de/codecentric/boot/admin/server/services/InstanceRegistryTest.java ================================================ /* * Copyright 2014-2024 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.server.services; import java.util.ArrayList; import java.util.Map; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import reactor.test.StepVerifier; import de.codecentric.boot.admin.server.domain.entities.EventsourcingInstanceRepository; 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.domain.values.InstanceId; import de.codecentric.boot.admin.server.domain.values.Registration; import de.codecentric.boot.admin.server.domain.values.StatusInfo; import de.codecentric.boot.admin.server.eventstore.InMemoryEventStore; import static java.util.Collections.singletonMap; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; class InstanceRegistryTest { private InstanceRepository repository; private InstanceIdGenerator idGenerator; private InstanceRegistry registry; @BeforeEach void setUp() { repository = new EventsourcingInstanceRepository(new InMemoryEventStore()); idGenerator = new HashingInstanceUrlIdGenerator(); registry = new InstanceRegistry(repository, idGenerator, (instance) -> { Map metadata = instance.getRegistration().getMetadata(); return !metadata.containsKey("displayed") || !metadata.get("displayed").equals("false"); }); } @Test void registerFailed_null() { assertThatThrownBy(() -> registry.register(null)).isInstanceOf(IllegalArgumentException.class); } @Test void register() { Registration registration = Registration.create("abc", "http://localhost:8080/health").build(); InstanceId id = registry.register(registration).block(); StepVerifier.create(registry.getInstance(id)).assertNext((app) -> { assertThat(app.getRegistration()).isEqualTo(registration); assertThat(app.getId()).isNotNull(); }).verifyComplete(); StepVerifier.create(registry.getInstances()).assertNext((app) -> { assertThat(app.getRegistration()).isEqualTo(registration); assertThat(app.getId()).isNotNull(); }).verifyComplete(); } @Test void deregister() { InstanceId id = registry.register(Registration.create("abc", "http://localhost:8080/health").build()).block(); registry.deregister(id).block(); StepVerifier.create(registry.getInstance(id)) .assertNext((app) -> assertThat(app.isRegistered()).isFalse()) .verifyComplete(); } @Test void refresh() { // Given instance is already registered and has status and info. StatusInfo status = StatusInfo.ofUp(); Info info = Info.from(singletonMap("foo", "bar")); Registration registration = Registration.create("abc", "http://localhost:8080/health").build(); InstanceId id = idGenerator.generateId(registration); Instance app = Instance.create(id).register(registration).withStatusInfo(status).withInfo(info); StepVerifier.create(repository.save(app)).expectNextCount(1).verifyComplete(); // When instance registers second time InstanceId refreshId = registry.register(Registration.create("abc", "http://localhost:8080/health").build()) .block(); assertThat(refreshId).isEqualTo(id); StepVerifier.create(registry.getInstance(id)).assertNext((registered) -> { // Then info and status are retained assertThat(registered.getInfo()).isEqualTo(info); assertThat(registered.getStatusInfo()).isEqualTo(status); }).verifyComplete(); } @Test void findByName() { InstanceId id1 = registry.register(Registration.create("abc", "http://localhost:8080/health").build()).block(); InstanceId id2 = registry.register(Registration.create("abc", "http://localhost:8081/health").build()).block(); InstanceId id3 = registry.register(Registration.create("zzz", "http://localhost:9999/health").build()).block(); StepVerifier.create(registry.getInstances("abc")) .recordWith(ArrayList::new) .thenConsumeWhile((a) -> true) .consumeRecordedWith( (applications) -> assertThat(applications.stream().map(Instance::getId)).doesNotContain(id3) .containsExactlyInAnyOrder(id1, id2)) .verifyComplete(); } @Test void findByNameAndFilter() { InstanceId id1 = registry.register(Registration.create("abc", "http://localhost:8080/health").build()).block(); registry .register(Registration.create("abc", "http://localhost:8081/health").metadata("displayed", "false").build()) .block(); StepVerifier.create(registry.getInstances("abc")) .recordWith(ArrayList::new) .thenConsumeWhile((a) -> true) .consumeRecordedWith( (applications) -> assertThat(applications.stream().map(Instance::getId)).containsExactly(id1)) .verifyComplete(); } } ================================================ FILE: spring-boot-admin-server/src/test/java/de/codecentric/boot/admin/server/services/IntervalCheckTest.java ================================================ /* * 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.server.services; import java.time.Duration; import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.CopyOnWriteArrayList; import java.util.function.Function; import java.util.stream.Collectors; import java.util.stream.IntStream; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.mockito.invocation.InvocationOnMock; import reactor.core.publisher.Mono; import de.codecentric.boot.admin.server.domain.values.InstanceId; import static org.assertj.core.api.Assertions.assertThat; import static org.awaitility.Awaitility.await; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.atLeast; import static org.mockito.Mockito.atLeastOnce; import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.reset; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; class IntervalCheckTest { private static final InstanceId INSTANCE_ID = InstanceId.of("Test"); @SuppressWarnings("unchecked") private final Function> checkFn = mock(Function.class, (i) -> Mono.empty()); private IntervalCheck intervalCheck; @BeforeEach void setUp() { reset(this.checkFn); this.intervalCheck = new IntervalCheck("test", this.checkFn, Duration.ofMillis(10), Duration.ofMillis(10), Duration.ofSeconds(1)); } @Test void should_check_after_being_started() { this.intervalCheck.markAsChecked(INSTANCE_ID); this.intervalCheck.start(); await().atMost(Duration.ofMillis(100)) .pollInterval(Duration.ofMillis(10)) .untilAsserted(() -> verify(this.checkFn, atLeastOnce()).apply(INSTANCE_ID)); } @Test void should_not_check_when_stopped() { this.intervalCheck.markAsChecked(INSTANCE_ID); this.intervalCheck.stop(); await().pollDelay(Duration.ofMillis(100)).untilAsserted(() -> verify(this.checkFn, never()).apply(any())); } @Test void should_not_check_in_retention_period() { this.intervalCheck.setMinRetention(Duration.ofSeconds(100)); this.intervalCheck.markAsChecked(INSTANCE_ID); this.intervalCheck.start(); await().pollDelay(Duration.ofMillis(100)).untilAsserted(() -> verify(this.checkFn, never()).apply(any())); } @Test void should_recheck_after_retention_period() { this.intervalCheck.setMinRetention(Duration.ofMillis(10)); this.intervalCheck.markAsChecked(INSTANCE_ID); this.intervalCheck.start(); await().atMost(Duration.ofMillis(100)) .pollInterval(Duration.ofMillis(10)) .untilAsserted(() -> verify(this.checkFn, atLeast(2)).apply(INSTANCE_ID)); } @Test void should_not_wait_longer_than_maxBackoff() { this.intervalCheck.setInterval(Duration.ofMillis(10)); this.intervalCheck.setMinRetention(Duration.ofMillis(10)); this.intervalCheck.setMaxBackoff(Duration.ofSeconds(2)); this.intervalCheck.markAsChecked(INSTANCE_ID); when(this.checkFn.apply(any())).thenReturn(Mono.error(new RuntimeException("Test"))); this.intervalCheck.start(); await().atMost(Duration.ofSeconds(10)).untilAsserted(() -> verify(this.checkFn, atLeast(7)).apply(INSTANCE_ID)); } @Test void should_check_after_error() { this.intervalCheck.markAsChecked(INSTANCE_ID); when(this.checkFn.apply(any())).thenReturn(Mono.error(new RuntimeException("Test"))).thenReturn(Mono.empty()); this.intervalCheck.start(); await().atMost(Duration.ofMillis(1500)) .untilAsserted(() -> verify(this.checkFn, atLeast(2)).apply(InstanceId.of("Test"))); } @Test void should_not_overflow_when_checks_timeout_randomly() { Duration CHECK_INTERVAL = Duration.ofMillis(500); @SuppressWarnings("unchecked") Function> timeoutCheckFn = mock(Function.class); java.util.concurrent.atomic.AtomicInteger invocationCount = new java.util.concurrent.atomic.AtomicInteger(0); doAnswer((invocation) -> { if (invocationCount.getAndIncrement() % 2 == 0) { // Succeed quickly on even invocations return Mono.empty(); } else { // Timeout on odd invocations return Mono.just("slow response").delayElement(CHECK_INTERVAL.plus(Duration.ofSeconds(1))).then(); } }).when(timeoutCheckFn).apply(any()); IntervalCheck timeoutCheck = new IntervalCheck("overflow-test", timeoutCheckFn, CHECK_INTERVAL, CHECK_INTERVAL, Duration.ofSeconds(1)); List retryErrors = new CopyOnWriteArrayList<>(); timeoutCheck.setRetryConsumer(retryErrors::add); timeoutCheck.markAsChecked(INSTANCE_ID); timeoutCheck.start(); try { await().atMost(Duration.ofSeconds(5)) .until(() -> retryErrors.stream() .noneMatch((Throwable er) -> "OverflowException".equalsIgnoreCase(er.getClass().getSimpleName()))); assertThat(retryErrors).noneMatch((Throwable e) -> e.getCause() != null && "OverflowException".equalsIgnoreCase(e.getCause().getClass().getSimpleName())); } finally { timeoutCheck.stop(); } } @Test void should_not_lose_checks_under_backpressure() { Duration CHECK_INTERVAL = Duration.ofMillis(100); @SuppressWarnings("unchecked") Function> slowCheckFn = mock(Function.class); IntervalCheck slowCheck = new IntervalCheck("backpressure-test", slowCheckFn, CHECK_INTERVAL, Duration.ofMillis(50), Duration.ofSeconds(1)); List checkTimes = new CopyOnWriteArrayList<>(); doAnswer((invocation) -> { checkTimes.add(System.currentTimeMillis()); return Mono.empty(); }).when(slowCheckFn).apply(any()); slowCheck.markAsChecked(INSTANCE_ID); slowCheck.start(); try { await().atMost(Duration.ofSeconds(2)).until(() -> checkTimes.size() >= 5); // With onBackpressureLatest, we should have processed multiple checks without // drops assertThat(checkTimes).hasSizeGreaterThanOrEqualTo(5); } finally { slowCheck.stop(); } } @Test void should_not_lose_checks_under_backpressure_latest() { Duration CHECK_INTERVAL = Duration.ofMillis(100); @SuppressWarnings("unchecked") Function> slowCheckFn = mock(Function.class); IntervalCheck slowCheck = new IntervalCheck("backpressure-test", slowCheckFn, CHECK_INTERVAL, CHECK_INTERVAL, Duration.ofSeconds(1)); // Add multiple instances to increase load and cause drops Set instanceIds = IntStream.range(0, 50) .mapToObj((i) -> InstanceId.of("Test" + i)) .collect(Collectors.toSet()); instanceIds.forEach((InstanceId instanceId) -> slowCheck.markAsChecked(instanceId)); List checkTimes = new CopyOnWriteArrayList<>(); Map> checkTimesPerInstance = new ConcurrentHashMap<>(); java.util.concurrent.atomic.AtomicInteger invocationCount = new java.util.concurrent.atomic.AtomicInteger(0); doAnswer((invocation) -> { long checkTime = System.currentTimeMillis(); String instanceId = instanceIdString(invocation); List checkTimesInstance = checkTimesPerInstance.computeIfAbsent(instanceId, (String k) -> new CopyOnWriteArrayList<>()); checkTimesInstance.add(checkTime); checkTimes.add(checkTime); if (invocationCount.getAndIncrement() % 2 == 0) { // Sometimes succeed quickly return Mono.empty(); } else { // Sometimes slow return Mono.delay(CHECK_INTERVAL.plus(Duration.ofMillis(500))).then(); } }).when(slowCheckFn).apply(any()); slowCheck.start(); try { await().atMost(Duration.ofSeconds(5)).until(() -> checkTimes.size() >= 500); // With onBackpressureLatest, we should process more checks without drops instanceIds.forEach((InstanceId instanceId) -> assertThat(checkTimesPerInstance.get(instanceId.getValue())) .hasSizeGreaterThanOrEqualTo(10)); } finally { slowCheck.stop(); } } @AfterEach void tearDown() { this.intervalCheck.stop(); } private static String instanceIdString(InvocationOnMock invocation) { return invocation.getArguments()[0].toString(); } } ================================================ FILE: spring-boot-admin-server/src/test/java/de/codecentric/boot/admin/server/services/StatusUpdateTriggerTest.java ================================================ /* * 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.server.services; import java.time.Duration; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import reactor.core.publisher.Mono; import reactor.test.publisher.TestPublisher; import de.codecentric.boot.admin.server.domain.entities.Instance; import de.codecentric.boot.admin.server.domain.events.InstanceEvent; import de.codecentric.boot.admin.server.domain.events.InstanceInfoChangedEvent; import de.codecentric.boot.admin.server.domain.events.InstanceRegisteredEvent; import de.codecentric.boot.admin.server.domain.events.InstanceRegistrationUpdatedEvent; import de.codecentric.boot.admin.server.domain.values.Info; import de.codecentric.boot.admin.server.domain.values.InstanceId; import de.codecentric.boot.admin.server.domain.values.Registration; import static org.awaitility.Awaitility.await; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.atLeast; import static org.mockito.Mockito.clearInvocations; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; class StatusUpdateTriggerTest { private final Instance instance = Instance.create(InstanceId.of("id-1")) .register(Registration.create("foo", "http://health-1").build()); private final StatusUpdater updater = mock(StatusUpdater.class); private final TestPublisher events = TestPublisher.create(); private StatusUpdateTrigger trigger; @BeforeEach void setUp() { when(this.updater.updateStatus(any(InstanceId.class))).thenReturn(Mono.empty()); when(this.updater.timeout(any())).thenReturn(this.updater); this.trigger = new StatusUpdateTrigger(this.updater, this.events.flux(), Duration.ofSeconds(10), Duration.ofSeconds(10), Duration.ofSeconds(60)); this.trigger.start(); await().until(this.events::wasSubscribed); } @Test void should_start_and_stop_monitor() { // given this.trigger.stop(); this.trigger.setInterval(Duration.ofMillis(10)); this.trigger.setLifetime(Duration.ofMillis(10)); this.trigger.start(); await().until(this.events::wasSubscribed); this.events.next(new InstanceRegisteredEvent(this.instance.getId(), 0L, this.instance.getRegistration())); // it should start updating one time for registration and at least once for // monitor await().atMost(Duration.ofMillis(50)) .pollInterval(Duration.ofMillis(10)) .untilAsserted(() -> verify(this.updater, atLeast(2)).updateStatus(this.instance.getId())); // given long lifetime this.trigger.setLifetime(Duration.ofSeconds(10)); clearInvocations(this.updater); // when the lifetime is not expired should never update await().pollDelay(Duration.ofMillis(50)) .untilAsserted(() -> verify(this.updater, never()).updateStatus(any(InstanceId.class))); this.trigger.setLifetime(Duration.ofMillis(10)); this.trigger.stop(); clearInvocations(this.updater); // when trigger ist destroyed it should stop updating await().pollDelay(Duration.ofMillis(15)) .untilAsserted(() -> verify(this.updater, never()).updateStatus(any(InstanceId.class))); } @Test void should_not_update_when_stopped() { // when registered event is emitted but the trigger has been stopped this.trigger.stop(); clearInvocations(this.updater); this.events.next(new InstanceRegisteredEvent(this.instance.getId(), this.instance.getVersion(), this.instance.getRegistration())); // then should not update verify(this.updater, never()).updateStatus(this.instance.getId()); } @Test void should_update_on_instance_registered_event() { // when registered event is emitted this.events.next(new InstanceRegisteredEvent(this.instance.getId(), this.instance.getVersion(), this.instance.getRegistration())); // then should update verify(this.updater, times(1)).updateStatus(this.instance.getId()); } @Test void should_update_on_instance_registration_update_event() { // when registered event is emitted this.events.next(new InstanceRegistrationUpdatedEvent(this.instance.getId(), this.instance.getVersion(), this.instance.getRegistration())); // then should update verify(this.updater, times(1)).updateStatus(this.instance.getId()); } @Test void should_not_update_on_non_relevant_event() { // when some non-registered event is emitted this.events.next(new InstanceInfoChangedEvent(this.instance.getId(), this.instance.getVersion(), Info.empty())); // then should not update verify(this.updater, never()).updateStatus(this.instance.getId()); } @Test void should_continue_update_after_error() { // when status-change event is emitted and an error is emitted when(this.updater.updateStatus(any())).thenReturn(Mono.error(IllegalStateException::new)) .thenReturn(Mono.empty()); this.events.next(new InstanceRegistrationUpdatedEvent(this.instance.getId(), this.instance.getVersion(), this.instance.getRegistration())); this.events.next(new InstanceRegistrationUpdatedEvent(this.instance.getId(), this.instance.getVersion(), this.instance.getRegistration())); // then should update verify(this.updater, times(2)).updateStatus(this.instance.getId()); } } ================================================ FILE: spring-boot-admin-server/src/test/java/de/codecentric/boot/admin/server/services/StatusUpdaterTest.java ================================================ /* * 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.server.services; import java.time.Duration; import com.github.tomakehurst.wiremock.WireMockServer; import com.github.tomakehurst.wiremock.core.Options; import com.github.tomakehurst.wiremock.http.Fault; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.boot.actuate.endpoint.ApiVersion; import org.springframework.http.MediaType; import reactor.core.publisher.Mono; import reactor.test.StepVerifier; import de.codecentric.boot.admin.server.domain.entities.EventsourcingInstanceRepository; 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.InstanceStatusChangedEvent; import de.codecentric.boot.admin.server.domain.values.Endpoint; import de.codecentric.boot.admin.server.domain.values.InstanceId; import de.codecentric.boot.admin.server.domain.values.Registration; import de.codecentric.boot.admin.server.eventstore.ConcurrentMapEventStore; import de.codecentric.boot.admin.server.eventstore.InMemoryEventStore; import de.codecentric.boot.admin.server.web.client.InstanceWebClient; import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; import static com.github.tomakehurst.wiremock.client.WireMock.get; import static com.github.tomakehurst.wiremock.client.WireMock.ok; import static com.github.tomakehurst.wiremock.client.WireMock.okForContentType; import static com.github.tomakehurst.wiremock.client.WireMock.okJson; import static com.github.tomakehurst.wiremock.client.WireMock.status; import static com.github.tomakehurst.wiremock.stubbing.Scenario.STARTED; import static de.codecentric.boot.admin.server.web.client.InstanceExchangeFilterFunctions.retry; import static de.codecentric.boot.admin.server.web.client.InstanceExchangeFilterFunctions.rewriteEndpointUrl; import static de.codecentric.boot.admin.server.web.client.InstanceExchangeFilterFunctions.timeout; import static java.util.Collections.emptyMap; import static java.util.Collections.singletonMap; import static org.assertj.core.api.Assertions.assertThat; class StatusUpdaterTest { // @Rule public final WireMockServer wireMock = new WireMockServer(Options.DYNAMIC_PORT); private StatusUpdater updater; private ConcurrentMapEventStore eventStore; private InstanceRepository repository; private Instance instance; @BeforeAll static void setUp() { StepVerifier.setDefaultTimeout(Duration.ofSeconds(5)); } @AfterAll static void tearDown() { StepVerifier.resetDefaultTimeout(); } @BeforeEach void setup() { this.wireMock.start(); this.eventStore = new InMemoryEventStore(); this.repository = new EventsourcingInstanceRepository(this.eventStore); this.instance = Instance.create(InstanceId.of("id")) .register(Registration.create("foo", this.wireMock.url("/health")).build()); StepVerifier.create(this.repository.save(this.instance)).expectNextCount(1).verifyComplete(); this.updater = new StatusUpdater(this.repository, InstanceWebClient.builder() .filter(rewriteEndpointUrl()) .filter(retry(0, singletonMap(Endpoint.HEALTH, 1))) .filter(timeout(Duration.ofSeconds(2), emptyMap())) .build(), new ApiMediaTypeHandler()); } @AfterEach void teardown() { this.wireMock.stop(); } @Test void should_change_status_to_down() { String body = "{ \"status\" : \"UP\", \"details\" : { \"foo\" : \"bar\" } }"; this.wireMock.stubFor( get("/health").willReturn(okForContentType(ApiVersion.LATEST.getProducedMimeType().toString(), body) .withHeader("Content-Length", Integer.toString(body.length())))); StepVerifier.create(this.eventStore) .expectSubscription() .then(() -> StepVerifier.create(this.updater.updateStatus(this.instance.getId())).verifyComplete()) .assertNext((event) -> { assertThat(event).isInstanceOf(InstanceStatusChangedEvent.class); assertThat(event.getInstance()).isEqualTo(this.instance.getId()); InstanceStatusChangedEvent statusChangedEvent = (InstanceStatusChangedEvent) event; assertThat(statusChangedEvent.getStatusInfo().getStatus()).isEqualTo("UP"); assertThat(statusChangedEvent.getStatusInfo().getDetails()).isEqualTo(singletonMap("foo", "bar")); }) .thenCancel() .verify(); StepVerifier.create(this.repository.find(this.instance.getId())) .assertNext((app) -> assertThat(app.getStatusInfo().getStatus()).isEqualTo("UP")) .verifyComplete(); StepVerifier .create(this.repository.computeIfPresent(this.instance.getId(), (key, instance) -> Mono.just(instance.deregister()))) .then(() -> StepVerifier.create(this.updater.updateStatus(this.instance.getId())).verifyComplete()) .thenCancel() .verify(); StepVerifier.create(this.repository.find(this.instance.getId())) .assertNext((app) -> assertThat(app.getStatusInfo().getStatus()).isEqualTo("UNKNOWN")) .verifyComplete(); } @Test void should_not_change_status() { String body = "{ \"status\" : \"UNKNOWN\" }"; this.wireMock.stubFor( get("/health").willReturn(okJson(body).withHeader("Content-Type", Integer.toString(body.length())))); StepVerifier.create(this.eventStore) .expectSubscription() .then(() -> StepVerifier.create(this.updater.updateStatus(this.instance.getId())).verifyComplete()) .expectNoEvent(Duration.ofMillis(100L)) .thenCancel() .verify(); } @Test void should_change_status_to_up() { this.wireMock.stubFor(get("/health").willReturn(ok())); StepVerifier.create(this.eventStore) .expectSubscription() .then(() -> StepVerifier.create(this.updater.updateStatus(this.instance.getId())).verifyComplete()) .assertNext((event) -> assertThat(event).isInstanceOf(InstanceStatusChangedEvent.class)) .thenCancel() .verify(); StepVerifier.create(this.repository.find(this.instance.getId())) .assertNext((app) -> assertThat(app.getStatusInfo().getStatus()).isEqualTo("UP")) .verifyComplete(); } @Test void should_change_status_to_down_with_details() { String body = "{ \"foo\" : \"bar\" }"; this.wireMock .stubFor(get("/health").willReturn(status(503).withHeader("Content-Type", MediaType.APPLICATION_JSON_VALUE) .withHeader("Content-Length", Integer.toString(body.length())) .withBody(body))); StepVerifier.create(this.eventStore) .expectSubscription() .then(() -> StepVerifier.create(this.updater.updateStatus(this.instance.getId())).verifyComplete()) .assertNext((event) -> assertThat(event).isInstanceOf(InstanceStatusChangedEvent.class)) .thenCancel() .verify(); StepVerifier.create(this.repository.find(this.instance.getId())).assertNext((app) -> { assertThat(app.getStatusInfo().getStatus()).isEqualTo("DOWN"); assertThat(app.getStatusInfo().getDetails()).containsEntry("foo", "bar"); }).verifyComplete(); } @Test void should_change_status_to_down_without_details_incompatible_content_type() { this.wireMock.stubFor(get("/health").willReturn(status(503))); StepVerifier.create(this.eventStore) .expectSubscription() .then(() -> StepVerifier.create(this.updater.updateStatus(this.instance.getId())).verifyComplete()) .assertNext((event) -> assertThat(event).isInstanceOf(InstanceStatusChangedEvent.class)) .thenCancel() .verify(); StepVerifier.create(this.repository.find(this.instance.getId())).assertNext((app) -> { assertThat(app.getStatusInfo().getStatus()).isEqualTo("DOWN"); assertThat(app.getStatusInfo().getDetails()).containsEntry("status", 503) .containsEntry("error", "Service Unavailable"); }).verifyComplete(); } @Test void should_change_status_to_down_without_details_no_body() { this.wireMock.stubFor( get("/health").willReturn(status(503).withHeader("Content-Type", MediaType.APPLICATION_JSON_VALUE))); StepVerifier.create(this.eventStore) .expectSubscription() .then(() -> StepVerifier.create(this.updater.updateStatus(this.instance.getId())).verifyComplete()) .assertNext((event) -> assertThat(event).isInstanceOf(InstanceStatusChangedEvent.class)) .thenCancel() .verify(); StepVerifier.create(this.repository.find(this.instance.getId())).assertNext((app) -> { assertThat(app.getStatusInfo().getStatus()).isEqualTo("DOWN"); assertThat(app.getStatusInfo().getDetails()).containsEntry("status", 503) .containsEntry("error", "Service Unavailable"); }).verifyComplete(); } @Test void should_change_status_to_offline() { this.wireMock.stubFor(get("/health").willReturn(aResponse().withFault(Fault.EMPTY_RESPONSE))); StepVerifier.create(this.eventStore) .expectSubscription() .then(() -> StepVerifier.create(this.updater.updateStatus(this.instance.getId())).verifyComplete()) .assertNext((event) -> assertThat(event).isInstanceOf(InstanceStatusChangedEvent.class)) .thenCancel() .verify(); StepVerifier.create(this.repository.find(this.instance.getId())).assertNext((app) -> { assertThat(app.getStatusInfo().getStatus()).isEqualTo("OFFLINE"); assertThat(app.getStatusInfo().getDetails()).containsKeys("message", "exception"); }).verifyComplete(); StepVerifier.create(this.updater.updateStatus(this.instance.getId())).verifyComplete(); } @Test void should_retry() { this.wireMock.stubFor(get("/health").inScenario("retry") .whenScenarioStateIs(STARTED) .willReturn(aResponse().withFixedDelay(5000)) .willSetStateTo("recovered")); this.wireMock.stubFor(get("/health").inScenario("retry").whenScenarioStateIs("recovered").willReturn(ok())); StepVerifier.create(this.eventStore) .expectSubscription() .then(() -> StepVerifier.create(this.updater.updateStatus(this.instance.getId())).verifyComplete()) .assertNext((event) -> assertThat(event).isInstanceOf(InstanceStatusChangedEvent.class)) .thenCancel() .verify(); StepVerifier.create(this.repository.find(this.instance.getId())) .assertNext((app) -> assertThat(app.getStatusInfo().getStatus()).isEqualTo("UP")) .verifyComplete(); } } ================================================ FILE: spring-boot-admin-server/src/test/java/de/codecentric/boot/admin/server/services/endpoints/ChainingStrategyTest.java ================================================ /* * 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.server.services.endpoints; import org.junit.jupiter.api.Test; import reactor.core.publisher.Mono; import reactor.test.StepVerifier; import de.codecentric.boot.admin.server.domain.entities.Instance; import de.codecentric.boot.admin.server.domain.values.Endpoints; import de.codecentric.boot.admin.server.domain.values.InstanceId; import static org.assertj.core.api.Assertions.assertThatThrownBy; class ChainingStrategyTest { @Test void invariants() { assertThatThrownBy(() -> new ChainingStrategy((EndpointDetectionStrategy[]) null)) .isInstanceOf(IllegalArgumentException.class) .hasMessage("'delegates' must not be null."); assertThatThrownBy(() -> new ChainingStrategy((EndpointDetectionStrategy) null)) .isInstanceOf(IllegalArgumentException.class) .hasMessage("'delegates' must not contain null."); } @Test void should_chain_on_empty() { // given Instance instance = Instance.create(InstanceId.of("id")); ChainingStrategy strategy = new ChainingStrategy((a) -> Mono.empty(), (a) -> Mono.empty(), (a) -> Mono.just(Endpoints.single("id", "path"))); // when/then StepVerifier.create(strategy.detectEndpoints(instance)) .expectNext(Endpoints.single("id", "path")) .verifyComplete(); } @Test void should_return_empty_endpoints_when_all_empty() { // given Instance instance = Instance.create(InstanceId.of("id")); ChainingStrategy strategy = new ChainingStrategy((a) -> Mono.empty()); // when/then StepVerifier.create(strategy.detectEndpoints(instance)).expectNext(Endpoints.empty()).verifyComplete(); } } ================================================ FILE: spring-boot-admin-server/src/test/java/de/codecentric/boot/admin/server/services/endpoints/ProbeEndpointsStrategyTest.java ================================================ /* * 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.server.services.endpoints; import java.time.Duration; import com.github.tomakehurst.wiremock.WireMockServer; import com.github.tomakehurst.wiremock.core.Options; import com.github.tomakehurst.wiremock.http.Fault; import org.eclipse.jetty.http.HttpStatus; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import reactor.test.StepVerifier; import de.codecentric.boot.admin.server.domain.entities.Instance; import de.codecentric.boot.admin.server.domain.values.Endpoints; import de.codecentric.boot.admin.server.domain.values.InstanceId; import de.codecentric.boot.admin.server.domain.values.Registration; import de.codecentric.boot.admin.server.web.client.InstanceWebClient; import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; import static com.github.tomakehurst.wiremock.client.WireMock.notFound; import static com.github.tomakehurst.wiremock.client.WireMock.ok; import static com.github.tomakehurst.wiremock.client.WireMock.options; import static com.github.tomakehurst.wiremock.client.WireMock.serverError; import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo; import static com.github.tomakehurst.wiremock.stubbing.Scenario.STARTED; import static de.codecentric.boot.admin.server.web.client.InstanceExchangeFilterFunctions.retry; import static de.codecentric.boot.admin.server.web.client.InstanceExchangeFilterFunctions.timeout; import static java.util.Collections.emptyMap; import static org.assertj.core.api.Assertions.assertThatThrownBy; class ProbeEndpointsStrategyTest { public final WireMockServer wireMock = new WireMockServer(Options.DYNAMIC_PORT); private final InstanceWebClient instanceWebClient = InstanceWebClient.builder() .filter(retry(1, emptyMap())) .filter(timeout(Duration.ofSeconds(1), emptyMap())) .build(); @BeforeAll static void setUp() { StepVerifier.setDefaultTimeout(Duration.ofSeconds(5)); } @AfterAll static void tearDown() { StepVerifier.resetDefaultTimeout(); } @BeforeEach void setup() { wireMock.start(); } @AfterEach void teardown() { wireMock.stop(); } @Test void invariants() { assertThatThrownBy(() -> new ProbeEndpointsStrategy(this.instanceWebClient, null)) .isInstanceOf(IllegalArgumentException.class) .hasMessage("'endpoints' must not be null."); assertThatThrownBy(() -> new ProbeEndpointsStrategy(this.instanceWebClient, new String[] { null })) .isInstanceOf(IllegalArgumentException.class) .hasMessage("'endpoints' must not contain null."); } @Test void should_return_detect_endpoints() { // given Instance instance = Instance.create(InstanceId.of("id")) .register(Registration.create("test", this.wireMock.url("/mgmt/health")) .managementUrl(this.wireMock.url("/mgmt")) .build()); this.wireMock.stubFor(options(urlEqualTo("/mgmt/metrics")).willReturn(ok())); this.wireMock.stubFor(options(urlEqualTo("/mgmt/stats")).willReturn(ok())); this.wireMock.stubFor(options(urlEqualTo("/mgmt/info")).willReturn(ok())); this.wireMock.stubFor(options(urlEqualTo("/mgmt/non-exist")).willReturn(notFound())); this.wireMock.stubFor(options(urlEqualTo("/mgmt/error")).willReturn(serverError())); this.wireMock .stubFor(options(urlEqualTo("/mgmt/exception")).willReturn(aResponse().withFault(Fault.EMPTY_RESPONSE))); ProbeEndpointsStrategy strategy = new ProbeEndpointsStrategy(this.instanceWebClient, new String[] { "metrics:stats", "metrics", "info", "non-exist", "error", "exception" }); // when StepVerifier.create(strategy.detectEndpoints(instance)) // then .expectNext(Endpoints.single("metrics", this.wireMock.url("/mgmt/stats")) .withEndpoint("info", this.wireMock.url("/mgmt/info")))// .verifyComplete(); } @Test void should_return_empty() { // given Instance instance = Instance.create(InstanceId.of("id")) .register(Registration.create("test", this.wireMock.url("/mgmt/health")) .managementUrl(this.wireMock.url("/mgmt")) .build()); this.wireMock .stubFor(options(urlEqualTo("/mgmt/stats")).willReturn(aResponse().withStatus(HttpStatus.NOT_FOUND_404))); ProbeEndpointsStrategy strategy = new ProbeEndpointsStrategy(this.instanceWebClient, new String[] { "metrics:stats" }); // when StepVerifier.create(strategy.detectEndpoints(instance)) // then .verifyComplete(); } @Test void should_retry() { // given Instance instance = Instance.create(InstanceId.of("id")) .register(Registration.create("test", this.wireMock.url("/mgmt/health")) .managementUrl(this.wireMock.url("/mgmt")) .build()); this.wireMock.stubFor(options(urlEqualTo("/mgmt/metrics")).inScenario("retry") .whenScenarioStateIs(STARTED) .willReturn(aResponse().withFixedDelay(5000)) .willSetStateTo("recovered")); this.wireMock.stubFor(options(urlEqualTo("/mgmt/metrics")).inScenario("retry") .whenScenarioStateIs("recovered") .willReturn(ok())); this.wireMock.stubFor(options(urlEqualTo("/mgmt/stats")).willReturn(ok())); this.wireMock.stubFor(options(urlEqualTo("/mgmt/info")).willReturn(ok())); this.wireMock.stubFor(options(urlEqualTo("/mgmt/non-exist")).willReturn(notFound())); ProbeEndpointsStrategy strategy = new ProbeEndpointsStrategy(this.instanceWebClient, new String[] { "metrics:stats", "metrics", "info", "non-exist" }); // when StepVerifier.create(strategy.detectEndpoints(instance)) // then .expectNext(Endpoints.single("metrics", this.wireMock.url("/mgmt/stats")) .withEndpoint("info", this.wireMock.url("/mgmt/info")))// .verifyComplete(); } } ================================================ FILE: spring-boot-admin-server/src/test/java/de/codecentric/boot/admin/server/services/endpoints/QueryIndexEndpointStrategyTest.java ================================================ /* * 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.server.services.endpoints; import java.time.Duration; import javax.net.ssl.SSLException; import com.github.tomakehurst.wiremock.WireMockServer; import com.github.tomakehurst.wiremock.http.Fault; import io.netty.handler.ssl.SslContextBuilder; import io.netty.handler.ssl.util.InsecureTrustManagerFactory; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.boot.actuate.endpoint.ApiVersion; import org.springframework.http.MediaType; import org.springframework.http.client.reactive.ReactorClientHttpConnector; import org.springframework.web.reactive.function.client.WebClient; import reactor.netty.http.client.HttpClient; import reactor.test.StepVerifier; 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.domain.values.InstanceId; import de.codecentric.boot.admin.server.domain.values.Registration; import de.codecentric.boot.admin.server.services.ApiMediaTypeHandler; import de.codecentric.boot.admin.server.web.client.InstanceWebClient; import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; import static com.github.tomakehurst.wiremock.client.WireMock.anyRequestedFor; import static com.github.tomakehurst.wiremock.client.WireMock.get; import static com.github.tomakehurst.wiremock.client.WireMock.notFound; import static com.github.tomakehurst.wiremock.client.WireMock.ok; import static com.github.tomakehurst.wiremock.client.WireMock.okJson; import static com.github.tomakehurst.wiremock.client.WireMock.urlPathEqualTo; import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.wireMockConfig; import static com.github.tomakehurst.wiremock.stubbing.Scenario.STARTED; import static de.codecentric.boot.admin.server.web.client.InstanceExchangeFilterFunctions.retry; import static de.codecentric.boot.admin.server.web.client.InstanceExchangeFilterFunctions.rewriteEndpointUrl; import static de.codecentric.boot.admin.server.web.client.InstanceExchangeFilterFunctions.timeout; import static java.util.Collections.emptyMap; import static java.util.Collections.singletonMap; class QueryIndexEndpointStrategyTest { private final ApiMediaTypeHandler apiMediaTypeHandler = new ApiMediaTypeHandler(); public final WireMockServer wireMock = new WireMockServer(wireMockConfig().dynamicPort().dynamicHttpsPort()); private final InstanceWebClient instanceWebClient = InstanceWebClient.builder() .webClient(WebClient.builder().clientConnector(httpConnector())) .filter(rewriteEndpointUrl()) .filter(retry(0, singletonMap(Endpoint.ACTUATOR_INDEX, 1))) .filter(timeout(Duration.ofSeconds(1), emptyMap())) .build(); @BeforeEach void setUp() { wireMock.start(); } @AfterEach void tearDown() { wireMock.stop(); } @Test void should_return_endpoints() { // given Instance instance = Instance.create(InstanceId.of("id")) .register(Registration.create("test", this.wireMock.url("/mgmt/health")) .managementUrl(this.wireMock.url("/mgmt")) .build()); String host = "https://localhost:" + this.wireMock.httpsPort(); String body = "{\"_links\":{\"metrics-requiredMetricName\":{\"templated\":true,\"href\":\"" + host + "/mgmt/metrics/{requiredMetricName}\"},\"self\":{\"templated\":false,\"href\":\"" + host + "/mgmt\"},\"metrics\":{\"templated\":false,\"href\":\"" + host + "/mgmt/stats\"},\"info\":{\"templated\":false,\"href\":\"" + host + "/mgmt/info\"}}}"; this.wireMock.stubFor(get("/mgmt") .willReturn(ok(body).withHeader("Content-Type", ApiVersion.LATEST.getProducedMimeType().toString()))); QueryIndexEndpointStrategy strategy = new QueryIndexEndpointStrategy(this.instanceWebClient, this.apiMediaTypeHandler); // when StepVerifier.create(strategy.detectEndpoints(instance)) // then .expectNext(Endpoints.single("metrics", host + "/mgmt/stats").withEndpoint("info", host + "/mgmt/info"))// .verifyComplete(); } @Test void should_return_endpoints_with_aligned_scheme() { // given Instance instance = Instance.create(InstanceId.of("id")) .register(Registration.create("test", this.wireMock.url("/mgmt/health")) .managementUrl(this.wireMock.url("/mgmt")) .build()); String host = "http://localhost:" + this.wireMock.httpsPort(); String body = "{\"_links\":{\"metrics-requiredMetricName\":{\"templated\":true,\"href\":\"" + host + "/mgmt/metrics/{requiredMetricName}\"},\"self\":{\"templated\":false,\"href\":\"" + host + "/mgmt\"},\"metrics\":{\"templated\":false,\"href\":\"" + host + "/mgmt/stats\"},\"info\":{\"templated\":false,\"href\":\"" + host + "/mgmt/info\"}}}"; this.wireMock.stubFor(get("/mgmt") .willReturn(ok(body).withHeader("Content-Type", ApiVersion.LATEST.getProducedMimeType().toString()))); QueryIndexEndpointStrategy strategy = new QueryIndexEndpointStrategy(this.instanceWebClient, this.apiMediaTypeHandler); // when String secureHost = "https://localhost:" + this.wireMock.httpsPort(); StepVerifier.create(strategy.detectEndpoints(instance)) // then .expectNext(Endpoints.single("metrics", secureHost + "/mgmt/stats") .withEndpoint("info", secureHost + "/mgmt/info"))// .verifyComplete(); } @Test void should_return_empty_on_empty_endpoints() { // given Instance instance = Instance.create(InstanceId.of("id")) .register(Registration.create("test", this.wireMock.url("/mgmt/health")) .managementUrl(this.wireMock.url("/mgmt")) .build()); String body = "{\"_links\":{}}"; this.wireMock.stubFor(get("/mgmt") .willReturn(okJson(body).withHeader("Content-Type", ApiVersion.LATEST.getProducedMimeType().toString()))); QueryIndexEndpointStrategy strategy = new QueryIndexEndpointStrategy(this.instanceWebClient, this.apiMediaTypeHandler); // when StepVerifier.create(strategy.detectEndpoints(instance)) // then .verifyComplete(); } @Test void should_return_empty_on_not_found() { // given Instance instance = Instance.create(InstanceId.of("id")) .register(Registration.create("test", this.wireMock.url("/mgmt/health")) .managementUrl(this.wireMock.url("/mgmt")) .build()); this.wireMock.stubFor(get("/mgmt").willReturn(notFound())); QueryIndexEndpointStrategy strategy = new QueryIndexEndpointStrategy(this.instanceWebClient, this.apiMediaTypeHandler); // when StepVerifier.create(strategy.detectEndpoints(instance)) // then .verifyComplete(); } @Test void should_return_empty_on_error() { // given Instance instance = Instance.create(InstanceId.of("id")) .register(Registration.create("test", this.wireMock.url("/mgmt/health")) .managementUrl(this.wireMock.url("/mgmt")) .build()); this.wireMock.stubFor(get("/mgmt").willReturn(aResponse().withFault(Fault.EMPTY_RESPONSE))); QueryIndexEndpointStrategy strategy = new QueryIndexEndpointStrategy(this.instanceWebClient, this.apiMediaTypeHandler); // when StepVerifier.create(strategy.detectEndpoints(instance)) // then .verifyComplete(); } @Test void should_return_empty_on_wrong_content_type() { // given Instance instance = Instance.create(InstanceId.of("id")) .register(Registration.create("test", this.wireMock.url("/mgmt/health")) .managementUrl(this.wireMock.url("/mgmt")) .build()); String body = "HELLO WORLD"; this.wireMock.stubFor(get("/mgmt").willReturn(ok(body).withHeader("Content-Type", MediaType.TEXT_PLAIN_VALUE))); QueryIndexEndpointStrategy strategy = new QueryIndexEndpointStrategy(this.instanceWebClient, this.apiMediaTypeHandler); // when StepVerifier.create(strategy.detectEndpoints(instance)) // then .verifyComplete(); } @Test void should_return_empty_when_mgmt_equals_service_url() { // given Instance instance = Instance.create(InstanceId.of("id")) .register(Registration.create("test", this.wireMock.url("/app/health")) .managementUrl(this.wireMock.url("/app")) .serviceUrl(this.wireMock.url("/app")) .build()); QueryIndexEndpointStrategy strategy = new QueryIndexEndpointStrategy(this.instanceWebClient, this.apiMediaTypeHandler); // when/then StepVerifier.create(strategy.detectEndpoints(instance)).verifyComplete(); this.wireMock.verify(0, anyRequestedFor(urlPathEqualTo("/app"))); } @Test void should_retry() { // given Instance instance = Instance.create(InstanceId.of("id")) .register(Registration.create("test", this.wireMock.url("/mgmt/health")) .managementUrl(this.wireMock.url("/mgmt")) .build()); String body = "{\"_links\":{\"metrics-requiredMetricName\":{\"templated\":true,\"href\":\"/mgmt/metrics/{requiredMetricName}\"},\"self\":{\"templated\":false,\"href\":\"/mgmt\"},\"metrics\":{\"templated\":false,\"href\":\"/mgmt/stats\"},\"info\":{\"templated\":false,\"href\":\"/mgmt/info\"}}}"; this.wireMock.stubFor(get("/mgmt").inScenario("retry") .whenScenarioStateIs(STARTED) .willReturn(aResponse().withFault(Fault.CONNECTION_RESET_BY_PEER)) .willSetStateTo("recovered")); this.wireMock.stubFor(get("/mgmt").inScenario("retry") .whenScenarioStateIs("recovered") .willReturn(ok(body).withHeader("Content-Type", ApiVersion.LATEST.getProducedMimeType().toString()))); QueryIndexEndpointStrategy strategy = new QueryIndexEndpointStrategy(this.instanceWebClient, this.apiMediaTypeHandler); // when StepVerifier.create(strategy.detectEndpoints(instance)) // then .expectNext(Endpoints.single("metrics", "/mgmt/stats").withEndpoint("info", "/mgmt/info"))// .verifyComplete(); } private ReactorClientHttpConnector httpConnector() { HttpClient client = HttpClient.create().secure((ssl) -> { try { SslContextBuilder sslCtx = SslContextBuilder.forClient() .trustManager(InsecureTrustManagerFactory.INSTANCE); ssl.sslContext(sslCtx.build()); } catch (SSLException ex) { throw new RuntimeException(ex); } }); return new ReactorClientHttpConnector(client); } } ================================================ FILE: spring-boot-admin-server/src/test/java/de/codecentric/boot/admin/server/utils/jackson/BuildVersionMixinTest.java ================================================ /* * 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.server.utils.jackson; import org.junit.jupiter.api.Test; import tools.jackson.core.JacksonException; import tools.jackson.databind.DeserializationFeature; import tools.jackson.databind.json.JsonMapper; import de.codecentric.boot.admin.server.domain.values.BuildVersion; import static java.util.Collections.singletonMap; import static org.assertj.core.api.Assertions.assertThat; class BuildVersionMixinTest { private final JsonMapper jsonMapper; protected BuildVersionMixinTest() { AdminServerModule adminServerModule = new AdminServerModule(new String[] { ".*password$" }); jsonMapper = JsonMapper.builder() .addModule(adminServerModule) .disable(DeserializationFeature.FAIL_ON_NULL_FOR_PRIMITIVES) .build(); } @Test void verifyDeserialize() throws JacksonException { BuildVersion buildVersion = jsonMapper.readValue("\"1.0.0\"", BuildVersion.class); assertThat(buildVersion).isEqualTo(BuildVersion.valueOf("1.0.0")); } @Test void verifySerialize() throws JacksonException { BuildVersion buildVersion = BuildVersion.valueOf("1.0.0"); String result = jsonMapper.writeValueAsString(buildVersion); assertThat(result).isEqualTo("\"1.0.0\""); } @Test void verifySerializeWithMapEntryVersion() throws JacksonException { BuildVersion buildVersion = BuildVersion.from(singletonMap("version", "1.0.0")); String result = jsonMapper.writeValueAsString(buildVersion); assertThat(result).isEqualTo("\"1.0.0\""); } @Test void verifySerializeWithNestedMapEntryVersion() throws JacksonException { BuildVersion buildVersion = BuildVersion.from(singletonMap("build", singletonMap("version", "1.0.0"))); String result = jsonMapper.writeValueAsString(buildVersion); assertThat(result).isEqualTo("\"1.0.0\""); } } ================================================ FILE: spring-boot-admin-server/src/test/java/de/codecentric/boot/admin/server/utils/jackson/EndpointMixinTest.java ================================================ /* * 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.server.utils.jackson; import java.io.IOException; import org.json.JSONException; import org.json.JSONObject; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.boot.test.json.JacksonTester; import org.springframework.boot.test.json.JsonContent; import tools.jackson.core.JacksonException; import tools.jackson.databind.DeserializationFeature; import tools.jackson.databind.json.JsonMapper; import de.codecentric.boot.admin.server.domain.values.Endpoint; import static org.assertj.core.api.Assertions.assertThat; class EndpointMixinTest { private final JsonMapper jsonMapper; private JacksonTester jsonTester; protected EndpointMixinTest() { AdminServerModule adminServerModule = new AdminServerModule(new String[] { ".*password$" }); jsonMapper = JsonMapper.builder() .addModule(adminServerModule) .disable(DeserializationFeature.FAIL_ON_NULL_FOR_PRIMITIVES) .build(); } @BeforeEach void setup() { JacksonTester.initFields(this, jsonMapper); } @Test void verifyDeserialize() throws JSONException, JacksonException { String json = new JSONObject().put("id", "info").put("url", "http://localhost:8080/info").toString(); Endpoint endpoint = jsonMapper.readValue(json, Endpoint.class); assertThat(endpoint).isNotNull(); assertThat(endpoint.getId()).isEqualTo("info"); assertThat(endpoint.getUrl()).isEqualTo("http://localhost:8080/info"); } @Test void verifySerialize() throws IOException { Endpoint endpoint = Endpoint.of("info", "http://localhost:8080/info"); JsonContent jsonContent = jsonTester.write(endpoint); assertThat(jsonContent).extractingJsonPathStringValue("$.id").isEqualTo("info"); assertThat(jsonContent).extractingJsonPathStringValue("$.url").isEqualTo("http://localhost:8080/info"); } } ================================================ FILE: spring-boot-admin-server/src/test/java/de/codecentric/boot/admin/server/utils/jackson/EndpointsMixinTest.java ================================================ /* * 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.server.utils.jackson; import java.io.IOException; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.boot.test.json.JacksonTester; import org.springframework.boot.test.json.JsonContent; import tools.jackson.core.JacksonException; import tools.jackson.databind.DeserializationFeature; import tools.jackson.databind.json.JsonMapper; import de.codecentric.boot.admin.server.domain.values.Endpoint; import de.codecentric.boot.admin.server.domain.values.Endpoints; import static org.assertj.core.api.Assertions.assertThat; class EndpointsMixinTest { private final JsonMapper jsonMapper; private JacksonTester jsonTester; protected EndpointsMixinTest() { AdminServerModule adminServerModule = new AdminServerModule(new String[] { ".*password$" }); jsonMapper = JsonMapper.builder() .addModule(adminServerModule) .disable(DeserializationFeature.FAIL_ON_NULL_FOR_PRIMITIVES) .build(); } @BeforeEach void setup() { JacksonTester.initFields(this, jsonMapper); } @Test void verifyDeserialize() throws JSONException, JacksonException { String json = new JSONArray().put(new JSONObject().put("id", "info").put("url", "http://localhost:8080/info")) .put(new JSONObject().put("id", "health").put("url", "http://localhost:8080/health")) .toString(); Endpoints endpoints = jsonMapper.readValue(json, Endpoints.class); assertThat(endpoints).isNotNull() .containsExactlyInAnyOrder(Endpoint.of("info", "http://localhost:8080/info"), Endpoint.of("health", "http://localhost:8080/health")); } @Test void verifySerialize() throws IOException { Endpoints endpoints = Endpoints.single("info", "http://localhost:8080/info") .withEndpoint("health", "http://localhost:8080/health"); JsonContent jsonContent = jsonTester.write(endpoints); assertThat(jsonContent).extractingJsonPathArrayValue("$").hasSize(2); assertThat(jsonContent).extractingJsonPathStringValue("$[0].id").isIn("info", "health"); assertThat(jsonContent).extractingJsonPathStringValue("$[0].url") .isIn("http://localhost:8080/info", "http://localhost:8080/health"); assertThat(jsonContent).extractingJsonPathStringValue("$[1].id").isIn("info", "health"); assertThat(jsonContent).extractingJsonPathStringValue("$[1].url") .isIn("http://localhost:8080/info", "http://localhost:8080/health"); } } ================================================ FILE: spring-boot-admin-server/src/test/java/de/codecentric/boot/admin/server/utils/jackson/InfoMixinTest.java ================================================ /* * 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.server.utils.jackson; import java.io.IOException; import java.util.Collections; import java.util.HashMap; import java.util.Map; import org.json.JSONException; import org.json.JSONObject; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.boot.test.json.JacksonTester; import org.springframework.boot.test.json.JsonContent; import tools.jackson.core.JacksonException; import tools.jackson.databind.DeserializationFeature; import tools.jackson.databind.json.JsonMapper; import de.codecentric.boot.admin.server.domain.values.Info; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.entry; class InfoMixinTest { private final JsonMapper jsonMapper; private JacksonTester jsonTester; protected InfoMixinTest() { AdminServerModule adminServerModule = new AdminServerModule(new String[] { ".*password$" }); jsonMapper = JsonMapper.builder() .addModule(adminServerModule) .disable(DeserializationFeature.FAIL_ON_NULL_FOR_PRIMITIVES) .build(); } @BeforeEach void setup() { JacksonTester.initFields(this, jsonMapper); } @Test void verifyDeserialize() throws JSONException, JacksonException { String json = new JSONObject().put("build", new JSONObject().put("version", "1.0.0")) .put("foo", "bar") .toString(); Info info = jsonMapper.readValue(json, Info.class); assertThat(info).isNotNull(); assertThat(info.getValues()).containsOnly(entry("build", Collections.singletonMap("version", "1.0.0")), entry("foo", "bar")); } @Test void verifySerialize() throws IOException { Map data = new HashMap<>(); data.put("build", Collections.singletonMap("version", "1.0.0")); data.put("foo", "bar"); Info info = Info.from(data); JsonContent jsonContent = jsonTester.write(info); assertThat(jsonContent).extractingJsonPathMapValue("$").containsOnlyKeys("build", "foo"); assertThat(jsonContent).extractingJsonPathStringValue("$['build'].['version']").isEqualTo("1.0.0"); assertThat(jsonContent).extractingJsonPathStringValue("$['foo']").isEqualTo("bar"); } } ================================================ FILE: spring-boot-admin-server/src/test/java/de/codecentric/boot/admin/server/utils/jackson/InstanceDeregisteredEventMixinTest.java ================================================ /* * 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.server.utils.jackson; import java.io.IOException; import java.time.Instant; import java.time.temporal.ChronoUnit; import org.json.JSONException; import org.json.JSONObject; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.boot.test.json.JacksonTester; import org.springframework.boot.test.json.JsonContent; import tools.jackson.core.JacksonException; import tools.jackson.databind.DeserializationFeature; import tools.jackson.databind.json.JsonMapper; import de.codecentric.boot.admin.server.domain.events.InstanceDeregisteredEvent; import de.codecentric.boot.admin.server.domain.values.InstanceId; import static org.assertj.core.api.Assertions.assertThat; class InstanceDeregisteredEventMixinTest { private final JsonMapper jsonMapper; private JacksonTester jsonTester; protected InstanceDeregisteredEventMixinTest() { AdminServerModule adminServerModule = new AdminServerModule(new String[] { ".*password$" }); jsonMapper = JsonMapper.builder() .addModule(adminServerModule) .disable(DeserializationFeature.FAIL_ON_NULL_FOR_PRIMITIVES) .build(); } @BeforeEach void setup() { JacksonTester.initFields(this, jsonMapper); } @Test void verifyDeserialize() throws JSONException, JacksonException { String json = new JSONObject().put("instance", "test123") .put("version", 12345678L) .put("timestamp", 1587751031.000000000) .put("type", "DEREGISTERED") .toString(); InstanceDeregisteredEvent event = jsonMapper.readValue(json, InstanceDeregisteredEvent.class); assertThat(event).isNotNull(); assertThat(event.getInstance()).isEqualTo(InstanceId.of("test123")); assertThat(event.getVersion()).isEqualTo(12345678L); assertThat(event.getTimestamp()).isEqualTo(Instant.ofEpochSecond(1587751031).truncatedTo(ChronoUnit.SECONDS)); } @Test void verifyDeserializeWithOnlyRequiredProperties() throws JSONException, JacksonException { String json = new JSONObject().put("instance", "test123") .put("timestamp", 1587751031.000000000) .put("type", "DEREGISTERED") .toString(); InstanceDeregisteredEvent event = jsonMapper.readValue(json, InstanceDeregisteredEvent.class); assertThat(event).isNotNull(); assertThat(event.getInstance()).isEqualTo(InstanceId.of("test123")); assertThat(event.getVersion()).isZero(); assertThat(event.getTimestamp()).isEqualTo(Instant.ofEpochSecond(1587751031).truncatedTo(ChronoUnit.SECONDS)); } @Test void verifySerialize() throws IOException { InstanceId id = InstanceId.of("test123"); Instant timestamp = Instant.ofEpochSecond(1587751031).truncatedTo(ChronoUnit.SECONDS); InstanceDeregisteredEvent event = new InstanceDeregisteredEvent(id, 12345678L, timestamp); JsonContent jsonContent = jsonTester.write(event); assertThat(jsonContent).extractingJsonPathStringValue("$.instance").isEqualTo("test123"); assertThat(jsonContent).extractingJsonPathNumberValue("$.version").isEqualTo(12345678); assertThat(jsonContent).extractingJsonPathStringValue("$.timestamp").isEqualTo("2020-04-24T17:57:11Z"); assertThat(jsonContent).extractingJsonPathStringValue("$.type").isEqualTo("DEREGISTERED"); } @Test void verifySerializeWithOnlyRequiredProperties() throws IOException { InstanceId id = InstanceId.of("test123"); Instant timestamp = Instant.ofEpochSecond(1587751031).truncatedTo(ChronoUnit.SECONDS); InstanceDeregisteredEvent event = new InstanceDeregisteredEvent(id, 0L, timestamp); JsonContent jsonContent = jsonTester.write(event); assertThat(jsonContent).extractingJsonPathStringValue("$.instance").isEqualTo("test123"); assertThat(jsonContent).extractingJsonPathNumberValue("$.version").isEqualTo(0); assertThat(jsonContent).extractingJsonPathStringValue("$.timestamp").isEqualTo("2020-04-24T17:57:11Z"); assertThat(jsonContent).extractingJsonPathStringValue("$.type").isEqualTo("DEREGISTERED"); } } ================================================ FILE: spring-boot-admin-server/src/test/java/de/codecentric/boot/admin/server/utils/jackson/InstanceEndpointsDetectedEventMixinTest.java ================================================ /* * 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.server.utils.jackson; import java.io.IOException; import java.time.Instant; import java.time.temporal.ChronoUnit; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.boot.test.json.JacksonTester; import org.springframework.boot.test.json.JsonContent; import tools.jackson.core.JacksonException; import tools.jackson.databind.DeserializationFeature; import tools.jackson.databind.json.JsonMapper; import de.codecentric.boot.admin.server.domain.events.InstanceEndpointsDetectedEvent; import de.codecentric.boot.admin.server.domain.values.Endpoint; import de.codecentric.boot.admin.server.domain.values.Endpoints; import de.codecentric.boot.admin.server.domain.values.InstanceId; import static org.assertj.core.api.Assertions.assertThat; class InstanceEndpointsDetectedEventMixinTest { private final JsonMapper jsonMapper; private JacksonTester jsonTester; protected InstanceEndpointsDetectedEventMixinTest() { AdminServerModule adminServerModule = new AdminServerModule(new String[] { ".*password$" }); jsonMapper = JsonMapper.builder() .addModule(adminServerModule) .disable(DeserializationFeature.FAIL_ON_NULL_FOR_PRIMITIVES) .build(); } @BeforeEach void setup() { JacksonTester.initFields(this, jsonMapper); } @Test void verifyDeserialize() throws JSONException, JacksonException { String json = new JSONObject().put("instance", "test123") .put("version", 12345678L) .put("timestamp", 1587751031.000000000) .put("type", "ENDPOINTS_DETECTED") .put("endpoints", new JSONArray().put(new JSONObject().put("id", "info").put("url", "http://localhost:8080/info")) .put(new JSONObject().put("id", "health").put("url", "http://localhost:8080/health"))) .toString(); InstanceEndpointsDetectedEvent event = jsonMapper.readValue(json, InstanceEndpointsDetectedEvent.class); assertThat(event).isNotNull(); assertThat(event.getInstance()).isEqualTo(InstanceId.of("test123")); assertThat(event.getVersion()).isEqualTo(12345678L); assertThat(event.getTimestamp()).isEqualTo(Instant.ofEpochSecond(1587751031).truncatedTo(ChronoUnit.SECONDS)); assertThat(event.getEndpoints()).containsExactlyInAnyOrder(Endpoint.of("info", "http://localhost:8080/info"), Endpoint.of("health", "http://localhost:8080/health")); } @Test void verifyDeserializeWithEmptyEndpoints() throws JSONException, JacksonException { String json = new JSONObject().put("instance", "test123") .put("version", 12345678L) .put("timestamp", 1587751031.000000000) .put("type", "ENDPOINTS_DETECTED") .put("endpoints", new JSONArray()) .toString(); InstanceEndpointsDetectedEvent event = jsonMapper.readValue(json, InstanceEndpointsDetectedEvent.class); assertThat(event).isNotNull(); assertThat(event.getInstance()).isEqualTo(InstanceId.of("test123")); assertThat(event.getVersion()).isEqualTo(12345678L); assertThat(event.getTimestamp()).isEqualTo(Instant.ofEpochSecond(1587751031).truncatedTo(ChronoUnit.SECONDS)); assertThat(event.getEndpoints()).isEmpty(); } @Test void verifyDeserializeWithOnlyRequiredProperties() throws JSONException, JacksonException { String json = new JSONObject().put("instance", "test123") .put("timestamp", 1587751031.000000000) .put("type", "ENDPOINTS_DETECTED") .toString(); InstanceEndpointsDetectedEvent event = jsonMapper.readValue(json, InstanceEndpointsDetectedEvent.class); assertThat(event).isNotNull(); assertThat(event.getInstance()).isEqualTo(InstanceId.of("test123")); assertThat(event.getVersion()).isZero(); assertThat(event.getTimestamp()).isEqualTo(Instant.ofEpochSecond(1587751031).truncatedTo(ChronoUnit.SECONDS)); assertThat(event.getEndpoints()).isNull(); } @Test void verifySerialize() throws IOException { InstanceId id = InstanceId.of("test123"); Instant timestamp = Instant.ofEpochSecond(1587751031).truncatedTo(ChronoUnit.SECONDS); Endpoints endpoints = Endpoints.single("info", "http://localhost:8080/info") .withEndpoint("health", "http://localhost:8080/health"); InstanceEndpointsDetectedEvent event = new InstanceEndpointsDetectedEvent(id, 12345678L, timestamp, endpoints); JsonContent jsonContent = jsonTester.write(event); assertThat(jsonContent).extractingJsonPathStringValue("$.instance").isEqualTo("test123"); assertThat(jsonContent).extractingJsonPathNumberValue("$.version").isEqualTo(12345678); assertThat(jsonContent).extractingJsonPathStringValue("$.timestamp").isEqualTo("2020-04-24T17:57:11Z"); assertThat(jsonContent).extractingJsonPathStringValue("$.type").isEqualTo("ENDPOINTS_DETECTED"); assertThat(jsonContent).extractingJsonPathArrayValue("$.endpoints").hasSize(2); assertThat(jsonContent).extractingJsonPathStringValue("$.endpoints[0].id").isIn("info", "health"); assertThat(jsonContent).extractingJsonPathStringValue("$.endpoints[0].url") .isIn("http://localhost:8080/info", "http://localhost:8080/health"); assertThat(jsonContent).extractingJsonPathStringValue("$.endpoints[1].id").isIn("info", "health"); assertThat(jsonContent).extractingJsonPathStringValue("$.endpoints[1].url") .isIn("http://localhost:8080/info", "http://localhost:8080/health"); } @Test void verifySerializeWithOnlyRequiredProperties() throws IOException { InstanceId id = InstanceId.of("test123"); Instant timestamp = Instant.ofEpochSecond(1587751031).truncatedTo(ChronoUnit.SECONDS); InstanceEndpointsDetectedEvent event = new InstanceEndpointsDetectedEvent(id, 0L, timestamp, null); JsonContent jsonContent = jsonTester.write(event); assertThat(jsonContent).extractingJsonPathStringValue("$.instance").isEqualTo("test123"); assertThat(jsonContent).extractingJsonPathNumberValue("$.version").isEqualTo(0); assertThat(jsonContent).extractingJsonPathStringValue("$.timestamp").isEqualTo("2020-04-24T17:57:11Z"); assertThat(jsonContent).extractingJsonPathStringValue("$.type").isEqualTo("ENDPOINTS_DETECTED"); assertThat(jsonContent).extractingJsonPathArrayValue("$.endpoints").isNull(); } @Test void verifySerializeWithEmptyEndpoints() throws IOException { InstanceId id = InstanceId.of("test123"); Instant timestamp = Instant.ofEpochSecond(1587751031).truncatedTo(ChronoUnit.SECONDS); InstanceEndpointsDetectedEvent event = new InstanceEndpointsDetectedEvent(id, 0L, timestamp, Endpoints.empty()); JsonContent jsonContent = jsonTester.write(event); assertThat(jsonContent).extractingJsonPathStringValue("$.instance").isEqualTo("test123"); assertThat(jsonContent).extractingJsonPathNumberValue("$.version").isEqualTo(0); assertThat(jsonContent).extractingJsonPathStringValue("$.timestamp").isEqualTo("2020-04-24T17:57:11Z"); assertThat(jsonContent).extractingJsonPathStringValue("$.type").isEqualTo("ENDPOINTS_DETECTED"); assertThat(jsonContent).extractingJsonPathArrayValue("$.endpoints").isEmpty(); } } ================================================ FILE: spring-boot-admin-server/src/test/java/de/codecentric/boot/admin/server/utils/jackson/InstanceEventMixinTest.java ================================================ /* * 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.server.utils.jackson; import org.json.JSONException; import org.json.JSONObject; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.springframework.boot.test.json.JacksonTester; import tools.jackson.core.JacksonException; import tools.jackson.databind.DeserializationFeature; import tools.jackson.databind.json.JsonMapper; import de.codecentric.boot.admin.server.domain.events.InstanceDeregisteredEvent; import de.codecentric.boot.admin.server.domain.events.InstanceEndpointsDetectedEvent; import de.codecentric.boot.admin.server.domain.events.InstanceEvent; import de.codecentric.boot.admin.server.domain.events.InstanceInfoChangedEvent; import de.codecentric.boot.admin.server.domain.events.InstanceRegisteredEvent; import de.codecentric.boot.admin.server.domain.events.InstanceRegistrationUpdatedEvent; import de.codecentric.boot.admin.server.domain.events.InstanceStatusChangedEvent; import static org.assertj.core.api.Assertions.assertThat; public class InstanceEventMixinTest { private final JsonMapper jsonMapper; protected InstanceEventMixinTest() { AdminServerModule adminServerModule = new AdminServerModule(new String[] { ".*password$" }); jsonMapper = JsonMapper.builder() .addModule(adminServerModule) .disable(DeserializationFeature.FAIL_ON_NULL_FOR_PRIMITIVES) .build(); } @Nested class InstanceEventTests { private JacksonTester jsonTester; @BeforeEach void setup() { JacksonTester.initFields(this, jsonMapper); } @Test void verifyDeserializeOfInstanceDeregisteredEvent() throws JSONException, JacksonException { String json = new JSONObject().put("instance", "test123") .put("timestamp", 1587751031.000000000) .put("type", "DEREGISTERED") .toString(); InstanceEvent event = jsonMapper.readValue(json, InstanceEvent.class); assertThat(event).isInstanceOf(InstanceDeregisteredEvent.class); } @Test void verifyDeserializeOfInstanceEndpointsDetectedEvent() throws JSONException, JacksonException { String json = new JSONObject().put("instance", "test123") .put("timestamp", 1587751031.000000000) .put("type", "ENDPOINTS_DETECTED") .toString(); InstanceEvent event = jsonMapper.readValue(json, InstanceEvent.class); assertThat(event).isInstanceOf(InstanceEndpointsDetectedEvent.class); } @Test void verifyDeserializeOfInstanceInfoChangedEvent() throws JSONException, JacksonException { String json = new JSONObject().put("instance", "test123") .put("timestamp", 1587751031.000000000) .put("type", "INFO_CHANGED") .toString(); InstanceEvent event = jsonMapper.readValue(json, InstanceEvent.class); assertThat(event).isInstanceOf(InstanceInfoChangedEvent.class); } @Test void verifyDeserializeOfInstanceRegisteredEvent() throws JSONException, JacksonException { String json = new JSONObject().put("instance", "test123") .put("timestamp", 1587751031.000000000) .put("type", "REGISTERED") .put("registration", new JSONObject().put("name", "test").put("healthUrl", "http://localhost:9080/heath")) .toString(); InstanceEvent event = jsonMapper.readValue(json, InstanceEvent.class); assertThat(event).isInstanceOf(InstanceRegisteredEvent.class); } @Test void verifyDeserializeOfInstanceRegistrationUpdatedEvent() throws JSONException, JacksonException { String json = new JSONObject().put("instance", "test123") .put("timestamp", 1587751031.000000000) .put("type", "REGISTRATION_UPDATED") .put("registration", new JSONObject().put("name", "test").put("healthUrl", "http://localhost:9080/heath")) .toString(); InstanceEvent event = jsonMapper.readValue(json, InstanceEvent.class); assertThat(event).isInstanceOf(InstanceRegistrationUpdatedEvent.class); } @Test void verifyDeserializeOfInstanceStatusChangedEvent() throws JSONException, JacksonException { String json = new JSONObject().put("instance", "test123") .put("timestamp", 1587751031.000000000) .put("type", "STATUS_CHANGED") .put("statusInfo", new JSONObject().put("status", "OFFLINE")) .toString(); InstanceEvent event = jsonMapper.readValue(json, InstanceEvent.class); assertThat(event).isInstanceOf(InstanceStatusChangedEvent.class); } } } ================================================ FILE: spring-boot-admin-server/src/test/java/de/codecentric/boot/admin/server/utils/jackson/InstanceIdMixinTest.java ================================================ /* * 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.server.utils.jackson; import java.io.IOException; import org.junit.jupiter.api.Test; import tools.jackson.core.JacksonException; import tools.jackson.databind.DeserializationFeature; import tools.jackson.databind.json.JsonMapper; import de.codecentric.boot.admin.server.domain.values.InstanceId; import static org.assertj.core.api.Assertions.assertThat; class InstanceIdMixinTest { private final JsonMapper jsonMapper; protected InstanceIdMixinTest() { AdminServerModule adminServerModule = new AdminServerModule(new String[] { ".*password$" }); jsonMapper = JsonMapper.builder() .addModule(adminServerModule) .disable(DeserializationFeature.FAIL_ON_NULL_FOR_PRIMITIVES) .build(); } @Test void verifyDeserialize() throws JacksonException { InstanceId id = jsonMapper.readValue("\"abc\"", InstanceId.class); assertThat(id).isEqualTo(InstanceId.of("abc")); } @Test void verifySerialize() throws IOException { InstanceId id = InstanceId.of("abc"); String result = jsonMapper.writeValueAsString(id); assertThat(result).isEqualTo("\"abc\""); } } ================================================ FILE: spring-boot-admin-server/src/test/java/de/codecentric/boot/admin/server/utils/jackson/InstanceInfoChangedEventMixinTest.java ================================================ /* * 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.server.utils.jackson; import java.io.IOException; import java.time.Instant; import java.time.temporal.ChronoUnit; import java.util.Collections; import java.util.HashMap; import java.util.Map; import org.json.JSONException; import org.json.JSONObject; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.boot.test.json.JacksonTester; import org.springframework.boot.test.json.JsonContent; import tools.jackson.core.JacksonException; import tools.jackson.databind.DeserializationFeature; import tools.jackson.databind.json.JsonMapper; import de.codecentric.boot.admin.server.domain.events.InstanceInfoChangedEvent; import de.codecentric.boot.admin.server.domain.values.Info; import de.codecentric.boot.admin.server.domain.values.InstanceId; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.entry; class InstanceInfoChangedEventMixinTest { private final JsonMapper jsonMapper; private JacksonTester jsonTester; protected InstanceInfoChangedEventMixinTest() { AdminServerModule adminServerModule = new AdminServerModule(new String[] { ".*password$" }); jsonMapper = JsonMapper.builder() .addModule(adminServerModule) .disable(DeserializationFeature.FAIL_ON_NULL_FOR_PRIMITIVES) .build(); } @BeforeEach void setup() { JacksonTester.initFields(this, jsonMapper); } @Test void verifyDeserialize() throws JSONException, JacksonException { String json = new JSONObject().put("instance", "test123") .put("version", 12345678L) .put("timestamp", 1587751031.000000000) .put("type", "INFO_CHANGED") .put("info", new JSONObject().put("build", new JSONObject().put("version", "1.0.0")).put("foo", "bar")) .toString(); InstanceInfoChangedEvent event = jsonMapper.readValue(json, InstanceInfoChangedEvent.class); assertThat(event).isNotNull(); assertThat(event.getInstance()).isEqualTo(InstanceId.of("test123")); assertThat(event.getVersion()).isEqualTo(12345678L); assertThat(event.getTimestamp()).isEqualTo(Instant.ofEpochSecond(1587751031).truncatedTo(ChronoUnit.SECONDS)); Info info = event.getInfo(); assertThat(info).isNotNull(); assertThat(info.getValues()).containsOnly(entry("build", Collections.singletonMap("version", "1.0.0")), entry("foo", "bar")); } @Test void verifyDeserializeWithOnlyRequiredProperties() throws JSONException { String json = new JSONObject().put("instance", "test123") .put("timestamp", 1587751031.000000000) .put("type", "INFO_CHANGED") .toString(); InstanceInfoChangedEvent event = jsonMapper.readValue(json, InstanceInfoChangedEvent.class); assertThat(event).isNotNull(); assertThat(event.getInstance()).isEqualTo(InstanceId.of("test123")); assertThat(event.getVersion()).isZero(); assertThat(event.getTimestamp()).isEqualTo(Instant.ofEpochSecond(1587751031).truncatedTo(ChronoUnit.SECONDS)); assertThat(event.getInfo()).isNull(); } @Test void verifyDeserializeWithEmptyInfo() throws JSONException { String json = new JSONObject().put("instance", "test123") .put("version", 12345678L) .put("timestamp", 1587751031.000000000) .put("type", "INFO_CHANGED") .put("info", new JSONObject()) .toString(); InstanceInfoChangedEvent event = jsonMapper.readValue(json, InstanceInfoChangedEvent.class); assertThat(event).isNotNull(); assertThat(event.getInstance()).isEqualTo(InstanceId.of("test123")); assertThat(event.getVersion()).isEqualTo(12345678L); assertThat(event.getTimestamp()).isEqualTo(Instant.ofEpochSecond(1587751031).truncatedTo(ChronoUnit.SECONDS)); Info info = event.getInfo(); assertThat(info).isNotNull(); assertThat(info.getValues()).isEmpty(); } @Test void verifySerialize() throws IOException { InstanceId id = InstanceId.of("test123"); Instant timestamp = Instant.ofEpochSecond(1587751031).truncatedTo(ChronoUnit.SECONDS); Map data = new HashMap<>(); data.put("build", Collections.singletonMap("version", "1.0.0")); data.put("foo", "bar"); InstanceInfoChangedEvent event = new InstanceInfoChangedEvent(id, 12345678L, timestamp, Info.from(data)); JsonContent jsonContent = jsonTester.write(event); assertThat(jsonContent).extractingJsonPathStringValue("$.instance").isEqualTo("test123"); assertThat(jsonContent).extractingJsonPathNumberValue("$.version").isEqualTo(12345678); assertThat(jsonContent).extractingJsonPathStringValue("$.timestamp").isEqualTo("2020-04-24T17:57:11Z"); assertThat(jsonContent).extractingJsonPathStringValue("$.type").isEqualTo("INFO_CHANGED"); assertThat(jsonContent).extractingJsonPathMapValue("$.info").containsOnlyKeys("build", "foo"); assertThat(jsonContent).extractingJsonPathStringValue("$.info['build'].['version']").isEqualTo("1.0.0"); assertThat(jsonContent).extractingJsonPathStringValue("$.info['foo']").isEqualTo("bar"); } @Test void verifySerializeWithOnlyRequiredProperties() throws IOException { InstanceId id = InstanceId.of("test123"); Instant timestamp = Instant.ofEpochSecond(1587751031).truncatedTo(ChronoUnit.SECONDS); InstanceInfoChangedEvent event = new InstanceInfoChangedEvent(id, 0L, timestamp, null); JsonContent jsonContent = jsonTester.write(event); assertThat(jsonContent).extractingJsonPathStringValue("$.instance").isEqualTo("test123"); assertThat(jsonContent).extractingJsonPathNumberValue("$.version").isEqualTo(0); assertThat(jsonContent).extractingJsonPathStringValue("$.timestamp").isEqualTo("2020-04-24T17:57:11Z"); assertThat(jsonContent).extractingJsonPathStringValue("$.type").isEqualTo("INFO_CHANGED"); assertThat(jsonContent).extractingJsonPathMapValue("$.info").isNull(); } @Test void verifySerializeWithEmptyInfo() throws IOException { InstanceId id = InstanceId.of("test123"); Instant timestamp = Instant.ofEpochSecond(1587751031).truncatedTo(ChronoUnit.SECONDS); InstanceInfoChangedEvent event = new InstanceInfoChangedEvent(id, 12345678L, timestamp, Info.from(Collections.emptyMap())); JsonContent jsonContent = jsonTester.write(event); assertThat(jsonContent).extractingJsonPathStringValue("$.instance").isEqualTo("test123"); assertThat(jsonContent).extractingJsonPathNumberValue("$.version").isEqualTo(12345678); assertThat(jsonContent).extractingJsonPathStringValue("$.timestamp").isEqualTo("2020-04-24T17:57:11Z"); assertThat(jsonContent).extractingJsonPathStringValue("$.type").isEqualTo("INFO_CHANGED"); assertThat(jsonContent).extractingJsonPathMapValue("$.info").isEmpty(); } } ================================================ FILE: spring-boot-admin-server/src/test/java/de/codecentric/boot/admin/server/utils/jackson/InstanceRegisteredEventMixinTest.java ================================================ /* * 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.server.utils.jackson; import java.io.IOException; import java.time.Instant; import java.time.temporal.ChronoUnit; import org.json.JSONException; import org.json.JSONObject; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.boot.test.json.JacksonTester; import org.springframework.boot.test.json.JsonContent; import tools.jackson.core.JacksonException; import tools.jackson.databind.DatabindException; import tools.jackson.databind.DeserializationFeature; import tools.jackson.databind.json.JsonMapper; import de.codecentric.boot.admin.server.domain.events.InstanceRegisteredEvent; import de.codecentric.boot.admin.server.domain.values.InstanceId; import de.codecentric.boot.admin.server.domain.values.Registration; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.assertj.core.api.Assertions.entry; class InstanceRegisteredEventMixinTest { private final JsonMapper jsonMapper; private JacksonTester jsonTester; protected InstanceRegisteredEventMixinTest() { AdminServerModule adminServerModule = new AdminServerModule(new String[] { ".*password$" }); jsonMapper = JsonMapper.builder() .addModule(adminServerModule) .disable(DeserializationFeature.FAIL_ON_NULL_FOR_PRIMITIVES) .build(); } @BeforeEach void setup() { JacksonTester.initFields(this, jsonMapper); } @Test void verifyDeserialize() throws JSONException, JacksonException { String json = new JSONObject().put("instance", "test123") .put("version", 12345678L) .put("timestamp", 1587751031.000000000) .put("type", "REGISTERED") .put("registration", new JSONObject().put("name", "test") .put("managementUrl", "http://localhost:9080/") .put("healthUrl", "http://localhost:9080/heath") .put("serviceUrl", "http://localhost:8080/") .put("source", "http-api") .put("metadata", new JSONObject().put("PASSWORD", "******").put("user", "humptydumpty"))) .toString(); InstanceRegisteredEvent event = jsonMapper.readValue(json, InstanceRegisteredEvent.class); assertThat(event).isNotNull(); assertThat(event.getInstance()).isEqualTo(InstanceId.of("test123")); assertThat(event.getVersion()).isEqualTo(12345678L); assertThat(event.getTimestamp()).isEqualTo(Instant.ofEpochSecond(1587751031).truncatedTo(ChronoUnit.SECONDS)); Registration registration = event.getRegistration(); assertThat(registration).isNotNull(); assertThat(registration.getName()).isEqualTo("test"); assertThat(registration.getManagementUrl()).isEqualTo("http://localhost:9080/"); assertThat(registration.getHealthUrl()).isEqualTo("http://localhost:9080/heath"); assertThat(registration.getServiceUrl()).isEqualTo("http://localhost:8080/"); assertThat(registration.getSource()).isEqualTo("http-api"); assertThat(registration.getMetadata()).containsOnly(entry("PASSWORD", "******"), entry("user", "humptydumpty")); } @Test void verifyDeserializeWithOnlyRequiredProperties() throws JSONException, JacksonException { String json = new JSONObject().put("instance", "test123") .put("timestamp", 1587751031.000000000) .put("type", "REGISTERED") .put("registration", new JSONObject().put("name", "test").put("healthUrl", "http://localhost:9080/heath")) .toString(); InstanceRegisteredEvent event = jsonMapper.readValue(json, InstanceRegisteredEvent.class); assertThat(event).isNotNull(); assertThat(event.getInstance()).isEqualTo(InstanceId.of("test123")); assertThat(event.getVersion()).isZero(); assertThat(event.getTimestamp()).isEqualTo(Instant.ofEpochSecond(1587751031).truncatedTo(ChronoUnit.SECONDS)); Registration registration = event.getRegistration(); assertThat(registration).isNotNull(); assertThat(registration.getName()).isEqualTo("test"); assertThat(registration.getManagementUrl()).isNull(); assertThat(registration.getHealthUrl()).isEqualTo("http://localhost:9080/heath"); assertThat(registration.getServiceUrl()).isNull(); assertThat(registration.getSource()).isNull(); assertThat(registration.getMetadata()).isEmpty(); } @Test void verifyDeserializeWithoutRegistration() throws JSONException, JacksonException { String json = new JSONObject().put("instance", "test123") .put("version", 12345678L) .put("timestamp", 1587751031.000000000) .put("type", "REGISTERED") .toString(); InstanceRegisteredEvent event = jsonMapper.readValue(json, InstanceRegisteredEvent.class); assertThat(event).isNotNull(); assertThat(event.getInstance()).isEqualTo(InstanceId.of("test123")); assertThat(event.getVersion()).isEqualTo(12345678L); assertThat(event.getTimestamp()).isEqualTo(Instant.ofEpochSecond(1587751031).truncatedTo(ChronoUnit.SECONDS)); assertThat(event.getRegistration()).isNull(); } @Test void verifyDeserializeWithEmptyRegistration() throws JSONException { String json = new JSONObject().put("instance", "test123") .put("version", 12345678L) .put("timestamp", 1587751031.000000000) .put("type", "REGISTERED") .put("registration", new JSONObject()) .toString(); assertThatThrownBy(() -> jsonMapper.readValue(json, InstanceRegisteredEvent.class)) .isInstanceOf(DatabindException.class) .hasCauseInstanceOf(IllegalArgumentException.class) .hasMessageContaining("must not be empty"); } @Test void verifySerialize() throws IOException { InstanceId id = InstanceId.of("test123"); Instant timestamp = Instant.ofEpochSecond(1587751031).truncatedTo(ChronoUnit.SECONDS); Registration registration = Registration.create("test", "http://localhost:9080/heath") .managementUrl("http://localhost:9080/") .serviceUrl("http://localhost:8080/") .source("http-api") .metadata("PASSWORD", "qwertz123") .metadata("user", "humptydumpty") .build(); InstanceRegisteredEvent event = new InstanceRegisteredEvent(id, 12345678L, timestamp, registration); JsonContent jsonContent = jsonTester.write(event); assertThat(jsonContent).extractingJsonPathStringValue("$.instance").isEqualTo("test123"); assertThat(jsonContent).extractingJsonPathNumberValue("$.version").isEqualTo(12345678); assertThat(jsonContent).extractingJsonPathStringValue("$.timestamp").isEqualTo("2020-04-24T17:57:11Z"); assertThat(jsonContent).extractingJsonPathStringValue("$.type").isEqualTo("REGISTERED"); assertThat(jsonContent).extractingJsonPathValue("$.registration").isNotNull(); assertThat(jsonContent).extractingJsonPathStringValue("$.registration.name").isEqualTo("test"); assertThat(jsonContent).extractingJsonPathStringValue("$.registration.managementUrl") .isEqualTo("http://localhost:9080/"); assertThat(jsonContent).extractingJsonPathStringValue("$.registration.healthUrl") .isEqualTo("http://localhost:9080/heath"); assertThat(jsonContent).extractingJsonPathStringValue("$.registration.serviceUrl") .isEqualTo("http://localhost:8080/"); assertThat(jsonContent).extractingJsonPathStringValue("$.registration.source").isEqualTo("http-api"); assertThat(jsonContent).extractingJsonPathMapValue("$.registration.metadata") .containsOnly(entry("PASSWORD", "******"), entry("user", "humptydumpty")); } @Test void verifySerializeWithOnlyRequiredProperties() throws IOException { InstanceId id = InstanceId.of("test123"); Instant timestamp = Instant.ofEpochSecond(1587751031).truncatedTo(ChronoUnit.SECONDS); Registration registration = Registration.create("test", "http://localhost:9080/heath").build(); InstanceRegisteredEvent event = new InstanceRegisteredEvent(id, 0L, timestamp, registration); JsonContent jsonContent = jsonTester.write(event); assertThat(jsonContent).extractingJsonPathStringValue("$.instance").isEqualTo("test123"); assertThat(jsonContent).extractingJsonPathNumberValue("$.version").isEqualTo(0); assertThat(jsonContent).extractingJsonPathStringValue("$.timestamp").isEqualTo("2020-04-24T17:57:11Z"); assertThat(jsonContent).extractingJsonPathStringValue("$.type").isEqualTo("REGISTERED"); assertThat(jsonContent).extractingJsonPathValue("$.registration").isNotNull(); assertThat(jsonContent).extractingJsonPathStringValue("$.registration.name").isEqualTo("test"); assertThat(jsonContent).extractingJsonPathStringValue("$.registration.managementUrl").isNull(); assertThat(jsonContent).extractingJsonPathStringValue("$.registration.healthUrl") .isEqualTo("http://localhost:9080/heath"); assertThat(jsonContent).extractingJsonPathStringValue("$.registration.serviceUrl").isNull(); assertThat(jsonContent).extractingJsonPathStringValue("$.registration.source").isNull(); assertThat(jsonContent).extractingJsonPathMapValue("$.registration.metadata").isEmpty(); } @Test void verifySerializeWithoutRegistration() throws IOException { InstanceId id = InstanceId.of("test123"); Instant timestamp = Instant.ofEpochSecond(1587751031).truncatedTo(ChronoUnit.SECONDS); InstanceRegisteredEvent event = new InstanceRegisteredEvent(id, 12345678L, timestamp, null); JsonContent jsonContent = jsonTester.write(event); assertThat(jsonContent).extractingJsonPathStringValue("$.instance").isEqualTo("test123"); assertThat(jsonContent).extractingJsonPathNumberValue("$.version").isEqualTo(12345678); assertThat(jsonContent).extractingJsonPathStringValue("$.timestamp").isEqualTo("2020-04-24T17:57:11Z"); assertThat(jsonContent).extractingJsonPathStringValue("$.type").isEqualTo("REGISTERED"); assertThat(jsonContent).extractingJsonPathValue("$.registration").isNull(); } } ================================================ FILE: spring-boot-admin-server/src/test/java/de/codecentric/boot/admin/server/utils/jackson/InstanceRegistrationUpdatedEventMixinTest.java ================================================ /* * 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.server.utils.jackson; import java.io.IOException; import java.time.Instant; import java.time.temporal.ChronoUnit; import org.json.JSONException; import org.json.JSONObject; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.boot.test.json.JacksonTester; import org.springframework.boot.test.json.JsonContent; import tools.jackson.core.JacksonException; import tools.jackson.databind.DatabindException; import tools.jackson.databind.DeserializationFeature; import tools.jackson.databind.json.JsonMapper; import de.codecentric.boot.admin.server.domain.events.InstanceRegistrationUpdatedEvent; import de.codecentric.boot.admin.server.domain.values.InstanceId; import de.codecentric.boot.admin.server.domain.values.Registration; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.assertj.core.api.Assertions.entry; class InstanceRegistrationUpdatedEventMixinTest { private final JsonMapper jsonMapper; private JacksonTester jsonTester; protected InstanceRegistrationUpdatedEventMixinTest() { AdminServerModule adminServerModule = new AdminServerModule(new String[] { ".*password$" }); jsonMapper = JsonMapper.builder() .addModule(adminServerModule) .disable(DeserializationFeature.FAIL_ON_NULL_FOR_PRIMITIVES) .build(); } @BeforeEach void setup() { JacksonTester.initFields(this, jsonMapper); } @Test void verifyDeserialize() throws JSONException, JacksonException { String json = new JSONObject().put("instance", "test123") .put("version", 12345678L) .put("timestamp", 1587751031.000000000) .put("type", "REGISTRATION_UPDATED") .put("registration", new JSONObject().put("name", "test") .put("managementUrl", "http://localhost:9080/") .put("healthUrl", "http://localhost:9080/heath") .put("serviceUrl", "http://localhost:8080/") .put("source", "http-api") .put("metadata", new JSONObject().put("PASSWORD", "******").put("user", "humptydumpty"))) .toString(); InstanceRegistrationUpdatedEvent event = jsonMapper.readValue(json, InstanceRegistrationUpdatedEvent.class); assertThat(event).isNotNull(); assertThat(event.getInstance()).isEqualTo(InstanceId.of("test123")); assertThat(event.getVersion()).isEqualTo(12345678L); assertThat(event.getTimestamp()).isEqualTo(Instant.ofEpochSecond(1587751031).truncatedTo(ChronoUnit.SECONDS)); Registration registration = event.getRegistration(); assertThat(registration).isNotNull(); assertThat(registration.getName()).isEqualTo("test"); assertThat(registration.getManagementUrl()).isEqualTo("http://localhost:9080/"); assertThat(registration.getHealthUrl()).isEqualTo("http://localhost:9080/heath"); assertThat(registration.getServiceUrl()).isEqualTo("http://localhost:8080/"); assertThat(registration.getSource()).isEqualTo("http-api"); assertThat(registration.getMetadata()).containsOnly(entry("PASSWORD", "******"), entry("user", "humptydumpty")); } @Test void verifyDeserializeWithOnlyRequiredProperties() throws JSONException, JacksonException { String json = new JSONObject().put("instance", "test123") .put("timestamp", 1587751031.000000000) .put("type", "REGISTRATION_UPDATED") .put("registration", new JSONObject().put("name", "test").put("healthUrl", "http://localhost:9080/heath")) .toString(); InstanceRegistrationUpdatedEvent event = jsonMapper.readValue(json, InstanceRegistrationUpdatedEvent.class); assertThat(event).isNotNull(); assertThat(event.getInstance()).isEqualTo(InstanceId.of("test123")); assertThat(event.getVersion()).isZero(); assertThat(event.getTimestamp()).isEqualTo(Instant.ofEpochSecond(1587751031).truncatedTo(ChronoUnit.SECONDS)); Registration registration = event.getRegistration(); assertThat(registration).isNotNull(); assertThat(registration.getName()).isEqualTo("test"); assertThat(registration.getManagementUrl()).isNull(); assertThat(registration.getHealthUrl()).isEqualTo("http://localhost:9080/heath"); assertThat(registration.getServiceUrl()).isNull(); assertThat(registration.getSource()).isNull(); assertThat(registration.getMetadata()).isEmpty(); } @Test void verifyDeserializeWithoutRegistration() throws JSONException, JacksonException { String json = new JSONObject().put("instance", "test123") .put("version", 12345678L) .put("timestamp", 1587751031.000000000) .put("type", "REGISTRATION_UPDATED") .toString(); InstanceRegistrationUpdatedEvent event = jsonMapper.readValue(json, InstanceRegistrationUpdatedEvent.class); assertThat(event).isNotNull(); assertThat(event.getInstance()).isEqualTo(InstanceId.of("test123")); assertThat(event.getVersion()).isEqualTo(12345678L); assertThat(event.getTimestamp()).isEqualTo(Instant.ofEpochSecond(1587751031).truncatedTo(ChronoUnit.SECONDS)); assertThat(event.getRegistration()).isNull(); } @Test void verifyDeserializeWithEmptyRegistration() throws JSONException { String json = new JSONObject().put("instance", "test123") .put("version", 12345678L) .put("timestamp", 1587751031.000000000) .put("type", "REGISTRATION_UPDATED") .put("registration", new JSONObject()) .toString(); assertThatThrownBy(() -> jsonMapper.readValue(json, InstanceRegistrationUpdatedEvent.class)) .isInstanceOf(DatabindException.class) .hasCauseInstanceOf(IllegalArgumentException.class) .hasMessageContaining("must not be empty"); } @Test void verifySerialize() throws IOException { InstanceId id = InstanceId.of("test123"); Instant timestamp = Instant.ofEpochSecond(1587751031).truncatedTo(ChronoUnit.SECONDS); Registration registration = Registration.create("test", "http://localhost:9080/heath") .managementUrl("http://localhost:9080/") .serviceUrl("http://localhost:8080/") .source("http-api") .metadata("PASSWORD", "qwertz123") .metadata("user", "humptydumpty") .build(); InstanceRegistrationUpdatedEvent event = new InstanceRegistrationUpdatedEvent(id, 12345678L, timestamp, registration); JsonContent jsonContent = jsonTester.write(event); assertThat(jsonContent).extractingJsonPathStringValue("$.instance").isEqualTo("test123"); assertThat(jsonContent).extractingJsonPathNumberValue("$.version").isEqualTo(12345678); assertThat(jsonContent).extractingJsonPathStringValue("$.timestamp").isEqualTo("2020-04-24T17:57:11Z"); assertThat(jsonContent).extractingJsonPathStringValue("$.type").isEqualTo("REGISTRATION_UPDATED"); assertThat(jsonContent).extractingJsonPathValue("$.registration").isNotNull(); assertThat(jsonContent).extractingJsonPathStringValue("$.registration.name").isEqualTo("test"); assertThat(jsonContent).extractingJsonPathStringValue("$.registration.managementUrl") .isEqualTo("http://localhost:9080/"); assertThat(jsonContent).extractingJsonPathStringValue("$.registration.healthUrl") .isEqualTo("http://localhost:9080/heath"); assertThat(jsonContent).extractingJsonPathStringValue("$.registration.serviceUrl") .isEqualTo("http://localhost:8080/"); assertThat(jsonContent).extractingJsonPathStringValue("$.registration.source").isEqualTo("http-api"); assertThat(jsonContent).extractingJsonPathMapValue("$.registration.metadata") .containsOnly(entry("PASSWORD", "******"), entry("user", "humptydumpty")); } @Test void verifySerializeWithOnlyRequiredProperties() throws IOException { InstanceId id = InstanceId.of("test123"); Instant timestamp = Instant.ofEpochSecond(1587751031).truncatedTo(ChronoUnit.SECONDS); Registration registration = Registration.create("test", "http://localhost:9080/heath").build(); InstanceRegistrationUpdatedEvent event = new InstanceRegistrationUpdatedEvent(id, 0L, timestamp, registration); JsonContent jsonContent = jsonTester.write(event); assertThat(jsonContent).extractingJsonPathStringValue("$.instance").isEqualTo("test123"); assertThat(jsonContent).extractingJsonPathNumberValue("$.version").isEqualTo(0); assertThat(jsonContent).extractingJsonPathStringValue("$.timestamp").isEqualTo("2020-04-24T17:57:11Z"); assertThat(jsonContent).extractingJsonPathStringValue("$.type").isEqualTo("REGISTRATION_UPDATED"); assertThat(jsonContent).extractingJsonPathValue("$.registration").isNotNull(); assertThat(jsonContent).extractingJsonPathStringValue("$.registration.name").isEqualTo("test"); assertThat(jsonContent).extractingJsonPathStringValue("$.registration.managementUrl").isNull(); assertThat(jsonContent).extractingJsonPathStringValue("$.registration.healthUrl") .isEqualTo("http://localhost:9080/heath"); assertThat(jsonContent).extractingJsonPathStringValue("$.registration.serviceUrl").isNull(); assertThat(jsonContent).extractingJsonPathStringValue("$.registration.source").isNull(); assertThat(jsonContent).extractingJsonPathMapValue("$.registration.metadata").isEmpty(); } @Test void verifySerializeWithoutRegistration() throws IOException { InstanceId id = InstanceId.of("test123"); Instant timestamp = Instant.ofEpochSecond(1587751031).truncatedTo(ChronoUnit.SECONDS); InstanceRegistrationUpdatedEvent event = new InstanceRegistrationUpdatedEvent(id, 12345678L, timestamp, null); JsonContent jsonContent = jsonTester.write(event); assertThat(jsonContent).extractingJsonPathStringValue("$.instance").isEqualTo("test123"); assertThat(jsonContent).extractingJsonPathNumberValue("$.version").isEqualTo(12345678); assertThat(jsonContent).extractingJsonPathStringValue("$.timestamp").isEqualTo("2020-04-24T17:57:11Z"); assertThat(jsonContent).extractingJsonPathStringValue("$.type").isEqualTo("REGISTRATION_UPDATED"); assertThat(jsonContent).extractingJsonPathMapValue("$.registration").isNull(); } } ================================================ FILE: spring-boot-admin-server/src/test/java/de/codecentric/boot/admin/server/utils/jackson/InstanceStatusChangedEventMixinTest.java ================================================ /* * 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.server.utils.jackson; import java.io.IOException; import java.time.Instant; import java.time.temporal.ChronoUnit; import java.util.Collections; import org.json.JSONException; import org.json.JSONObject; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.boot.test.json.JacksonTester; import org.springframework.boot.test.json.JsonContent; import tools.jackson.core.JacksonException; import tools.jackson.databind.DatabindException; import tools.jackson.databind.DeserializationFeature; import tools.jackson.databind.json.JsonMapper; import de.codecentric.boot.admin.server.domain.events.InstanceStatusChangedEvent; import de.codecentric.boot.admin.server.domain.values.InstanceId; import de.codecentric.boot.admin.server.domain.values.StatusInfo; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.assertj.core.api.Assertions.entry; class InstanceStatusChangedEventMixinTest { private final JsonMapper jsonMapper; private JacksonTester jsonTester; protected InstanceStatusChangedEventMixinTest() { AdminServerModule adminServerModule = new AdminServerModule(new String[] { ".*password$" }); jsonMapper = JsonMapper.builder() .addModule(adminServerModule) .disable(DeserializationFeature.FAIL_ON_NULL_FOR_PRIMITIVES) .build(); } @BeforeEach void setup() { JacksonTester.initFields(this, jsonMapper); } @Test void verifyDeserialize() throws JSONException, JacksonException { String json = new JSONObject().put("instance", "test123") .put("version", 12345678L) .put("timestamp", 1587751031.000000000) .put("type", "STATUS_CHANGED") .put("statusInfo", new JSONObject().put("status", "OFFLINE").put("details", new JSONObject().put("foo", "bar"))) .toString(); InstanceStatusChangedEvent event = jsonMapper.readValue(json, InstanceStatusChangedEvent.class); assertThat(event).isNotNull(); assertThat(event.getInstance()).isEqualTo(InstanceId.of("test123")); assertThat(event.getVersion()).isEqualTo(12345678L); assertThat(event.getTimestamp()).isEqualTo(Instant.ofEpochSecond(1587751031).truncatedTo(ChronoUnit.SECONDS)); StatusInfo statusInfo = event.getStatusInfo(); assertThat(statusInfo).isNotNull(); assertThat(statusInfo.getStatus()).isEqualTo("OFFLINE"); assertThat(statusInfo.getDetails()).containsOnly(entry("foo", "bar")); } @Test void verifyDeserializeWithOnlyRequiredProperties() throws JSONException, JacksonException { String json = new JSONObject().put("instance", "test123") .put("timestamp", 1587751031.000000000) .put("type", "STATUS_CHANGED") .put("statusInfo", new JSONObject().put("status", "OFFLINE")) .toString(); InstanceStatusChangedEvent event = jsonMapper.readValue(json, InstanceStatusChangedEvent.class); assertThat(event).isNotNull(); assertThat(event.getInstance()).isEqualTo(InstanceId.of("test123")); assertThat(event.getVersion()).isZero(); assertThat(event.getTimestamp()).isEqualTo(Instant.ofEpochSecond(1587751031).truncatedTo(ChronoUnit.SECONDS)); StatusInfo statusInfo = event.getStatusInfo(); assertThat(statusInfo).isNotNull(); assertThat(statusInfo.getStatus()).isEqualTo("OFFLINE"); assertThat(statusInfo.getDetails()).isEmpty(); } @Test void verifyDeserializeWithoutStatusInfo() throws JSONException, JacksonException { String json = new JSONObject().put("instance", "test123") .put("version", 12345678L) .put("timestamp", 1587751031.000000000) .put("type", "STATUS_CHANGED") .toString(); InstanceStatusChangedEvent event = jsonMapper.readValue(json, InstanceStatusChangedEvent.class); assertThat(event).isNotNull(); assertThat(event.getInstance()).isEqualTo(InstanceId.of("test123")); assertThat(event.getVersion()).isEqualTo(12345678L); assertThat(event.getTimestamp()).isEqualTo(Instant.ofEpochSecond(1587751031).truncatedTo(ChronoUnit.SECONDS)); assertThat(event.getStatusInfo()).isNull(); } @Test void verifyDeserializeWithEmptyStatusInfo() throws JSONException { String json = new JSONObject().put("instance", "test123") .put("version", 12345678L) .put("timestamp", 1587751031.000000000) .put("type", "STATUS_CHANGED") .put("statusInfo", new JSONObject()) .toString(); assertThatThrownBy(() -> jsonMapper.readValue(json, InstanceStatusChangedEvent.class)) .isInstanceOf(DatabindException.class) .hasCauseInstanceOf(IllegalArgumentException.class) .hasMessageContaining("must not be empty"); } @Test void verifySerialize() throws IOException { InstanceId id = InstanceId.of("test123"); Instant timestamp = Instant.ofEpochSecond(1587751031).truncatedTo(ChronoUnit.SECONDS); StatusInfo statusInfo = StatusInfo.valueOf("OFFLINE", Collections.singletonMap("foo", "bar")); InstanceStatusChangedEvent event = new InstanceStatusChangedEvent(id, 12345678L, timestamp, statusInfo); JsonContent jsonContent = jsonTester.write(event); assertThat(jsonContent).extractingJsonPathStringValue("$.instance").isEqualTo("test123"); assertThat(jsonContent).extractingJsonPathNumberValue("$.version").isEqualTo(12345678); assertThat(jsonContent).extractingJsonPathStringValue("$.timestamp").isEqualTo("2020-04-24T17:57:11Z"); assertThat(jsonContent).extractingJsonPathStringValue("$.type").isEqualTo("STATUS_CHANGED"); assertThat(jsonContent).extractingJsonPathValue("$.statusInfo").isNotNull(); assertThat(jsonContent).extractingJsonPathStringValue("$.statusInfo.status").isEqualTo("OFFLINE"); assertThat(jsonContent).extractingJsonPathMapValue("$.statusInfo.details").containsOnly(entry("foo", "bar")); } @Test void verifySerializeWithOnlyRequiredProperties() throws IOException { InstanceId id = InstanceId.of("test123"); Instant timestamp = Instant.ofEpochSecond(1587751031).truncatedTo(ChronoUnit.SECONDS); StatusInfo statusInfo = StatusInfo.valueOf("OFFLINE"); InstanceStatusChangedEvent event = new InstanceStatusChangedEvent(id, 0L, timestamp, statusInfo); JsonContent jsonContent = jsonTester.write(event); assertThat(jsonContent).extractingJsonPathStringValue("$.instance").isEqualTo("test123"); assertThat(jsonContent).extractingJsonPathNumberValue("$.version").isEqualTo(0); assertThat(jsonContent).extractingJsonPathStringValue("$.timestamp").isEqualTo("2020-04-24T17:57:11Z"); assertThat(jsonContent).extractingJsonPathStringValue("$.type").isEqualTo("STATUS_CHANGED"); assertThat(jsonContent).extractingJsonPathValue("$.statusInfo").isNotNull(); assertThat(jsonContent).extractingJsonPathStringValue("$.statusInfo.status").isEqualTo("OFFLINE"); assertThat(jsonContent).extractingJsonPathMapValue("$.statusInfo.details").isEmpty(); } @Test void verifySerializeWithoutStatusInfo() throws IOException { InstanceId id = InstanceId.of("test123"); Instant timestamp = Instant.ofEpochSecond(1587751031).truncatedTo(ChronoUnit.SECONDS); InstanceStatusChangedEvent event = new InstanceStatusChangedEvent(id, 12345678L, timestamp, null); JsonContent jsonContent = jsonTester.write(event); assertThat(jsonContent).extractingJsonPathStringValue("$.instance").isEqualTo("test123"); assertThat(jsonContent).extractingJsonPathNumberValue("$.version").isEqualTo(12345678); assertThat(jsonContent).extractingJsonPathStringValue("$.timestamp").isEqualTo("2020-04-24T17:57:11Z"); assertThat(jsonContent).extractingJsonPathStringValue("$.type").isEqualTo("STATUS_CHANGED"); assertThat(jsonContent).extractingJsonPathValue("$.statusInfo").isNull(); } } ================================================ FILE: spring-boot-admin-server/src/test/java/de/codecentric/boot/admin/server/utils/jackson/RegistrationDeserializerTest.java ================================================ /* * 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.server.utils.jackson; import org.json.JSONObject; import org.junit.jupiter.api.Test; import tools.jackson.core.JacksonException; import tools.jackson.databind.DeserializationFeature; import tools.jackson.databind.json.JsonMapper; import de.codecentric.boot.admin.server.domain.values.Registration; import static java.util.Collections.singletonMap; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; class RegistrationDeserializerTest { private final JsonMapper jsonMapper; protected RegistrationDeserializerTest() { AdminServerModule adminServerModule = new AdminServerModule(new String[] { ".*password$" }); jsonMapper = JsonMapper.builder() .addModule(adminServerModule) .disable(DeserializationFeature.FAIL_ON_NULL_FOR_PRIMITIVES) .build(); } @Test void test_1_2_json_format() throws Exception { String json = new JSONObject().put("name", "test").put("url", "https://test").toString(); Registration value = jsonMapper.readValue(json, Registration.class); assertThat(value.getName()).isEqualTo("test"); assertThat(value.getManagementUrl()).isEqualTo("https://test"); assertThat(value.getHealthUrl()).isEqualTo("https://test/health"); assertThat(value.getServiceUrl()).isNull(); } @Test void test_1_4_json_format() throws Exception { String json = new JSONObject().put("name", "test") .put("managementUrl", "https://test") .put("healthUrl", "https://health") .put("serviceUrl", "https://service") .put("statusInfo", new JSONObject().put("status", "UNKNOWN")) .toString(); Registration value = jsonMapper.readValue(json, Registration.class); assertThat(value.getName()).isEqualTo("test"); assertThat(value.getManagementUrl()).isEqualTo("https://test"); assertThat(value.getHealthUrl()).isEqualTo("https://health"); assertThat(value.getServiceUrl()).isEqualTo("https://service"); } @Test void test_1_5_json_format() throws Exception { String json = new JSONObject().put("name", "test") .put("managementUrl", "https://test") .put("healthUrl", "https://health") .put("serviceUrl", "https://service") .put("metadata", new JSONObject().put("labels", "foo,bar")) .toString(); Registration value = jsonMapper.readValue(json, Registration.class); assertThat(value.getName()).isEqualTo("test"); assertThat(value.getManagementUrl()).isEqualTo("https://test"); assertThat(value.getHealthUrl()).isEqualTo("https://health"); assertThat(value.getServiceUrl()).isEqualTo("https://service"); assertThat(value.getMetadata()).isEqualTo(singletonMap("labels", "foo,bar")); } @Test void test_onlyHealthUrl() throws Exception { String json = new JSONObject().put("name", "test").put("healthUrl", "https://test").toString(); Registration value = jsonMapper.readValue(json, Registration.class); assertThat(value.getName()).isEqualTo("test"); assertThat(value.getHealthUrl()).isEqualTo("https://test"); assertThat(value.getManagementUrl()).isNull(); assertThat(value.getServiceUrl()).isNull(); } @Test void test_name_expected() throws Exception { String json = new JSONObject().put("name", "") .put("managementUrl", "https://test") .put("healthUrl", "https://health") .put("serviceUrl", "https://service") .toString(); assertThatThrownBy(() -> jsonMapper.readValue(json, Registration.class)) .isInstanceOf(IllegalArgumentException.class); } @Test void test_healthUrl_expected() throws Exception { String json = new JSONObject().put("name", "test") .put("managementUrl", "https://test") .put("healthUrl", "") .put("serviceUrl", "https://service") .toString(); assertThatThrownBy(() -> jsonMapper.readValue(json, Registration.class)) .isInstanceOf(IllegalArgumentException.class); } @Test void test_sanitize_metadata() throws JacksonException { Registration app = Registration.create("test", "https://health") .metadata("PASSWORD", "qwertz123") .metadata("user", "humptydumpty") .build(); String json = jsonMapper.writeValueAsString(app); assertThat(json).doesNotContain("qwertz123").contains("humptydumpty"); } @Test void test_snake_case() throws Exception { String json = new JSONObject().put("name", "test") .put("management_url", "https://test") .put("health_url", "https://health") .put("service_url", "https://service") .put("metadata", new JSONObject().put("labels", "foo,bar")) .toString(); Registration value = jsonMapper.readValue(json, Registration.class); assertThat(value.getName()).isEqualTo("test"); assertThat(value.getManagementUrl()).isEqualTo("https://test"); assertThat(value.getHealthUrl()).isEqualTo("https://health"); assertThat(value.getServiceUrl()).isEqualTo("https://service"); assertThat(value.getMetadata()).isEqualTo(singletonMap("labels", "foo,bar")); } } ================================================ FILE: spring-boot-admin-server/src/test/java/de/codecentric/boot/admin/server/utils/jackson/StatusInfoMixinTest.java ================================================ /* * 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.server.utils.jackson; import java.io.IOException; import java.util.Collections; import org.json.JSONException; import org.json.JSONObject; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.boot.test.json.JacksonTester; import org.springframework.boot.test.json.JsonContent; import tools.jackson.core.JacksonException; import tools.jackson.databind.DeserializationFeature; import tools.jackson.databind.json.JsonMapper; import de.codecentric.boot.admin.server.domain.values.StatusInfo; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.entry; class StatusInfoMixinTest { private final JsonMapper jsonMapper; private JacksonTester jsonTester; protected StatusInfoMixinTest() { AdminServerModule adminServerModule = new AdminServerModule(new String[] { ".*password$" }); jsonMapper = JsonMapper.builder() .addModule(adminServerModule) .disable(DeserializationFeature.FAIL_ON_NULL_FOR_PRIMITIVES) .build(); } @BeforeEach void setup() { JacksonTester.initFields(this, jsonMapper); } @Test void verifyDeserialize() throws JSONException, JacksonException { String json = new JSONObject().put("status", "OFFLINE") .put("details", new JSONObject().put("foo", "bar")) .toString(); StatusInfo statusInfo = jsonMapper.readValue(json, StatusInfo.class); assertThat(statusInfo).isNotNull(); assertThat(statusInfo.getStatus()).isEqualTo("OFFLINE"); assertThat(statusInfo.getDetails()).containsOnly(entry("foo", "bar")); } @Test void verifySerialize() throws IOException { StatusInfo statusInfo = StatusInfo.valueOf("OFFLINE", Collections.singletonMap("foo", "bar")); JsonContent jsonContent = jsonTester.write(statusInfo); assertThat(jsonContent).extractingJsonPathStringValue("$.status").isEqualTo("OFFLINE"); assertThat(jsonContent).extractingJsonPathMapValue("$.details").containsOnly(entry("foo", "bar")); assertThat(jsonContent).doesNotHaveJsonPath("$.up") .doesNotHaveJsonPath("$.offline") .doesNotHaveJsonPath("$.down") .doesNotHaveJsonPath("$.unknown"); } } ================================================ FILE: spring-boot-admin-server/src/test/java/de/codecentric/boot/admin/server/utils/jackson/TagsMixinTest.java ================================================ /* * 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.server.utils.jackson; import java.io.IOException; import java.util.HashMap; import java.util.Map; import org.json.JSONException; import org.json.JSONObject; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.boot.test.json.JacksonTester; import org.springframework.boot.test.json.JsonContent; import tools.jackson.core.JacksonException; import tools.jackson.databind.DeserializationFeature; import tools.jackson.databind.json.JsonMapper; import de.codecentric.boot.admin.server.domain.values.Tags; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.entry; class TagsMixinTest { private final JsonMapper jsonMapper; private JacksonTester jsonTester; protected TagsMixinTest() { AdminServerModule adminServerModule = new AdminServerModule(new String[] { ".*password$" }); jsonMapper = JsonMapper.builder() .addModule(adminServerModule) .disable(DeserializationFeature.FAIL_ON_NULL_FOR_PRIMITIVES) .build(); } @BeforeEach void setup() { JacksonTester.initFields(this, jsonMapper); } @Test void verifyDeserialize() throws JSONException, JacksonException { String json = new JSONObject().put("env", "test").put("foo", "bar").toString(); Tags tags = jsonMapper.readValue(json, Tags.class); assertThat(tags).isNotNull(); assertThat(tags.getValues()).containsOnly(entry("env", "test"), entry("foo", "bar")); } @Test void verifySerialize() throws IOException { Map data = new HashMap<>(); data.put("env", "test"); data.put("foo", "bar"); Tags tags = Tags.from(data); JsonContent jsonContent = jsonTester.write(tags); assertThat(jsonContent).extractingJsonPathMapValue("$").containsOnly(entry("env", "test"), entry("foo", "bar")); } } ================================================ FILE: spring-boot-admin-server/src/test/java/de/codecentric/boot/admin/server/web/AbstractInstancesProxyControllerIntegrationTest.java ================================================ /* * 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.server.web; import java.time.Duration; import java.util.Map; import java.util.concurrent.atomic.AtomicReference; import com.github.tomakehurst.wiremock.WireMockServer; import com.github.tomakehurst.wiremock.core.WireMockConfiguration; import com.github.tomakehurst.wiremock.http.Fault; import org.jetbrains.annotations.NotNull; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.boot.actuate.endpoint.ApiVersion; import org.springframework.context.ConfigurableApplicationContext; import org.springframework.core.ParameterizedTypeReference; import org.springframework.http.HttpMethod; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.test.web.reactive.server.WebTestClient; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import reactor.test.StepVerifier; import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; import static com.github.tomakehurst.wiremock.client.WireMock.delete; import static com.github.tomakehurst.wiremock.client.WireMock.deleteRequestedFor; import static com.github.tomakehurst.wiremock.client.WireMock.equalTo; import static com.github.tomakehurst.wiremock.client.WireMock.get; import static com.github.tomakehurst.wiremock.client.WireMock.getRequestedFor; import static com.github.tomakehurst.wiremock.client.WireMock.ok; import static com.github.tomakehurst.wiremock.client.WireMock.options; import static com.github.tomakehurst.wiremock.client.WireMock.post; import static com.github.tomakehurst.wiremock.client.WireMock.postRequestedFor; import static com.github.tomakehurst.wiremock.client.WireMock.serverError; import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo; import static org.assertj.core.api.Assertions.assertThat; import static org.springframework.http.HttpHeaders.ALLOW; import static org.springframework.http.HttpHeaders.CONTENT_TYPE; public abstract class AbstractInstancesProxyControllerIntegrationTest { private static final String ACTUATOR_CONTENT_TYPE = ApiVersion.LATEST.getProducedMimeType().toString() + ";charset=UTF-8"; private static final ParameterizedTypeReference> RESPONSE_TYPE = new ParameterizedTypeReference<>() { }; private final WireMockServer wireMock = new WireMockServer( WireMockConfiguration.options().dynamicPort().extensions(new ConnectionCloseExtension())); private WebTestClient client; private String instanceId; private ConfigurableApplicationContext context; @BeforeAll public static void setUp() { StepVerifier.setDefaultTimeout(Duration.ofSeconds(600)); } @AfterAll public static void tearDown() { StepVerifier.resetDefaultTimeout(); } @BeforeEach void setup() { this.wireMock.start(); } @AfterEach void teardown() { this.wireMock.stop(); } protected void setUpClient(ConfigurableApplicationContext context) { this.context = context; this.client = createWebTestClientBuilder().build(); this.instanceId = registerInstance("/instance1"); } @NotNull private WebTestClient.Builder createWebTestClientBuilder() { int localPort = this.context.getEnvironment().getProperty("local.server.port", Integer.class, 0); return WebTestClient.bindToServer() .baseUrl("http://localhost:" + localPort) .responseTimeout(Duration.ofSeconds(10)); } @Test public void should_return_status_503() { // 503 on invalid instance this.client.get() .uri("/instances/{instanceId}/actuator/info", "UNKNOWN") .accept(new MediaType(ApiVersion.LATEST.getProducedMimeType())) .exchange() .expectStatus() .isEqualTo(HttpStatus.SERVICE_UNAVAILABLE); } @Test public void should_return_status_404() { // 404 on non-existent endpoint this.client.get() .uri("/instances/{instanceId}/actuator/not-exist", this.instanceId) .accept(new MediaType(ApiVersion.LATEST.getProducedMimeType())) .exchange() .expectStatus() .isNotFound(); } @Test public void should_return_status_502() { // 502 on invalid response this.client.get() .uri("/instances/{instanceId}/actuator/invalid", this.instanceId) .accept(new MediaType(ApiVersion.LATEST.getProducedMimeType())) .exchange() .expectStatus() .isEqualTo(HttpStatus.BAD_GATEWAY); } @Test public void should_return_status_504() { // 504 on read timeout this.client.get() .uri("/instances/{instanceId}/actuator/timeout", this.instanceId) .accept(new MediaType(ApiVersion.LATEST.getProducedMimeType())) .exchange() .expectStatus() .isEqualTo(HttpStatus.GATEWAY_TIMEOUT); } @Test public void should_forward_requests() { this.client.options() .uri("/instances/{instanceId}/actuator/env", this.instanceId) .accept(new MediaType(ApiVersion.LATEST.getProducedMimeType())) .exchange() .expectStatus() .isOk() .expectHeader() .valueEquals(ALLOW, HttpMethod.HEAD.name(), HttpMethod.GET.name(), HttpMethod.OPTIONS.name()); this.client.get() .uri("/instances/{instanceId}/actuator/test", this.instanceId) .accept(new MediaType(ApiVersion.LATEST.getProducedMimeType())) .exchange() .expectStatus() .isOk() .expectBody(String.class) .isEqualTo("{ \"foo\" : \"bar\" }"); this.client.post() .uri("/instances/{instanceId}/actuator/post", this.instanceId) .bodyValue("PAYLOAD") .exchange() .expectStatus() .isOk(); this.wireMock.verify(postRequestedFor(urlEqualTo("/instance1/post")).withRequestBody(equalTo("PAYLOAD"))); this.client.delete() .uri("/instances/{instanceId}/actuator/delete", this.instanceId) .exchange() .expectStatus() .isEqualTo(500) .expectBody(String.class) .isEqualTo("{\"error\": \"You're doing it wrong!\"}"); this.wireMock.verify(deleteRequestedFor(urlEqualTo("/instance1/delete"))); } @Test public void should_forward_requests_with_spaces_in_path() { this.client.get() .uri("/instances/{instanceId}/actuator/test/has spaces", this.instanceId) .accept(new MediaType(ApiVersion.LATEST.getProducedMimeType())) .exchange() .expectStatus() .isOk() .expectBody(String.class) .isEqualTo("{ \"foo\" : \"bar-with-spaces\" }"); this.wireMock.verify(getRequestedFor(urlEqualTo("/instance1/test/has%20spaces"))); } @Test public void should_forward_requests_to_multiple_instances() { String instance2Id = registerInstance("/instance2"); //@formatter:off StepVerifier .create(this.client.get() .uri("applications/test/actuator/test") .accept(new MediaType(ApiVersion.LATEST.getProducedMimeType())) .exchange() .returnResult(String.class).getResponseBody().single()) .assertNext((body) -> { assertThat(body).contains("\"instanceId\":\"" + this.instanceId + "\""); assertThat(body).contains("\"instanceId\":\"" + instance2Id + "\""); assertThat(body).contains("\"status\":200"); assertThat(body).contains("{ \\\"foo\\\" : \\\"bar\\\" }"); }) .verifyComplete(); //@formatter:on this.client.post().uri("applications/test/actuator/post").bodyValue("PAYLOAD").exchange().expectStatus().isOk(); this.wireMock.verify(postRequestedFor(urlEqualTo("/instance1/post")).withRequestBody(equalTo("PAYLOAD"))); this.wireMock.verify(postRequestedFor(urlEqualTo("/instance2/post")).withRequestBody(equalTo("PAYLOAD"))); //@formatter:off StepVerifier .create(this.client.delete() .uri("applications/test/actuator/delete") .exchange() .returnResult(String.class).getResponseBody().single()) .assertNext((body) -> { assertThat(body).contains("\"instanceId\":\"" + this.instanceId + "\""); assertThat(body).contains("\"instanceId\":\"" + instance2Id + "\""); assertThat(body).contains("\"status\":500"); assertThat(body).contains("{\\\"error\\\": \\\"You're doing it wrong!\\\"}"); }) .verifyComplete(); //@formatter:on this.wireMock.verify(deleteRequestedFor(urlEqualTo("/instance1/delete"))); this.wireMock.verify(deleteRequestedFor(urlEqualTo("/instance2/delete"))); } private void stubForInstance(String managementPath) { String managementUrl = this.wireMock.url(managementPath); //@formatter:off String actuatorIndex = "{ \"_links\": { " + "\"env\": { \"href\": \"" + managementUrl + "/env\", \"templated\": false }," + "\"test\": { \"href\": \"" + managementUrl + "/test\", \"templated\": false }," + "\"post\": { \"href\": \"" + managementUrl + "/post\", \"templated\": false }," + "\"delete\": { \"href\": \"" + managementUrl + "/delete\", \"templated\": false }," + "\"invalid\": { \"href\": \"" + managementUrl + "/invalid\", \"templated\": false }," + "\"timeout\": { \"href\": \"" + managementUrl + "/timeout\", \"templated\": false }" + " } }"; //@formatter:on this.wireMock.stubFor(get(urlEqualTo(managementPath + "/health")).willReturn(ok("{ \"status\" : \"UP\" }") .withHeader(CONTENT_TYPE, ApiVersion.LATEST.getProducedMimeType().toString()))); this.wireMock.stubFor(get(urlEqualTo(managementPath + "/info")) .willReturn(ok("{ }").withHeader(CONTENT_TYPE, ACTUATOR_CONTENT_TYPE))); this.wireMock.stubFor(options(urlEqualTo(managementPath + "/env")).willReturn( ok().withHeader(ALLOW, HttpMethod.HEAD.name(), HttpMethod.GET.name(), HttpMethod.OPTIONS.name()))); this.wireMock.stubFor(get(urlEqualTo(managementPath)) .willReturn(ok(actuatorIndex).withHeader(CONTENT_TYPE, ACTUATOR_CONTENT_TYPE))); this.wireMock.stubFor(get(urlEqualTo(managementPath + "/invalid")) .willReturn(aResponse().withFault(Fault.CONNECTION_RESET_BY_PEER))); this.wireMock.stubFor(get(urlEqualTo(managementPath + "/timeout")).willReturn(ok().withFixedDelay(10000))); this.wireMock.stubFor(get(urlEqualTo(managementPath + "/test")) .willReturn(ok("{ \"foo\" : \"bar\" }").withHeader(CONTENT_TYPE, ACTUATOR_CONTENT_TYPE))); this.wireMock.stubFor(get(urlEqualTo(managementPath + "/test/has%20spaces")) .willReturn(ok("{ \"foo\" : \"bar-with-spaces\" }").withHeader(CONTENT_TYPE, ACTUATOR_CONTENT_TYPE))); this.wireMock.stubFor(post(urlEqualTo(managementPath + "/post")).willReturn(ok())); this.wireMock.stubFor(delete(urlEqualTo(managementPath + "/delete")) .willReturn(serverError().withBody("{\"error\": \"You're doing it wrong!\"}") .withHeader(CONTENT_TYPE, ACTUATOR_CONTENT_TYPE))); } private String registerInstance(String managementPath) { stubForInstance(managementPath); AtomicReference instanceIdRef = new AtomicReference<>(); StepVerifier.create(getEventStream()) .expectSubscription() .then(() -> StepVerifier.create(sendRegistration(managementPath)) .consumeNextWith(instanceIdRef::set) .verifyComplete()) .thenConsumeWhile((event) -> !event.get("type").equals("ENDPOINTS_DETECTED")) .assertNext((event) -> assertThat(event).containsEntry("type", "ENDPOINTS_DETECTED")) .thenCancel() .verify(); return instanceIdRef.get(); } private Mono sendRegistration(String managementPath) { String managementUrl = this.wireMock.url(managementPath); //@formatter:off String registration = "{ \"name\": \"test\", " + "\"healthUrl\": \"" + managementUrl + "/health\", " + "\"managementUrl\": \"" + managementUrl + "\" }"; return this.client.post() .uri("/instances") .accept(MediaType.APPLICATION_JSON).contentType(MediaType.APPLICATION_JSON) .bodyValue(registration) .exchange() .expectStatus().isCreated() .returnResult(RESPONSE_TYPE).getResponseBody().single() .map((body) -> { assertThat(body).containsKeys("id"); return body.get("id").toString(); }); //@formatter:on } private Flux> getEventStream() { //@formatter:off return this.client.get().uri("/instances/events").accept(MediaType.TEXT_EVENT_STREAM) .exchange() .expectStatus().isOk() .returnResult(RESPONSE_TYPE).getResponseBody(); //@formatter:on } } ================================================ FILE: spring-boot-admin-server/src/test/java/de/codecentric/boot/admin/server/web/ConnectionCloseExtension.java ================================================ /* * 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.server.web; import com.github.tomakehurst.wiremock.extension.ResponseTransformerV2; import com.github.tomakehurst.wiremock.http.HttpHeader; import com.github.tomakehurst.wiremock.http.HttpHeaders; import com.github.tomakehurst.wiremock.http.Response; import com.github.tomakehurst.wiremock.stubbing.ServeEvent; // Force the connections to be closed... // see https://github.com/tomakehurst/wiremock/issues/485 class ConnectionCloseExtension implements ResponseTransformerV2 { @Override public Response transform(Response response, ServeEvent serveEvent) { return Response.Builder.like(response) .headers(HttpHeaders.copyOf(response.getHeaders()).plus(new HttpHeader("Connection", "Close"))) .build(); } @Override public String getName() { return "ConnectionCloseExtension"; } } ================================================ FILE: spring-boot-admin-server/src/test/java/de/codecentric/boot/admin/server/web/InstancesControllerIntegrationTest.java ================================================ /* * 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.server.web; import java.time.Duration; import java.util.Map; import java.util.concurrent.atomic.AtomicReference; import com.jayway.jsonpath.DocumentContext; import com.jayway.jsonpath.JsonPath; import lombok.extern.slf4j.Slf4j; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.boot.WebApplicationType; import org.springframework.boot.builder.SpringApplicationBuilder; import org.springframework.context.ConfigurableApplicationContext; import org.springframework.core.ParameterizedTypeReference; import org.springframework.http.MediaType; import org.springframework.test.web.reactive.server.WebTestClient; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import reactor.test.StepVerifier; import de.codecentric.boot.admin.server.AdminReactiveApplicationTest; import static java.util.Collections.emptyList; import static java.util.Collections.singletonMap; import static org.assertj.core.api.Assertions.assertThat; @Slf4j class InstancesControllerIntegrationTest { private int localPort; private WebTestClient client; private String registerAsTest; private String registerAsTwice; private ConfigurableApplicationContext instance; private final ParameterizedTypeReference> responseType = new ParameterizedTypeReference<>() { }; @AfterAll static void tearDown() { StepVerifier.resetDefaultTimeout(); } @BeforeAll static void beforeAll() { StepVerifier.setDefaultTimeout(Duration.ofSeconds(600)); } @BeforeEach void setUp() { instance = new SpringApplicationBuilder().sources(AdminReactiveApplicationTest.TestAdminApplication.class) .web(WebApplicationType.REACTIVE) .run("--server.port=0", "--eureka.client.enabled=false"); localPort = instance.getEnvironment().getProperty("local.server.port", Integer.class, 0); this.client = WebTestClient.bindToServer().baseUrl("http://localhost:" + localPort).build(); this.registerAsTest = "{ \"name\": \"test\", \"healthUrl\": \"http://localhost:" + localPort + "/application/health\" }"; this.registerAsTwice = "{ \"name\": \"twice\", \"healthUrl\": \"http://localhost:" + localPort + "/application/health\" }"; } @AfterEach void shutdown() { instance.close(); } @Test void should_return_not_found_when_get_unknown_instance() { this.client.get().uri("/instances/unknown").exchange().expectStatus().isNotFound(); } @Test void should_return_empty_list() { this.client.get() .uri("/instances?name=unknown") .exchange() .expectStatus() .isOk() .expectBody(java.util.List.class) .isEqualTo(emptyList()); } @Test void should_return_not_found_when_deleting_unknown_instance() { this.client.delete().uri("/instances/unknown").exchange().expectStatus().isNotFound(); } @Test void should_return_registered_instances() { AtomicReference id = new AtomicReference<>(); StepVerifier.create(this.getEventStream().log()) .expectSubscription() .then(() -> StepVerifier.create(register()).consumeNextWith(id::set).verifyComplete()) .assertNext((body) -> { assertThat(body).containsEntry("version", 0).containsEntry("type", "REGISTERED"); // The id might not be set yet if event arrives before registration // completes if (id.get() == null) { id.set((String) body.get("instance")); } assertThat(body).containsEntry("instance", id.get()); }) .then(() -> { StepVerifier.create(assertInstances(id.get())).expectNext(true).verifyComplete(); StepVerifier.create(assertInstancesByName("test", id.get())).expectNext(true).verifyComplete(); StepVerifier.create(assertInstanceById(id.get())).expectNext(true).verifyComplete(); }) .assertNext((body) -> assertThat(body).containsEntry("instance", id.get()) .containsEntry("version", 1) .containsEntry("type", "STATUS_CHANGED")) .then(() -> StepVerifier.create(registerSecondTime(id.get())).expectNext(true).verifyComplete()) .assertNext((body) -> assertThat(body).containsEntry("instance", id.get()) .containsEntry("version", 2) .containsEntry("type", "REGISTRATION_UPDATED")) .then(() -> StepVerifier.create(deregister(id.get())).expectNext(true).verifyComplete()) .assertNext((body) -> assertThat(body).containsEntry("instance", id.get()) .containsEntry("version", 3) .containsEntry("type", "DEREGISTERED")) .then(() -> { StepVerifier.create(assertInstanceNotFound(id.get())).expectNext(true).verifyComplete(); StepVerifier.create(assertEvents(id.get())).expectNext(true).verifyComplete(); }) .thenCancel() .verify(); } private Mono assertEvents(String id) { //@formatter:off return this.client.get() .uri("/instances/events") .accept(MediaType.APPLICATION_JSON) .exchange() .returnResult(String.class).getResponseBody().single() .map((responseBody) -> { DocumentContext json = JsonPath.parse(responseBody); assertThat(json.read("$[0].instance", String.class)).isEqualTo(id); assertThat(json.read("$[0].version", Long.class)).isZero(); assertThat(json.read("$[0].type", String.class)).isEqualTo("REGISTERED"); assertThat(json.read("$[1].instance", String.class)).isEqualTo(id); assertThat(json.read("$[1].version", Long.class)).isEqualTo(1L); assertThat(json.read("$[1].type", String.class)).isEqualTo("STATUS_CHANGED"); assertThat(json.read("$[2].instance", String.class)).isEqualTo(id); assertThat(json.read("$[2].version", Long.class)).isEqualTo(2L); assertThat(json.read("$[2].type", String.class)).isEqualTo("REGISTRATION_UPDATED"); assertThat(json.read("$[3].instance", String.class)).isEqualTo(id); assertThat(json.read("$[3].version", Long.class)).isEqualTo(3L); assertThat(json.read("$[3].type", String.class)).isEqualTo("DEREGISTERED"); return true; }); //@formatter:on } private Mono assertInstanceNotFound(String id) { //@formatter:off return this.client.get() .uri(getLocation(id)) .exchange() .expectStatus().isNotFound() .returnResult(Void.class).getResponseBody() .then(Mono.just(true)); //@formatter:on } private Mono deregister(String id) { //@formatter:off return this.client.delete() .uri(getLocation(id)) .exchange() .expectStatus().isNoContent() .returnResult(Void.class).getResponseBody() .then(Mono.just(true)); //@formatter:on } private Mono assertInstanceById(String id) { //@formatter:off return this.client.get() .uri(getLocation(id)) .accept(MediaType.APPLICATION_JSON) .exchange() .returnResult(String.class).getResponseBody().single() .map((body) -> { DocumentContext json = JsonPath.parse(body); assertThat(json.read("$.id", String.class)).isEqualTo(id); return true; }); //@formatter:on } private Mono assertInstancesByName(String name, String id) { //@formatter:off return this.client.get() .uri("/instances?name=" + name) .accept(MediaType.APPLICATION_JSON) .exchange() .returnResult(String.class).getResponseBody().single() .map((body) -> { DocumentContext json = JsonPath.parse(body); assertThat(json.read("$[0].id", String.class)).isEqualTo(id); return true; }); //@formatter:on } private Mono assertInstances(String id) { //@formatter:off return this.client.get() .uri("/instances") .accept(MediaType.APPLICATION_JSON) .exchange() .returnResult(String.class).getResponseBody().single() .map((body) -> { DocumentContext json = JsonPath.parse(body); assertThat(json.read("$[0].id", String.class)).isEqualTo(id); return true; }); //@formatter:on } private Mono registerSecondTime(String id) { //@formatter:off return this.client.post() .uri("/instances") .accept(MediaType.APPLICATION_JSON) .contentType(MediaType.APPLICATION_JSON) .bodyValue(registerAsTwice) .exchange() .expectStatus().isCreated() .expectHeader().valueEquals("location", getLocation(id)) .returnResult(responseType).getResponseBody().single() .map((body) -> { assertThat(body).isEqualTo(singletonMap("id", id)); return true; }); //@formatter:on } private Mono register() { //@formatter:off return this.client.post() .uri("/instances") .accept(MediaType.APPLICATION_JSON) .contentType(MediaType.APPLICATION_JSON) .bodyValue(registerAsTest) .exchange() .expectStatus().isCreated() .expectHeader().valueMatches("location", "http://localhost:" + localPort + "/instances/[0-9a-f]+") .returnResult(responseType).getResponseBody().single() .map((body) -> { assertThat(body).containsKeys("id"); return body.get("id").toString(); }); //@formatter:on } private String getLocation(String id) { return "http://localhost:" + localPort + "/instances/" + id; } private Flux> getEventStream() { //@formatter:off return this.client.get().uri("/instances/events").accept(MediaType.TEXT_EVENT_STREAM) .exchange() .expectStatus().isOk() .expectHeader().contentTypeCompatibleWith(MediaType.TEXT_EVENT_STREAM) .returnResult(responseType).getResponseBody(); //@formatter:on } } ================================================ FILE: spring-boot-admin-server/src/test/java/de/codecentric/boot/admin/server/web/PathUtilsTest.java ================================================ /* * 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.server.web; import org.junit.jupiter.api.Test; import static org.assertj.core.api.Assertions.assertThat; class PathUtilsTest { @Test void normalizePath() { assertThat(PathUtils.normalizePath(null)).isNull(); assertThat(PathUtils.normalizePath("")).isEmpty(); assertThat(PathUtils.normalizePath("/")).isEmpty(); assertThat(PathUtils.normalizePath("admin")).isEqualTo("/admin"); assertThat(PathUtils.normalizePath("/admin")).isEqualTo("/admin"); assertThat(PathUtils.normalizePath("/admin/")).isEqualTo("/admin"); assertThat(PathUtils.normalizePath("/admin/")).isEqualTo("/admin"); assertThat(PathUtils.normalizePath("//admin/")).isEqualTo("/admin"); } } ================================================ FILE: spring-boot-admin-server/src/test/java/de/codecentric/boot/admin/server/web/client/BasicAuthHttpHeaderProviderTest.java ================================================ /* * 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.server.web.client; import java.util.Collections; import org.junit.jupiter.api.Test; import org.springframework.http.HttpHeaders; 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.web.client.BasicAuthHttpHeaderProvider.InstanceCredentials; import static org.assertj.core.api.Assertions.assertThat; class BasicAuthHttpHeaderProviderTest { private final BasicAuthHttpHeaderProvider headersProvider = new BasicAuthHttpHeaderProvider(); private final BasicAuthHttpHeaderProvider headersProviderEnableInstanceAuth = new BasicAuthHttpHeaderProvider( "client", "client", Collections.singletonMap("sb-admin-server", new InstanceCredentials("admin", "admin"))); @Test void test_auth_header() { Registration registration = Registration.create("foo", "https://health") .metadata("user.name", "test") .metadata("user.password", "drowssap") .build(); Instance instance = Instance.create(InstanceId.of("id")).register(registration); assertThat(this.headersProvider.getHeaders(instance).get(HttpHeaders.AUTHORIZATION)) .containsOnly("Basic dGVzdDpkcm93c3NhcA=="); } @Test void test_auth_header_with_dashes() { Registration registration = Registration.create("foo", "https://health") .metadata("user-name", "test") .metadata("user-password", "drowssap") .build(); Instance instance = Instance.create(InstanceId.of("id")).register(registration); assertThat(this.headersProvider.getHeaders(instance).get(HttpHeaders.AUTHORIZATION)) .containsOnly("Basic dGVzdDpkcm93c3NhcA=="); } @Test void test_auth_header_no_separator() { Registration registration = Registration.create("foo", "https://health") .metadata("username", "test") .metadata("userpassword", "drowssap") .build(); Instance instance = Instance.create(InstanceId.of("id")).register(registration); assertThat(this.headersProvider.getHeaders(instance).get(HttpHeaders.AUTHORIZATION)) .containsOnly("Basic dGVzdDpkcm93c3NhcA=="); } @Test void test_no_header() { Registration registration = Registration.create("foo", "https://health").build(); Instance instance = Instance.create(InstanceId.of("id")).register(registration); assertThat(this.headersProvider.getHeaders(instance).toSingleValueMap()).isEmpty(); } @Test void test_auth_instance_enabled_use_default_creds() { Registration registration = Registration.create("foo", "https://health").name("xyz-server").build(); Instance instance = Instance.create(InstanceId.of("id")).register(registration); assertThat(this.headersProviderEnableInstanceAuth.getHeaders(instance).get(HttpHeaders.AUTHORIZATION)) .containsOnly("Basic Y2xpZW50OmNsaWVudA=="); } @Test void test_auth_instance_enabled_use_service_specific_creds() { Registration registration = Registration.create("foo", "https://health").name("sb-admin-server").build(); Instance instance = Instance.create(InstanceId.of("id")).register(registration); assertThat(this.headersProviderEnableInstanceAuth.getHeaders(instance).get(HttpHeaders.AUTHORIZATION)) .containsOnly("Basic YWRtaW46YWRtaW4="); } @Test void test_auth_instance_enabled_use_metadata_over_props() { Registration registration = Registration.create("foo", "https://health") .metadata("username", "test") .metadata("userpassword", "drowssap") .name("xyz-server") .build(); Instance instance = Instance.create(InstanceId.of("id")).register(registration); assertThat(this.headersProviderEnableInstanceAuth.getHeaders(instance).get(HttpHeaders.AUTHORIZATION)) .containsOnly("Basic dGVzdDpkcm93c3NhcA=="); } } ================================================ FILE: spring-boot-admin-server/src/test/java/de/codecentric/boot/admin/server/web/client/CloudFoundryHttpHeaderProviderTest.java ================================================ /* * 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.server.web.client; 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 static org.assertj.core.api.Assertions.assertThat; class CloudFoundryHttpHeaderProviderTest { private final CloudFoundryHttpHeaderProvider headersProvider = new CloudFoundryHttpHeaderProvider(); @Test void test_cloud_foundry_header() { Registration registration = Registration.create("foo", "https://health") .metadata("applicationId", "549e64cf-a478-423d-9d6d-02d803a028a8") .metadata("instanceId", "0") .build(); Instance instance = Instance.create(InstanceId.of("id")).register(registration); assertThat(headersProvider.getHeaders(instance).get("X-CF-APP-INSTANCE")) .containsOnly("549e64cf-a478-423d-9d6d-02d803a028a8:0"); } @Test void test_no_header() { Registration registration = Registration.create("foo", "https://health").build(); Instance instance = Instance.create(InstanceId.of("id")).register(registration); assertThat(headersProvider.getHeaders(instance).toSingleValueMap()).isEmpty(); } } ================================================ FILE: spring-boot-admin-server/src/test/java/de/codecentric/boot/admin/server/web/client/CompositeHttpHeadersProviderTest.java ================================================ /* * 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.server.web.client; import org.junit.jupiter.api.Test; import org.springframework.http.HttpHeaders; import static java.util.Arrays.asList; import static java.util.Collections.emptyList; import static java.util.Collections.singletonList; import static org.assertj.core.api.Assertions.assertThat; class CompositeHttpHeadersProviderTest { @Test void should_return_all_headers() { HttpHeadersProvider provider = new CompositeHttpHeadersProvider(asList((i) -> { HttpHeaders headers = new HttpHeaders(); headers.set("a", "1"); headers.set("b", "2-a"); return headers; }, (i) -> { HttpHeaders headers = new HttpHeaders(); headers.set("b", "2-b"); headers.set("c", "3"); return headers; })); HttpHeaders headers = provider.getHeaders(null); assertThat(headers.asMultiValueMap()).containsEntry("a", singletonList("1")) .containsEntry("b", asList("2-a", "2-b")) .containsEntry("c", singletonList("3")); } @Test void should_return_empty_headers() { HttpHeadersProvider provider = new CompositeHttpHeadersProvider(emptyList()); HttpHeaders headers = provider.getHeaders(null); assertThat(headers.toSingleValueMap()).isEmpty(); } } ================================================ FILE: spring-boot-admin-server/src/test/java/de/codecentric/boot/admin/server/web/client/InstanceExchangeFilterFunctionsTest.java ================================================ /* * 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.server.web.client; import java.net.URI; import java.nio.charset.StandardCharsets; import java.time.Duration; import java.util.concurrent.TimeoutException; import java.util.concurrent.atomic.AtomicLong; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.mockito.ArgumentCaptor; import org.springframework.boot.actuate.endpoint.ApiVersion; import org.springframework.core.io.buffer.DataBuffer; import org.springframework.core.io.buffer.DefaultDataBufferFactory; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; import org.springframework.web.reactive.function.BodyExtractors; import org.springframework.web.reactive.function.client.ClientRequest; import org.springframework.web.reactive.function.client.ClientResponse; import org.springframework.web.reactive.function.client.ExchangeFunction; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import reactor.test.StepVerifier; 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.domain.values.InstanceId; import de.codecentric.boot.admin.server.domain.values.Registration; import de.codecentric.boot.admin.server.web.client.cookies.PerInstanceCookieStore; import de.codecentric.boot.admin.server.web.client.exception.ResolveEndpointException; import static de.codecentric.boot.admin.server.web.client.InstanceExchangeFilterFunctions.ATTRIBUTE_ENDPOINT; import static de.codecentric.boot.admin.server.web.client.InstanceWebClient.ATTRIBUTE_INSTANCE; import static java.util.Collections.emptyMap; import static java.util.Collections.singletonList; import static java.util.Collections.singletonMap; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import static org.springframework.http.HttpHeaders.ACCEPT; import static org.springframework.http.HttpHeaders.CONTENT_LENGTH; import static org.springframework.http.HttpHeaders.CONTENT_TYPE; import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE; import static org.springframework.http.MediaType.TEXT_PLAIN_VALUE; class InstanceExchangeFilterFunctionsTest { private static final Instance INSTANCE = Instance.create(InstanceId.of("i")); @Nested class ConvertLegacyEndpoints { private final DefaultDataBufferFactory bufferFactory = new DefaultDataBufferFactory(); private final DataBuffer original = this.bufferFactory.wrap("ORIGINAL".getBytes(StandardCharsets.UTF_8)); private final DataBuffer converted = this.bufferFactory.wrap("CONVERTED".getBytes(StandardCharsets.UTF_8)); private final InstanceExchangeFilterFunction filter = InstanceExchangeFilterFunctions.convertLegacyEndpoints( singletonList(new LegacyEndpointConverter("test", (from) -> Flux.just(this.converted)) { })); @Test void should_convert_v1_actuator() { ClientRequest request = ClientRequest.create(HttpMethod.GET, URI.create("/test")) .attribute(ATTRIBUTE_ENDPOINT, "test") .build(); ClientResponse legacyResponse = ClientResponse.create(HttpStatus.OK) .header(CONTENT_TYPE, InstanceExchangeFilterFunctions.V1_ACTUATOR_JSON.toString()) .header(CONTENT_LENGTH, Integer.toString(this.original.readableByteCount())) .body(Flux.just(this.original)) .build(); Mono response = this.filter.filter(INSTANCE, request, (r) -> Mono.just(legacyResponse)); StepVerifier.create(response).assertNext((r) -> { assertThat(r.headers().contentType()).hasValue(new MediaType(ApiVersion.LATEST.getProducedMimeType())); assertThat(r.headers().contentLength()).isEmpty(); StepVerifier.create(r.body(BodyExtractors.toDataBuffers())).expectNext(this.converted).verifyComplete(); }).verifyComplete(); } @Test void should_convert_json() { ClientRequest request = ClientRequest.create(HttpMethod.GET, URI.create("/test")) .attribute(ATTRIBUTE_ENDPOINT, "test") .build(); ClientResponse legacyResponse = ClientResponse.create(HttpStatus.OK) .header(CONTENT_TYPE, APPLICATION_JSON_VALUE) .header(CONTENT_LENGTH, Integer.toString(this.original.readableByteCount())) .body(Flux.just(this.original)) .build(); Mono response = this.filter.filter(INSTANCE, request, (r) -> Mono.just(legacyResponse)); StepVerifier.create(response).assertNext((r) -> { assertThat(r.headers().contentType()).hasValue(new MediaType(ApiVersion.LATEST.getProducedMimeType())); assertThat(r.headers().contentLength()).isEmpty(); StepVerifier.create(r.body(BodyExtractors.toDataBuffers())).expectNext(this.converted).verifyComplete(); }).verifyComplete(); } @Test void should_not_convert_v2_actuator() { InstanceExchangeFilterFunction filter = InstanceExchangeFilterFunctions.convertLegacyEndpoints( singletonList(new LegacyEndpointConverter("test", (from) -> Flux.just(this.converted)) { })); ClientRequest request = ClientRequest.create(HttpMethod.GET, URI.create("/test")) .attribute(ATTRIBUTE_ENDPOINT, "test") .build(); ClientResponse response = ClientResponse.create(HttpStatus.OK) .header(CONTENT_TYPE, ApiVersion.LATEST.getProducedMimeType().toString()) .header(CONTENT_LENGTH, Integer.toString(this.original.readableByteCount())) .body(Flux.just(this.original)) .build(); Mono convertedResponse = filter.filter(INSTANCE, request, (r) -> Mono.just(response)); StepVerifier.create(convertedResponse).assertNext((r) -> { assertThat(r.headers().contentType()).hasValue(new MediaType(ApiVersion.LATEST.getProducedMimeType())); assertThat(r.headers().contentLength()).hasValue(this.original.readableByteCount()); StepVerifier.create(r.body(BodyExtractors.toDataBuffers())).expectNext(this.original).verifyComplete(); }).verifyComplete(); } @Test void should_not_convert_unknown_endpoint() { InstanceExchangeFilterFunction filter = InstanceExchangeFilterFunctions.convertLegacyEndpoints( singletonList(new LegacyEndpointConverter("test", (from) -> Flux.just(this.converted)) { })); ClientRequest request = ClientRequest.create(HttpMethod.GET, URI.create("/test")).build(); ClientResponse response = ClientResponse.create(HttpStatus.OK) .header(CONTENT_TYPE, APPLICATION_JSON_VALUE) .header(CONTENT_LENGTH, Integer.toString(this.original.readableByteCount())) .body(Flux.just(this.original)) .build(); Mono convertedResponse = filter.filter(INSTANCE, request, (r) -> Mono.just(response)); StepVerifier.create(convertedResponse).assertNext((r) -> { assertThat(r.headers().contentType()).hasValue(MediaType.APPLICATION_JSON); assertThat(r.headers().contentLength()).hasValue(this.original.readableByteCount()); StepVerifier.create(r.body(BodyExtractors.toDataBuffers())).expectNext(this.original).verifyComplete(); }).verifyComplete(); } @Test void should_not_convert_without_converter() { InstanceExchangeFilterFunction filter = InstanceExchangeFilterFunctions.convertLegacyEndpoints( singletonList(new LegacyEndpointConverter("test", (from) -> Flux.just(this.converted)) { })); ClientRequest request = ClientRequest.create(HttpMethod.GET, URI.create("/unknown")) .attribute(ATTRIBUTE_ENDPOINT, "unknown") .build(); ClientResponse response = ClientResponse.create(HttpStatus.OK) .header(CONTENT_TYPE, APPLICATION_JSON_VALUE) .header(CONTENT_LENGTH, Integer.toString(this.original.readableByteCount())) .body(Flux.just(this.original)) .build(); Mono convertedResponse = filter.filter(INSTANCE, request, (r) -> Mono.just(response)); StepVerifier.create(convertedResponse).assertNext((r) -> { assertThat(r.headers().contentType()).hasValue(MediaType.APPLICATION_JSON); assertThat(r.headers().contentLength()).hasValue(this.original.readableByteCount()); StepVerifier.create(r.body(BodyExtractors.toDataBuffers())).expectNext(this.original).verifyComplete(); }).verifyComplete(); } } @Nested class Retry { @Test void should_retry_using_default() { InstanceExchangeFilterFunction filter = InstanceExchangeFilterFunctions.retry(1, emptyMap()); ClientRequest request = ClientRequest.create(HttpMethod.GET, URI.create("/test")).build(); ClientResponse response = ClientResponse.create(HttpStatus.OK).build(); AtomicLong invocationCount = new AtomicLong(0L); ExchangeFunction exchange = (r) -> Mono.fromSupplier(() -> { if (invocationCount.getAndIncrement() == 0) { throw new IllegalStateException("Test"); } return response; }); StepVerifier.create(filter.filter(INSTANCE, request, exchange)).expectNext(response).verifyComplete(); assertThat(invocationCount.get()).isEqualTo(2); } @Test void should_retry_using_endpoint_value_default() { InstanceExchangeFilterFunction filter = InstanceExchangeFilterFunctions.retry(0, singletonMap("test", 1)); ClientRequest request = ClientRequest.create(HttpMethod.GET, URI.create("/test")) .attribute(ATTRIBUTE_ENDPOINT, "test") .build(); ClientResponse response = ClientResponse.create(HttpStatus.OK).build(); AtomicLong invocationCount = new AtomicLong(0L); ExchangeFunction exchange = (r) -> Mono.fromSupplier(() -> { if (invocationCount.getAndIncrement() == 0) { throw new IllegalStateException("Test"); } return response; }); StepVerifier.create(filter.filter(INSTANCE, request, exchange)).expectNext(response).verifyComplete(); assertThat(invocationCount.get()).isEqualTo(2); } @Test void should_not_retry_for_put_post_patch_delete() { InstanceExchangeFilterFunction filter = InstanceExchangeFilterFunctions.retry(1, emptyMap()); AtomicLong invocationCount = new AtomicLong(0L); ExchangeFunction exchange = (r) -> Mono.fromSupplier(() -> { invocationCount.incrementAndGet(); throw new IllegalStateException("Test"); }); ClientRequest patchRequest = ClientRequest.create(HttpMethod.PATCH, URI.create("/test")).build(); StepVerifier.create(filter.filter(INSTANCE, patchRequest, exchange)) .verifyError(IllegalStateException.class); assertThat(invocationCount.get()).isEqualTo(1); invocationCount.set(0L); ClientRequest putRequest = ClientRequest.create(HttpMethod.PUT, URI.create("/test")).build(); StepVerifier.create(filter.filter(INSTANCE, putRequest, exchange)).verifyError(IllegalStateException.class); assertThat(invocationCount.get()).isEqualTo(1); invocationCount.set(0L); ClientRequest postRequest = ClientRequest.create(HttpMethod.POST, URI.create("/test")).build(); StepVerifier.create(filter.filter(INSTANCE, postRequest, exchange)) .verifyError(IllegalStateException.class); assertThat(invocationCount.get()).isEqualTo(1); invocationCount.set(0L); ClientRequest deleteRequest = ClientRequest.create(HttpMethod.DELETE, URI.create("/test")).build(); StepVerifier.create(filter.filter(INSTANCE, deleteRequest, exchange)) .verifyError(IllegalStateException.class); assertThat(invocationCount.get()).isEqualTo(1); } } @Nested class AddHeaders { private final InstanceExchangeFilterFunction filter = InstanceExchangeFilterFunctions.addHeaders((i) -> { HttpHeaders headers = new HttpHeaders(); headers.add("X-INSTANCE-ID", i.getId().getValue()); return headers; }); @Test void should_add_headers_from_provider() { ClientRequest request = ClientRequest.create(HttpMethod.GET, URI.create("/test")) .attribute(ATTRIBUTE_INSTANCE, INSTANCE) .build(); Mono response = this.filter.filter(INSTANCE, request, (req) -> { assertThat(req.headers().get("X-INSTANCE-ID")).containsExactly(INSTANCE.getId().getValue()); return Mono.just(ClientResponse.create(HttpStatus.OK).build()); }); StepVerifier.create(response).expectNextCount(1).verifyComplete(); } } @Nested class AddHeadersReactive { @Test void should_add_headers_from_provider() { InstanceExchangeFilterFunction filter = InstanceExchangeFilterFunctions.addHeadersReactive((i) -> { HttpHeaders headers = new HttpHeaders(); headers.add("X-INSTANCE-ID", i.getId().getValue()); return Mono.just(headers); }); ClientRequest request = ClientRequest.create(HttpMethod.GET, URI.create("/test")) .attribute(ATTRIBUTE_INSTANCE, INSTANCE) .build(); Mono response = filter.filter(INSTANCE, request, (req) -> { assertThat(req.headers().get("X-INSTANCE-ID")).containsExactly(INSTANCE.getId().getValue()); return Mono.just(ClientResponse.create(HttpStatus.OK).build()); }); StepVerifier.create(response).expectNextCount(1).verifyComplete(); } @Test void should_pass_on_mono_empty() { InstanceExchangeFilterFunction filter = InstanceExchangeFilterFunctions .addHeadersReactive((i) -> Mono.empty()); ClientRequest request = ClientRequest.create(HttpMethod.GET, URI.create("/test")) .attribute(ATTRIBUTE_INSTANCE, INSTANCE) .build(); Mono response = filter.filter(INSTANCE, request, (req) -> { assertThat(req.headers().size()).isEqualTo(0); return Mono.just(ClientResponse.create(HttpStatus.OK).build()); }); StepVerifier.create(response).expectNextCount(1).verifyComplete(); } } @Nested class AddDefaultHeaders { private final InstanceExchangeFilterFunction filter = InstanceExchangeFilterFunctions.setDefaultAcceptHeader(); @Test void should_add_default_accept_headers() { ClientRequest request = ClientRequest.create(HttpMethod.GET, URI.create("/test")).build(); Mono response = this.filter.filter(INSTANCE, request, (req) -> { assertThat(req.headers().getAccept()).containsExactly( new MediaType(ApiVersion.V3.getProducedMimeType()), new MediaType(ApiVersion.V2.getProducedMimeType()), InstanceExchangeFilterFunctions.V1_ACTUATOR_JSON, MediaType.APPLICATION_JSON); return Mono.just(ClientResponse.create(HttpStatus.OK).build()); }); StepVerifier.create(response).expectNextCount(1).verifyComplete(); } @Test void should_not_add_default_accept_headers() { ClientRequest request = ClientRequest.create(HttpMethod.GET, URI.create("/test")) .header(HttpHeaders.ACCEPT, MediaType.APPLICATION_XML_VALUE) .build(); Mono response = this.filter.filter(INSTANCE, request, (req) -> { assertThat(req.headers().getAccept()).containsExactly(MediaType.APPLICATION_XML); return Mono.just(ClientResponse.create(HttpStatus.OK).build()); }); StepVerifier.create(response).expectNextCount(1).verifyComplete(); } @Test void should_add_default_logfile_accept_headers() { ClientRequest request = ClientRequest.create(HttpMethod.GET, URI.create("/test")) .attribute(ATTRIBUTE_ENDPOINT, Endpoint.LOGFILE) .build(); Mono response = this.filter.filter(INSTANCE, request, (req) -> { assertThat(req.headers().getAccept()).containsExactly(MediaType.TEXT_PLAIN); return Mono.just(ClientResponse.create(HttpStatus.OK).build()); }); StepVerifier.create(response).expectNextCount(1).verifyComplete(); } } @Nested class RewriteEndpointUrl { private final InstanceExchangeFilterFunction filter = InstanceExchangeFilterFunctions.rewriteEndpointUrl(); private final Registration registration = Registration.create("R", "http://test/actuator/health") .managementUrl("http://test/actuator") .build(); private final Endpoints endpoints = Endpoints.single(Endpoint.ENV, "http://test/actuator/env"); private final Instance instance = Instance.create(InstanceId.of("R")) .register(this.registration) .withEndpoints(this.endpoints); @Test void should_rewrite_url_and_add_endpoint_attribute() { ClientRequest request = ClientRequest.create(HttpMethod.GET, URI.create("health/database")) .attribute(ATTRIBUTE_INSTANCE, this.instance) .build(); Mono response = this.filter.filter(this.instance, request, (req) -> { assertThat(req.url()).isEqualTo(URI.create(this.registration.getHealthUrl() + "/database")); assertThat(req.attribute(ATTRIBUTE_ENDPOINT)).hasValue(Endpoint.HEALTH); return Mono.just(ClientResponse.create(HttpStatus.OK).build()); }); StepVerifier.create(response).expectNextCount(1).verifyComplete(); } @Test void should_not_rewrite_absolute_url() { ClientRequest request = ClientRequest.create(HttpMethod.GET, URI.create("http://test/actuator/unknown")) .attribute(ATTRIBUTE_INSTANCE, this.instance) .build(); Mono response = this.filter.filter(this.instance, request, (req) -> { assertThat(req.url()).isEqualTo(URI.create("http://test/actuator/unknown")); assertThat(req.attribute(ATTRIBUTE_ENDPOINT)).isEmpty(); return Mono.just(ClientResponse.create(HttpStatus.OK).build()); }); StepVerifier.create(response).expectNextCount(1).verifyComplete(); } @Test void should_set_endpoint_attribute_for_management_url() { ClientRequest request = ClientRequest.create(HttpMethod.GET, URI.create("http://test/actuator")) .attribute(ATTRIBUTE_INSTANCE, this.instance) .build(); Mono response = this.filter.filter(this.instance, request, (req) -> { assertThat(req.attribute(ATTRIBUTE_ENDPOINT)).hasValue(Endpoint.ACTUATOR_INDEX); return Mono.just(ClientResponse.create(HttpStatus.OK).build()); }); StepVerifier.create(response).expectNextCount(1).verifyComplete(); } @Test void should_error_on_unspecified_endpoint() { ClientRequest request = ClientRequest.create(HttpMethod.GET, URI.create("")) .attribute(ATTRIBUTE_INSTANCE, this.instance) .build(); Mono response = this.filter.filter(this.instance, request, (req) -> Mono.just(ClientResponse.create(HttpStatus.OK).build())); StepVerifier.create(response) .verifyErrorSatisfies((e) -> assertThat(e).isInstanceOf(ResolveEndpointException.class) .hasMessage("No endpoint specified")); } @Test void should_error_on_unknown_endpoint() { ClientRequest request = ClientRequest.create(HttpMethod.GET, URI.create("unknown")) .attribute(ATTRIBUTE_INSTANCE, this.instance) .build(); Mono response = this.filter.filter(this.instance, request, (req) -> Mono.just(ClientResponse.create(HttpStatus.OK).build())); StepVerifier.create(response) .verifyErrorSatisfies((e) -> assertThat(e).isInstanceOf(ResolveEndpointException.class) .hasMessage("Endpoint 'unknown' not found")); } } @Nested class Timeout { @Test void should_timeout_using_default() { InstanceExchangeFilterFunction filter = InstanceExchangeFilterFunctions.timeout(Duration.ofSeconds(1), emptyMap()); ClientRequest request = ClientRequest.create(HttpMethod.GET, URI.create("/test")).build(); Mono response = filter.filter(INSTANCE, request, (req) -> Mono.just(ClientResponse.create(HttpStatus.OK).build()) .delayElement(Duration.ofSeconds(10))); StepVerifier.create(response).expectError(TimeoutException.class).verify(Duration.ofSeconds(2)); } @Test void should_timeout_using_endpoint_value_default() { InstanceExchangeFilterFunction filter = InstanceExchangeFilterFunctions.timeout(Duration.ofSeconds(10), singletonMap("test", Duration.ofSeconds(1))); ClientRequest request = ClientRequest.create(HttpMethod.GET, URI.create("/test")) .attribute(ATTRIBUTE_ENDPOINT, "test") .build(); Mono response = filter.filter(INSTANCE, request, (req) -> Mono.just(ClientResponse.create(HttpStatus.OK).build()) .delayElement(Duration.ofSeconds(10))); StepVerifier.create(response).expectError(TimeoutException.class).verify(Duration.ofSeconds(2)); } } @Nested class LogfileAcceptWorkaround { private final InstanceExchangeFilterFunction filter = InstanceExchangeFilterFunctions.logfileAcceptWorkaround(); @Test void should_add_accept_all_to_headers_for_logfile() { ClientRequest request = ClientRequest.create(HttpMethod.GET, URI.create("/test")) .attribute(ATTRIBUTE_ENDPOINT, Endpoint.LOGFILE) .header(ACCEPT, TEXT_PLAIN_VALUE) .build(); Mono response = this.filter.filter(INSTANCE, request, (req) -> { assertThat(req.headers().getAccept()).containsExactly(MediaType.TEXT_PLAIN, MediaType.ALL); return Mono.just(ClientResponse.create(HttpStatus.OK).build()); }); StepVerifier.create(response).expectNextCount(1).verifyComplete(); } @Test void should_not_add_accept_all_to_headers_for_non_logfile() { ClientRequest request = ClientRequest.create(HttpMethod.GET, URI.create("/test")) .attribute(ATTRIBUTE_ENDPOINT, Endpoint.HTTPTRACE) .header(ACCEPT, APPLICATION_JSON_VALUE) .build(); Mono response = this.filter.filter(INSTANCE, request, (req) -> { assertThat(req.headers().getAccept()).containsExactly(MediaType.APPLICATION_JSON); return Mono.just(ClientResponse.create(HttpStatus.OK).build()); }); StepVerifier.create(response).expectNextCount(1).verifyComplete(); } } @Nested public class CookieHandling { private PerInstanceCookieStore cookieStore; private InstanceExchangeFilterFunction filter; @BeforeEach public void setUp() { this.cookieStore = mock(PerInstanceCookieStore.class); this.filter = InstanceExchangeFilterFunctions.handleCookies(cookieStore); } @Test void should_store_retrieved_cookie() { ClientRequest request = ClientRequest.create(HttpMethod.GET, URI.create("http://localhost/test")).build(); Mono response = this.filter.filter(INSTANCE, request, (req) -> Mono .just(ClientResponse.create(HttpStatus.OK).header("Set-Cookie", "testCookie=testCookieValue").build())); StepVerifier.create(response).expectNextCount(1).verifyComplete(); @SuppressWarnings("unchecked") ArgumentCaptor> captor = ArgumentCaptor.forClass(MultiValueMap.class); verify(this.cookieStore).put(eq(INSTANCE.getId()), eq(request.url()), captor.capture()); assertThat(captor.getValue()).containsEntry("Set-Cookie", singletonList("testCookie=testCookieValue")); } @Test void should_add_stored_cookie_to_request() { ClientRequest request = ClientRequest.create(HttpMethod.GET, URI.create("http://localhost/test")).build(); MultiValueMap cookieMap = new LinkedMultiValueMap<>(); cookieMap.add("testCookie", "testCookieValue"); when(this.cookieStore.get(eq(INSTANCE.getId()), eq(request.url()), any())).thenReturn(cookieMap); Mono response = this.filter.filter(INSTANCE, request, (req) -> { assertThat(req.cookies()).containsEntry("testCookie", singletonList("testCookieValue")); return Mono.just(ClientResponse.create(HttpStatus.OK).build()); }); StepVerifier.create(response).expectNextCount(1).verifyComplete(); } } } ================================================ FILE: spring-boot-admin-server/src/test/java/de/codecentric/boot/admin/server/web/client/InstanceWebClientTest.java ================================================ /* * 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.server.web.client; import org.junit.jupiter.api.Test; import org.springframework.http.HttpStatus; import org.springframework.web.reactive.function.client.ClientResponse; import reactor.core.publisher.Mono; import reactor.test.StepVerifier; import de.codecentric.boot.admin.server.domain.entities.Instance; import de.codecentric.boot.admin.server.domain.values.InstanceId; import de.codecentric.boot.admin.server.web.client.exception.ResolveInstanceException; import static de.codecentric.boot.admin.server.web.client.InstanceWebClient.ATTRIBUTE_INSTANCE; import static org.assertj.core.api.Assertions.assertThat; class InstanceWebClientTest { @Test void should_error_without_instance() { Mono response = InstanceWebClient.builder() .build() .instance(Mono.empty()) .get() .uri("health") .exchangeToMono((r) -> Mono.empty()); StepVerifier.create(response) .verifyErrorSatisfies((ex) -> assertThat(ex).isInstanceOf(ResolveInstanceException.class) .hasMessageContaining("Could not resolve Instance")); } @Test void should_add_instance_attribute() { Instance instance = Instance.create(InstanceId.of("i")); Mono response = InstanceWebClient.builder().filter((inst, req, next) -> { assertThat(req.attribute(ATTRIBUTE_INSTANCE)).hasValue(instance); assertThat(inst).isEqualTo(instance); return Mono.just(ClientResponse.create(HttpStatus.OK).build()); }).build().instance(Mono.just(instance)).get().uri("http://test/health").exchangeToMono(Mono::just); StepVerifier.create(response) .assertNext((r) -> assertThat(r.statusCode()).isEqualTo(HttpStatus.OK)) .verifyComplete(); } } ================================================ FILE: spring-boot-admin-server/src/test/java/de/codecentric/boot/admin/server/web/client/LegacyEndpointConvertersTest.java ================================================ /* * 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.server.web.client; import java.util.Map; import java.util.stream.Stream; import org.assertj.core.api.WithAssertions; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; import org.springframework.core.ParameterizedTypeReference; import org.springframework.core.ResolvableType; import org.springframework.core.io.buffer.DataBuffer; import org.springframework.core.io.buffer.DataBufferFactory; import org.springframework.core.io.buffer.DataBufferUtils; import org.springframework.core.io.buffer.DefaultDataBufferFactory; import org.springframework.http.codec.json.JacksonJsonDecoder; import reactor.core.publisher.Flux; import reactor.test.StepVerifier; public class LegacyEndpointConvertersTest implements WithAssertions { private final DataBufferFactory bufferFactory = new DefaultDataBufferFactory(); private final JacksonJsonDecoder decoder = new JacksonJsonDecoder(); private final ResolvableType type = ResolvableType.forType(new ParameterizedTypeReference>() { }); public static Stream methodSignatureToExpectedMap() { return Stream.of( Arguments.of("public java.lang.Object bar.Handler.handle(java.util.List)", Map.of("className", "bar.Handler", "descriptor", "(Ljava/util/List;)Ljava/lang/Object;", "name", "handle")), Arguments.of("public SomeBean bar.Handler.handle(java.util.List)", Map.of("className", "bar.Handler", "descriptor", "(Ljava/util/List;)LSomeBean;", "name", "handle")), Arguments.of("public synchronized SomeBean bar.Handler.handle(java.util.List)", Map.of("className", "bar.Handler", "descriptor", "(Ljava/util/List;)LSomeBean;", "name", "handle"))); } @Test void should_convert_health() { LegacyEndpointConverter converter = LegacyEndpointConverters.health(); assertThat(converter.canConvert("health")).isTrue(); assertThat(converter.canConvert("foo")).isFalse(); Flux legacyInput = this.read("health-legacy.json"); Flux converted = converter.convert(legacyInput).transform(this::unmarshal); Flux expected = this.read("health-expected.json").transform(this::unmarshal); StepVerifier.create(Flux.zip(converted, expected)) .assertNext((t) -> assertThat(t.getT1()).isEqualTo(t.getT2())) .verifyComplete(); } @Test void should_convert_env() { LegacyEndpointConverter converter = LegacyEndpointConverters.env(); assertThat(converter.canConvert("env")).isTrue(); assertThat(converter.canConvert("foo")).isFalse(); Flux legacyInput = this.read("env-legacy.json"); Flux converted = converter.convert(legacyInput).transform(this::unmarshal); Flux expected = this.read("env-expected.json").transform(this::unmarshal); StepVerifier.create(Flux.zip(converted, expected)) .assertNext((t) -> assertThat(t.getT1()).isEqualTo(t.getT2())) .verifyComplete(); } @Test void should_convert_trace() { LegacyEndpointConverter converter = LegacyEndpointConverters.httptrace(); assertThat(converter.canConvert("httptrace")).isTrue(); assertThat(converter.canConvert("foo")).isFalse(); Flux legacyInput = this.read("httptrace-legacy.json"); Flux converted = converter.convert(legacyInput).transform(this::unmarshal); Flux expected = this.read("httptrace-expected.json").transform(this::unmarshal); StepVerifier.create(Flux.zip(converted, expected)) .assertNext((t) -> assertThat(t.getT1()).isEqualTo(t.getT2())) .verifyComplete(); } @Test void should_convert_threaddump() { LegacyEndpointConverter converter = LegacyEndpointConverters.threaddump(); assertThat(converter.canConvert("threaddump")).isTrue(); assertThat(converter.canConvert("foo")).isFalse(); Flux legacyInput = this.read("threaddump-legacy.json"); Flux converted = converter.convert(legacyInput).transform(this::unmarshal); Flux expected = this.read("threaddump-expected.json").transform(this::unmarshal); StepVerifier.create(Flux.zip(converted, expected)) .assertNext((t) -> assertThat(t.getT1()).isEqualTo(t.getT2())) .verifyComplete(); } @Test void should_convert_liquibase() { LegacyEndpointConverter converter = LegacyEndpointConverters.liquibase(); assertThat(converter.canConvert("liquibase")).isTrue(); assertThat(converter.canConvert("foo")).isFalse(); Flux legacyInput = this.read("liquibase-legacy.json"); Flux converted = converter.convert(legacyInput).transform(this::unmarshal); Flux expected = this.read("liquibase-expected.json").transform(this::unmarshal); StepVerifier.create(Flux.zip(converted, expected)) .assertNext((t) -> assertThat(t.getT1()).isEqualTo(t.getT2())) .verifyComplete(); } @Test void should_convert_flyway() { LegacyEndpointConverter converter = LegacyEndpointConverters.flyway(); assertThat(converter.canConvert("flyway")).isTrue(); assertThat(converter.canConvert("foo")).isFalse(); Flux legacyInput = this.read("flyway-legacy.json"); Flux converted = converter.convert(legacyInput).transform(this::unmarshal); Flux expected = this.read("flyway-expected.json").transform(this::unmarshal); StepVerifier.create(Flux.zip(converted, expected)) .assertNext((t) -> assertThat(t.getT1()).isEqualTo(t.getT2())) .verifyComplete(); } @Test void should_convert_beans() { LegacyEndpointConverter converter = LegacyEndpointConverters.beans(); assertThat(converter.canConvert("beans")).isTrue(); assertThat(converter.canConvert("foo")).isFalse(); Flux legacyInput = this.read("beans-legacy.json"); Flux converted = converter.convert(legacyInput).transform(this::unmarshal); Flux expected = this.read("beans-expected.json").transform(this::unmarshal); StepVerifier.create(Flux.zip(converted, expected)) .assertNext((t) -> assertThat(t.getT1()).isEqualTo(t.getT2())) .verifyComplete(); } @Test void should_convert_configprops() { LegacyEndpointConverter converter = LegacyEndpointConverters.configprops(); assertThat(converter.canConvert("configprops")).isTrue(); assertThat(converter.canConvert("foo")).isFalse(); Flux legacyInput = this.read("configprops-legacy.json"); Flux converted = converter.convert(legacyInput).transform(this::unmarshal); Flux expected = this.read("configprops-expected.json").transform(this::unmarshal); StepVerifier.create(Flux.zip(converted, expected)) .assertNext((t) -> assertThat(t.getT1()).isEqualTo(t.getT2())) .verifyComplete(); } @Test void should_convert_mappings() { LegacyEndpointConverter converter = LegacyEndpointConverters.mappings(); assertThat(converter.canConvert("mappings")).isTrue(); assertThat(converter.canConvert("foo")).isFalse(); Flux legacyInput = this.read("mappings-legacy.json"); Flux converted = converter.convert(legacyInput).transform(this::unmarshal); Flux expected = this.read("mappings-expected.json").transform(this::unmarshal); StepVerifier.create(Flux.zip(converted, expected)) .assertNext((t) -> assertThat(t.getT1()).isEqualTo(t.getT2())) .verifyComplete(); } /* * see Bugticket #2107 */ @ParameterizedTest @MethodSource("methodSignatureToExpectedMap") void convertMappingHandlerMethod__should_map_method_signature_to_Handler_method_description_map( String methodDeclaration, Map expectedHandlerDescriptionMap) { Map convertMappingHandlerMethodMap = LegacyEndpointConverters .convertMappingHandlerMethod(methodDeclaration); assertThat(convertMappingHandlerMethodMap).isEqualTo(expectedHandlerDescriptionMap); } private Flux unmarshal(Flux buffer) { return decoder.decode(buffer, type, null, null); } private Flux read(String resourceName) { return DataBufferUtils.readInputStream( () -> LegacyEndpointConvertersTest.class.getResourceAsStream(resourceName), bufferFactory, 10); } } ================================================ FILE: spring-boot-admin-server/src/test/java/de/codecentric/boot/admin/server/web/client/cookies/CookieStoreCleanupTriggerTest.java ================================================ /* * 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.server.web.client.cookies; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import reactor.test.publisher.TestPublisher; import de.codecentric.boot.admin.server.domain.entities.Instance; import de.codecentric.boot.admin.server.domain.events.InstanceDeregisteredEvent; import de.codecentric.boot.admin.server.domain.events.InstanceEvent; import de.codecentric.boot.admin.server.domain.values.InstanceId; import static org.awaitility.Awaitility.await; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; class CookieStoreCleanupTriggerTest { private static final Instance INSTANCE = Instance.create(InstanceId.of("i")); private final TestPublisher events = TestPublisher.create(); private PerInstanceCookieStore cookieStore; @BeforeEach void setUp() { cookieStore = mock(PerInstanceCookieStore.class); CookieStoreCleanupTrigger trigger = new CookieStoreCleanupTrigger(this.events.flux(), cookieStore); trigger.start(); await().until(this.events::wasSubscribed); } @Test void deregister_event_should_trigger_cleanup_cookie_store() { this.events.next(new InstanceDeregisteredEvent(INSTANCE.getId(), 42L)); verify(cookieStore).cleanupInstance(INSTANCE.getId()); } } ================================================ FILE: spring-boot-admin-server/src/test/java/de/codecentric/boot/admin/server/web/client/cookies/JdkPerInstanceCookieStoreTest.java ================================================ /* * 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.server.web.client.cookies; import java.io.IOException; import java.net.CookieHandler; import java.net.URI; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; import de.codecentric.boot.admin.server.domain.values.InstanceId; import static java.util.Collections.singletonList; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; class JdkPerInstanceCookieStoreTest { private static final InstanceId INSTANCE_ID = InstanceId.of("i"); private CookieHandler cookieHandler; private JdkPerInstanceCookieStore store; @BeforeEach void setUp() { store = spy(new JdkPerInstanceCookieStore()); cookieHandler = mock(CookieHandler.class); when(store.createCookieHandler(INSTANCE_ID)).thenReturn(cookieHandler); } @Test void cookies_should_be_fetched_and_converted_from_store() throws IOException { MultiValueMap storeMap = new LinkedMultiValueMap<>(); storeMap.add("Cookie", "name=value"); storeMap.add("Cookie", "name2=tricky=value"); final URI uri = URI.create("http://localhost/test"); when(cookieHandler.get(eq(uri), any())).thenReturn(storeMap); final MultiValueMap cookieMap = store.get(INSTANCE_ID, uri, new LinkedMultiValueMap<>()); assertThat(cookieMap).containsEntry("name", singletonList("value")) .containsEntry("name2", singletonList("tricky=value")); } @Test void same_handler_should_be_used_for_same_instance_id_on_different_uri() throws IOException { MultiValueMap storeMap = new LinkedMultiValueMap<>(); final URI uri = URI.create("http://localhost/test"); final URI uri2 = URI.create("http://localhost/test2"); store.get(INSTANCE_ID, uri, storeMap); store.get(INSTANCE_ID, uri2, storeMap); verify(cookieHandler).get(uri, storeMap); verify(cookieHandler).get(uri2, storeMap); } @Test void different_handler_should_be_used_for_different_instance_id() throws IOException { InstanceId instanceId2 = InstanceId.of("j"); CookieHandler cookieHandler2 = mock(CookieHandler.class); when(store.createCookieHandler(instanceId2)).thenReturn(cookieHandler2); MultiValueMap storeMap = new LinkedMultiValueMap<>(); final URI uri = URI.create("http://localhost/test"); store.get(INSTANCE_ID, uri, storeMap); store.get(instanceId2, uri, storeMap); verify(cookieHandler).get(uri, storeMap); verify(cookieHandler2).get(uri, storeMap); } } ================================================ FILE: spring-boot-admin-server/src/test/java/de/codecentric/boot/admin/server/web/client/reactive/CompositeReactiveHttpHeadersProviderTest.java ================================================ /* * 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.server.web.client.reactive; import org.junit.jupiter.api.Test; import org.springframework.http.HttpHeaders; import reactor.core.publisher.Mono; import reactor.test.StepVerifier; import static java.util.Arrays.asList; import static java.util.Collections.emptyList; import static java.util.Collections.singletonList; import static org.assertj.core.api.Assertions.assertThat; class CompositeReactiveHttpHeadersProviderTest { @Test void should_return_all_headers() { ReactiveHttpHeadersProvider provider = new CompositeReactiveHttpHeadersProvider(asList((i) -> { HttpHeaders headers = new HttpHeaders(); headers.set("a", "1"); headers.set("b", "2-a"); return Mono.just(headers); }, (i) -> { HttpHeaders headers = new HttpHeaders(); headers.set("b", "2-b"); headers.set("c", "3"); return Mono.just(headers); })); StepVerifier.create(provider.getHeaders(null)).thenConsumeWhile((headers) -> { assertThat(headers.asMultiValueMap()).containsEntry("a", singletonList("1")) .containsEntry("b", asList("2-a", "2-b")) .containsEntry("c", singletonList("3")); return true; }).verifyComplete(); } @Test void should_return_empty_headers() { CompositeReactiveHttpHeadersProvider provider = new CompositeReactiveHttpHeadersProvider(emptyList()); StepVerifier.create(provider.getHeaders(null)).thenConsumeWhile((headers) -> { assertThat(headers.toSingleValueMap()).isEmpty(); return true; }).verifyComplete(); } } ================================================ FILE: spring-boot-admin-server/src/test/java/de/codecentric/boot/admin/server/web/reactive/InstancesProxyControllerIntegrationTest.java ================================================ /* * 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.server.web.reactive; import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.springframework.boot.WebApplicationType; import org.springframework.boot.builder.SpringApplicationBuilder; import org.springframework.context.ConfigurableApplicationContext; import de.codecentric.boot.admin.server.AdminReactiveApplicationTest; import de.codecentric.boot.admin.server.web.AbstractInstancesProxyControllerIntegrationTest; class InstancesProxyControllerIntegrationTest extends AbstractInstancesProxyControllerIntegrationTest { @Nullable private ConfigurableApplicationContext context; @BeforeEach void setUpClient() { context = new SpringApplicationBuilder().sources(AdminReactiveApplicationTest.TestAdminApplication.class) .web(WebApplicationType.REACTIVE) .run("--server.port=0", "--spring.boot.admin.monitor.default-timeout=2500"); super.setUpClient(context); } @AfterEach void tearDownContext() { if (context != null) { context.close(); context = null; } } } ================================================ FILE: spring-boot-admin-server/src/test/java/de/codecentric/boot/admin/server/web/servlet/InstancesProxyControllerIntegrationTest.java ================================================ /* * 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.server.web.servlet; import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.springframework.boot.WebApplicationType; import org.springframework.boot.builder.SpringApplicationBuilder; import org.springframework.context.ConfigurableApplicationContext; import de.codecentric.boot.admin.server.AdminServletApplicationTest; import de.codecentric.boot.admin.server.web.AbstractInstancesProxyControllerIntegrationTest; class InstancesProxyControllerIntegrationTest extends AbstractInstancesProxyControllerIntegrationTest { @Nullable private ConfigurableApplicationContext context; @BeforeEach void setUpClient() { context = new SpringApplicationBuilder().sources(AdminServletApplicationTest.TestAdminApplication.class) .web(WebApplicationType.SERVLET) .run("--server.port=0", "--spring.boot.admin.monitor.default-timeout=2500"); super.setUpClient(context); } @AfterEach void tearDownContext() { if (context != null) { context.close(); context = null; } } } ================================================ FILE: spring-boot-admin-server/src/test/resources/application.yml ================================================ server: port: 8080 shutdown: immediate spring: application: name: spring-boot-admin-server-test jmx: enabled: false mvc: pathmatch: matching-strategy: ant_path_matcher management: info: env: enabled: true logging: level: de.codecentric: DEBUG ================================================ FILE: spring-boot-admin-server/src/test/resources/de/codecentric/boot/admin/server/junit-platform.properties ================================================ junit.jupiter.execution.timeout.test.method.default=1m ================================================ FILE: spring-boot-admin-server/src/test/resources/de/codecentric/boot/admin/server/notify/allowed-file.html ================================================ I am fine! ================================================ FILE: spring-boot-admin-server/src/test/resources/de/codecentric/boot/admin/server/notify/custom-mail.html ================================================ [[${instance.registration.name}]] ([[(${instance.id})]]) is [[${event.statusInfo.status}]]

() status changed from to
================================================ FILE: spring-boot-admin-server/src/test/resources/de/codecentric/boot/admin/server/notify/expected-custom-mail ================================================

HELLO WORLD!

application-name (cafebabe) status changed from UNKNOWN to DOWN
http://localhost:8081/actuator/health ================================================ FILE: spring-boot-admin-server/src/test/resources/de/codecentric/boot/admin/server/notify/expected-default-mail ================================================

application-name (cafebabe) is DOWN

Instance cafebabe changed status from UNKNOWN to DOWN

Status Details

Complex Value
Nested Simple Value
99!
Simple Value
1234

Registration

Service Url http://localhost:8081/
Health Url http://localhost:8081/actuator/health
Management Url http://localhost:8081/actuator
================================================ FILE: spring-boot-admin-server/src/test/resources/de/codecentric/boot/admin/server/notify/vulnerable-file.html ================================================ ================================================ FILE: spring-boot-admin-server/src/test/resources/de/codecentric/boot/admin/server/web/client/beans-expected.json ================================================ { "contexts": { "sample-project:8080": { "beans": { "propertySourcesPlaceholderConfigurer": { "aliases": [], "bean": "propertySourcesPlaceholderConfigurer", "dependencies": [], "resource": "class path resource [org/springframework/boot/autoconfigure/context/PropertyPlaceholderAutoConfiguration.class]", "scope": "singleton", "type": "org.springframework.context.support.PropertySourcesPlaceholderConfigurer" } }, "contextName": "sample-project:8080", "parentId": null }, "sample-project:8080.child": { "beans": { "configClientProperties": { "aliases": [], "bean": "configClientProperties", "dependencies": [], "resource": "org.springframework.cloud.config.client.ConfigServiceBootstrapConfiguration", "scope": "singleton", "type": "org.springframework.cloud.config.client.ConfigClientProperties" }, "configServicePropertySource": { "aliases": [], "bean": "configServicePropertySource", "dependencies": [ "configClientProperties" ], "resource": "org.springframework.cloud.config.client.ConfigServiceBootstrapConfiguration", "scope": "singleton", "type": "org.springframework.cloud.config.client.ConfigServicePropertySourceLocator" } }, "contextName": "sample-project:8080.child", "parentId": "sample-project:8080" } } } ================================================ FILE: spring-boot-admin-server/src/test/resources/de/codecentric/boot/admin/server/web/client/beans-legacy.json ================================================ [ { "context": "sample-project:8080", "parent": null, "beans": [ { "bean": "propertySourcesPlaceholderConfigurer", "aliases": [], "scope": "singleton", "type": "org.springframework.context.support.PropertySourcesPlaceholderConfigurer", "resource": "class path resource [org/springframework/boot/autoconfigure/context/PropertyPlaceholderAutoConfiguration.class]", "dependencies": [] } ] }, { "context": "sample-project:8080", "parent": "sample-project:8080", "beans": [ { "bean": "configClientProperties", "aliases": [], "scope": "singleton", "type": "org.springframework.cloud.config.client.ConfigClientProperties", "resource": "org.springframework.cloud.config.client.ConfigServiceBootstrapConfiguration", "dependencies": [] }, { "bean": "configServicePropertySource", "aliases": [], "scope": "singleton", "type": "org.springframework.cloud.config.client.ConfigServicePropertySourceLocator", "resource": "org.springframework.cloud.config.client.ConfigServiceBootstrapConfiguration", "dependencies": [ "configClientProperties" ] } ] } ] ================================================ FILE: spring-boot-admin-server/src/test/resources/de/codecentric/boot/admin/server/web/client/configprops-expected.json ================================================ { "contexts": { "application": { "beans": { "spring.jpa-org.springframework.boot.autoconfigure.orm.jpa.JpaProperties": { "prefix": "spring.jpa", "properties": { "error": "Cannot serialize 'spring.jpa'" } }, "endpoints-org.springframework.boot.actuate.endpoint.EndpointProperties": { "prefix": "endpoints", "properties": { "enabled": true, "sensitive": null } } } }, "parentContext": { "beans": { "spring.cloud.config-org.springframework.cloud.bootstrap.config.PropertySourceBootstrapProperties": { "prefix": "spring.cloud.config", "properties": { "overrideSystemProperties": true, "overrideNone": false, "allowOverride": true } } } } } } ================================================ FILE: spring-boot-admin-server/src/test/resources/de/codecentric/boot/admin/server/web/client/configprops-legacy.json ================================================ { "spring.jpa-org.springframework.boot.autoconfigure.orm.jpa.JpaProperties": { "prefix": "spring.jpa", "properties": { "error": "Cannot serialize 'spring.jpa'" } }, "endpoints-org.springframework.boot.actuate.endpoint.EndpointProperties": { "prefix": "endpoints", "properties": { "enabled": true, "sensitive": null } }, "parent": { "spring.cloud.config-org.springframework.cloud.bootstrap.config.PropertySourceBootstrapProperties": { "prefix": "spring.cloud.config", "properties": { "overrideSystemProperties": true, "overrideNone": false, "allowOverride": true } } } } ================================================ FILE: spring-boot-admin-server/src/test/resources/de/codecentric/boot/admin/server/web/client/env-expected.json ================================================ { "activeProfiles": [ "one" ], "propertySources": [ { "name": "server.ports", "properties": { "local.server.port": { "value": 9000 } } }, { "name": "servletContextInitParams", "properties": {} }, { "name": "systemProperties", "properties": { "java.runtime.name": { "value": "OpenJDK Runtime Environment" }, "java.protocol.handler.pkgs": { "value": "org.springframework.boot.loader" }, "sun.boot.library.path": { "value": "/usr/lib/jvm/java-1.8.0-openjdk-1.8.0.121-0.b13.el7_3.x86_64/jre/lib/amd64" } } }, { "name": "class path resource [spring-boot-starter-batch-web.properties]", "properties": { "spring.batch.job.enabled": { "value": "false" } } } ] } ================================================ FILE: spring-boot-admin-server/src/test/resources/de/codecentric/boot/admin/server/web/client/env-legacy.json ================================================ { "profiles": [ "one" ], "server.ports": { "local.server.port": 9000 }, "servletContextInitParams": {}, "systemProperties": { "java.runtime.name": "OpenJDK Runtime Environment", "java.protocol.handler.pkgs": "org.springframework.boot.loader", "sun.boot.library.path": "/usr/lib/jvm/java-1.8.0-openjdk-1.8.0.121-0.b13.el7_3.x86_64/jre/lib/amd64" }, "class path resource [spring-boot-starter-batch-web.properties]": { "spring.batch.job.enabled": "false" } } ================================================ FILE: spring-boot-admin-server/src/test/resources/de/codecentric/boot/admin/server/web/client/flyway-expected.json ================================================ { "contexts": { "application": { "flywayBeans": { "flyway": { "migrations": [ { "type": "SQL", "checksum": 710039845, "version": "1", "description": "init", "script": "V1__init.sql", "state": "SUCCESS", "installedOn": "2017-12-30T11:12:18.544Z", "executionTime": 10 } ] }, "secondary": { "migrations": [ { "type": "SQL", "checksum": 710039845, "version": "1", "description": "init", "script": "V1__init.sql", "state": "SUCCESS", "installedOn": "2017-12-30T11:12:18.544Z", "executionTime": 10 } ] } } } } } ================================================ FILE: spring-boot-admin-server/src/test/resources/de/codecentric/boot/admin/server/web/client/flyway-legacy.json ================================================ [ { "name": "flyway", "migrations": [ { "type": "SQL", "checksum": 710039845, "version": "1", "description": "init", "script": "V1__init.sql", "state": "SUCCESS", "installedOn": 1514632338544, "executionTime": 10 } ] }, { "name": "secondary", "migrations": [ { "type": "SQL", "checksum": 710039845, "version": "1", "description": "init", "script": "V1__init.sql", "state": "SUCCESS", "installedOn": 1514632338544, "executionTime": 10 } ] } ] ================================================ FILE: spring-boot-admin-server/src/test/resources/de/codecentric/boot/admin/server/web/client/health-expected.json ================================================ { "status": "DOWN", "details": { "info": "Hello", "sub-1": { "status": "DOWN", "details": { "info-1": false, "sub-1-1": { "status": "UP" }, "sub-1-2": { "status": "DOWN", "details": { "info-1-2": "World" } } } }, "sub-2": { "status": "UP", "details": { "info-2": 1 } } } } ================================================ FILE: spring-boot-admin-server/src/test/resources/de/codecentric/boot/admin/server/web/client/health-legacy.json ================================================ { "status": "DOWN", "info": "Hello", "sub-1": { "status": "DOWN", "info-1": false, "sub-1-1": { "status": "UP" }, "sub-1-2": { "status": "DOWN", "info-1-2": "World" } }, "sub-2": { "status": "UP", "info-2": 1 } } ================================================ FILE: spring-boot-admin-server/src/test/resources/de/codecentric/boot/admin/server/web/client/httptrace-expected.json ================================================ { "traces": [ { "timestamp": "2018-02-04T21:58:53.427Z", "request": { "method": "HEAD", "uri": "/actuator/liquibase", "headers": { "pragma": [ "no-cache" ], "cache-control": [ "no-cache" ], "accept": [ "application/json, text/plain, */*" ], "user-agent": [ "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.132 Safari/537.36" ], "accept-language": [ "de-DE,de;q=0.9,en-US;q=0.8,en;q=0.7" ], "accept-encoding": [ "gzip" ], "host": [ "172.17.0.17:8080" ], "connection": [ "Keep-Alive" ] } }, "response": { "status": 404, "headers": { "X-Application-Context": [ "spring-application" ] } }, "timeTaken": 2 }, { "timestamp": "2018-02-19T17:34:51.207Z", "request": { "method": "GET", "uri": "", "headers": { "accept": [ "application/json, text/plain, */*" ], "user-agent": [ "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.132 Safari/537.36" ], "accept-language": [ "de-DE,de;q=0.9,en-US;q=0.8,en;q=0.7" ], "accept-encoding": [ "gzip, deflate, br" ], "x-requested-with": [ "XMLHttpRequest" ] } }, "response": { "status": 200, "headers": { "X-Application-Context": [ "spring-application" ], "Content-Type": [ "application/json;charset=UTF-8" ], "Transfer-Encoding": [ "chunked" ], "Date": [ "Sun, 04 Feb 2018 21:58:44 GMT" ] } } } ] } ================================================ FILE: spring-boot-admin-server/src/test/resources/de/codecentric/boot/admin/server/web/client/httptrace-legacy.json ================================================ [ { "timestamp": 1517781533427, "info": { "method": "HEAD", "path": "/actuator/liquibase", "headers": { "request": { "pragma": "no-cache", "cache-control": "no-cache", "accept": "application/json, text/plain, */*", "user-agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.132 Safari/537.36", "accept-language": "de-DE,de;q=0.9,en-US;q=0.8,en;q=0.7", "accept-encoding": "gzip", "host": "172.17.0.17:8080", "connection": "Keep-Alive" }, "response": { "X-Application-Context": "spring-application", "status": "404" } }, "timeTaken": "2" } }, { "timestamp": "2018-02-19T17:34:51.207+0000", "info": { "method": "GET", "path": "", "headers": { "request": { "accept": "application/json, text/plain, */*", "user-agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.132 Safari/537.36", "accept-language": "de-DE,de;q=0.9,en-US;q=0.8,en;q=0.7", "accept-encoding": "gzip, deflate, br", "x-requested-with": "XMLHttpRequest" }, "response": { "X-Application-Context": "spring-application", "Content-Type": "application/json;charset=UTF-8", "Transfer-Encoding": "chunked", "Date": "Sun, 04 Feb 2018 21:58:44 GMT", "status": "200" } } } } ] ================================================ FILE: spring-boot-admin-server/src/test/resources/de/codecentric/boot/admin/server/web/client/liquibase-expected.json ================================================ { "contexts": { "application": { "liquibaseBeans": { "liquibase": { "changeSets": [ { "author": "marceloverdijk", "changeLog": "classpath:/db/changelog/db.changelog-master.yaml", "comments": "", "contexts": [], "dateExecuted": "2017-12-29T23:05:35.890Z", "deploymentId": "4588735849", "description": "createTable tableName=person", "execType": "EXECUTED", "id": "1", "labels": [], "checksum": "7:b8f2ae9c88deabd32666dff9bc5d7f5d", "orderExecuted": 1, "tag": null }, { "author": "marceloverdijk", "changeLog": "classpath:/db/changelog/db.changelog-master.yaml", "comments": "", "contexts": [ "dev", "db2" ], "dateExecuted": "2017-12-29T23:05:35.899Z", "deploymentId": "4588735849", "description": "insert tableName=person", "execType": "EXECUTED", "id": "2", "labels": [ "hard", "core" ], "checksum": "7:a8006415097ebb8b3334a23347847322", "orderExecuted": 2, "tag": null } ] }, "secondary": { "changeSets": [ { "author": "marceloverdijk", "changeLog": "classpath:/db/changelog/db.changelog-master.yaml", "comments": "", "contexts": [], "dateExecuted": "2017-12-29T23:05:35.890Z", "deploymentId": "4588735849", "description": "createTable tableName=person", "execType": "EXECUTED", "id": "1", "labels": [], "checksum": "7:b8f2ae9c88deabd32666dff9bc5d7f5d", "orderExecuted": 1, "tag": null } ] } } } } } ================================================ FILE: spring-boot-admin-server/src/test/resources/de/codecentric/boot/admin/server/web/client/liquibase-legacy.json ================================================ [ { "name": "liquibase", "changeLogs": [ { "ID": "1", "AUTHOR": "marceloverdijk", "FILENAME": "classpath:/db/changelog/db.changelog-master.yaml", "DATEEXECUTED": 1514588735890, "ORDEREXECUTED": 1, "EXECTYPE": "EXECUTED", "MD5SUM": "7:b8f2ae9c88deabd32666dff9bc5d7f5d", "DESCRIPTION": "createTable tableName=person", "COMMENTS": "", "TAG": null, "LIQUIBASE": "3.5.3", "CONTEXTS": null, "LABELS": null, "DEPLOYMENT_ID": "4588735849" }, { "ID": "2", "AUTHOR": "marceloverdijk", "FILENAME": "classpath:/db/changelog/db.changelog-master.yaml", "DATEEXECUTED": 1514588735899, "ORDEREXECUTED": 2, "EXECTYPE": "EXECUTED", "MD5SUM": "7:a8006415097ebb8b3334a23347847322", "DESCRIPTION": "insert tableName=person", "COMMENTS": "", "TAG": null, "LIQUIBASE": "3.5.3", "CONTEXTS": "dev,db2", "LABELS": "hard,core", "DEPLOYMENT_ID": "4588735849" } ] }, { "name": "secondary", "changeLogs": [ { "ID": "1", "AUTHOR": "marceloverdijk", "FILENAME": "classpath:/db/changelog/db.changelog-master.yaml", "DATEEXECUTED": 1514588735890, "ORDEREXECUTED": 1, "EXECTYPE": "EXECUTED", "MD5SUM": "7:b8f2ae9c88deabd32666dff9bc5d7f5d", "DESCRIPTION": "createTable tableName=person", "COMMENTS": "", "TAG": null, "LIQUIBASE": "3.5.3", "CONTEXTS": null, "LABELS": null, "DEPLOYMENT_ID": "4588735849" } ] } ] ================================================ FILE: spring-boot-admin-server/src/test/resources/de/codecentric/boot/admin/server/web/client/mappings-expected.json ================================================ { "contexts": { "application": { "mappings": { "dispatcherServlets": { "dispatcherServlet": [ { "details": { "requestMappingConditions": { "consumes": [], "headers": [], "methods": [], "params": [], "patterns": [ "/**/favicon.ico" ], "produces": [] } }, "predicate": "/**/favicon.ico" }, { "details": { "handlerMethod": { "className": "org.springframework.boot.autoconfigure.web.BasicErrorController", "descriptor": "(Ljavax/servlet/http/HttpServletRequest;)Lorg/springframework/http/ResponseEntity;", "name": "error" }, "requestMappingConditions": { "consumes": [], "headers": [], "methods": [], "params": [], "patterns": [ "/error" ], "produces": [] } }, "predicate": "{[/error]}", "handler": "public org.springframework.http.ResponseEntity> org.springframework.boot.autoconfigure.web.BasicErrorController.error(javax.servlet.http.HttpServletRequest)" }, { "details": { "handlerMethod": { "className": "org.springframework.boot.actuate.endpoint.mvc.LoggersMvcEndpoint", "descriptor": "(Ljava/lang/String;Ljava/util/Map;)Ljava/lang/Object;", "name": "set" }, "requestMappingConditions": { "consumes": [ { "mediaType": "application/vnd.spring-boot.actuator.v1+json" }, { "mediaType": "application/json" } ], "headers": [], "methods": [ "POST" ], "params": [], "patterns": [ "/actuator/loggers/{name:.*}" ], "produces": [ { "mediaType": "application/vnd.spring-boot.actuator.v1+json" }, { "mediaType": "application/json" } ] } }, "predicate": "{[/actuator/loggers/{name:.*}],methods=[POST],consumes=[application/vnd.spring-boot.actuator.v1+json || application/json],produces=[application/vnd.spring-boot.actuator.v1+json || application/json]}", "handler": "public java.lang.Object org.springframework.boot.actuate.endpoint.mvc.LoggersMvcEndpoint.set(java.lang.String,java.util.Map)" } ] } } } } } ================================================ FILE: spring-boot-admin-server/src/test/resources/de/codecentric/boot/admin/server/web/client/mappings-legacy.json ================================================ { "/**/favicon.ico": { "bean": "faviconHandlerMapping" }, "{[/error]}": { "bean": "requestMappingHandlerMapping", "method": "public org.springframework.http.ResponseEntity> org.springframework.boot.autoconfigure.web.BasicErrorController.error(javax.servlet.http.HttpServletRequest)" }, "{[/actuator/loggers/{name:.*}],methods=[POST],consumes=[application/vnd.spring-boot.actuator.v1+json || application/json],produces=[application/vnd.spring-boot.actuator.v1+json || application/json]}": { "bean": "endpointHandlerMapping", "method": "public java.lang.Object org.springframework.boot.actuate.endpoint.mvc.LoggersMvcEndpoint.set(java.lang.String,java.util.Map)" } } ================================================ FILE: spring-boot-admin-server/src/test/resources/de/codecentric/boot/admin/server/web/client/threaddump-expected.json ================================================ { "threads": [ { "threadId": "1", "threadName": "foo" }, { "threadId": "2", "threadName": "bar" } ] } ================================================ FILE: spring-boot-admin-server/src/test/resources/de/codecentric/boot/admin/server/web/client/threaddump-legacy.json ================================================ [ { "threadId": "1", "threadName": "foo" }, { "threadId": "2", "threadName": "bar" } ] ================================================ FILE: spring-boot-admin-server/src/test/resources/logback-test.xml ================================================ ================================================ FILE: spring-boot-admin-server/src/test/resources/server-config-test.properties ================================================ spring.boot.admin.contextPath=/admin spring.boot.admin.instance-auth.default-user-name=admin spring.boot.admin.instance-auth.default-password=topsecret spring.boot.admin.instance-auth.service-map.my-service.userName=me spring.boot.admin.instance-auth.service-map.my-service.userPassword=secret ================================================ FILE: spring-boot-admin-server-cloud/pom.xml ================================================ 4.0.0 spring-boot-admin-server-cloud Spring Boot Admin Server Cloud Spring Boot Admin Server Cloud de.codecentric spring-boot-admin-build ${revision} ../spring-boot-admin-build de.codecentric spring-boot-admin-server org.springframework.cloud spring-cloud-starter true org.springframework.cloud spring-cloud-starter-netflix-eureka-client true org.springframework.boot spring-boot-starter-webmvc org.springframework.cloud spring-cloud-starter-kubernetes-client true org.springframework.boot spring-boot-starter-webmvc org.springframework.cloud spring-cloud-starter-kubernetes-fabric8 true org.springframework.boot spring-boot-starter-webmvc org.springframework.boot spring-boot-autoconfigure-processor true com.google.code.findbugs jsr305 org.springframework.boot spring-boot-configuration-processor true org.springframework.boot spring-boot-starter-test test org.springframework.boot spring-boot-starter-security test tools.jackson.datatype jackson-datatype-json-org test io.projectreactor reactor-test test ================================================ FILE: spring-boot-admin-server-cloud/src/main/java/de/codecentric/boot/admin/server/cloud/config/AdminServerDiscoveryAutoConfiguration.java ================================================ /* * 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.server.cloud.config; import com.netflix.discovery.EurekaClient; import org.springframework.boot.autoconfigure.AutoConfigureAfter; 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.commons.discovery.KubernetesDiscoveryProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import de.codecentric.boot.admin.server.cloud.discovery.DefaultServiceInstanceConverter; import de.codecentric.boot.admin.server.cloud.discovery.EurekaServiceInstanceConverter; import de.codecentric.boot.admin.server.cloud.discovery.InstanceDiscoveryListener; import de.codecentric.boot.admin.server.cloud.discovery.KubernetesServiceInstanceConverter; import de.codecentric.boot.admin.server.cloud.discovery.ServiceInstanceConverter; import de.codecentric.boot.admin.server.config.AdminServerAutoConfiguration; import de.codecentric.boot.admin.server.config.AdminServerMarkerConfiguration; import de.codecentric.boot.admin.server.domain.entities.InstanceRepository; import de.codecentric.boot.admin.server.services.InstanceRegistry; @Configuration(proxyBeanMethods = false) @ConditionalOnSingleCandidate(DiscoveryClient.class) @ConditionalOnBean(AdminServerMarkerConfiguration.Marker.class) @ConditionalOnProperty(prefix = "spring.boot.admin.discovery", name = "enabled", matchIfMissing = true) @AutoConfigureAfter(value = AdminServerAutoConfiguration.class, name = { "org.springframework.cloud.netflix.eureka.EurekaClientAutoConfiguration", "org.springframework.cloud.client.discovery.simple.SimpleDiscoveryClientAutoConfiguration" }) public class AdminServerDiscoveryAutoConfiguration { @Bean @ConditionalOnMissingBean @ConfigurationProperties(prefix = "spring.boot.admin.discovery") public InstanceDiscoveryListener instanceDiscoveryListener(ServiceInstanceConverter serviceInstanceConverter, DiscoveryClient discoveryClient, InstanceRegistry registry, InstanceRepository repository) { InstanceDiscoveryListener listener = new InstanceDiscoveryListener(discoveryClient, registry, repository); listener.setConverter(serviceInstanceConverter); return listener; } @Bean @ConditionalOnMissingBean({ ServiceInstanceConverter.class }) @ConfigurationProperties(prefix = "spring.boot.admin.discovery.converter") public DefaultServiceInstanceConverter serviceInstanceConverter() { return new DefaultServiceInstanceConverter(); } @Configuration(proxyBeanMethods = false) @ConditionalOnMissingBean({ ServiceInstanceConverter.class }) @ConditionalOnBean(EurekaClient.class) public static class EurekaConverterConfiguration { @Bean @ConfigurationProperties(prefix = "spring.boot.admin.discovery.converter") public EurekaServiceInstanceConverter serviceInstanceConverter() { return new EurekaServiceInstanceConverter(); } } @Configuration(proxyBeanMethods = false) @ConditionalOnBean(KubernetesDiscoveryProperties.class) @ConditionalOnMissingBean({ ServiceInstanceConverter.class }) @ConditionalOnCloudPlatform(CloudPlatform.KUBERNETES) public static class KubernetesConverterConfiguration { @Bean @ConfigurationProperties(prefix = "spring.boot.admin.discovery.converter") public KubernetesServiceInstanceConverter serviceInstanceConverter( KubernetesDiscoveryProperties discoveryProperties) { return new KubernetesServiceInstanceConverter(discoveryProperties); } } } ================================================ FILE: spring-boot-admin-server-cloud/src/main/java/de/codecentric/boot/admin/server/cloud/config/package-info.java ================================================ /* * 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. */ @NullMarked package de.codecentric.boot.admin.server.cloud.config; import org.jspecify.annotations.NullMarked; ================================================ FILE: spring-boot-admin-server-cloud/src/main/java/de/codecentric/boot/admin/server/cloud/discovery/DefaultServiceInstanceConverter.java ================================================ /* * 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.server.cloud.discovery; import java.net.URI; 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.web.util.UriComponentsBuilder; import de.codecentric.boot.admin.server.domain.entities.Instance; import de.codecentric.boot.admin.server.domain.values.Registration; import static java.util.Collections.emptyMap; import static org.springframework.util.StringUtils.hasText; /** * Converts any {@link ServiceInstance}s to {@link Instance}s. To customize the health- or * management-url for all instances you can set healthEndpointPath or * managementContextPath respectively. If you want to influence the url per service you * can add management.scheme, management.address, * management.port, management.context-path or * health.path to the instances metadata. * * @author Johannes Edmeier */ public class DefaultServiceInstanceConverter implements ServiceInstanceConverter { private static final Logger LOGGER = LoggerFactory.getLogger(DefaultServiceInstanceConverter.class); private static final String[] KEYS_MANAGEMENT_SCHEME = { "management.scheme", "management-scheme" }; private static final String[] KEYS_MANAGEMENT_ADDRESS = { "management.address", "management-address" }; private static final String[] KEYS_MANAGEMENT_PORT = { "management.port", "management-port", "port.management" }; private static final String[] KEYS_MANAGEMENT_PATH = { "management.context-path", "management-context-path" }; private static final String[] KEYS_HEALTH_PATH = { "health.path", "health-path" }; /** * Default context-path to be appended to the url of the discovered service for the * management-url. */ private String managementContextPath = "/actuator"; /** * Default path of the health-endpoint to be used for the health-url of the discovered * service. */ private String healthEndpointPath = "health"; protected static @Nullable String getMetadataValue(ServiceInstance instance, String... keys) { Map metadata = instance.getMetadata(); for (String key : keys) { String value = metadata.get(key); if (value != null) { return value; } } return null; } @Override public Registration convert(ServiceInstance instance) { LOGGER.debug("Converting service '{}' running at '{}' with metadata {}", instance.getServiceId(), instance.getUri(), instance.getMetadata()); String healthUrl = getHealthUrl(instance).toString(); String managementUrl = getManagementUrl(instance).toString(); String serviceUrl = getServiceUrl(instance).toString(); return Registration.create(instance.getServiceId(), healthUrl) .managementUrl(managementUrl) .serviceUrl(serviceUrl) .metadata(getMetadata(instance)) .build(); } protected URI getHealthUrl(ServiceInstance instance) { return UriComponentsBuilder.fromUri(getManagementUrl(instance)) .path("/") .path(getHealthPath(instance)) .build() .toUri(); } protected String getHealthPath(ServiceInstance instance) { String healthPath = getMetadataValue(instance, KEYS_HEALTH_PATH); if (hasText(healthPath)) { return healthPath; } return this.healthEndpointPath; } protected URI getManagementUrl(ServiceInstance instance) { URI serviceUrl = this.getServiceUrl(instance); String managementScheme = this.getManagementScheme(instance); String managementHost = this.getManagementHost(instance); int managementPort = this.getManagementPort(instance); UriComponentsBuilder builder; if (serviceUrl.getHost().equals(managementHost) && serviceUrl.getScheme().equals(managementScheme) && serviceUrl.getPort() == managementPort) { builder = UriComponentsBuilder.fromUri(serviceUrl); } else { builder = UriComponentsBuilder.newInstance().scheme(managementScheme).host(managementHost); if (managementPort != -1) { builder.port(managementPort); } } return builder.path("/").path(getManagementPath(instance)).build().toUri(); } private String getManagementScheme(ServiceInstance instance) { String managementServerScheme = getMetadataValue(instance, KEYS_MANAGEMENT_SCHEME); if (hasText(managementServerScheme)) { return managementServerScheme; } return getServiceUrl(instance).getScheme(); } protected String getManagementHost(ServiceInstance instance) { String managementServerHost = getMetadataValue(instance, KEYS_MANAGEMENT_ADDRESS); if (hasText(managementServerHost)) { return managementServerHost; } return getServiceUrl(instance).getHost(); } protected int getManagementPort(ServiceInstance instance) { String managementPort = getMetadataValue(instance, KEYS_MANAGEMENT_PORT); if (hasText(managementPort)) { return Integer.parseInt(managementPort); } return getServiceUrl(instance).getPort(); } protected String getManagementPath(ServiceInstance instance) { String managementPath = getMetadataValue(instance, KEYS_MANAGEMENT_PATH); if (hasText(managementPath)) { return managementPath; } return this.managementContextPath; } protected URI getServiceUrl(ServiceInstance instance) { return instance.getUri(); } protected Map getMetadata(ServiceInstance instance) { return (instance.getMetadata() != null) ? instance.getMetadata() .entrySet() .stream() .filter((e) -> e.getKey() != null && e.getValue() != null) .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)) : emptyMap(); } public String getManagementContextPath() { return this.managementContextPath; } public void setManagementContextPath(String managementContextPath) { this.managementContextPath = managementContextPath; } public String getHealthEndpointPath() { return this.healthEndpointPath; } public void setHealthEndpointPath(String healthEndpointPath) { this.healthEndpointPath = healthEndpointPath; } } ================================================ FILE: spring-boot-admin-server-cloud/src/main/java/de/codecentric/boot/admin/server/cloud/discovery/EurekaServiceInstanceConverter.java ================================================ /* * 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.server.cloud.discovery; import java.net.URI; import com.netflix.appinfo.InstanceInfo; import org.springframework.cloud.client.ServiceInstance; import org.springframework.cloud.netflix.eureka.EurekaServiceInstance; import org.springframework.util.StringUtils; import de.codecentric.boot.admin.server.domain.entities.Instance; /** * Converts {@link EurekaServiceInstance}s to {@link Instance}s * * @author Johannes Edmeier */ public class EurekaServiceInstanceConverter extends DefaultServiceInstanceConverter { @Override protected URI getHealthUrl(ServiceInstance instance) { if (!(instance instanceof EurekaServiceInstance)) { return super.getHealthUrl(instance); } InstanceInfo instanceInfo = ((EurekaServiceInstance) instance).getInstanceInfo(); String healthUrl = instanceInfo.getSecureHealthCheckUrl(); if (!StringUtils.hasText(healthUrl)) { healthUrl = instanceInfo.getHealthCheckUrl(); } return URI.create(healthUrl); } } ================================================ FILE: spring-boot-admin-server-cloud/src/main/java/de/codecentric/boot/admin/server/cloud/discovery/InstanceDiscoveryListener.java ================================================ /* * Copyright 2014-2024 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.server.cloud.discovery; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.Map; import java.util.Set; import java.util.stream.Collectors; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.boot.context.event.ApplicationReadyEvent; import org.springframework.cloud.client.ServiceInstance; import org.springframework.cloud.client.discovery.DiscoveryClient; import org.springframework.cloud.client.discovery.event.HeartbeatEvent; import org.springframework.cloud.client.discovery.event.HeartbeatMonitor; import org.springframework.cloud.client.discovery.event.InstanceRegisteredEvent; import org.springframework.cloud.client.discovery.event.ParentHeartbeatEvent; import org.springframework.context.event.EventListener; import org.springframework.util.PatternMatchUtils; import reactor.core.publisher.Flux; 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.InstanceId; import de.codecentric.boot.admin.server.domain.values.Registration; import de.codecentric.boot.admin.server.services.InstanceRegistry; import de.codecentric.boot.admin.server.web.client.RefreshInstancesEvent; /** * Listener for Heartbeats events to publish all services to the instance registry. * * @author Johannes Edmeier */ public class InstanceDiscoveryListener { private static final Logger log = LoggerFactory.getLogger(InstanceDiscoveryListener.class); private static final String SOURCE = "discovery"; private final DiscoveryClient discoveryClient; private final InstanceRegistry registry; private final InstanceRepository repository; private final HeartbeatMonitor monitor = new HeartbeatMonitor(); private ServiceInstanceConverter converter = new DefaultServiceInstanceConverter(); /** * Set of serviceIds to be ignored and not to be registered as application. Supports * simple patterns (e.g. "foo*", "*foo", "foo*bar"). */ private Set ignoredServices = new HashSet<>(); /** * Set of serviceIds that has to match to be registered as application. Supports * simple patterns (e.g. "foo*", "*foo", "foo*bar"). Default value is everything */ private Set services = new HashSet<>(Collections.singletonList("*")); /** * Map of metadata that has to be matched by service instance that is to be * registered. (e.g. "discoverable=true") */ private Map instancesMetadata = new HashMap<>(); /** * Map of metadata that has to be matched by service instance that is to be ignored. * (e.g. "discoverable=false") */ private Map ignoredInstancesMetadata = new HashMap<>(); public InstanceDiscoveryListener(DiscoveryClient discoveryClient, InstanceRegistry registry, InstanceRepository repository) { this.discoveryClient = discoveryClient; this.registry = registry; this.repository = repository; } @EventListener public void onApplicationReady(ApplicationReadyEvent event) { discover(); } @EventListener public void onInstanceRegistered(InstanceRegisteredEvent event) { discover(); } @EventListener public void onRefreshInstances(RefreshInstancesEvent event) { discover(); } @EventListener public void onParentHeartbeat(ParentHeartbeatEvent event) { discoverIfNeeded(event.getValue()); } @EventListener public void onApplicationEvent(HeartbeatEvent event) { discoverIfNeeded(event.getValue()); } private void discoverIfNeeded(Object value) { if (this.monitor.update(value)) { discover(); } } protected void discover() { log.debug("Discovering new instances from DiscoveryClient"); Flux.fromIterable(discoveryClient.getServices()) .filter(this::shouldRegisterService) .flatMapIterable(discoveryClient::getInstances) .filter(this::shouldRegisterInstanceBasedOnMetadata) .flatMap(this::registerInstance) .collect(Collectors.toSet()) .flatMap(this::removeStaleInstances) .subscribe((v) -> { }, (ex) -> log.error("Unexpected error.", ex)); } protected Mono removeStaleInstances(Set registeredInstanceIds) { return repository.findAll() .filter(Instance::isRegistered) .filter((instance) -> SOURCE.equals(instance.getRegistration().getSource())) .map(Instance::getId) .filter((id) -> !registeredInstanceIds.contains(id)) .doOnNext((id) -> log.info("Instance '{}' missing in DiscoveryClient services and will be removed.", id)) .flatMap(registry::deregister) .then(); } protected boolean shouldRegisterService(final String serviceId) { boolean shouldRegister = matchesPattern(serviceId, services) && !matchesPattern(serviceId, ignoredServices); if (!shouldRegister) { log.debug("Ignoring service '{}' from discovery.", serviceId); } return shouldRegister; } protected boolean matchesPattern(String serviceId, Set patterns) { return patterns.stream() .anyMatch((pattern) -> PatternMatchUtils.simpleMatch(pattern.toLowerCase(), serviceId.toLowerCase())); } protected boolean shouldRegisterInstanceBasedOnMetadata(ServiceInstance instance) { boolean shouldRegister = isInstanceAllowedBasedOnMetadata(instance) && !isInstanceIgnoredBasedOnMetadata(instance); if (!shouldRegister) { log.debug("Ignoring instance '{}' of '{}' service from discovery based on metadata.", instance.getInstanceId(), instance.getServiceId()); } return shouldRegister; } protected Mono registerInstance(ServiceInstance instance) { try { Registration registration = converter.convert(instance).toBuilder().source(SOURCE).build(); log.debug("Registering discovered instance {}", registration); return registry.register(registration); } catch (Exception ex) { log.error("Couldn't register instance for discovered instance ({})", toString(instance), ex); return Mono.empty(); } } protected String toString(ServiceInstance instance) { String httpScheme = instance.isSecure() ? "https" : "http"; return String.format("serviceId=%s, instanceId=%s, url= %s://%s:%d", instance.getServiceId(), instance.getInstanceId(), (instance.getScheme() != null) ? instance.getScheme() : httpScheme, instance.getHost(), instance.getPort()); } public void setConverter(ServiceInstanceConverter converter) { this.converter = converter; } public void setIgnoredServices(Set ignoredServices) { this.ignoredServices = ignoredServices; } public Set getIgnoredServices() { return ignoredServices; } public Set getServices() { return services; } public void setServices(Set services) { this.services = services; } public Map getInstancesMetadata() { return instancesMetadata; } public void setInstancesMetadata(Map instancesMetadata) { this.instancesMetadata = instancesMetadata; } public Map getIgnoredInstancesMetadata() { return ignoredInstancesMetadata; } public void setIgnoredInstancesMetadata(Map ignoredInstancesMetadata) { this.ignoredInstancesMetadata = ignoredInstancesMetadata; } private boolean isInstanceAllowedBasedOnMetadata(ServiceInstance instance) { if (instancesMetadata.isEmpty()) { return true; } for (Map.Entry metadata : instance.getMetadata().entrySet()) { if (isMapContainsEntry(instancesMetadata, metadata)) { return true; } } return false; } private boolean isInstanceIgnoredBasedOnMetadata(ServiceInstance instance) { if (ignoredInstancesMetadata.isEmpty()) { return false; } for (Map.Entry metadata : instance.getMetadata().entrySet()) { if (isMapContainsEntry(ignoredInstancesMetadata, metadata)) { return true; } } return false; } private boolean isMapContainsEntry(Map map, Map.Entry entry) { String value = map.get(entry.getKey()); return value != null && value.equals(entry.getValue()); } } ================================================ FILE: spring-boot-admin-server-cloud/src/main/java/de/codecentric/boot/admin/server/cloud/discovery/KubernetesServiceInstanceConverter.java ================================================ /* * 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.server.cloud.discovery; import org.springframework.cloud.client.ServiceInstance; import org.springframework.cloud.kubernetes.commons.discovery.KubernetesDiscoveryProperties; import static org.springframework.util.StringUtils.hasText; public class KubernetesServiceInstanceConverter extends DefaultServiceInstanceConverter { public static final String MANAGEMENT_PORT_NAME = "management"; private final String portsPrefix; public KubernetesServiceInstanceConverter(KubernetesDiscoveryProperties discoveryProperties) { if (discoveryProperties.metadata() != null && discoveryProperties.metadata().portsPrefix() != null) { this.portsPrefix = discoveryProperties.metadata().portsPrefix(); } else { this.portsPrefix = ""; } } @Override protected int getManagementPort(ServiceInstance instance) { // the DiscoveryClient implementation using Kubernetes Client // (KubernetesInformerDiscoveryClient) currently ignores // the portsPrefix from KubernetesDiscoveryProperties String managementPort = getMetadataValue(instance, portsPrefix + MANAGEMENT_PORT_NAME, MANAGEMENT_PORT_NAME); if (hasText(managementPort)) { return Integer.parseInt(managementPort); } return super.getManagementPort(instance); } } ================================================ FILE: spring-boot-admin-server-cloud/src/main/java/de/codecentric/boot/admin/server/cloud/discovery/ServiceInstanceConverter.java ================================================ /* * 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.server.cloud.discovery; import org.springframework.cloud.client.ServiceInstance; import de.codecentric.boot.admin.server.domain.entities.Instance; import de.codecentric.boot.admin.server.domain.values.Registration; /** * Converts {@link ServiceInstance}s to {@link Instance}s. * * @author Johannes Edmeier */ public interface ServiceInstanceConverter { /** * Converts a service instance to an application instance to be registered. * @param instance the service instance. * @return the Registration */ Registration convert(ServiceInstance instance); } ================================================ FILE: spring-boot-admin-server-cloud/src/main/java/de/codecentric/boot/admin/server/cloud/discovery/package-info.java ================================================ /* * 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. */ @NullMarked package de.codecentric.boot.admin.server.cloud.discovery; import org.jspecify.annotations.NullMarked; ================================================ FILE: spring-boot-admin-server-cloud/src/main/resources/META-INF/additional-spring-configuration-metadata.json ================================================ { "groups": [ ], "properties": [ { "name": "spring.boot.admin.discovery.enabled", "type": "java.lang.Boolean", "description": "Enable Spring Cloud Discovery support.", "defaultValue": "true" } ] } ================================================ FILE: spring-boot-admin-server-cloud/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports ================================================ de.codecentric.boot.admin.server.cloud.config.AdminServerDiscoveryAutoConfiguration ================================================ FILE: spring-boot-admin-server-cloud/src/test/java/de/codecentric/boot/admin/server/cloud/AdminApplicationDiscoveryTest.java ================================================ /* * 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.server.cloud; import java.net.URI; import java.time.Duration; import java.util.concurrent.atomic.AtomicReference; import org.json.JSONObject; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.boot.SpringBootConfiguration; import org.springframework.boot.WebApplicationType; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.builder.SpringApplicationBuilder; 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.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; import static java.util.Collections.singletonList; import static org.assertj.core.api.Assertions.assertThat; class AdminApplicationDiscoveryTest { private ConfigurableApplicationContext instance; private SimpleDiscoveryProperties simpleDiscovery; private WebTestClient webClient; private int port; @BeforeEach void setUp() { this.instance = new SpringApplicationBuilder().sources(TestAdminApplication.class) .web(WebApplicationType.REACTIVE) .run("--server.port=0", "--management.endpoints.web.base-path=/mgmt", "--management.endpoints.web.exposure.include=info,health", "--info.test=foobar", "--eureka.client.enabled=false", "--spring.cloud.kubernetes.enabled=false", "--spring.cloud.kubernetes.discovery.enabled=false", "--management.info.env.enabled=true"); this.simpleDiscovery = this.instance.getBean(SimpleDiscoveryProperties.class); this.port = this.instance.getEnvironment().getProperty("local.server.port", Integer.class, 0); this.webClient = createWebClient(this.port); } @Test void lifecycle() { AtomicReference location = new AtomicReference<>(); StepVerifier.create(getEventStream().log()).expectSubscription().then(() -> { 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(() -> { StepVerifier.create(getInstance(location.get())).expectNext(true).verifyComplete(); StepVerifier.create(listInstances()).expectNext(true).verifyComplete(); deregisterInstance(); }) .assertNext((event) -> assertThat(event.opt("type")).isEqualTo("DEREGISTERED")) .then(() -> StepVerifier.create(listEmptyInstances()).expectNext(true).verifyComplete()) .thenCancel() .verify(Duration.ofSeconds(60)); } 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. 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. //@formatter:off return this.webClient.get() .uri("/instances?name=Test-Instance") .accept(MediaType.APPLICATION_JSON) .exchange() .returnResult(JSONObject.class).getResponseBody() .collectList() .map((applications) -> { assertThat(applications).hasSize(1); return URI .create("http://localhost:" + this.port + "/instances/" + applications.get(0).optString("id")); }); //@formatter:on } private void deregisterInstance() { this.simpleDiscovery.getInstances().clear(); this.instance.publishEvent(new InstanceRegisteredEvent<>(new Object(), null)); } private Flux getEventStream() { //@formatter:off return this.webClient.get().uri("/instances/events").accept(MediaType.TEXT_EVENT_STREAM) .exchange() .expectStatus().isOk() .expectHeader().contentTypeCompatibleWith(MediaType.TEXT_EVENT_STREAM) .returnResult(JSONObject.class).getResponseBody(); //@formatter:on } private Mono getInstance(URI uri) { //@formatter:off return this.webClient.get().uri(uri).accept(MediaType.APPLICATION_JSON) .exchange() .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 Mono listInstances() { //@formatter:off return this.webClient.get().uri("/instances").accept(MediaType.APPLICATION_JSON) .exchange() .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 Mono listEmptyInstances() { //@formatter:off return this.webClient.get().uri("/instances").accept(MediaType.APPLICATION_JSON) .exchange() .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) { JsonMapper mapper = JsonMapper.builder().addModule(new JsonOrgModule()).build(); return WebTestClient.bindToServer() .baseUrl("http://localhost:" + port) .exchangeStrategies(ExchangeStrategies.builder().codecs((configurer) -> { configurer.defaultCodecs().jacksonJsonDecoder(new JacksonJsonDecoder(mapper)); configurer.defaultCodecs().jacksonJsonEncoder(new JacksonJsonEncoder(mapper)); }).build()) .build(); } @AfterEach void shutdown() { this.instance.close(); } @EnableAdminServer @EnableAutoConfiguration @SpringBootConfiguration @EnableWebFluxSecurity public static class TestAdminApplication { @Bean SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) { return http.authorizeExchange((authorizeExchange) -> authorizeExchange.anyExchange().permitAll()) .csrf(ServerHttpSecurity.CsrfSpec::disable) .build(); } } } ================================================ FILE: spring-boot-admin-server-cloud/src/test/java/de/codecentric/boot/admin/server/cloud/config/AdminServerDiscoveryAutoConfigurationTest.java ================================================ /* * 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.server.cloud.config; import com.netflix.discovery.EurekaClient; import org.junit.jupiter.api.Test; import org.springframework.boot.autoconfigure.AutoConfigurations; 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.commons.discovery.KubernetesDiscoveryProperties; import de.codecentric.boot.admin.server.cloud.discovery.DefaultServiceInstanceConverter; import de.codecentric.boot.admin.server.cloud.discovery.EurekaServiceInstanceConverter; import de.codecentric.boot.admin.server.cloud.discovery.KubernetesServiceInstanceConverter; import de.codecentric.boot.admin.server.cloud.discovery.ServiceInstanceConverter; import de.codecentric.boot.admin.server.config.AdminServerAutoConfiguration; import de.codecentric.boot.admin.server.config.AdminServerMarkerConfiguration; import de.codecentric.boot.admin.server.domain.values.Registration; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.mock; class AdminServerDiscoveryAutoConfigurationTest { private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() .withConfiguration(AutoConfigurations.of(UtilAutoConfiguration.class, ReactiveHttpClientAutoConfiguration.class, WebClientAutoConfiguration.class, AdminServerAutoConfiguration.class, AdminServerDiscoveryAutoConfiguration.class)) .withUserConfiguration(AdminServerMarkerConfiguration.class); @Test void defaultServiceInstanceConverter() { this.contextRunner.withUserConfiguration(SimpleDiscoveryClientAutoConfiguration.class) .run((context) -> assertThat(context.getBean(ServiceInstanceConverter.class)) .isInstanceOf(DefaultServiceInstanceConverter.class)); } @Test void eurekaServiceInstanceConverter() { this.contextRunner.withBean(EurekaClient.class, () -> mock(EurekaClient.class)) .withBean(DiscoveryClient.class, () -> mock(DiscoveryClient.class)) .run((context) -> assertThat(context.getBean(ServiceInstanceConverter.class)) .isInstanceOf(EurekaServiceInstanceConverter.class)); } @Test void kubernetesServiceInstanceConverter() { this.contextRunner.withBean(DiscoveryClient.class, () -> mock(DiscoveryClient.class)) .withBean(KubernetesDiscoveryProperties.class, () -> mock(KubernetesDiscoveryProperties.class)) .withPropertyValues("spring.main.cloud-platform=KUBERNETES") .run((context) -> assertThat(context.getBean(ServiceInstanceConverter.class)) .isInstanceOf(KubernetesServiceInstanceConverter.class)); } @Test void customServiceInstanceConverter() { this.contextRunner.withUserConfiguration(SimpleDiscoveryClientAutoConfiguration.class) .withBean(CustomServiceInstanceConverter.class) .run((context) -> assertThat(context.getBean(ServiceInstanceConverter.class)) .isInstanceOf(CustomServiceInstanceConverter.class)); } public static class CustomServiceInstanceConverter implements ServiceInstanceConverter { @Override public Registration convert(ServiceInstance instance) { return null; } } } ================================================ FILE: spring-boot-admin-server-cloud/src/test/java/de/codecentric/boot/admin/server/cloud/discovery/DefaultServiceInstanceConverterTest.java ================================================ /* * 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.server.cloud.discovery; import java.net.URI; import java.util.Collections; import java.util.HashMap; import java.util.Map; import org.junit.jupiter.api.Test; import org.springframework.cloud.client.DefaultServiceInstance; import org.springframework.cloud.client.ServiceInstance; import de.codecentric.boot.admin.server.domain.values.Registration; import static org.assertj.core.api.Assertions.assertThat; class DefaultServiceInstanceConverterTest { @Test void should_convert_with_defaults() { ServiceInstance service = new DefaultServiceInstance("test-1", "test", "localhost", 80, false); Registration registration = new DefaultServiceInstanceConverter().convert(service); assertThat(registration.getName()).isEqualTo("test"); assertThat(registration.getServiceUrl()).isEqualTo("http://localhost:80"); assertThat(registration.getManagementUrl()).isEqualTo("http://localhost:80/actuator"); assertThat(registration.getHealthUrl()).isEqualTo("http://localhost:80/actuator/health"); } @Test void should_convert_with_custom_defaults() { DefaultServiceInstanceConverter converter = new DefaultServiceInstanceConverter(); converter.setHealthEndpointPath("ping"); converter.setManagementContextPath("mgmt"); ServiceInstance service = new DefaultServiceInstance("test-1", "test", "localhost", 80, false); Registration registration = converter.convert(service); assertThat(registration.getName()).isEqualTo("test"); assertThat(registration.getServiceUrl()).isEqualTo("http://localhost:80"); assertThat(registration.getManagementUrl()).isEqualTo("http://localhost:80/mgmt"); assertThat(registration.getHealthUrl()).isEqualTo("http://localhost:80/mgmt/ping"); } @Test void should_convert_with_metadata() { ServiceInstance service = new DefaultServiceInstance("test-1", "test", "localhost", 80, false); Map metadata = new HashMap<>(); metadata.put("health.path", "ping"); metadata.put("management.scheme", "https"); metadata.put("management.address", "127.0.0.1"); metadata.put("management.port", "1234"); metadata.put("management.context-path", "mgmt"); service.getMetadata().putAll(metadata); Registration registration = new DefaultServiceInstanceConverter().convert(service); assertThat(registration.getName()).isEqualTo("test"); assertThat(registration.getServiceUrl()).isEqualTo("http://localhost:80"); assertThat(registration.getManagementUrl()).isEqualTo("https://127.0.0.1:1234/mgmt"); assertThat(registration.getHealthUrl()).isEqualTo("https://127.0.0.1:1234/mgmt/ping"); assertThat(registration.getMetadata()).isEqualTo(metadata); } // Fix for Issue #2076, #1737 @Test void should_convert_with_metadata_without_dots() { ServiceInstance service = new DefaultServiceInstance("test-1", "test", "localhost", 80, false); Map metadata = new HashMap<>(); metadata.put("health-path", "ping"); metadata.put("management-scheme", "https"); metadata.put("management-address", "127.0.0.1"); metadata.put("management-port", "1234"); metadata.put("management-context-path", "mgmt"); service.getMetadata().putAll(metadata); Registration registration = new DefaultServiceInstanceConverter().convert(service); assertThat(registration.getName()).isEqualTo("test"); assertThat(registration.getServiceUrl()).isEqualTo("http://localhost:80"); assertThat(registration.getManagementUrl()).isEqualTo("https://127.0.0.1:1234/mgmt"); assertThat(registration.getHealthUrl()).isEqualTo("https://127.0.0.1:1234/mgmt/ping"); assertThat(registration.getMetadata()).isEqualTo(metadata); } @Test void should_convert_with_metadata_having_null_value() { ServiceInstance service = new DefaultServiceInstance("test-1", "test", "localhost", 80, false); Map metadata = new HashMap<>(); metadata.put("health.path", "ping"); metadata.put("management.scheme", "https"); metadata.put("management.address", "127.0.0.1"); metadata.put("management.port", "1234"); metadata.put("null.value", null); metadata.put(null, "null.key"); service.getMetadata().putAll(metadata); Registration registration = new DefaultServiceInstanceConverter().convert(service); assertThat(registration.getHealthUrl()).isEqualTo("https://127.0.0.1:1234/actuator/ping"); } @Test void should_convert_service_with_uri() { ServiceInstance service = new TestServiceInstance("test", URI.create("http://localhost/test"), Collections.emptyMap()); Registration registration = new DefaultServiceInstanceConverter().convert(service); assertThat(registration.getName()).isEqualTo("test"); assertThat(registration.getServiceUrl()).isEqualTo("http://localhost/test"); assertThat(registration.getManagementUrl()).isEqualTo("http://localhost/test/actuator"); assertThat(registration.getHealthUrl()).isEqualTo("http://localhost/test/actuator/health"); assertThat(registration.getMetadata()).isEmpty(); } @Test void should_convert_service_with_uri_and_custom_defaults() { DefaultServiceInstanceConverter converter = new DefaultServiceInstanceConverter(); converter.setHealthEndpointPath("ping"); converter.setManagementContextPath("mgmt"); ServiceInstance service = new TestServiceInstance("test", URI.create("http://localhost/test"), Collections.emptyMap()); Registration registration = converter.convert(service); assertThat(registration.getName()).isEqualTo("test"); assertThat(registration.getServiceUrl()).isEqualTo("http://localhost/test"); assertThat(registration.getManagementUrl()).isEqualTo("http://localhost/test/mgmt"); assertThat(registration.getHealthUrl()).isEqualTo("http://localhost/test/mgmt/ping"); } @Test void should_convert_service_with_uri_and_metadata_different_port() { Map metadata = new HashMap<>(); metadata.put("health.path", "ping"); metadata.put("management.context-path", "mgmt"); metadata.put("management.port", "1234"); metadata.put("management.address", "127.0.0.1"); ServiceInstance service = new TestServiceInstance("test", URI.create("http://localhost/test"), metadata); Registration registration = new DefaultServiceInstanceConverter().convert(service); assertThat(registration.getName()).isEqualTo("test"); assertThat(registration.getServiceUrl()).isEqualTo("http://localhost/test"); assertThat(registration.getManagementUrl()).isEqualTo("http://127.0.0.1:1234/mgmt"); assertThat(registration.getHealthUrl()).isEqualTo("http://127.0.0.1:1234/mgmt/ping"); assertThat(registration.getMetadata()).isEqualTo(metadata); } @Test void should_convert_service_with_uri_and_metadata() { Map metadata = new HashMap<>(); metadata.put("health.path", "ping"); metadata.put("management.context-path", "mgmt"); ServiceInstance service = new TestServiceInstance("test", URI.create("http://localhost/test"), metadata); Registration registration = new DefaultServiceInstanceConverter().convert(service); assertThat(registration.getName()).isEqualTo("test"); assertThat(registration.getServiceUrl()).isEqualTo("http://localhost/test"); assertThat(registration.getManagementUrl()).isEqualTo("http://localhost/test/mgmt"); assertThat(registration.getHealthUrl()).isEqualTo("http://localhost/test/mgmt/ping"); assertThat(registration.getMetadata()).isEqualTo(metadata); } private record TestServiceInstance(String serviceId, URI uri, Map metadata) implements ServiceInstance { @Override public String getServiceId() { return this.serviceId; } @Override public String getHost() { return this.uri.getHost(); } @Override public int getPort() { if (this.uri.getPort() != -1) { return this.uri.getPort(); } return this.isSecure() ? 443 : 80; } @Override public boolean isSecure() { return this.uri.getScheme().equals("https"); } @Override public URI getUri() { return this.uri; } @Override public Map getMetadata() { return this.metadata; } } } ================================================ FILE: spring-boot-admin-server-cloud/src/test/java/de/codecentric/boot/admin/server/cloud/discovery/EurekaServiceInstanceConverterTest.java ================================================ /* * 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.server.cloud.discovery; import java.net.URI; import com.netflix.appinfo.InstanceInfo; import org.junit.jupiter.api.Test; import org.springframework.cloud.netflix.eureka.EurekaServiceInstance; import de.codecentric.boot.admin.server.domain.values.Registration; import static java.util.Collections.singletonMap; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; class EurekaServiceInstanceConverterTest { @Test void convert_secure() { InstanceInfo instanceInfo = mock(InstanceInfo.class); when(instanceInfo.getSecureHealthCheckUrl()).thenReturn(""); when(instanceInfo.getHealthCheckUrl()).thenReturn("http://localhost:80/mgmt/ping"); EurekaServiceInstance service = mock(EurekaServiceInstance.class); when(service.getInstanceInfo()).thenReturn(instanceInfo); when(service.getUri()).thenReturn(URI.create("http://localhost:80")); when(service.getServiceId()).thenReturn("test"); when(service.getMetadata()).thenReturn(singletonMap("management.context-path", "/mgmt")); Registration registration = new EurekaServiceInstanceConverter().convert(service); assertThat(registration.getName()).isEqualTo("test"); assertThat(registration.getServiceUrl()).isEqualTo("http://localhost:80"); assertThat(registration.getManagementUrl()).isEqualTo("http://localhost:80/mgmt"); assertThat(registration.getHealthUrl()).isEqualTo("http://localhost:80/mgmt/ping"); } @Test void convert_missing_mgmt_path() { InstanceInfo instanceInfo = mock(InstanceInfo.class); when(instanceInfo.getHealthCheckUrl()).thenReturn("http://localhost:80/mgmt/ping"); EurekaServiceInstance service = mock(EurekaServiceInstance.class); when(service.getInstanceInfo()).thenReturn(instanceInfo); when(service.getUri()).thenReturn(URI.create("http://localhost:80")); when(service.getServiceId()).thenReturn("test"); Registration registration = new EurekaServiceInstanceConverter().convert(service); assertThat(registration.getManagementUrl()).isEqualTo("http://localhost:80/actuator"); } @Test void convert_secure_healthUrl() { InstanceInfo instanceInfo = mock(InstanceInfo.class); when(instanceInfo.getSecureHealthCheckUrl()).thenReturn("https://localhost:80/health"); EurekaServiceInstance service = mock(EurekaServiceInstance.class); when(service.getInstanceInfo()).thenReturn(instanceInfo); when(service.getUri()).thenReturn(URI.create("http://localhost:80")); when(service.getServiceId()).thenReturn("test"); Registration registration = new EurekaServiceInstanceConverter().convert(service); assertThat(registration.getHealthUrl()).isEqualTo("https://localhost:80/health"); } } ================================================ FILE: spring-boot-admin-server-cloud/src/test/java/de/codecentric/boot/admin/server/cloud/discovery/InstanceDiscoveryListenerTest.java ================================================ /* * Copyright 2014-2024 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.server.cloud.discovery; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.List; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.cloud.client.DefaultServiceInstance; import org.springframework.cloud.client.ServiceInstance; import org.springframework.cloud.client.discovery.DiscoveryClient; import org.springframework.cloud.client.discovery.event.HeartbeatEvent; import org.springframework.cloud.client.discovery.event.InstanceRegisteredEvent; import org.springframework.cloud.client.discovery.event.ParentHeartbeatEvent; import reactor.test.StepVerifier; import de.codecentric.boot.admin.server.domain.entities.EventsourcingInstanceRepository; import de.codecentric.boot.admin.server.domain.entities.InstanceRepository; import de.codecentric.boot.admin.server.domain.values.InstanceId; import de.codecentric.boot.admin.server.domain.values.Registration; import de.codecentric.boot.admin.server.eventstore.InMemoryEventStore; import de.codecentric.boot.admin.server.services.HashingInstanceUrlIdGenerator; import de.codecentric.boot.admin.server.services.InstanceRegistry; import static java.util.Arrays.asList; import static java.util.Collections.singleton; import static java.util.Collections.singletonList; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; class InstanceDiscoveryListenerTest { private InstanceDiscoveryListener listener; private DiscoveryClient discovery; private InstanceRegistry registry; @BeforeEach void setup() { this.discovery = mock(DiscoveryClient.class); InstanceRepository repository = new EventsourcingInstanceRepository(new InMemoryEventStore()); this.registry = spy(new InstanceRegistry(repository, new HashingInstanceUrlIdGenerator(), (instance) -> true)); this.listener = new InstanceDiscoveryListener(this.discovery, this.registry, repository); } @Test void should_discover_instances_when_application_is_ready() { when(this.discovery.getServices()).thenReturn(Collections.singletonList("service")); when(this.discovery.getInstances("service")).thenReturn( Collections.singletonList(new DefaultServiceInstance("test-1", "service", "localhost", 80, false))); this.listener.onApplicationReady(null); StepVerifier.create(this.registry.getInstances()) .assertNext((a) -> assertThat(a.getRegistration().getName()).isEqualTo("service")) .verifyComplete(); } @Test void should_not_register_instance_when_serviceId_is_ignored() { when(this.discovery.getServices()).thenReturn(singletonList("service")); when(this.discovery.getInstances("service")) .thenReturn(singletonList(new DefaultServiceInstance("test-1", "service", "localhost", 80, false))); this.listener.setIgnoredServices(singleton("service")); this.listener.onInstanceRegistered(new InstanceRegisteredEvent<>(new Object(), null)); StepVerifier.create(this.registry.getInstances()).verifyComplete(); } @Test void should_not_register_instance_when_serviceId_is_ignored_caseInsensitive() { when(this.discovery.getServices()).thenReturn(singletonList("service")); when(this.discovery.getInstances("service")) .thenReturn(singletonList(new DefaultServiceInstance("test-1", "service", "localhost", 80, false))); this.listener.setIgnoredServices(singleton("SERVICE")); this.listener.onInstanceRegistered(new InstanceRegisteredEvent<>(new Object(), null)); StepVerifier.create(this.registry.getInstances()).verifyComplete(); } @Test void should_not_register_instance_when_instanceMetadata_is_ignored() { when(this.discovery.getServices()).thenReturn(singletonList("service")); when(this.discovery.getInstances("service")).thenReturn(singletonList(new DefaultServiceInstance("test-1", "service", "localhost", 80, false, Collections.singletonMap("monitoring", "false")))); this.listener.setIgnoredInstancesMetadata(Collections.singletonMap("monitoring", "false")); this.listener.onInstanceRegistered(new InstanceRegisteredEvent<>(new Object(), null)); StepVerifier.create(this.registry.getInstances()).verifyComplete(); } @Test void should_register_instance_when_serviceId_is_not_ignored() { when(this.discovery.getServices()).thenReturn(singletonList("service")); when(this.discovery.getInstances("service")) .thenReturn(singletonList(new DefaultServiceInstance("test-1", "service", "localhost", 80, false))); this.listener.setServices(singleton("notService2")); this.listener.onInstanceRegistered(new InstanceRegisteredEvent<>(new Object(), null)); StepVerifier.create(this.registry.getInstances()).verifyComplete(); } @Test void should_register_instance_when_instanceMetadata_is_not_ignored() { when(this.discovery.getServices()).thenReturn(singletonList("service")); when(this.discovery.getInstances("service")).thenReturn(singletonList(new DefaultServiceInstance("test-1", "service", "localhost", 80, false, Collections.singletonMap("monitoring", "true")))); this.listener.setInstancesMetadata(Collections.singletonMap("monitoring", "false")); this.listener.onInstanceRegistered(new InstanceRegisteredEvent<>(new Object(), null)); StepVerifier.create(this.registry.getInstances()).verifyComplete(); } @Test void should_not_register_instance_when_serviceId_matches_ignored_pattern() { when(this.discovery.getServices()).thenReturn(asList("service", "rabbit-1", "rabbit-2")); when(this.discovery.getInstances("service")) .thenReturn(singletonList(new DefaultServiceInstance("test-1", "service", "localhost", 80, false))); this.listener.setIgnoredServices(singleton("rabbit-*")); this.listener.onInstanceRegistered(new InstanceRegisteredEvent<>(new Object(), null)); StepVerifier.create(this.registry.getInstances()) .assertNext((a) -> assertThat(a.getRegistration().getName()).isEqualTo("service")) .verifyComplete(); } @Test void should_not_register_instance_when_instanceMetadata_matches_ignored_metadata() { when(this.discovery.getServices()).thenReturn(asList("service", "rabbit-1", "rabbit-2")); when(this.discovery.getInstances("service")).thenReturn(singletonList(new DefaultServiceInstance("test-1", "service", "localhost", 80, false, Collections.singletonMap("monitoring", "true")))); when(this.discovery.getInstances("rabbit-1")) .thenReturn(singletonList(new DefaultServiceInstance("rabbit-test-1", "rabbit-1", "localhost", 80, false, Collections.singletonMap("monitoring", "false")))); when(this.discovery.getInstances("rabbit-2")) .thenReturn(singletonList(new DefaultServiceInstance("rabbit-test-1", "rabbit-2", "localhost", 80, false, Collections.singletonMap("monitoring", "false")))); this.listener.setIgnoredInstancesMetadata(Collections.singletonMap("monitoring", "false")); this.listener.onInstanceRegistered(new InstanceRegisteredEvent<>(new Object(), null)); StepVerifier.create(this.registry.getInstances()) .assertNext((a) -> assertThat(a.getRegistration().getName()).isEqualTo("service")) .verifyComplete(); } @Test void should_register_instances_when_serviceId_matches_wanted_pattern() { when(this.discovery.getServices()).thenReturn(asList("service", "rabbit-1", "rabbit-2")); when(this.discovery.getInstances("service")) .thenReturn(singletonList(new DefaultServiceInstance("test-1", "service", "localhost", 80, false))); this.listener.setServices(singleton("ser*")); this.listener.onInstanceRegistered(new InstanceRegisteredEvent<>(new Object(), null)); StepVerifier.create(this.registry.getInstances()) .assertNext((a) -> assertThat(a.getRegistration().getName()).isEqualTo("service")) .verifyComplete(); } @Test void should_register_instances_when_instanceMetadata_matches_wanted_metadata() { when(this.discovery.getServices()).thenReturn(asList("service", "rabbit-1", "rabbit-2")); when(this.discovery.getInstances("service")).thenReturn(singletonList(new DefaultServiceInstance("test-1", "service", "localhost", 80, false, Collections.singletonMap("monitoring", "true")))); when(this.discovery.getInstances("rabbit-1")) .thenReturn(singletonList(new DefaultServiceInstance("rabbit-test-1", "rabbit-1", "localhost", 80, false, Collections.singletonMap("monitoring", "false")))); when(this.discovery.getInstances("rabbit-2")) .thenReturn(singletonList(new DefaultServiceInstance("rabbit-test-1", "rabbit-2", "localhost", 80, false, Collections.singletonMap("monitoring", "false")))); this.listener.setInstancesMetadata(Collections.singletonMap("monitoring", "true")); this.listener.onInstanceRegistered(new InstanceRegisteredEvent<>(new Object(), null)); StepVerifier.create(this.registry.getInstances()) .assertNext((a) -> assertThat(a.getRegistration().getName()).isEqualTo("service")) .verifyComplete(); } @Test void should_register_instances_when_serviceId_matches_wanted_pattern_and_ignored_pattern() { when(this.discovery.getServices()).thenReturn(asList("service-1", "service", "rabbit-1", "rabbit-2")); when(this.discovery.getInstances("service")) .thenReturn(singletonList(new DefaultServiceInstance("test-1", "service", "localhost", 80, false))); when(this.discovery.getInstances("service-1")) .thenReturn(singletonList(new DefaultServiceInstance("test-1", "service-1", "localhost", 80, false))); this.listener.setServices(singleton("ser*")); this.listener.setIgnoredServices(singleton("service-*")); this.listener.onInstanceRegistered(new InstanceRegisteredEvent<>(new Object(), null)); StepVerifier.create(this.registry.getInstances()) .assertNext((a) -> assertThat(a.getRegistration().getName()).isEqualTo("service")) .verifyComplete(); } @Test void should_not_register_instances_when_instanceMetadata_matches_wanted_metadata_and_ignored_metadata() { when(this.discovery.getServices()).thenReturn(asList("service", "service-1")); when(this.discovery.getInstances("service")).thenReturn( singletonList(new DefaultServiceInstance("test-1", "service", "localhost", 80, false, new HashMap<>() { { put("monitoring", "true"); put("management", "true"); } }))); when(this.discovery.getInstances("service-1")).thenReturn(singletonList( new DefaultServiceInstance("test-1", "service-1", "localhost", 80, false, new HashMap<>() { { put("monitoring", "true"); put("management", "false"); } }))); this.listener.setInstancesMetadata(Collections.singletonMap("monitoring", "true")); this.listener.setIgnoredInstancesMetadata(Collections.singletonMap("management", "true")); this.listener.onInstanceRegistered(new InstanceRegisteredEvent<>(new Object(), null)); StepVerifier.create(this.registry.getInstances()) .assertNext((a) -> assertThat(a.getRegistration().getName()).isEqualTo("service-1")) .verifyComplete(); } @Test void should_register_instance_when_new_service_instance_is_discovered() { when(this.discovery.getServices()).thenReturn(singletonList("service")); when(this.discovery.getInstances("service")) .thenReturn(singletonList(new DefaultServiceInstance("test-1", "service", "localhost", 80, false))); this.listener.onInstanceRegistered(new InstanceRegisteredEvent<>(new Object(), null)); StepVerifier.create(this.registry.getInstances()).assertNext((application) -> { Registration registration = application.getRegistration(); assertThat(registration.getHealthUrl()).isEqualTo("http://localhost:80/actuator/health"); assertThat(registration.getManagementUrl()).isEqualTo("http://localhost:80/actuator"); assertThat(registration.getServiceUrl()).isEqualTo("http://localhost:80"); assertThat(registration.getName()).isEqualTo("service"); }).verifyComplete(); } @Test void should_only_discover_new_instances_when_new_heartbeat_is_emitted() { Object heartbeat = new Object(); this.listener.onParentHeartbeat(new ParentHeartbeatEvent(new Object(), heartbeat)); when(this.discovery.getServices()).thenReturn(singletonList("service")); when(this.discovery.getInstances("service")) .thenReturn(singletonList(new DefaultServiceInstance("test-1", "service", "localhost", 80, false))); this.listener.onApplicationEvent(new HeartbeatEvent(new Object(), heartbeat)); StepVerifier.create(this.registry.getInstances()).verifyComplete(); this.listener.onApplicationEvent(new HeartbeatEvent(new Object(), new Object())); StepVerifier.create(this.registry.getInstances()) .assertNext((a) -> assertThat(a.getRegistration().getName()).isEqualTo("service")) .verifyComplete(); } @Test void should_remove_instances_when_they_are_no_longer_available_in_discovery() { StepVerifier.create(this.registry.register(Registration.create("ignored", "https://health").build())) .consumeNextWith((id) -> { }) .verifyComplete(); StepVerifier .create(this.registry .register(Registration.create("different-source", "https://health2").source("http-api").build())) .consumeNextWith((id) -> { }) .verifyComplete(); this.listener.setIgnoredServices(singleton("ignored")); List instances = new ArrayList<>(); instances.add(new DefaultServiceInstance("test-1", "service", "localhost", 80, false)); instances.add(new DefaultServiceInstance("test-1", "service", "example.net", 80, false)); when(this.discovery.getServices()).thenReturn(singletonList("service")); when(this.discovery.getInstances("service")).thenReturn(instances); this.listener.onApplicationEvent(new HeartbeatEvent(new Object(), new Object())); StepVerifier.create(this.registry.getInstances("service")) .assertNext((a) -> assertThat(a.getRegistration().getName()).isEqualTo("service")) .assertNext((a) -> assertThat(a.getRegistration().getName()).isEqualTo("service")) .verifyComplete(); StepVerifier.create(this.registry.getInstances("ignored")) .assertNext((a) -> assertThat(a.getRegistration().getName()).isEqualTo("ignored")) .verifyComplete(); StepVerifier.create(this.registry.getInstances("different-source")) .assertNext((a) -> assertThat(a.getRegistration().getName()).isEqualTo("different-source")) .verifyComplete(); instances.remove(0); this.listener.onApplicationEvent(new HeartbeatEvent(new Object(), new Object())); StepVerifier.create(this.registry.getInstances("service")) .assertNext((a) -> assertThat(a.getRegistration().getName()).isEqualTo("service")) .verifyComplete(); StepVerifier.create(this.registry.getInstances("ignored")) .assertNext((a) -> assertThat(a.getRegistration().getName()).isEqualTo("ignored")) .verifyComplete(); StepVerifier.create(this.registry.getInstances("different-source")) .assertNext((a) -> assertThat(a.getRegistration().getName()).isEqualTo("different-source")) .verifyComplete(); // shouldn't deregister a second time this.listener.onApplicationEvent(new HeartbeatEvent(new Object(), new Object())); verify(this.registry, times(1)).deregister(any(InstanceId.class)); } @Test void should_not_throw_error_when_conversion_fails_and_proceed_with_next_instance() { when(this.discovery.getServices()).thenReturn(singletonList("service")); when(this.discovery.getInstances("service")) .thenReturn(asList(new DefaultServiceInstance("test-1", "service", "localhost", 80, false), new DefaultServiceInstance("error-1", "error", "localhost", 80, false))); this.listener.setConverter((instance) -> { if (instance.getServiceId().equals("error")) { throw new IllegalStateException("Test-Error"); } else { return new DefaultServiceInstanceConverter().convert(instance); } }); this.listener.onInstanceRegistered(new InstanceRegisteredEvent<>(new Object(), null)); StepVerifier.create(this.registry.getInstances()) .assertNext((a) -> assertThat(a.getRegistration().getName()).isEqualTo("service")) .verifyComplete(); } } ================================================ FILE: spring-boot-admin-server-cloud/src/test/java/de/codecentric/boot/admin/server/cloud/discovery/KubernetesServiceInstanceConverterTest.java ================================================ /* * 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.server.cloud.discovery; import java.net.URI; import java.util.Collections; import org.junit.jupiter.api.Test; import org.springframework.cloud.client.ServiceInstance; import org.springframework.cloud.kubernetes.commons.discovery.KubernetesDiscoveryProperties; import de.codecentric.boot.admin.server.domain.values.Registration; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; class KubernetesServiceInstanceConverterTest { @Test void convert_using_port_mgmt() { KubernetesDiscoveryProperties discoveryProperties = KubernetesDiscoveryProperties.DEFAULT; ServiceInstance service = mockServiceInstanceWithManagementPort( discoveryProperties.metadata().portsPrefix() + "management"); Registration registration = new KubernetesServiceInstanceConverter(discoveryProperties).convert(service); assertThat(registration.getManagementUrl()).isEqualTo("http://localhost:9080/actuator"); assertThat(registration.getHealthUrl()).isEqualTo("http://localhost:9080/actuator/health"); } @Test void fallback_for_port_mgmt() { KubernetesDiscoveryProperties discoveryProperties = KubernetesDiscoveryProperties.DEFAULT; ServiceInstance service = mockServiceInstanceWithManagementPort("management"); Registration registration = new KubernetesServiceInstanceConverter(discoveryProperties).convert(service); assertThat(registration.getManagementUrl()).isEqualTo("http://localhost:9080/actuator"); assertThat(registration.getHealthUrl()).isEqualTo("http://localhost:9080/actuator/health"); } private static ServiceInstance mockServiceInstanceWithManagementPort(String managementPortName) { ServiceInstance service = mock(ServiceInstance.class); when(service.getUri()).thenReturn(URI.create("http://localhost:80")); when(service.getServiceId()).thenReturn("test"); when(service.getMetadata()).thenReturn(Collections.singletonMap(managementPortName, "9080")); return service; } } ================================================ FILE: spring-boot-admin-server-cloud/src/test/resources/application.yml ================================================ server: shutdown: immediate ================================================ FILE: spring-boot-admin-server-cloud/src/test/resources/logback-test.xml ================================================ ================================================ FILE: spring-boot-admin-server-ui/.gitignore ================================================ /coverage/ ================================================ FILE: spring-boot-admin-server-ui/.npmrc ================================================ legacy-peer-deps=true ================================================ FILE: spring-boot-admin-server-ui/.nvmrc ================================================ 24.14.0 ================================================ FILE: spring-boot-admin-server-ui/.prettierrc.json ================================================ { "singleQuote": true, "plugins": ["@trivago/prettier-plugin-sort-imports"], "importOrder": ["(.*).css$","^[./]" ,"^@/components/(.*)$","^@/(.*)$"], "importOrderSeparation": true, "importOrderSortSpecifiers": true } ================================================ FILE: spring-boot-admin-server-ui/.storybook/main.js ================================================ const { mergeConfig } = require('vite'); const path = require('path'); const frontend = path.resolve(__dirname, '../src/main/frontend/'); module.exports = { stories: ['../src/main/frontend/**/*.stories.@(js|jsx|ts|tsx|mdx)'], addons: ['@storybook/addon-links', '@storybook/addon-docs'], framework: { name: '@storybook/vue3-vite', options: {}, }, async viteFinal(config) { config.plugins = config.plugins.filter((p) => p.name !== 'vue-docgen'); return mergeConfig(config, { resolve: { alias: { '@': frontend, }, extensions: ['.vue', '.js', '.json'], }, }); } }; ================================================ FILE: spring-boot-admin-server-ui/.storybook/preview-head.html ================================================ ================================================ FILE: spring-boot-admin-server-ui/.storybook/preview.js ================================================ import { setup } from '@storybook/vue3-vite'; import { initialize, mswLoader } from 'msw-storybook-addon'; import { createRouter, createWebHistory } from 'vue-router'; import './storybook.css'; import '@/index.css'; import components from '@/components'; import { createApplicationStore } from '@/composables/useApplicationStore'; import i18n from '@/i18n'; import applicationsEndpoint from '@/mocks/applications'; import mappingsEndpoint from '@/mocks/instance/mappings'; initialize(); const router = createRouter({ history: createWebHistory(), routes: [], }); const applicationStore = createApplicationStore(); setup((app) => { app.use(components); app.use(i18n); app.use(router); app.use(applicationStore); }); export const parameters = { actions: { argTypesRegex: '^on[A-Z].*' }, controls: { matchers: { color: /(background|color)$/i, date: /Date$/, }, }, msw: { handlers: { auth: null, others: [...mappingsEndpoint, ...applicationsEndpoint], }, }, loader: { '.js': 'jsx' }, }; export const preview = { parameters, loaders: [mswLoader], }; export const tags = ['autodocs']; ================================================ FILE: spring-boot-admin-server-ui/.storybook/storybook.css ================================================ :root { --main-50: 238, 252, 250; --main-100: 217, 247, 244; --main-200: 183, 240, 234; --main-300: 145, 232, 224; --main-400: 107, 224, 213; --main-500: 71, 217, 203; --main-600: 39, 190, 175; --main-700: 30, 144, 132; --main-800: 20, 97, 90; --main-900: 10, 47, 43; --bg-color-start: #91E8E0; --bg-color-stop: #1E9084; } ================================================ FILE: spring-boot-admin-server-ui/README.md ================================================ spring-boot-admin-server-ui ================================ ### Building this module The jar **can be built with Maven** with the maven-exec-plugin. To do this node.js and npm must be installed on your machine and be on your `$PATH`. If you don't want to use the maven exec run the following commands: ### Running Spring Boot Admin Server for development To develop the ui on a running server the best to do is 1. Running the ui build in watch mode so the resources get updated: ```shell npm run build:watch ``` 2. Run a Spring Boot Admin Server instances with the template-location and resource-location pointing to the build output and disable caching: ``` spring.boot.admin.ui.cache.no-cache: true spring.boot.admin.ui.resource-locations: file:../../spring-boot-admin-server-ui/target/dist/ spring.boot.admin.ui.template-location: file:../../spring-boot-admin-server-ui/target/dist/ spring.boot.admin.ui.cache-templates: false ``` Or just start the [spring-boot-admin-sample-servlet](../spring-boot-admin-samples/spring-boot-admin-sample-servlet) project using the `dev` profile. You also might want to use the `insecure` profile so you don't need to login. If you are using hierarchical projects (like the samples here), you have to point "Working directory" in your run config to the Project you are running. In IntelliJ IDEA you can simply use "$MODULE_DIR$". ### Build ```shell npm install npm run build ``` Repeated build with watching the files: ```shell npm run watch ``` ## Run tests ```shell npm run test ``` Repeated tests with watching the files: ```shell npm run test:watch ``` ## Run Storybook ```shell npm run storybook ``` For some recurring UI elements we have created components that should be reused. To see how they work and which options (props) they provide, use Storybook (see https://storybook.js.org/). ================================================ FILE: spring-boot-admin-server-ui/components.d.ts ================================================ /* eslint-disable */ // @ts-nocheck // Generated by unplugin-vue-components // Read more: https://github.com/vuejs/core/pull/3399 // biome-ignore lint: disable export {} /* prettier-ignore */ declare module 'vue' { export interface GlobalComponents { Button: typeof import('primevue/button')['default'] Column: typeof import('primevue/column')['default'] DataTable: typeof import('primevue/datatable')['default'] DatePicker: typeof import('primevue/datepicker')['default'] Dialog: typeof import('primevue/dialog')['default'] IconField: typeof import('primevue/iconfield')['default'] InputIcon: typeof import('primevue/inputicon')['default'] InputText: typeof import('primevue/inputtext')['default'] MultiSelect: typeof import('primevue/multiselect')['default'] RouterLink: typeof import('vue-router')['RouterLink'] RouterView: typeof import('vue-router')['RouterView'] } } ================================================ FILE: spring-boot-admin-server-ui/eslint.config.mjs ================================================ import { FlatCompat } from '@eslint/eslintrc'; import js from '@eslint/js'; import typescriptEslint from '@typescript-eslint/eslint-plugin'; import prettier from 'eslint-plugin-prettier'; import pluginVue from 'eslint-plugin-vue'; import { defineConfig, globalIgnores } from 'eslint/config'; import globals from 'globals'; import parser from 'vue-eslint-parser'; const compat = new FlatCompat({ recommendedConfig: js.configs.recommended, allConfig: js.configs.all, }); export default defineConfig([ ...pluginVue.configs['flat/recommended'], { languageOptions: { sourceType: 'module', globals: { ...globals.browser, ...globals.node, globalThis: false, window: false, }, parser: parser, ecmaVersion: 2020, parserOptions: { parser: '@typescript-eslint/parser', }, }, extends: compat.extends( 'plugin:@typescript-eslint/recommended', 'plugin:prettier/recommended', ), plugins: { prettier, '@typescript-eslint': typescriptEslint, }, rules: { 'no-console': [ 'error', { allow: ['warn', 'error'], }, ], 'vue/multi-word-component-names': 'off', 'vue/no-v-html': 'off', 'vue/no-reserved-component-names': 'off', 'vue/no-v-text-v-html-on-component': 'off', 'no-restricted-syntax': [ 'error', { message: 'Please do not commit this.', selector: 'MemberExpression > Identifier[name="logTestingPlaygroundURL"]', }, ], quotes: [ 2, 'single', { avoidEscape: true, }, ], }, }, globalIgnores(['**/dist']), { rules: { 'no-undef': 'off', '@typescript-eslint/no-explicit-any': 'off', }, }, ]); ================================================ FILE: spring-boot-admin-server-ui/package.json ================================================ { "name": "spring-boot-admin-server-ui", "private": true, "description": "Spring Boot Admin UI", "scripts": { "build": "vite build --emptyOutDir --sourcemap", "build:dev": "NODE_ENV=development vite build --emptyOutDir --sourcemap --mode development", "build:watch": "NODE_ENV=development vite build --emptyOutDir --sourcemap --watch --mode development", "dev": "vite", "test": "vitest run --silent", "test:watch": "vitest", "storybook": "storybook dev -p 6006", "build-storybook": "storybook build", "lint": "eslint --ext .js,.ts,.vue src/main/frontend", "lint:fix": "eslint --ext .js,.ts,.vue --fix src/main/frontend", "format": "prettier src/main/frontend --check", "format:fix": "prettier src/main/frontend --write" }, "dependencies": { "@fortawesome/fontawesome-svg-core": "7.2.0", "@fortawesome/free-brands-svg-icons": "7.2.0", "@fortawesome/free-regular-svg-icons": "7.2.0", "@fortawesome/free-solid-svg-icons": "7.2.0", "@fortawesome/vue-fontawesome": "3.1.3", "@headlessui/vue": "1.7.23", "@primeuix/themes": "^2.0.0", "@primevue/core": "^4.4.1", "@primevue/forms": "4.5.4", "@stekoe/vue-toast-notificationcenter": "https://github.com/SteKoe/vue-toast-notificationcenter/archive/refs/tags/1.0.0-RC5.tar.gz", "@tailwindcss/forms": "0.5.11", "@tailwindcss/typography": "0.5.19", "@types/sanitize-html": "^2.16.0", "ansi_up": "6.0.6", "autolinker": "4.1.5", "axios": "1.13.6", "chart.js": "4.5.1", "chartjs-adapter-moment": "1.0.1", "classnames": "2.5.1", "cronstrue": "^3.0.0", "d3": "^7.9.0", "d3-array": "3.2.4", "d3-axis": "3.0.0", "d3-brush": "3.0.0", "d3-scale": "4.0.2", "d3-selection": "3.0.0", "d3-shape": "3.2.0", "d3-time": "3.1.0", "event-source-polyfill": "1.0.31", "file-saver": "2.0.5", "fuse.js": "^7.0.0", "iso8601-duration": "2.1.3", "js-yaml": "^4.1.0", "lodash-es": "4.17.23", "mitt": "^3.0.0", "moment": "2.30.1", "popper.js": "1.16.1", "pretty-bytes": "7.1.0", "primelocale": "^2.1.7", "primevue": "^4.4.1", "qs": "^6.13.0", "random-string": "0.2.0", "react": "19.2.4", "react-dom": "19.2.4", "resize-observer-polyfill": "1.5.1", "rxjs": "7.8.2", "sanitize-html": "^2.17.0", "uuid": "13.0.0", "v3-infinite-loading": "1.3.2", "vue": "3.5.30", "vue-i18n": "11.3.0", "vue-router": "5.0.3", "vue3-click-away": "1.2.4" }, "devDependencies": { "@eslint/eslintrc": "^3.3.1", "@eslint/js": "^10.0.0", "@storybook/addon-docs": "^10.0.0", "@storybook/addon-links": "10.2.19", "@storybook/vue3-vite": "10.2.19", "@testing-library/jest-dom": "6.9.1", "@testing-library/user-event": "14.6.1", "@testing-library/vue": "8.1.0", "@trivago/prettier-plugin-sort-imports": "^6.0.0", "@types/d3": "^7.4.3", "@types/lodash-es": "^4.17.7", "@typescript-eslint/eslint-plugin": "^8.0.0", "@typescript-eslint/parser": "^8.0.0", "@vitejs/plugin-vue": "6.0.5", "@vue/eslint-config-typescript": "^14.0.0", "@vue/test-utils": "2.4.6", "autoprefixer": "10.4.27", "babel-loader": "10.1.1", "eslint": "^10.0.0", "eslint-config-prettier": "^10.0.0", "eslint-plugin-prettier": "^5.0.0", "eslint-plugin-storybook": "10.2.19", "eslint-plugin-vue": "^10.0.0", "globals": "^17.0.0", "happy-dom": "^20.0.0", "jsdom": "^28.0.0", "msw": "2.12.10", "msw-storybook-addon": "2.0.6", "postcss": "8.5.8", "prettier": "^3.0.3", "rollup-plugin-visualizer": "7.0.1", "sass": "^1.57.1", "storybook": "10.2.19", "storybook-vue3-router": "^7.0.0", "tailwindcss": "3.4.19", "ts-node-dev": "^2.0.0", "typescript": "^5.0.3", "unplugin-vue-components": "^31.0.0", "vite": "7.3.1", "vite-plugin-static-copy": "3.3.0", "vitest": "4.1.0", "vue-eslint-parser": "^10.0.0", "vue-loader": "17.4.2" }, "browserslist": [ "> .5%, last 2 versions" ], "engines": { "node": "24.14.0" }, "msw": { "workerDirectory": [ "./src/main/frontend/public", "src/main/frontend/public", "public" ] }, "overrides": { "esbuild": "0.27.4" } } ================================================ FILE: spring-boot-admin-server-ui/pom.xml ================================================ 4.0.0 spring-boot-admin-server-ui Spring Boot Admin Server UI Spring Boot Admin Server UI de.codecentric spring-boot-admin-build ${revision} ../spring-boot-admin-build de.codecentric spring-boot-admin-server org.springframework.boot spring-boot-starter-webmvc true org.projectlombok lombok true org.springframework.boot spring-boot-autoconfigure-processor true org.springframework.boot spring-boot-configuration-processor true com.google.code.findbugs jsr305 org.springframework.boot spring-boot-starter-test test org.springframework.boot spring-boot-starter-security test com.github.eirslett frontend-maven-plugin install-node-and-npm install-node-and-npm ${node.version} npm-install npm ci --prefer-offline --no-progress --no-audit --silent npm-build npm run build ${project.version} npm-lint npm verify run lint npm-test npm test run test org.apache.maven.plugins maven-resources-plugin woff ttf woff2 eot swf ico png true copy-resources process-resources copy-resources ${project.build.outputDirectory}/META-INF/spring-boot-admin-server-ui ${project.build.directory}/dist false ================================================ FILE: spring-boot-admin-server-ui/postcss.config.js ================================================ // Must be JavaScript as Storybook does not work otherwise. import autoprefixer from 'autoprefixer'; import tailwindcss from 'tailwindcss'; module.exports = { plugins: [tailwindcss, autoprefixer], }; ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/HealthStatus.ts ================================================ class HealthStatusEnum { static DOWN = 'DOWN'; static UP = 'UP'; static RESTRICTED = 'RESTRICTED'; static UNKNOWN = 'UNKNOWN'; static OUT_OF_SERVICE = 'OUT_OF_SERVICE'; static OFFLINE = 'OFFLINE'; } export const HealthStatus = Object.freeze(HealthStatusEnum); ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/components/ActionScope.ts ================================================ class ActionScopeEnum { static INSTANCE = 'instance'; static APPLICATION = 'application'; } export const ActionScope = Object.freeze(ActionScopeEnum); ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/components/__snapshots__/sba-formatted-obj.spec.ts.snap ================================================ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html exports[`SbaFormattedObject > processes json > autolinks urls 1`] = ` "
a:
  nested:
    object:
      key: This is a text with a link https://www.codecentric.de that is automagically created.
" `; exports[`SbaFormattedObject > processes json > does not render unsafe ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/components/sba-action-button-scoped.spec.tsx ================================================ /* * 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 userEvent from '@testing-library/user-event'; import { screen } from '@testing-library/vue'; import { beforeEach, describe, expect, it, vi } from 'vitest'; import SbaActionButtonScoped from './sba-action-button-scoped.vue'; import { render } from '@/test-utils'; describe('SbaActionButtonScoped', function () { const actionFn = vi.fn().mockResolvedValue([]); beforeEach(() => { render(SbaActionButtonScoped, { props: { instanceCount: 10, actionFn, }, slots: { default: 'Execute', }, }); }); it('should cal actionFn when confirmed', async () => { await userEvent.click( await screen.findByRole('button', { name: 'Execute' }), ); await userEvent.click( await screen.findByRole('button', { name: 'Confirm' }), ); expect(actionFn).toHaveBeenCalledTimes(1); }); }); ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/components/sba-action-button-scoped.stories.ts ================================================ /* * Copyright 2014-2019 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 SbaActionButtonScoped from './sba-action-button-scoped.vue'; import i18n from '@/i18n'; export default { component: SbaActionButtonScoped, title: 'Components/Buttons/Action Button Scoped', }; const TemplateWithProps = (args) => ({ components: { SbaActionButtonScoped }, setup() { return { args }; }, template: '', i18n, }); export const OneInstanceSuccessful = { render: TemplateWithProps, args: { instanceCount: 1, label: 'Push me!', actionFn() { return new Promise((resolve) => { setTimeout(() => resolve(), 2000); }); }, }, }; export const MultipleInstancesSuccessful = { render: TemplateWithProps, args: { ...OneInstanceSuccessful.args, instanceCount: 10, }, }; export const OneInstanceFailing = { render: TemplateWithProps, args: { ...OneInstanceSuccessful.args, instanceCount: 1, actionFn() { return new Promise((resolve, reject) => { setTimeout(() => { reject(); }, 2000); }); }, }, }; const TemplateWithSlot = (args) => ({ components: { SbaActionButtonScoped }, setup() { return { args }; }, template: ` `, i18n, }); export const SlottedOneInstanceSuccessful = { render: TemplateWithSlot, args: { instanceCount: 1, actionFn() { return new Promise((resolve) => { setTimeout(() => { resolve(); }, 2000); }); }, }, }; export const SlottedOneInstanceFailing = { render: TemplateWithSlot, args: { instanceCount: 1, actionFn() { return new Promise((resolve, reject) => { setTimeout(() => { reject(); }, 2000); }); }, }, }; ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/components/sba-action-button-scoped.vue ================================================ ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/components/sba-alert.stories.ts ================================================ /* * Copyright 2014-2019 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 SbaAlert, { Severity } from './sba-alert.vue'; export default { component: SbaAlert, title: 'Components/Alert', }; const Template = (args) => ({ components: { SbaAlert }, setup() { return { args }; }, template: '', }); export const AlertError = { render: Template, args: { title: 'Server error', error: new Error('Error reading from endpoint /applications'), severity: Severity.ERROR, }, }; export const AlertErrorWithoutTitle = { render: Template, args: { error: new Error('Error reading from endpoint /applications'), severity: Severity.ERROR, }, }; export const AlertWarning = { render: Template, args: { ...AlertError.args, title: 'Warning', error: new Error('The response took longer than expected.'), severity: 'WARN', }, }; export const AlertInfo = { render: Template, args: { ...AlertError.args, title: 'Hint', error: new Error('Check GC information as well!'), severity: 'INFO', }, }; export const AlertSuccess = { render: Template, args: { ...AlertError.args, title: 'Successful', error: new Error('Changes have been applied.'), severity: 'SUCCESS', }, }; ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/components/sba-alert.vue ================================================ ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/components/sba-button-group.stories.ts ================================================ /* * Copyright 2014-2019 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 SbaButtonGroup from './sba-button-group.vue'; import SbaButton from './sba-button.vue'; export default { component: SbaButtonGroup, title: 'Components/Buttons/Button Group', }; const Template = (args) => { return { components: { SbaButtonGroup, SbaButton }, setup() { return { args }; }, template: `${args.slot}`, }; }; export const OneButton = { render: Template, args: { slot: 'First', }, }; export const TwoButtons = { render: Template, args: { slot: ` First Second `, }, }; export const MoreButtons = { render: Template, args: { slot: ` First Second Third `, }, }; ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/components/sba-button-group.vue ================================================ ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/components/sba-button.stories.ts ================================================ /* * Copyright 2014-2019 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 SbaButton from './sba-button.vue'; export default { component: SbaButton, title: 'Components/Buttons/Button', }; const Template = (args) => { return { components: { SbaButton }, setup() { return { args }; }, template: `${args.label}`, }; }; export const DefaultButton = { render: Template, args: { label: 'Default button', }, }; export const PrimaryButton = { render: Template, args: { label: 'Primary button', primary: 'primary', }, }; export const DangerButton = { render: Template, args: { label: 'Danger button', class: 'is-danger', }, }; export const SuccessButton = { render: Template, args: { label: 'Danger button', class: 'is-success', }, }; const SizeTemplate = () => { return { components: { SbaButton }, template: ` button xs button sm button base `, }; }; export const ButtonSizes = { render: SizeTemplate, }; ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/components/sba-button.vue ================================================ ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/components/sba-checkbox.stories.ts ================================================ /* * Copyright 2014-2019 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 SbaCheckbox from './sba-checkbox.vue'; export default { component: SbaCheckbox, title: 'Components/Checkbox', }; const Template = (args) => { return { components: { SbaCheckbox }, setup() { return { args }; }, template: '{{args}}', }; }; export const OneButton = { render: Template, args: { modelValue: true, label: 'I am a label', }, }; ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/components/sba-checkbox.vue ================================================ ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/components/sba-confirm-button.spec.ts ================================================ /* * 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 userEvent from '@testing-library/user-event'; import { screen } from '@testing-library/vue'; import { beforeEach, describe, expect, it } from 'vitest'; import SbaConfirmButton from './sba-confirm-button.vue'; import { render } from '@/test-utils'; describe('SbaConfirmButton', function () { let emitted; beforeEach(() => { const vm = render(SbaConfirmButton, { slots: { default: 'Execute' } }); emitted = vm.emitted; }); it('should not emit when clicked once', async () => { await userEvent.click( await screen.findByRole('button', { name: 'Execute' }), ); expect(emitted().click).toBeUndefined(); }); it('should emit when click confirmed', async () => { await userEvent.click( await screen.findByRole('button', { name: 'Execute' }), ); await userEvent.click( await screen.findByRole('button', { name: 'Confirm' }), ); expect(emitted().click).toBeDefined(); }); }); ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/components/sba-confirm-button.stories.ts ================================================ /* * Copyright 2014-2019 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 SbaConfirmButton from './sba-confirm-button.vue'; export default { component: SbaConfirmButton, title: 'Components/Buttons/Confirm Button', }; const Template = (args) => { return { components: { SbaConfirmButton }, setup() { return { args }; }, template: `${args.label}`, }; }; export const DefaultButton = { render: Template, args: { label: 'Default confirm button', }, }; ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/components/sba-confirm-button.vue ================================================ ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/components/sba-dropdown/sba-dropdown-divider.vue ================================================ ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/components/sba-dropdown/sba-dropdown-item.vue ================================================ ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/components/sba-dropdown/sba-dropdown.stories.ts ================================================ import SbaDropdownDivider from '@/components/sba-dropdown/sba-dropdown-divider.vue'; import SbaDropdownItem from '@/components/sba-dropdown/sba-dropdown-item.vue'; import SbaDropdown from '@/components/sba-dropdown/sba-dropdown.vue'; export default { component: SbaDropdown, title: 'Components/Dropdown', }; const Template = (args) => { return { components: { SbaDropdown, SbaDropdownItem, SbaDropdownDivider, }, setup() { const click = () => alert('Clicked button!'); return { ...args, click }; }, template: `
On Click A link! router-link Active action Disabled action Button
`, }; }; export const Default = Template.bind({}); Default.args = {}; ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/components/sba-dropdown/sba-dropdown.vue ================================================ ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/components/sba-formatted-obj.spec.ts ================================================ import { describe } from 'vitest'; import SbaFormattedObj from '@/components/sba-formatted-obj.vue'; import { render } from '@/test-utils'; describe('SbaFormattedObject', () => { describe('processes simple text', () => { it('autolinks urls', async () => { render(SbaFormattedObj, { props: { value: 'This is a text with a link https://www.codecentric.de that is automagically created.', }, }); expect(document.body.innerHTML).toMatchSnapshot(); }); it('does render (safe) html', async () => { render(SbaFormattedObj, { props: { value: "This is a text with a link and a bold text bold.", }, }); expect(document.body.innerHTML).toMatchSnapshot(); }); it('does not render unsafe ", }, }); expect(document.body.innerHTML).toMatchSnapshot(); }); it('does not render unsafe in html', async () => { render(SbaFormattedObj, { props: { value: "", }, }); expect(document.body.innerHTML).toMatchSnapshot(); }); }); describe('processes json', () => { it('autolinks urls', async () => { render(SbaFormattedObj, { props: { value: { a: { nested: { object: { key: 'This is a text with a link https://www.codecentric.de that is automagically created.', }, }, }, }, }, }); expect(document.body.innerHTML).toMatchSnapshot(); }); it('does not render unsafe ", }, }, }, }, }, }); expect(document.body.innerHTML).toMatchSnapshot(); }); }); }); ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/components/sba-formatted-obj.vue ================================================ ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/components/sba-icon-button.stories.ts ================================================ /* * Copyright 2014-2019 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 SbaIconButton from './sba-icon-button.vue'; export default { component: SbaIconButton, title: 'Components/Buttons/Icon Button', }; const Template = (args) => { return { components: { SbaIconButton }, setup() { return { args }; }, template: ` ${args.label}`, }; }; export const DefaultButton = { render: Template, args: { icon: 'trash', title: 'unregister', }, }; ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/components/sba-icon-button.vue ================================================ ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/components/sba-input.stories.ts ================================================ /* * Copyright 2014-2019 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 SbaInput from './sba-input.vue'; export default { component: SbaInput, title: 'Components/Form/Input', }; const Template = (args) => { return { components: { SbaInput }, setup() { return { args }; }, template: ` ${args.slots} `, }; }; export const SimpleInput = { render: Template, args: { modelValue: 'Hello there!', }, }; export const InputWithError = { render: Template, args: { ...SimpleInput.args, error: 'Please provide a value.', }, }; export const InputWithHint = { render: Template, args: { ...SimpleInput.args, hint: 'This is a hint', }, }; export const InputWithPrepend = { render: Template, args: { ...SimpleInput.args, slots: ` `, }, }; export const InputWithAppend = { render: Template, args: { ...SimpleInput.args, inputClass: 'text-right', slots: ` `, }, }; export const Complex = { render: Template, args: { ...SimpleInput.args, label: 'Label', error: 'Please provide a value.', hint: 'I am a hint!', }, }; ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/components/sba-input.vue ================================================ ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/components/sba-key-value-table.stories.ts ================================================ /* * Copyright 2014-2019 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 SbaKeyValueTable from './sba-key-value-table.vue'; export default { component: SbaKeyValueTable, title: 'Components/Key-Value Table', }; const Template = (args) => { return { components: { SbaKeyValueTable }, setup() { return { args }; }, template: ` `, }; }; export const Default = { render: Template, args: { map: { key1: 'value 1', key2: 'value 2', key3: 'value 3', }, }, }; export const UsingSlots = { render: Template, args: { map: { ...Default.args.map, slotProps: 'Special Value', 'a key with sapces': { id: 'customId', value: 'Custom value', }, }, }, }; ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/components/sba-key-value-table.vue ================================================ ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/components/sba-loading-spinner.vue ================================================ ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/components/sba-modal.spec.ts ================================================ /* * Copyright 2014-2021 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 userEvent from '@testing-library/user-event'; import { screen, waitFor } from '@testing-library/vue'; import { describe, expect, it } from 'vitest'; import { render } from '../test-utils'; import SbaModal from './sba-modal.vue'; describe('sba-modal.vue', () => { it('modal is closed when close button is clicked', async () => { const { emitted } = render(SbaModal, { props: { modelValue: true }, slots: { body: '
test
' }, }); await waitFor(() => { expect(screen.queryByLabelText('Close')).toBeInTheDocument(); }); await userEvent.click(screen.getByLabelText('Close')); expect(emitted()['update:modelValue'][0]).toContain(false); }); }); ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/components/sba-modal.stories.ts ================================================ /* * Copyright 2014-2019 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 SbaModal from './sba-modal.vue'; import i18n from '@/i18n'; export default { component: SbaModal, title: 'Components/Modal', }; const Template = (args) => ({ components: { SbaModal }, setup() { return { args, }; }, template: ` `, i18n, }); export const ModalWithBody = { render: Template, args: { modelValue: true, body: 'I am a body', }, }; export const ModalWithHeaderAndFooter = { render: Template, args: { modelValue: true, header: 'You are awesome!', body: '

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla accumsan, metus ultrices eleifend gravida, nulla nunc varius lectus, nec rutrum justo nibh eu lectus.

', footer: 'Close me!', }, }; ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/components/sba-modal.vue ================================================ ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/components/sba-nav/sba-nav-dropdown.stories.ts ================================================ import SbaDropdownDivider from '@/components/sba-dropdown/sba-dropdown-divider.vue'; import SbaDropdownItem from '@/components/sba-dropdown/sba-dropdown-item.vue'; import SbaNavDropdown from '@/components/sba-nav/sba-nav-dropdown.vue'; import SbaNavItem from '@/components/sba-nav/sba-nav-item.vue'; export default { component: SbaNavDropdown, title: 'Components/Navbar/Nav/Dropdown', }; const Template = (args) => { return { components: { SbaDropdownItem, SbaNavDropdown, SbaNavItem, SbaDropdownDivider, }, setup() { return { ...args }; }, template: `
A Menu Item A Menu Item A Menu Item A Menu Item A Menu Item A Menu Item A Menu Item A Menu Item A Menu Item A Menu Item Signed in as:   Logout
`, }; }; export const Default = Template.bind({}); Default.args = {}; ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/components/sba-nav/sba-nav-dropdown.vue ================================================ ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/components/sba-nav/sba-nav-item.stories.ts ================================================ import SbaNavItem from '@/components/sba-nav/sba-nav-item.vue'; export default { component: SbaNavItem, title: 'Components/Navbar/Nav/Item', }; const Template = (args) => { return { components: { SbaNavItem, }, setup() { return { ...args }; }, template: ` Just an item Link Router Link `, }; }; export const Default = Template.bind({}); Default.args = {}; ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/components/sba-nav/sba-nav-item.vue ================================================ ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/components/sba-navbar/sba-navbar-brand.vue ================================================ ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/components/sba-navbar/sba-navbar-nav.vue ================================================ ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/components/sba-navbar/sba-navbar-toggle.stories.ts ================================================ import { ref } from 'vue'; import SbaNavbarToggle from '@/components/sba-navbar/sba-navbar-toggle.vue'; export default { component: SbaNavbarToggle, title: 'Components/Navbar/Toggle', }; const Template = (args) => { return { components: { SbaNavbarToggle, }, setup() { const showMenu = ref(false); // eslint-disable-next-line no-console const handleClick = (e) => console.log(e); return { ...args, showMenu, onClick: handleClick }; }, template: ` `, }; }; export const Default = Template.bind({}); Default.args = {}; ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/components/sba-navbar/sba-navbar-toggle.vue ================================================ ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/components/sba-navbar/sba-navbar.stories.ts ================================================ import { onUnmounted, ref } from 'vue'; import SbaNavbar from './sba-navbar.vue'; import SbaDropdownDivider from '@/components/sba-dropdown/sba-dropdown-divider.vue'; import SbaDropdownItem from '@/components/sba-dropdown/sba-dropdown-item.vue'; import SbaNavDropdown from '@/components/sba-nav/sba-nav-dropdown.vue'; import SbaNavItem from '@/components/sba-nav/sba-nav-item.vue'; import SbaNavbarNav from '@/components/sba-navbar/sba-navbar-nav.vue'; export default { component: SbaNavbar, title: 'Components/Navbar', }; const Template = (args) => { return { components: { SbaNavbar, SbaDropdownItem, SbaNavDropdown, SbaNavItem, SbaNavbarNav, SbaDropdownDivider, }, setup() { const random = ref(0); const intervalId = setInterval(() => { random.value = Math.round(Math.random() * 10); }, 1000); onUnmounted(() => clearInterval(intervalId)); // ✅ Cleanup return { ...args, random, username: 'Admin' }; }, template: ` Wallboard Applications About UP: {{ random }} A Menu Item A Menu Item A Menu Item A Menu Item A Menu Item A Menu Item A Menu Item A Menu Item A Menu Item A Menu Item 한국어 简体中文 繁體中文 português (Brasil) A Menu Item Signed in as:   Logout

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Curabitur mollis vulputate elit eget hendrerit. Suspendisse pretium odio sit amet nibh rhoncus, non cursus magna mollis. Mauris blandit aliquet consequat. Donec et volutpat mauris, in volutpat tortor. Ut sed nisl sed est sodales interdum eget ac sem. Mauris neque mauris, fringilla quis dictum quis, aliquam ac ipsum. Phasellus congue eu massa pharetra finibus. Aenean massa libero, egestas sit amet sem consequat, vestibulum porta sem. Duis nisi nibh, gravida a molestie a, gravida a turpis. Quisque sagittis nisl eu ultricies mollis. Etiam rutrum faucibus dolor faucibus hendrerit. Curabitur volutpat fermentum dolor, nec consectetur urna blandit non. Proin sed eleifend tellus, aliquam semper mauris.

Duis sapien diam, tempus vel quam rhoncus, commodo pulvinar magna. Suspendisse condimentum, lectus quis varius iaculis, nibh lectus mattis neque, aliquam ornare mauris diam quis leo. Nunc efficitur mollis accumsan. Fusce eget lacinia nisl. Sed congue leo sem, non fermentum mauris blandit ut. Nulla aliquam mattis erat at venenatis. Donec nec nisl nec tortor convallis rhoncus. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Maecenas id tempor orci. Quisque sit amet nisi quis neque maximus facilisis quis rutrum mi. Aenean vel blandit mauris. In quis lobortis diam. Quisque fringilla mauris sit amet magna aliquet, vulputate hendrerit sapien blandit.

`, }; }; export const Default = Template.bind({}); Default.args = { brand: 'Spring Boot Admin', }; ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/components/sba-navbar/sba-navbar.vue ================================================ ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/components/sba-pagination-nav.spec.ts ================================================ /* * Copyright 2014-2019 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 { screen } from '@testing-library/vue'; import { describe, expect, it } from 'vitest'; import SbaPaginationNav from '@/components/sba-pagination-nav.vue'; import { render } from '@/test-utils'; describe('sba-pagination-nav.vue', () => { it('should show a button when page count is 0', async () => { render(SbaPaginationNav, { props: { pageCount: 0, }, }); const prevPage = screen.getByRole('button', { name: 'Go to previous page', }); expect(prevPage).toBeDisabled(); const nextPage = screen.getByRole('button', { name: 'Go to next page' }); expect(nextPage).toBeDisabled(); }); it('should show a button when page count is 1', async () => { render(SbaPaginationNav, { props: { pageCount: 1, }, }); const prevPage = screen.getByRole('button', { name: 'Go to previous page', }); expect(prevPage).toBeDisabled(); const nextPage = screen.getByRole('button', { name: 'Go to next page' }); expect(nextPage).toBeDisabled(); screen.getByRole('button', { name: 'Page 1, current page' }); }); it('should show first and last page when page count is 12 including intermediate pages', async () => { render(SbaPaginationNav, { props: { pageCount: 11, modelValue: 6, }, }); expect( screen.queryByRole('button', { name: 'Go to previous page' }), ).not.toBeDisabled(); expect( screen.queryByRole('button', { name: 'Go to page 1' }), ).toBeVisible(); expect( screen.queryByRole('button', { name: 'Go to page 2' }), ).not.toBeInTheDocument(); expect( screen.queryByRole('button', { name: 'Go to page 3' }), ).not.toBeInTheDocument(); expect( screen.queryByRole('button', { name: 'Go to page 4' }), ).toBeVisible(); expect( screen.queryByRole('button', { name: 'Go to page 5' }), ).toBeVisible(); const selectedButton = screen.getByRole('button', { name: 'Page 6, current page', }); expect(selectedButton).toBeVisible(); expect(selectedButton).toBeDisabled(); expect(selectedButton).toHaveClass('is-active'); expect( screen.queryByRole('button', { name: 'Go to page 7' }), ).toBeVisible(); expect( screen.queryByRole('button', { name: 'Go to page 8' }), ).toBeVisible(); expect( screen.queryByRole('button', { name: 'Go to page 9' }), ).not.toBeInTheDocument(); expect( screen.queryByRole('button', { name: 'Go to page 10' }), ).not.toBeInTheDocument(); expect( screen.queryByRole('button', { name: 'Go to page 11' }), ).toBeVisible(); expect( screen.queryByRole('button', { name: 'Go to previous page' }), ).not.toBeDisabled(); }); }); ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/components/sba-pagination-nav.stories.ts ================================================ /* * Copyright 2014-2019 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 SbaPaginationNav from './sba-pagination-nav.vue'; import i18n from '@/i18n'; export default { component: SbaPaginationNav, title: 'Components/Pagination', }; const Template = (args) => { return { components: { SbaPaginationNav }, setup() { return { args }; }, methods: { change($event) { this.current = $event; }, }, data() { return { current: 1, }; }, template: ` `, i18n, }; }; export const NoPages = { render: Template, args: { pageCount: 0, modelValue: 1, }, }; export const OnePage = { render: Template, args: { pageCount: 1, }, }; export const ManyPages = { render: Template, args: { modelValue: 1, pageCount: 12, }, }; ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/components/sba-pagination-nav.vue ================================================ ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/components/sba-panel.stories.ts ================================================ import SbaPanel from './sba-panel.vue'; export default { component: SbaPanel, title: 'Components/Panel', }; const Template = (args) => { return { components: { SbaPanel }, setup() { return { args }; }, methods: { onClose(event) { alert('Close clicked! ' + JSON.stringify(event)); }, }, template: ` `, }; }; export const WithTitle = { render: Template, args: { title: 'Title', 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. Proin vehicula, lacus vehicula lacinia tristique, dui turpis sodales orci, ac pretium nibh nisl sed est. Vivamus pharetra tristique mi. Nam libero lorem, pharetra eu sagittis ac, elementum quis quam. Integer sed feugiat dui. In euismod, ante id lobortis vehicula, libero leo pellentesque orci, ac consectetur leo sem nec erat. Nunc dapibus eu est at pretium. Curabitur eget elementum risus.

Aenean convallis tempus dolor. Mauris eget ipsum tortor. Mauris congue facilisis eros. Phasellus tortor urna, semper congue nisl maximus, pulvinar luctus justo. Vestibulum dignissim malesuada magna, imperdiet blandit est commodo vitae. Sed a suscipit nisi, non imperdiet orci. Nulla rutrum ligula ut velit ultrices, non tincidunt lacus elementum. Etiam vitae blandit arcu, nec congue felis. Praesent fermentum vehicula risus, vitae finibus felis vestibulum ac. In ullamcorper tellus vitae ante euismod, eget consectetur nibh efficitur. Donec iaculis placerat erat a rutrum. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Donec semper erat nec ipsum molestie, eu commodo dui lobortis.

Aenean convallis tempus dolor. Mauris eget ipsum tortor. Mauris congue facilisis eros. Phasellus tortor urna, semper congue nisl maximus, pulvinar luctus justo. Vestibulum dignissim malesuada magna, imperdiet blandit est commodo vitae. Sed a suscipit nisi, non imperdiet orci. Nulla rutrum ligula ut velit ultrices, non tincidunt lacus elementum. Etiam vitae blandit arcu, nec congue felis. Praesent fermentum vehicula risus, vitae finibus felis vestibulum ac. In ullamcorper tellus vitae ante euismod, eget consectetur nibh efficitur. Donec iaculis placerat erat a rutrum. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Donec semper erat nec ipsum molestie, eu commodo dui lobortis.

`, }, }; export const WithTable = { render: Template, args: { ...WithTitle.args, slot: `
Count
97
Time total
0.4890s
Max duration
0.0040s
`, }, }; export const Closable = { render: Template, args: { ...WithTitle.args, closeable: true, }, }; export const ClosableWithActions = { render: Template, args: { ...WithTitle.args, closeable: true, actions: 'Action Slot', }, }; export const StickyHeader = { render: Template, args: { ...WithTitle.args, closeable: true, }, }; export const NoTitle = { render: Template, args: { ...WithTitle.args, title: undefined, }, }; export const WithTitleAndFooter = { render: Template, args: { ...WithTitle.args, footer: 'Hello from the footer!', }, }; export const LoadingContent = { render: Template, args: { ...WithTitle.args, loading: true, }, }; ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/components/sba-panel.vue ================================================ ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/components/sba-select.stories.ts ================================================ /* * Copyright 2014-2019 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 SbaSelect from './sba-select.vue'; export default { component: SbaSelect, title: 'Components/Form/Select', }; const Template = (args) => { return { components: { SbaSelect }, setup() { return { args }; }, template: ` `, }; }; export const SimpleSelect = { render: Template, args: { modelValue: 'berlin', options: [ { value: 'beer', label: 'Beer' }, { value: 'water', label: 'Water' }, { value: 'wine', label: 'Wine' }, ], }, }; ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/components/sba-select.vue ================================================ ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/components/sba-status-badge.spec.ts ================================================ /* * 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 { render } from '@testing-library/vue'; import { describe, it } from 'vitest'; import { HealthStatus } from '../HealthStatus'; import sbaStatusBadge from './sba-status-badge.vue'; describe('sba-status-badge.vue', () => { it('should accept valid HealthStatus', () => { render(sbaStatusBadge, { props: { status: HealthStatus.DOWN, }, }); }); it('should accept String', () => { render(sbaStatusBadge, { props: { status: 'down', }, }); }); }); ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/components/sba-status-badge.vue ================================================ ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/components/sba-status.spec.ts ================================================ /* * 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 { screen } from '@testing-library/vue'; import moment from 'moment'; import { describe, expect, it } from 'vitest'; import sbaStatus from './sba-status.vue'; import { render } from '@/test-utils'; moment.now = () => +new Date(1318781879406); describe('application-status', () => { function testSnapshotForStatus(status: string, date?: number) { render(sbaStatus, { propsData: { status, date, }, }); } it('should match the snapshot with status UP with Timestamp', async () => { const status = 'UP'; testSnapshotForStatus(status, 1318781000000); expect(await screen.findByLabelText(status)).toBeDefined(); }); it('should match the snapshot with status RESTRICTED', async () => { const status = 'RESTRICTED'; testSnapshotForStatus(status); expect(await screen.findByLabelText(status)).toBeDefined(); }); it('should match the snapshot with status OUT_OF_SERVICE', async () => { const status = 'OUT_OF_SERVICE'; testSnapshotForStatus(status); expect(await screen.findByLabelText(status)).toBeDefined(); }); it('should match the snapshot with status DOWN', async () => { const status = 'DOWN'; testSnapshotForStatus(status); expect(await screen.findByLabelText(status)).toBeDefined(); }); it('should match the snapshot with status UNKNOWN', async () => { const status = 'UNKNOWN'; testSnapshotForStatus(status); expect(await screen.findByLabelText(status)).toBeDefined(); }); it('should match the snapshot with status OFFLINE', async () => { const status = 'OFFLINE'; testSnapshotForStatus(status); expect(await screen.findByLabelText(status)).toBeDefined(); }); it('should match the snapshot with custom status', async () => { const status = '?'; testSnapshotForStatus(status); expect(await screen.findByLabelText(status)).toBeDefined(); }); }); ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/components/sba-status.stories.ts ================================================ import SbaStatus from './sba-status.vue'; export default { component: SbaStatus, title: 'Components/Status', }; const Template = (args) => ({ components: { SbaStatus }, setup() { return { args }; }, template: '', }); export const Status = { render: Template, }; export const StatusUp = { render: Template, args: { date: Date.now(), status: 'UP', }, }; export const StatusRestricted = { render: Template, args: { ...StatusUp.args, status: 'RESTRICTED', }, }; export const StatusOos = { render: Template, args: { ...StatusUp.args, status: 'OUT_OF_SERVICE', }, }; export const StatusDown = { render: Template, args: { ...StatusUp.args, status: 'DOWN', }, }; export const StatusOffline = { render: Template, args: { ...StatusUp.args, status: 'OFFLINE', }, }; export const StatusUnknown = { render: Template, args: { ...StatusUp.args, status: 'UNKNOWN', }, }; ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/components/sba-status.vue ================================================ ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/components/sba-sticky-subnav.vue ================================================ ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/components/sba-tag.stories.ts ================================================ import SbaTag from './sba-tag.vue'; export default { component: SbaTag, title: 'Components/Tag', argTypes: { value: { description: 'A value to show', example: 'asd', control: 'text', }, label: { description: 'An optional label', example: 'asd', control: 'text', }, small: { description: 'A single value to show', control: 'boolean', }, }, }; const Template = (args) => ({ components: { SbaTag }, setup() { return { args }; }, template: '', }); export const Tag = { render: Template, args: { value: 'I am a tag', }, }; ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/components/sba-tag.vue ================================================ ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/components/sba-tags.stories.ts ================================================ import SbaTags from './sba-tags.vue'; export default { component: SbaTags, title: 'Components/Tags', }; const Template = (args) => ({ components: { SbaTags }, setup() { return { args }; }, template: '', }); export const SingleTag = { render: Template, args: { tags: { 'This is a key': 'This a value', }, }, }; export const SingleTagSmall = { render: Template, args: { small: true, tags: { 'This is a key': 'This a value', }, }, }; export const MultipleTags = { render: Template, args: { tags: { 'This is a key': 'This a value', simpleKey: 'value', }, }, }; ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/components/sba-tags.vue ================================================ ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/components/sba-time-ago.spec.ts ================================================ /* * 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 { screen } from '@testing-library/vue'; import moment from 'moment'; import { describe, expect, it } from 'vitest'; import sbaTimeAgo from './sba-time-ago.vue'; import { render } from '@/test-utils'; // Sun Oct 16 2011 18:00:00 GMT+0200 (Mitteleuropäische Sommerzeit) moment.now = () => Date.now(); describe('time-ago', () => { it('should show short period', async () => { render(sbaTimeAgo, { propsData: { date: moment().subtract(15, 'minutes').utc(), }, }); expect(await screen.findByText('15m')); }); it('multiple minutes', async () => { render(sbaTimeAgo, { propsData: { date: moment().subtract(800, 'minutes').utc(), }, }); expect(await screen.findByText('13h')); }); it('multiple days', async () => { render(sbaTimeAgo, { propsData: { date: moment().subtract(5, 'days'), }, }); expect(await screen.findByText('5d')); }); }); ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/components/sba-time-ago.vue ================================================ /* * 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. */ ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/components/sba-toggle-scope-button.spec.ts ================================================ /* * 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 userEvent from '@testing-library/user-event'; import { screen } from '@testing-library/vue'; import { beforeEach, describe, expect, it } from 'vitest'; import { render } from '../test-utils'; import { ActionScope } from './ActionScope'; import SbaToggleScopeButton from './sba-toggle-scope-button.vue'; describe('SbaToggleScopeButton', function () { let wrapper; beforeEach(() => { wrapper = render(SbaToggleScopeButton, { props: { instanceCount: 2, modelValue: ActionScope.INSTANCE }, }); }); it('should emit changed scope when clicked', async () => { await userEvent.click( await screen.findByRole('button', { name: /instance/i }), ); expect(wrapper.emitted()['update:modelValue'][0]).toEqual(['application']); }); it('should toggle the scope when clicked twice', async () => { await userEvent.click( await screen.findByRole('button', { name: /instance/i }), ); expect(wrapper.emitted()['update:modelValue'][0]).toEqual(['application']); await wrapper.rerender({ modelValue: ActionScope.APPLICATION }); await userEvent.click( await screen.findByRole('button', { name: /application/i }), ); expect(wrapper.emitted()['update:modelValue'][1]).toEqual(['instance']); }); }); ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/components/sba-toggle-scope-button.stories.ts ================================================ import { reactive } from 'vue'; import { ActionScope } from './ActionScope'; import SbaToggleScopeButton from './sba-toggle-scope-button.vue'; export default { component: SbaToggleScopeButton, title: 'Components/Buttons/Toggle Scope Button', }; const Template = (args) => ({ components: { SbaToggleScopeButton }, setup() { return reactive({ args }); }, template: '', }); export const Default = { render: Template, args: { modelValue: ActionScope.INSTANCE, instanceCount: 2, showInfo: true, }, }; ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/components/sba-toggle-scope-button.vue ================================================ ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/components/sba-wave.vue ================================================ ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/components/table.stories.ts ================================================ /* * Copyright 2014-2019 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. */ export default { title: 'Components/Table', }; const TemplateWithProps = () => ({ template: `
Anwendung Instanzen Zeit Ereignis
spring-boot-admin-sample-servlet 25b07dc98984 04/29/2022 16:03:45.778 INFO_CHANGED
spring-boot-admin-sample-servlet 25b07dc98984 04/29/2022 16:03:45.776 ENDPOINTS_DETECTED
spring-boot-admin-sample-servlet 25b07dc98984 04/29/2022 16:03:45.768 STATUS_CHANGED (UP)
spring-boot-admin-sample-servlet 25b07dc98984 04/29/2022 16:03:45.755 REGISTERED
spring-boot-admin-sample-servlet af578c480d41 04/29/2022 16:03:44.666 INFO_CHANGED
spring-boot-admin-sample-servlet af578c480d41 04/29/2022 16:03:44.662 ENDPOINTS_DETECTED
spring-boot-admin-sample-servlet af578c480d41 04/29/2022 16:03:44.657 STATUS_CHANGED (UP)
spring-boot-admin-sample-servlet af578c480d41 04/29/2022 16:03:44.639 REGISTERED
spring-boot-admin-sample-servlet af578c480d41 04/29/2022 16:03:39.801 DEREGISTERED
spring-boot-admin-sample-servlet 25b07dc98984 04/29/2022 16:03:39.799 DEREGISTERED
spring-boot-admin-sample-servlet 25b07dc98984 04/29/2022 15:51:45.716 INFO_CHANGED
spring-boot-admin-sample-servlet 25b07dc98984 04/29/2022 15:51:45.712 ENDPOINTS_DETECTED
spring-boot-admin-sample-servlet 25b07dc98984 04/29/2022 15:51:45.702 STATUS_CHANGED (UP)
spring-boot-admin-sample-servlet 25b07dc98984 04/29/2022 15:51:45.690 REGISTERED
spring-boot-admin-sample-servlet af578c480d41 04/29/2022 15:51:44.591 INFO_CHANGED
spring-boot-admin-sample-servlet af578c480d41 04/29/2022 15:51:44.586 ENDPOINTS_DETECTED
spring-boot-admin-sample-servlet af578c480d41 04/29/2022 15:51:44.578 STATUS_CHANGED (UP)
spring-boot-admin-sample-servlet af578c480d41 04/29/2022 15:51:44.564 REGISTERED
spring-boot-admin-sample-servlet 25b07dc98984 04/29/2022 15:51:40.611 DEREGISTERED
spring-boot-admin-sample-servlet af578c480d41 04/29/2022 15:51:39.600 DEREGISTERED
spring-boot-admin-sample-servlet af578c480d41 04/29/2022 15:46:54.948 INFO_CHANGED
spring-boot-admin-sample-servlet af578c480d41 04/29/2022 15:46:54.943 ENDPOINTS_DETECTED
spring-boot-admin-sample-servlet af578c480d41 04/29/2022 15:46:54.935 STATUS_CHANGED (UP)
spring-boot-admin-sample-servlet af578c480d41 04/29/2022 15:46:54.606 REGISTERED
spring-boot-1.5 5914e4fcb78b 04/29/2022 14:14:56.321 ENDPOINTS_DETECTED
`, }); export const Default = { render: TemplateWithProps, args: { startColor: '#ff0000', stopColor: '#00fa73', }, }; ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/components.d.ts ================================================ /* eslint-disable */ // @ts-nocheck // Generated by unplugin-vue-components // Read more: https://github.com/vuejs/core/pull/3399 // biome-ignore lint: disable export {}; /* prettier-ignore */ declare module 'vue' { export interface GlobalComponents { Button: typeof import('primevue/button')['default'] Column: typeof import('primevue/column')['default'] DataTable: typeof import('primevue/datatable')['default'] DatePicker: typeof import('primevue/datepicker')['default'] Dialog: typeof import('primevue/dialog')['default'] IconField: typeof import('primevue/iconfield')['default'] InputIcon: typeof import('primevue/inputicon')['default'] InputText: typeof import('primevue/inputtext')['default'] MultiSelect: typeof import('primevue/multiselect')['default'] RouterLink: typeof import('vue-router')['RouterLink'] RouterView: typeof import('vue-router')['RouterView'] Sidebar: typeof import('primevue/sidebar')['default'] TreeTable: typeof import('primevue/treetable')['default'] } } ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/composables/ViewRegistry.ts ================================================ import { debounce } from 'lodash-es'; import ViewRegistry from '../viewRegistry'; import eventBus from '@/services/bus'; export const CUSTOM_ROUTES_ADDED_EVENT = 'custom-routes-added'; let viewRegistry: ViewRegistry; export function createViewRegistry() { if (viewRegistry) throw new Error('ViewRegistry already created!'); viewRegistry = new ViewRegistry(); return viewRegistry; } const emitCustomRouteAddedEvent = debounce(() => { eventBus.emit(CUSTOM_ROUTES_ADDED_EVENT); }); export function useViewRegistry() { return { views: viewRegistry.views, setGroupIcon(name, icon) { viewRegistry.setGroupIcon(name, icon); }, addView(viewToAdd) { const view = viewRegistry.addView(viewToAdd)[0]; if (view.parent) { viewRegistry.router.addRoute(view.parent, { path: view.path, name: view.name, component: view.component, props: view.props, meta: { view: view }, }); } else { viewRegistry.router.addRoute({ path: view.path, name: view.name, component: view.component, props: view.props, meta: { view: view }, }); } emitCustomRouteAddedEvent(); }, getViewByName(name: string) { return viewRegistry.views.find((view) => view.name === name); }, }; } ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/composables/useApplicationStore.ts ================================================ import { Ref, ref } from 'vue'; import ApplicationStore from '../store'; import Application from '@/services/application'; let applicationStore: ApplicationStore | null = null; const applications: Ref = ref([]); const applicationsInitialized = ref(false); const error = ref(null); let listenersRegistered = false; export function createApplicationStore() { if (applicationStore) throw new Error('ApplicationStore already created!'); applicationStore = new ApplicationStore(); return applicationStore; } type ApplicationStoreValue = { applications: Ref; applicationsInitialized: Ref; error: Ref; applicationStore: ApplicationStore; findApplicationByInstanceId: (instanceId: string) => Ref; }; export function useApplicationStore(): ApplicationStoreValue { if (!applicationStore) { throw new Error( 'ApplicationStore not created yet! Call createApplicationStore() first.', ); } if (!listenersRegistered) { applicationStore.addEventListener('connected', () => { applicationsInitialized.value = true; error.value = null; }); applicationStore.addEventListener('changed', (newApplications) => { applicationsInitialized.value = true; applications.value = newApplications; error.value = null; }); applicationStore.addEventListener('error', (errorResponse) => { applicationsInitialized.value = true; error.value = errorResponse; }); applicationStore.addEventListener('removed', () => { applicationsInitialized.value = false; }); listenersRegistered = true; } return { applications, applicationsInitialized, error, applicationStore, }; } ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/composables/useClassnameShortener.spec.ts ================================================ import { describe, expect, it } from 'vitest'; import { useClassnameShortener } from '@/composables/useClassnameShortener'; describe('useClassnameShortener', () => { const { truncateClassname } = useClassnameShortener(); it.each` given | expected ${'com.example.MyService'} | ${'com.example.MyService'} ${'MyTopLevelClass'} | ${'MyTopLevelClass'} ${'com.example.Outer$Inner'} | ${'com.example.Outer$Inner'} ${'org.springframework.boot.web.embedded.tomcat.TomcatWebServer'} | ${'o.s.b.w.embedded.tomcat.TomcatWebServer'} `('$given => $expected', ({ given, expected }) => { expect(truncateClassname(given)).toEqual(expected); }); }); ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/composables/useClassnameShortener.ts ================================================ type Options = { maxLen?: number; }; export const useClassnameShortener = (options?: Options) => { return { truncateClassname: (fqcn: string) => abbreviateLoggerName(fqcn, options?.maxLen), }; }; /** * Abbreviate a fully qualified Java class name like Spring Boot logging does. * * Examples: * - org.springframework.boot.web.embedded.tomcat.TomcatWebServer * -> o.s.b.w.e.tomcat.TomcatWebServer * * - com.example.very.long.package.name.MyService with maxLen=30 * -> c.e.v.l.p.name.MyService (then drops leftmost segments if needed) * * @param fqcn Fully-qualified class name (e.g., "org.example.FooBar") * @param maxLen Maximum length of the resulting string * @returns Abbreviated name */ function abbreviateLoggerName(fqcn: string, maxLen = 40): string { const input = (fqcn || '').trim(); if (!input) return ''; if (input.length <= maxLen) return input; // already fits const parts = input.split('.'); if (parts.length === 1) { // No package, just crop if needed return input.slice(input.length - maxLen); } const simpleName = parts[parts.length - 1]; let packages = parts.slice(0, -1); // 1. Start with full name let out = packages.join('.') + '.' + simpleName; // 2. Abbreviate left-to-right for (let i = 0; i < packages.length && out.length > maxLen; i++) { packages[i] = packages[i].charAt(0); // abbreviate one package out = packages.join('.') + '.' + simpleName; if (out.length <= maxLen) return out; } // 3. If still too long, drop leftmost package segments while (packages.length > 0 && out.length > maxLen) { packages = packages.slice(1); out = packages.join('.') + (packages.length ? '.' : '') + simpleName; if (out.length <= maxLen) return out; } // 4. Fallback: left-crop return out.slice(out.length - maxLen); } ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/composables/useDateTimeFormatter.ts ================================================ type DateFormatterOptions = { dateTimeFormat?: Intl.DateTimeFormatOptions; timeFormat?: Intl.DateTimeFormatOptions; }; export const useDateTimeFormatter = (options?: DateFormatterOptions) => { const userLocale = navigator.languages ? navigator.languages[0] : navigator.language; const dateTimeFormat = new Intl.DateTimeFormat(userLocale, { ...{ dateStyle: 'medium', timeStyle: 'medium' }, ...(options?.dateTimeFormat ?? {}), }); return { formatDateTime: (date: Date) => { return dateTimeFormat.format(date); }, }; }; ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/directives/on-resize.ts ================================================ /* * 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 ResizeObserver from 'resize-observer-polyfill'; const observers = new WeakMap(); const mounted = (el, binding) => { beforeUnmount(el); const observer = new ResizeObserver(binding.value); observer.observe(el); observers.set(el, observer); }; const beforeUnmount = (el) => { const observer = observers.get(el); if (observer) { observer.disconnect(); observers.delete(el); } }; export default { mounted, update(el, binding) { if (binding.value === binding.oldValue) { return; } mounted(el, binding); }, beforeUnmount, }; ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/directives/popper.ts ================================================ /* * 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 Popper from 'popper.js'; const poppers = new WeakMap(); const mounted = (el, binding) => { const reference = typeof binding.value === 'string' ? document.getElementById(binding.value) : binding.value; if (reference) { const popper = new Popper(reference, el); poppers.set(el, popper); } }; const beforeUnmount = (el) => { const popper = poppers.get(el); if (popper) { popper.destroy(el); } }; export default { mounted, update(el, binding) { if (binding.value === binding.oldValue) { return; } mounted(el, binding); }, beforeUnmount, }; ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/directives/sticks-below.ts ================================================ /* * 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. */ const mounted = (el, binding) => { if (!binding.value) { return; } const targetElement = document.querySelector(binding.value); if (targetElement) { const clientRect = targetElement.getBoundingClientRect(); const top = clientRect.height + clientRect.top; el.style.top = `${top}px`; el.style.position = 'sticky'; } }; export default { mounted, update(el, binding) { if (binding.value === binding.oldValue) { return; } mounted(el, binding); }, }; ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/global.d.ts ================================================ import { Component, Raw, RenderFunction } from 'vue'; import ViewRegistry from '@/viewRegistry'; export {}; declare global { type ApplicationStream = { data: any; } & MessageEvent; interface Window { SBA: SBASettings; } type Extension = { resourcePath: string; resourceLocation: string; }; type UITheme = { color: string; backgroundEnabled: boolean; palette?: { shade50: string; shade100: string; shade200: string; shade300: string; shade400: string; shade500: string; shade600: string; shade700: string; shade800: string; shade900: string; }; }; type PollTimer = { cache: number; datasource: number; gc: number; process: number; memory: number; threads: number; logfile: number; }; type ViewSettings = { name: string; enabled: boolean; }; type ExternalView = { label: string; url: string; order: number; iframe: boolean; children: ExternalView[]; }; type UISettings = { title: string; brand: string; favicon: string; faviconDanger: string; pollTimer: PollTimer; theme: UITheme; notificationFilterEnabled: boolean; rememberMeEnabled: boolean; availableLanguages: string[]; routes: string[]; externalViews: ExternalView[]; viewSettings: ViewSettings[]; enableToasts: boolean; hideInstanceUrl: boolean; disableInstanceUrl: boolean; allowUnsafeHtml: boolean; }; type SBASettings = { uiSettings: UISettings; user: { name: string; [key: string]: any; }; extensions: { js?: Extension[]; css?: Extension[]; }; csrf: { headerName: string; parameterName: string; }; [key: string]: any; }; type ViewInstallFunctionParams = { viewRegistry: ViewRegistry; }; type SbaView = { id: string; name?: string; parent: string; handle: string | Component | RenderFunction; path?: string; href?: string; order: number; isEnabled: () => boolean; component: Raw; group: string; hasChildren: boolean; props: any; }; type View = ComponentView | LinkView; interface ComponentView { name: string; path: string; label?: string; handle?: Component | RenderFunction; order?: number; group?: string; component: Component; isEnabled?: () => boolean; } interface LinkView { name: string; href?: string; label: string; order?: number; } type HealthStatus = | 'DOWN' | 'UP' | 'RESTRICTED' | 'UNKNOWN' | 'OUT_OF_SERVICE' | 'OFFLINE'; } ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/i18n/PrimeLocale.ts ================================================ import { PrimeVueConfiguration } from '@primevue/core/config'; import { all } from 'primelocale'; export const PrimeLocale = { setLocale(primevue: PrimeVueConfiguration, locale: string) { const primeLocale = all[locale]; primevue.config.locale = primeLocale ?? all.en; }, }; ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/i18n/i18n.de.json ================================================ { "error": { "server_connection_failed": "Verbindung zum Server fehlgeschlagen." }, "term": { "actuator_endpoint": "Actuator-Endpunkt", "affects_all_instances": "Betrifft alle {count} Instanzen", "affects_this_instance_only": "Betrifft nur diese Instanz", "all": "Alle", "application": "Anwendung", "attributes": "Attribute", "bytes": "Bytes", "cancel": "Abbrechen", "clear": "Leeren", "cleared": "Geleert", "close": "Schließen", "confirm": "Bestätigen", "delete": "Löschen", "deleted": "Gelöscht", "duration": "Dauer", "epoch_time": "Epoch Zeit", "event": "Ereignis", "ever": "immer", "executing": "Wird ausgeführt...", "execution_failed": "Ausführung fehlgeschlagen.", "execution_successful": "Ausführung erfolgreich.", "failed": "Fehlgeschlagen", "fetching_data": "Lade Daten...", "fetch_failed": "Abruf der Daten fehlgeschlagen.", "filter_action": { "reset": "Filter zurücksetzen" }, "float": "Float", "homepage": "Startseite", "hours": "{count} Stunde | {count} Stunden", "instance": "Instanz", "instances": "Instanzen", "instances_tc": "{count} Instanz | {count} Instanzen", "integer": "Integer", "keyword_search": "Schlagwortsuche", "menu": { "open": "Menü öffnen" }, "milliseconds": "Millisekunden", "minutes": "{count} Minute | {count} Minuten", "name": "Name", "operations": "Operationen", "save": "Speichern", "stacktrace": "Stacktrace", "suppress": "Unterdrücken", "time": "Zeit", "today": "Heute", "unsuppress": "Reaktivieren", "no_group": "Keine Gruppe", "username": "Username", "go_to_previous_page": "Gehe zur vorherigen Seite", "go_to_page_n": "Gehe zu Seite {page}", "current_page": "Seite {page}, aktuelle Seite", "go_to_next_page": "Gehe zur nächsten Seite", "no_results_for_term": "Keine Ergebnisse für \"{term}\"." }, "health": { "label": "Zustand", "status": { "DOWN": "down", "UP": "up", "RESTRICTED": "eingeschränkt", "UNKNOWN": "unbekannt", "OUT_OF_SERVICE": "außer Betrieb", "OFFLINE": "offline" } }, "time_short": { "unknown": "unbekannt", "milliseconds": "{count}ms", "seconds": "{count}s", "minutes": "{count}min", "hours": "{count}h", "days": "{count}d", "weeks": "{count}Wo", "months": "{count}Mo", "years": "{count}a" } } ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/i18n/i18n.en.json ================================================ { "error": { "server_connection_failed": "Server connection failed. " }, "term": { "actuator_endpoint": "Actuator-Endpoint", "affects_all_instances": "Affects all {count} instances", "affects_this_instance_only": "Affects this instance only", "all": "all", "application": "Application", "applications_tc": "{n} application | {n} applications", "attributes": "Attributes", "bytes": "Bytes", "ok": "OK", "cancel": "Cancel", "clear": "Clear", "cleared": "Cleared", "close": "Close", "confirm": "Confirm", "context_refresh": "Refresh context", "context_refresh_failed": "Failed", "context_refreshed": "Context refreshed", "delete": "Delete", "deleted": "Deleted", "duration": "Duration", "epoch_time": "Epoch Time", "event": "Event", "ever": "ever", "execute": "Execute", "executing": "Executing...", "execution_failed": "Execution failed", "execution_successful": "Execution successful", "failed": "Failed", "fetching_data": "Fetching data...", "fetch_failed": "Fetching of data failed.", "float": "Float", "homepage": "Homepage", "filter": "Filter", "filter_action": { "reset": "Reset filters" }, "hours": "{count} hour | {count} hours", "instance": "Instance", "instances": "Instances", "instances_tc": "{count} instance | {count} instances", "integer": "Integer", "keyword_search": "Keyword search", "menu": { "open": "Open menu" }, "milliseconds": "Milliseconds", "minutes": "{count} minute | {count} minutes", "name": "Name", "operations": "Operations", "save": "Save", "stacktrace": "Stacktrace", "suppress": "Suppress", "time": "Time", "today": "Today", "unsuppress": "Unsuppress", "no_group": "No Group", "username": "Username", "go_to_previous_page": "Go to previous page", "go_to_page_n": "Go to page {page}", "current_page": "Page {page}, current page", "go_to_next_page": "Go to next page", "no_results_for_term": "No results for \"{term}\"." }, "health": { "label": "Health status", "status": { "DOWN": "down", "UP": "up", "RESTRICTED": "restricted", "UNKNOWN": "unknown", "OUT_OF_SERVICE": "out of service", "OFFLINE": "offline" } }, "time_short": { "unknown": "unknown", "milliseconds": "{count}ms", "seconds": "{count}s", "minutes": "{count}min", "hours": "{count}h", "days": "{count}d", "weeks": "{count}wk", "months": "{count}mo", "years": "{count}y" } } ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/i18n/i18n.es.json ================================================ { "error": { "server_connection_failed": "Falló la conexión al servidor. " }, "term": { "affects_all_instances": "Afecta a {count} instancias", "affects_this_instance_only": "Afectar sólo a esta instancia", "application": "Aplicación", "attributes": "Atributos", "bytes": "Bytes", "cancel": "Cancelar", "clear": "Borrar", "cleared": "Borrado", "close": "Cerrado", "confirm": "Confirmar", "context_refresh": "Actualizar contexto", "context_refresh_failed": "Fallido", "context_refreshed": "Contexto actualizado", "delete": "Eliminar", "deleted": "Eliminado", "duration": "Duración", "event": "Evento", "ever": "Siempre", "execute": "Ejecutar", "executing": "Ejecutando...", "execution_failed": "Ejecución fallida", "execution_successful": "Ejecución exitosa", "failed": "Fallido", "float": "Float", "hours": "{count} hora | {count} horas", "instance": "Instancia", "instances": "Instancias", "integer": "Entero", "milliseconds": "Milisegundos", "minutes": "{count} minuto | {count} minutos", "name": "Nombre", "operations": "Operaciones", "save": "Guardar", "stacktrace": "Stacktrace", "suppress": "Suprimir", "time": "Horario", "unsuppress": "Anular", "username": "Usuario", "go_to_previous_page": "Página anterior", "go_to_page_n": "Ir a página {page}", "go_to_next_page": "Página siguiente" }, "time_short": { "unknown": "unknown", "milliseconds": "{count}ms", "seconds": "{count}s", "minutes": "{count}min", "hours": "{count}h", "days": "{count}d", "weeks": "{count}sem", "months": "{count}mes", "years": "{count}a" } } ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/i18n/i18n.fr.json ================================================ { "error": { "server_connection_failed": "Erreur de connexion au serveur. " }, "term": { "affects_all_instances": "Affects all {count} instances", "affects_this_instance_only": "Affects only this instance", "application": "Application", "attributes": "Attributs", "bytes": "Octets", "cancel": "Annuler", "clear": "Effacer", "cleared": "Effacé", "close": "Fermer", "confirm": "Confirmer", "delete": "Supprimer", "deleted": "Supprimé", "duration": "Durée", "event": "Evènement", "ever": "Toujours", "executing": "Exécution...", "execution_failed": "Exécution en échec.", "execution_successful": "Exécution en succès.", "failed": "Echec", "float": "Décimal", "hours": "{count} heure | {count} heures", "instances": "Instances", "integer": "Entier", "milliseconds": "Millisecondes", "minutes": "{count} minute | {count} minutes", "name": "Nom", "operations": "Opérations", "save": "Sauvegarder", "stacktrace": "Stacktrace", "suppress": "Supprimer", "time": "Temps", "unsuppress": "Désinscription", "username": "Identifiant" }, "time_short": { "unknown": "unknown", "milliseconds": "{count}ms", "seconds": "{count}s", "minutes": "{count}min", "hours": "{count}h", "days": "{count}j", "weeks": "{count}sem", "months": "{count}m", "years": "{count}a" } } ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/i18n/i18n.is.json ================================================ { "error": { "server_connection_failed": "Mistókst sambandið við þjón." }, "term": { "affects_all_instances": "Affects all {count} instances", "affects_this_instance_only": "Affects only this instance", "application": "Forrit", "attributes": "Einkenni", "bytes": "Bæti", "cancel": "Hætta við", "clear": "Tæma", "cleared": "Tæmt", "close": "Ljúka", "confirm": "Staðfesta", "delete": "Eyđa", "deleted": "Eytt", "duration": "Tímalengd", "event": "Atburður", "ever": "alltaf", "executing": "Í vinnslu…", "execution_failed": "Framkvæmd mistekist.", "execution_successful": "Framkvæmd farsæl.", "failed": "Mistekist", "float": "Float", "hours": "{count} klst. | {count} klst.", "instances": "Eintök", "integer": "Integer", "milliseconds": "Millisekúndar", "minutes": "{count} Mínúta | {count} Mínútur", "name": "Nafn", "operations": "Rökaðgerðir", "save": "Vista", "stacktrace": "Stacktrace", "suppress": "Hunsa", "time": "Tími", "unsuppress": "Endurvekja", "username": "Notandanafn" }, "time_short": { "unknown": "óþekkt", "milliseconds": "{count}ms", "seconds": "{count}sek", "minutes": "{count}mín", "hours": "{count}klst", "days": "{count}dag", "weeks": "{count}vika", "months": "{count}mán", "years": "{count}ár" } } ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/i18n/i18n.ko.json ================================================ { "error": { "server_connection_failed": "서버 연결에 실패했습니다. " }, "term": { "affects_all_instances": "{count} 개의 인스턴스에 대해 영향을 미칩니다.", "affects_this_instance_only": "이 인스턴스에만 영향을 미칩니다.", "all": "전체", "application": "애플리케이션", "applications_tc": "{n} 애플리케이션 | {n} 애플리케이션", "attributes": "속성", "bytes": "Bytes", "ok": "예", "cancel": "취소", "clear": "초기화", "cleared": "초기화됨", "close": "닫기", "confirm": "확인", "context_refresh": "컨텍스트 갱신", "context_refresh_failed": "실패", "context_refreshed": "컨텍스트 갱신됨", "delete": "삭제", "deleted": "삭제됨", "duration": "기간", "event": "이벤트", "ever": "ever", "execute": "실행", "executing": "실행 중...", "execution_failed": "실행 실패", "execution_successful": "실행 성공.", "failed": "실패했습니다", "fetching_data": "데이터 불러오는 중...", "fetch_failed": "데이터를 불러오지 못하였습니다.", "float": "Float", "filter": "필터", "hours": "{count} 시간 | {count} 시간", "instance": "인스턴스", "instances": "인스턴스", "instances_tc": "{count} 인스턴스 | {count} 인스턴스", "integer": "Integer", "menu": { "open": "메뉴 열기" }, "milliseconds": "Milliseconds", "minutes": "{count} 분 | {count} 분", "name": "이름", "operations": "Operations", "save": "저장", "stacktrace": "Stacktrace", "suppress": "비활성화", "time": "시간", "unsuppress": "활성화", "no_group": "그룹 없음", "username": "사용자명", "go_to_previous_page": "이전 페이지로 이동", "go_to_page_n": "{page} 페이지로 이동", "current_page": "페이지 {page}, 현재 페이지", "go_to_next_page": "다음 페이지로 이동", "no_results_for_term": "\"{term}\" 에 대한 결과가 없습니다." }, "health": { "label": "Health status", "status": { "DOWN": "down", "UP": "up", "RESTRICTED": "restricted", "UNKNOWN": "unknown", "OUT_OF_SERVICE": "out of service", "OFFLINE": "offline" } }, "time_short": { "unknown": "알 수 없음", "milliseconds": "{count}ms", "seconds": "{count}초", "minutes": "{count}분", "hours": "{count}시간", "days": "{count}일", "weeks": "{count}주", "months": "{count}개월", "years": "{count}년" } } ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/i18n/i18n.pt-BR.json ================================================ { "error": { "server_connection_failed": "Falha na conexão com o servidor. " }, "term": { "affects_all_instances": "Affects all {count} instances", "affects_this_instance_only": "Affects only this instance", "application": "Aplicação", "attributes": "Atributos", "bytes": "Bytes", "cancel": "Cancelar", "clear": "Clear", "cleared": "Limpo", "close": "Fechar", "confirm": "Confirmar", "delete": "Remover", "deleted": "Removido", "duration": "Duração", "event": "Evento", "ever": "nunca", "executing": "Executando...", "execution_failed": "Falha na execução.", "execution_successful": "Executado com sucesso.", "failed": "Falha", "float": "Float", "hours": "{count} hora | {count} horas", "instances": "Instâncias", "integer": "Integer", "milliseconds": "Milissegundos", "minutes": "{count} minuto | {count} minutos", "name": "Nome", "operations": "Operações", "save": "Salvar", "stacktrace": "Stacktrace", "suppress": "Esconder", "time": "Tempo", "unsuppress": "Exibir", "username": "Nome do usuário" }, "time_short": { "unknown": "desconhecido", "milliseconds": "{count}ms", "seconds": "{count}s", "minutes": "{count}min", "hours": "{count}h", "days": "{count}d", "weeks": "{count}sem", "months": "{count}mês", "years": "{count}a" } } ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/i18n/i18n.ru.json ================================================ { "error": { "server_connection_failed": "Ошибка подключения к серверу. " }, "term": { "affects_all_instances": "Affects all {count} instances", "affects_this_instance_only": "Affects only this instance", "application": "Приложение", "attributes": "Атрибуты", "bytes": "Байты", "cancel": "Отменить", "clear": "Очистить", "cleared": "Очищено", "close": "Закрыть", "confirm": "Подтвердить", "delete": "Удалить", "deleted": "Удален", "duration": "Задержка", "event": "Событие", "ever": "всегда", "executing": "Выполнение...", "execution_failed": "Ошибка при выполнении.", "execution_successful": "Успешно выполнено.", "failed": "Ошибка", "float": "Float", "hours": "{count} ч. | {count} ч.", "instances": "Экземпляры", "integer": "Integer", "milliseconds": "Мс.", "minutes": "{count} мин. | {count} мин.", "name": "Имя", "operations": "Операции", "save": "Сохранить", "stacktrace": "Stacktrace", "suppress": "Скрыть", "time": "Время", "unsuppress": "Включить", "username": "Имя пользователя" }, "time_short": { "unknown": "неизвестно", "milliseconds": "{count}мс", "seconds": "{count}с", "minutes": "{count}мин", "hours": "{count}ч", "days": "{count}д", "weeks": "{count}нед", "months": "{count}мес", "years": "{count}г" } } ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/i18n/i18n.zh-CN.json ================================================ { "error": { "server_connection_failed": "服务连接失败。" }, "term": { "affects_all_instances": "Affects all {count} instances", "affects_this_instance_only": "Affects only this instance", "application": "应用", "attributes": "属性", "bytes": "字节", "cancel": "取消", "clear": "清除", "cleared": "已清除", "close": "关闭", "confirm": "确认", "delete": "删除", "deleted": "已删除", "duration": "持续时间", "event": "事件", "ever": "ever", "executing": "执行中...", "execution_failed": "执行失败。", "execution_successful": "执行成功。", "failed": "失败", "float": "浮点型", "hours": "{count} 小时 | {count} 小时", "instances": "实例", "integer": "整型", "milliseconds": "毫秒", "minutes": "{count} 分钟 | {count} 分钟", "name": "名称", "operations": "操作", "save": "保存", "stacktrace": "堆栈信息", "suppress": "Suppress", "time": "时间", "unsuppress": "Unsuppress", "username": "用户名" }, "time_short": { "unknown": "未知", "milliseconds": "{count}毫秒", "seconds": "{count}秒", "minutes": "{count}分", "hours": "{count}小时", "days": "{count}天", "weeks": "{count}周", "months": "{count}月", "years": "{count}年" } } ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/i18n/i18n.zh-TW.json ================================================ { "error": { "server_connection_failed": "伺服器連線失敗。" }, "term": { "actuator_endpoint": "Actuator 端點", "affects_all_instances": "影響全部 {count} 個執行個體", "affects_this_instance_only": "僅影響此執行個體", "all": "全部", "application": "應用程式", "applications_tc": "{n} 個應用程式 | {n} 個應用程式", "attributes": "屬性", "bytes": "位元組", "ok": "確定", "cancel": "取消", "clear": "清除", "cleared": "已清除", "close": "關閉", "confirm": "確認", "context_refresh": "重新整理內容", "context_refresh_failed": "失敗", "context_refreshed": "內容已重新整理", "delete": "刪除", "deleted": "已刪除", "duration": "持續時間", "epoch_time": "Epoch 時間", "event": "事件", "ever": "任何時候", "execute": "執行", "executing": "執行中...", "execution_failed": "執行失敗。", "execution_successful": "執行成功。", "failed": "失敗", "fetching_data": "正在取得資料...", "fetch_failed": "取得資料失敗。", "float": "浮點數", "homepage": "首頁", "filter": "篩選", "filter_action": { "reset": "重設篩選條件" }, "hours": "{count} 小時 | {count} 小時", "instance": "執行個體", "instances": "執行個體", "instances_tc": "{count} 個執行個體 | {count} 個執行個體", "integer": "整數", "keyword_search": "關鍵字搜尋", "menu": { "open": "開啟選單" }, "milliseconds": "毫秒", "minutes": "{count} 分鐘 | {count} 分鐘", "name": "名稱", "operations": "作業", "save": "儲存", "stacktrace": "堆疊追蹤", "suppress": "隱藏通知", "time": "時間", "today": "今天", "unsuppress": "取消隱藏", "no_group": "無群組", "username": "使用者名稱", "go_to_previous_page": "前往上一頁", "go_to_page_n": "前往第 {page} 頁", "current_page": "第 {page} 頁,目前頁面", "go_to_next_page": "前往下一頁", "no_results_for_term": "找不到「{term}」的結果。" }, "health": { "label": "健康狀態", "status": { "DOWN": "停止", "UP": "正常", "RESTRICTED": "受限", "UNKNOWN": "未知", "OUT_OF_SERVICE": "停止服務", "OFFLINE": "離線" } }, "time_short": { "unknown": "未知", "milliseconds": "{count} 毫秒", "seconds": "{count} 秒", "minutes": "{count} 分", "hours": "{count} 小時", "days": "{count} 天", "weeks": "{count} 週", "months": "{count} 個月", "years": "{count} 年" } } ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/i18n/index.ts ================================================ import { isEmpty, merge } from 'lodash-es'; import { createI18n } from 'vue-i18n'; import sbaConfig from '@/sba-config'; const context = import.meta.glob('../**/(*.)?i18n.*.json', { eager: true }); const messages = Object.keys(context) .map((key) => { const localeFromFile = /\.*i18n\.?([^/]*)\.json$/.exec(key); const messages = (context[key] as { default: never }).default; if (localeFromFile[1]) { return { [localeFromFile[1]]: messages, }; } else { return messages; } }) .reduce((prev, cur) => merge(prev, cur), {}); export function getAvailableLocales() { const valueFromServer = sbaConfig.uiSettings.availableLanguages; const strings = Object.keys(messages); return isEmpty(valueFromServer) ? strings : valueFromServer.filter((language) => strings.includes(language)); } let browserLanguage = navigator.language; if (!browserLanguage.includes('zh')) { browserLanguage = browserLanguage.split('-')[0]; } const i18n = createI18n({ locale: getAvailableLocales().includes(browserLanguage) ? browserLanguage : 'en', fallbackLocale: 'en', legacy: false, silentFallbackWarn: process.env.NODE_ENV === 'production', silentTranslationWarn: process.env.NODE_ENV === 'production', messages, }); export default i18n; ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/index.css ================================================ /*! * Copyright 2014-2019 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 './toast-theme.css'; @tailwind base; @tailwind components; @tailwind utilities; body { font-family: BlinkMacSystemFont, -apple-system, Segoe UI, Roboto, Oxygen, Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, Helvetica, Arial, sans-serif; @apply text-base; } #app { @apply h-full; } .container { @apply px-2; } th { text-align: left; } .table { @apply text-left text-gray-500 dark:text-gray-400 bg-white border-collapse; } .table thead { @apply border-b bg-gray-100 rounded-md; } .table th, .table td { @apply py-3 px-3; } .table.table-full { @apply w-full; } .table.table-sm { @apply text-sm; } .table thead { @apply text-gray-700 bg-gray-50 dark:bg-gray-700 dark:text-gray-400; } .table tr:not(:last-of-type) { @apply border-b; } .-rotate-90 { --tw-rotate: -90deg; transform: rotate(var(--tw-rotate)); } .rotate-90 { --tw-rotate: 90deg; transform: rotate(var(--tw-rotate)); } table.table-wide td { @apply px-2 py-1.5; } table.table-striped tr:nth-child(even) { @apply bg-gray-50; } td.label, td .label { @apply text-sm font-medium text-gray-500; } ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/index.html ================================================ Spring Boot Admin
================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/index.stories.jsx ================================================ import { DocsContainer } from '@storybook/addon-docs/blocks'; const ExampleContainer = ({ children, ...props }) => { return (
{children}
); }; export default { parameters: { controls: { matchers: { color: /(background|color)$/i, date: /Date$/, }, }, docs: { container: ExampleContainer, }, }, }; ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/index.ts ================================================ /* * Copyright 2014-2020 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 { definePreset } from '@primeuix/themes'; import Aura from '@primeuix/themes/aura'; import { usePrimeVue } from '@primevue/core'; import NotificationcenterPlugin from '@stekoe/vue-toast-notificationcenter'; import moment from 'moment'; import { Tooltip } from 'primevue'; import PrimeVue from 'primevue/config'; import * as Vue from 'vue'; import { createApp, h, onBeforeMount, onBeforeUnmount, reactive, watch, } from 'vue'; import { useI18n } from 'vue-i18n'; import { useRoute, useRouter } from 'vue-router'; import components from './components'; import { CUSTOM_ROUTES_ADDED_EVENT, createViewRegistry, useViewRegistry, } from './composables/ViewRegistry'; import { createApplicationStore, useApplicationStore, } from './composables/useApplicationStore'; import i18n from './i18n'; import Notifications from './notifications'; import SbaModalPlugin from './plugins/modal'; import sbaConfig from './sba-config'; import views from './views'; import { PrimeLocale } from '@/i18n/PrimeLocale'; import eventBus from '@/services/bus'; import sbaShell from '@/shell'; const applicationStore = createApplicationStore(); const viewRegistry = createViewRegistry(); globalThis.Vue = Vue; globalThis.SBA.viewRegistry = useViewRegistry(); globalThis.SBA.useApplicationStore = useApplicationStore; globalThis.SBA.useI18n = () => i18n.global; globalThis.SBA.use = ({ install }) => { install({ viewRegistry: globalThis.SBA.viewRegistry, applicationStore: globalThis.SBA.useApplicationStore, i18n: i18n.global, }); }; sbaConfig.extensions.js.forEach((extension) => { const script = document.createElement('script'); script.src = `extensions/${extension.resourcePath}`; document.head.appendChild(script); }); sbaConfig.extensions.css.forEach((extension) => { const link = document.createElement('link'); link.rel = 'stylesheet'; link.href = `extensions/${extension.resourcePath}`; document.head.appendChild(link); }); moment.locale(navigator.language.split('-')[0]); const installables = [Notifications, ...views]; installables.forEach((installable) => { try { installable.install({ viewRegistry, applicationStore, }); } catch (e) { console.error('Error while installing ', installable, e); } }); const app = createApp({ setup() { const router = useRouter(); const route = useRoute(); const { applications, applicationsInitialized, error } = useApplicationStore(); const { t, locale } = useI18n(); const primevue = usePrimeVue(); onBeforeMount(() => { applicationStore.start(); }); onBeforeUnmount(() => { applicationStore.stop(); }); watch( locale, () => { PrimeLocale.setLocale(primevue, locale.value); }, { immediate: true }, ); const routesAddedEventHandler = async () => { eventBus.off(CUSTOM_ROUTES_ADDED_EVENT, routesAddedEventHandler); await router.replace(route); }; eventBus.on(CUSTOM_ROUTES_ADDED_EVENT, routesAddedEventHandler); return () => h( sbaShell, reactive({ applications, applicationsInitialized, error, t, }), ); }, }); app.use(i18n); app.use(components); app.use(NotificationcenterPlugin, { duration: 10_000, }); app.use(SbaModalPlugin, { i18n }); app.use(viewRegistry.createRouter()); app.directive('tooltip', Tooltip); app.use(PrimeVue, { theme: { preset: definePreset(Aura, { semantic: { primary: { 50: 'rgb(var(--main-50))', 100: 'rgb(var(--main-100))', 200: 'rgb(var(--main-200))', 300: 'rgb(var(--main-300))', 400: 'rgb(var(--main-400))', 500: 'rgb(var(--main-500))', 600: 'rgb(var(--main-600))', 700: 'rgb(var(--main-700))', 800: 'rgb(var(--main-800))', 900: 'rgb(var(--main-900))', }, }, }), options: { darkModeSelector: false, }, }, }); app.mount('#app'); ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/login/login.i18n.de.json ================================================ { "login": { "button_login": "Anmelden", "logout_successful": "Erfolgreich abgemeldet", "remember_me": "Angemeldet bleiben", "error": { "invalid_username_or_password": "Benutzername oder Passwort falsch" }, "placeholder": { "username": "Benutzername", "password": "Passwort" } } } ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/login/login.i18n.en.json ================================================ { "login": { "button_login": "Login", "logout_successful": "Logout successful", "remember_me": "Remember me", "error": { "login_required": "Login required to access the resource (Error: {code}).", "invalid_username_or_password": "Invalid username or password" }, "placeholder": { "username": "Username", "password": "Password" } } } ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/login/login.i18n.es.json ================================================ { "login": { "button_login": "Ingresar", "logout_successful": "Logout exitoso", "remember_me": "Recordarme", "error": { "invalid_username_or_password": "Usuario o contraseña inválida" }, "placeholder": { "username": "Usuario", "password": "Contraseña" } } } ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/login/login.i18n.fr.json ================================================ { "login": { "button_login": "Se Connecter", "logout_successful": "Déconnexion réussie", "remember_me": "Se souvenir de moi", "error": { "invalid_username_or_password": "Nom d'utilisateur ou mot de passe incorrect" }, "placeholder": { "username": "Nom d'utilisateur", "password": "Mot de passe" } } } ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/login/login.i18n.is.json ================================================ { "login": { "button_login": "Skrá inn", "logout_successful": "Skráð út", "remember_me": "Mundu eftir mér", "error": { "invalid_username_or_password": "Notandanafn eða lykilorð rángt" }, "placeholder": { "username": "Notandanafn", "password": "Lykilorð" } } } ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/login/login.i18n.ko.json ================================================ { "login": { "button_login": "로그인", "logout_successful": "로그아웃을 성공했습니다.", "remember_me": "사용자 기억", "error": { "invalid_username_or_password": "잘못된 사용자명 또는 암호 입니다." }, "placeholder": { "username": "사용자명", "password": "암호" } } } ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/login/login.i18n.pt-BR.json ================================================ { "login": { "button_login": "Entrar", "logout_successful": "Logout com sucesso", "remember_me": "Me lembrar", "error": { "invalid_username_or_password": "Usuário ou senha inválidos" }, "placeholder": { "username": "Nome do usuário", "password": "Senha" } } } ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/login/login.i18n.ru.json ================================================ { "login": { "button_login": "Войти", "logout_successful": "Вы успешно вышли из системы", "remember_me": "Запомнить меня", "error": { "invalid_username_or_password": "Неверные имя пользователя или пароль" }, "placeholder": { "username": "Имя пользователя", "password": "Пароль" } } } ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/login/login.i18n.zh-CN.json ================================================ { "login": { "button_login": "登录", "logout_successful": "注销成功", "remember_me": "记住用户", "error": { "invalid_username_or_password": "无效的用户名或密码" }, "placeholder": { "username": "用户名", "password": "密码" } } } ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/login/login.i18n.zh-TW.json ================================================ { "login": { "button_login": "登入", "logout_successful": "登出成功", "remember_me": "記住我", "error": { "login_required": "需要登入才能存取此資源 (錯誤:{code})。", "invalid_username_or_password": "無效的使用者名稱或密碼" }, "placeholder": { "username": "使用者名稱", "password": "密碼" } } } ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/login/login.stories.ts ================================================ import Login from './login.vue'; export default { component: Login, title: 'Components/Form/Login', }; const Template = (args) => ({ components: { Login }, setup() { return { args }; }, template: '', }); export const LoginForm = { render: Template, args: { title: 'Spring Boot Admin', param: {}, }, }; ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/login/login.vue ================================================ ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/login.css ================================================ /*! * Copyright 2014-2019 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. */ @tailwind base; @tailwind components; @tailwind utilities; :root { --bg-color-start: #71e69c; --bg-color-stop: #09351a; } .bg-color-start { transition: 0.4s ease; stop-color: var(--bg-color-start); } .bg-color-stop { transition: 0.4s ease; stop-color: var(--bg-color-stop); } body { font-family: BlinkMacSystemFont, -apple-system, Segoe UI, Roboto, Oxygen, Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, Helvetica, Arial, sans-serif; } ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/login.html ================================================ Spring Boot Admin - Login
================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/login.ts ================================================ /* * 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 { createApp } from 'vue'; import './login.css'; import i18n from './i18n'; import Login from './login/login.vue'; const app = createApp(Login, { csrf: window.csrf, icon: window.uiSettings.loginIcon, title: window.uiSettings.title, theme: window.uiSettings.theme, param: window.param, }); app.use(i18n); app.mount('#login'); ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/mixins/subscribing.ts ================================================ /* * 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. */ export default { created() { this.subscribe(); }, beforeUnmount() { this.unsubscribe(); }, methods: { async subscribe() { if (!this.subscription) { this.subscription = await this.createSubscription(); } }, unsubscribe() { if (this.subscription && !this.subscription.closed) { try { this.subscription.unsubscribe(); } finally { this.subscription = null; } } }, }, }; ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/mocks/applications/data.ts ================================================ /* * Copyright 2014-2021 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 { HealthStatus } from '../../HealthStatus'; export const instance = { id: 'bba333956ae6', version: 3, registration: { name: 'spring-boot-admin-sample-servlet', managementUrl: 'http://localhost:8080/actuator', healthUrl: 'http://localhost:8080/actuator/health', serviceUrl: 'http://localhost:8080/', source: 'http-api', metadata: { startup: '2021-10-29T08:50:07.486289+02:00', 'tags.environment': 'test', }, }, registered: true, statusInfo: { status: 'UP', details: { db: { status: 'UP', details: { database: 'HSQL Database Engine', validationQuery: 'isValid()', }, }, diskSpace: { status: 'UP', details: { total: 499963174912, free: 108980899840, threshold: 10485760, exists: true, }, }, ping: { status: 'UP' }, }, }, statusTimestamp: '2021-10-29T06:50:09.600276Z', info: { tags: { security: 'insecure' }, 'scm-url': '@scm.url@', 'build-url': 'https://travis-ci.org/codecentric/spring-boot-admin', build: { artifact: 'spring-boot-admin-sample-servlet', name: 'Spring Boot Admin Sample Servlet', time: '2021-09-17T09:53:18.987Z', version: '2.5.2-SNAPSHOT', group: 'de.codecentric', }, }, endpoints: [ { id: 'sessions', url: 'http://localhost:8080/actuator/sessions' }, { id: 'httptrace', url: 'http://localhost:8080/actuator/httptrace', }, { id: 'httptexchanges', url: 'http://localhost:8080/actuator/httpexchanges', }, { id: 'caches', url: 'http://localhost:8080/actuator/caches' }, { id: 'loggers', url: 'http://localhost:8080/actuator/loggers', }, { id: 'logfile', url: 'http://localhost:8080/actuator/logfile' }, { id: 'custom', url: 'http://localhost:8080/actuator/custom', }, { id: 'health', url: 'http://localhost:8080/actuator/health' }, { id: 'env', url: 'http://localhost:8080/actuator/env', }, { id: 'heapdump', url: 'http://localhost:8080/actuator/heapdump' }, { id: 'scheduledtasks', url: 'http://localhost:8080/actuator/scheduledtasks', }, { id: 'mappings', url: 'http://localhost:8080/actuator/mappings' }, { id: 'startup', url: 'http://localhost:8080/actuator/startup', }, { id: 'beans', url: 'http://localhost:8080/actuator/beans' }, { id: 'configprops', url: 'http://localhost:8080/actuator/configprops', }, { id: 'threaddump', url: 'http://localhost:8080/actuator/threaddump' }, { id: 'metrics', url: 'http://localhost:8080/actuator/metrics', }, { id: 'conditions', url: 'http://localhost:8080/actuator/conditions' }, { id: 'auditevents', url: 'http://localhost:8080/actuator/auditevents', }, { id: 'info', url: 'http://localhost:8080/actuator/info' }, { id: 'shutdown', url: 'http://localhost:8080/actuator/shutdown', }, { id: 'restart', url: 'http://localhost:8080/actuator/restart', }, ], buildVersion: '2.5.2-SNAPSHOT', tags: { environment: 'test', security: 'insecure' }, }; export const applications = Object.entries(HealthStatus).map((e) => { const STATUS = e[0]; return { name: `application-${STATUS}`, buildVersion: '2.5.2-SNAPSHOT', status: STATUS, statusTimestamp: '2021-10-29T06:50:09.600276Z', instances: [instance], }; }); ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/mocks/applications/index.ts ================================================ import { http } from 'msw'; import { applications } from './data'; const applicationsEndpoint = [ http.get('/applications', () => { return res(ctx.status(404), ctx.json(applications)); }), ]; export default applicationsEndpoint; ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/mocks/browser.ts ================================================ import { setupWorker } from 'msw/browser'; import auditEventsEndpoint from './instance/auditevents/index'; import flywayEndpoints from './instance/flyway/index'; import httpTraceEndpoints from './instance/httptrace/index'; import liquibaseEndpoints from './instance/liquibase/index'; import mappingsEndpoint from './instance/mappings/index'; import metricsEndpoint from './instance/metrics/index'; import sessionEndpoints from './instance/sessions/index'; const handler = [ ...mappingsEndpoint, ...liquibaseEndpoints, ...flywayEndpoints, ...auditEventsEndpoint, ...metricsEndpoint, ...httpTraceEndpoints, ...sessionEndpoints, ]; export const worker = setupWorker(...handler); ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/mocks/fixtures/eventStream/registerWithOneInstance.ts ================================================ export const registerWithOneInstance = { name: 'spring-boot-admin-sample-servlet', buildVersion: '3.0.0-SNAPSHOT', status: 'UP', statusTimestamp: '2022-12-19T10:07:20.646905Z', instances: [ { id: '25b07dc98984', version: 3, registration: { name: 'spring-boot-admin-sample-servlet', managementUrl: 'http://ip-192-168-178-20.eu-central-1.compute.internal:8080/actuator', healthUrl: 'http://ip-192-168-178-20.eu-central-1.compute.internal:8080/actuator/health', serviceUrl: 'http://ip-192-168-178-20.eu-central-1.compute.internal:8080/', source: 'http-api', metadata: { 'tags.de-service-test-6': 'A large content', 'tags.de-service-test-4': 'A large content', 'tags.de-service-test-5': 'A large content', startup: '2022-12-19T11:04:38.171314+01:00', 'kubectl.kubernetes.iolast-applied-configuration': '{"name":"jvm.threads.peak","description":"The peak live thread count since the Java virtual machine started or peak was reset","baseUnit":"threads","measurements":[{"statistic":"VALUE","value":64.0}],"availableTags":[]}', 'user.name': 'user', 'tags.de-service-test-2': 'A large content', 'user.password': '******', 'tags.de-service-test-3': 'A large content', 'tags.environment': 'test', 'tags.de-service-test-1': 'A large content', }, }, registered: true, statusInfo: { status: 'UP', details: { reactiveDiscoveryClients: { description: 'Discovery Client not initialized', status: 'UNKNOWN', components: { 'Simple Reactive Discovery Client': { description: 'Discovery Client not initialized', status: 'UNKNOWN', }, }, }, clientConfigServer: { status: 'UNKNOWN', details: { error: 'no property sources located' }, }, diskSpace: { status: 'UP', details: { total: 994662584320, free: 333813846016, threshold: 10485760, path: '/Users/stekoe/workspaces/cc/spring-boot-admin/spring-boot-admin-samples/spring-boot-admin-sample-servlet/.', exists: true, }, }, ping: { status: 'UP' }, discoveryComposite: { description: 'Discovery Client not initialized', status: 'UNKNOWN', components: { discoveryClient: { description: 'Discovery Client not initialized', status: 'UNKNOWN', }, }, }, refreshScope: { status: 'UP' }, db: { status: 'UP', details: { database: 'HSQL Database Engine', validationQuery: 'isValid()', }, }, }, }, statusTimestamp: '2022-12-19T10:04:39.457363Z', info: { build: { artifact: 'spring-boot-admin-sample-servlet', name: 'Spring Boot Admin Sample Servlet', time: '2022-12-16T07:23:45.732Z', version: '3.0.0-SNAPSHOT', group: 'de.codecentric', }, }, endpoints: [ { id: 'caches', url: 'http://ip-192-168-178-20.eu-central-1.compute.internal:8080/actuator/caches', }, { id: 'loggers', url: 'http://ip-192-168-178-20.eu-central-1.compute.internal:8080/actuator/loggers', }, { id: 'heapdump', url: 'http://ip-192-168-178-20.eu-central-1.compute.internal:8080/actuator/heapdump', }, { id: 'features', url: 'http://ip-192-168-178-20.eu-central-1.compute.internal:8080/actuator/features', }, { id: 'startup', url: 'http://ip-192-168-178-20.eu-central-1.compute.internal:8080/actuator/startup', }, { id: 'beans', url: 'http://ip-192-168-178-20.eu-central-1.compute.internal:8080/actuator/beans', }, { id: 'configprops', url: 'http://ip-192-168-178-20.eu-central-1.compute.internal:8080/actuator/configprops', }, { id: 'threaddump', url: 'http://ip-192-168-178-20.eu-central-1.compute.internal:8080/actuator/threaddump', }, { id: 'auditevents', url: 'http://ip-192-168-178-20.eu-central-1.compute.internal:8080/actuator/auditevents', }, { id: 'info', url: 'http://ip-192-168-178-20.eu-central-1.compute.internal:8080/actuator/info', }, { id: 'resume', url: 'http://ip-192-168-178-20.eu-central-1.compute.internal:8080/actuator/resume', }, { id: 'sessions', url: 'http://ip-192-168-178-20.eu-central-1.compute.internal:8080/actuator/sessions', }, { id: 'restart', url: 'http://ip-192-168-178-20.eu-central-1.compute.internal:8080/actuator/restart', }, { id: 'logfile', url: 'http://ip-192-168-178-20.eu-central-1.compute.internal:8080/actuator/logfile', }, { id: 'custom', url: 'http://ip-192-168-178-20.eu-central-1.compute.internal:8080/actuator/custom', }, { id: 'health', url: 'http://ip-192-168-178-20.eu-central-1.compute.internal:8080/actuator/health', }, { id: 'refresh', url: 'http://ip-192-168-178-20.eu-central-1.compute.internal:8080/actuator/refresh', }, { id: 'env', url: 'http://ip-192-168-178-20.eu-central-1.compute.internal:8080/actuator/env', }, { id: 'pause', url: 'http://ip-192-168-178-20.eu-central-1.compute.internal:8080/actuator/pause', }, { id: 'scheduledtasks', url: 'http://ip-192-168-178-20.eu-central-1.compute.internal:8080/actuator/scheduledtasks', }, { id: 'mappings', url: 'http://ip-192-168-178-20.eu-central-1.compute.internal:8080/actuator/mappings', }, { id: 'metrics', url: 'http://ip-192-168-178-20.eu-central-1.compute.internal:8080/actuator/metrics', }, { id: 'conditions', url: 'http://ip-192-168-178-20.eu-central-1.compute.internal:8080/actuator/conditions', }, { id: 'httpexchanges', url: 'http://ip-192-168-178-20.eu-central-1.compute.internal:8080/actuator/httpexchanges', }, { id: 'shutdown', url: 'http://ip-192-168-178-20.eu-central-1.compute.internal:8080/actuator/shutdown', }, ], buildVersion: '3.0.0-SNAPSHOT', tags: { 'de-service-test-6': 'A large content', 'de-service-test-4': 'A large content', 'de-service-test-5': 'A large content', 'de-service-test-2': 'A large content', 'de-service-test-3': 'A large content', environment: 'test', 'de-service-test-1': 'A large content', }, }, ], }; ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/mocks/fixtures/eventStream/registerWithTwoInstances.ts ================================================ export const registerWithTwoInstances = { name: 'spring-boot-admin-sample-servlet', buildVersion: '3.0.0-SNAPSHOT', status: 'UP', statusTimestamp: '2022-12-19T10:07:20.646905Z', instances: [ { id: '25b07dc98984', version: 3, registration: { name: 'spring-boot-admin-sample-servlet', managementUrl: 'http://ip-192-168-178-20.eu-central-1.compute.internal:8080/actuator', healthUrl: 'http://ip-192-168-178-20.eu-central-1.compute.internal:8080/actuator/health', serviceUrl: 'http://ip-192-168-178-20.eu-central-1.compute.internal:8080/', source: 'http-api', metadata: { 'tags.de-service-test-6': 'A large content', 'tags.de-service-test-4': 'A large content', 'tags.de-service-test-5': 'A large content', startup: '2022-12-19T11:04:38.171314+01:00', 'kubectl.kubernetes.iolast-applied-configuration': '{"name":"jvm.threads.peak","description":"The peak live thread count since the Java virtual machine started or peak was reset","baseUnit":"threads","measurements":[{"statistic":"VALUE","value":64.0}],"availableTags":[]}', 'user.name': 'user', 'tags.de-service-test-2': 'A large content', 'user.password': '******', 'tags.de-service-test-3': 'A large content', 'tags.environment': 'test', 'tags.de-service-test-1': 'A large content', }, }, registered: true, statusInfo: { status: 'UP', details: { reactiveDiscoveryClients: { description: 'Discovery Client not initialized', status: 'UNKNOWN', components: { 'Simple Reactive Discovery Client': { description: 'Discovery Client not initialized', status: 'UNKNOWN', }, }, }, clientConfigServer: { status: 'UNKNOWN', details: { error: 'no property sources located', }, }, diskSpace: { status: 'UP', details: { total: 994662584320, free: 333813846016, threshold: 10485760, path: '/Users/stekoe/workspaces/cc/spring-boot-admin/spring-boot-admin-samples/spring-boot-admin-sample-servlet/.', exists: true, }, }, ping: { status: 'UP', }, discoveryComposite: { description: 'Discovery Client not initialized', status: 'UNKNOWN', components: { discoveryClient: { description: 'Discovery Client not initialized', status: 'UNKNOWN', }, }, }, refreshScope: { status: 'UP', }, db: { status: 'UP', details: { database: 'HSQL Database Engine', validationQuery: 'isValid()', }, }, }, }, statusTimestamp: '2022-12-19T10:04:39.457363Z', info: { build: { artifact: 'spring-boot-admin-sample-servlet', name: 'Spring Boot Admin Sample Servlet', time: '2022-12-16T07:23:45.732Z', version: '3.0.0-SNAPSHOT', group: 'de.codecentric', }, }, endpoints: [ { id: 'caches', url: 'http://ip-192-168-178-20.eu-central-1.compute.internal:8080/actuator/caches', }, { id: 'loggers', url: 'http://ip-192-168-178-20.eu-central-1.compute.internal:8080/actuator/loggers', }, { id: 'heapdump', url: 'http://ip-192-168-178-20.eu-central-1.compute.internal:8080/actuator/heapdump', }, { id: 'features', url: 'http://ip-192-168-178-20.eu-central-1.compute.internal:8080/actuator/features', }, { id: 'startup', url: 'http://ip-192-168-178-20.eu-central-1.compute.internal:8080/actuator/startup', }, { id: 'beans', url: 'http://ip-192-168-178-20.eu-central-1.compute.internal:8080/actuator/beans', }, { id: 'configprops', url: 'http://ip-192-168-178-20.eu-central-1.compute.internal:8080/actuator/configprops', }, { id: 'threaddump', url: 'http://ip-192-168-178-20.eu-central-1.compute.internal:8080/actuator/threaddump', }, { id: 'auditevents', url: 'http://ip-192-168-178-20.eu-central-1.compute.internal:8080/actuator/auditevents', }, { id: 'info', url: 'http://ip-192-168-178-20.eu-central-1.compute.internal:8080/actuator/info', }, { id: 'resume', url: 'http://ip-192-168-178-20.eu-central-1.compute.internal:8080/actuator/resume', }, { id: 'sessions', url: 'http://ip-192-168-178-20.eu-central-1.compute.internal:8080/actuator/sessions', }, { id: 'restart', url: 'http://ip-192-168-178-20.eu-central-1.compute.internal:8080/actuator/restart', }, { id: 'logfile', url: 'http://ip-192-168-178-20.eu-central-1.compute.internal:8080/actuator/logfile', }, { id: 'custom', url: 'http://ip-192-168-178-20.eu-central-1.compute.internal:8080/actuator/custom', }, { id: 'health', url: 'http://ip-192-168-178-20.eu-central-1.compute.internal:8080/actuator/health', }, { id: 'refresh', url: 'http://ip-192-168-178-20.eu-central-1.compute.internal:8080/actuator/refresh', }, { id: 'env', url: 'http://ip-192-168-178-20.eu-central-1.compute.internal:8080/actuator/env', }, { id: 'pause', url: 'http://ip-192-168-178-20.eu-central-1.compute.internal:8080/actuator/pause', }, { id: 'scheduledtasks', url: 'http://ip-192-168-178-20.eu-central-1.compute.internal:8080/actuator/scheduledtasks', }, { id: 'mappings', url: 'http://ip-192-168-178-20.eu-central-1.compute.internal:8080/actuator/mappings', }, { id: 'metrics', url: 'http://ip-192-168-178-20.eu-central-1.compute.internal:8080/actuator/metrics', }, { id: 'conditions', url: 'http://ip-192-168-178-20.eu-central-1.compute.internal:8080/actuator/conditions', }, { id: 'httpexchanges', url: 'http://ip-192-168-178-20.eu-central-1.compute.internal:8080/actuator/httpexchanges', }, { id: 'shutdown', url: 'http://ip-192-168-178-20.eu-central-1.compute.internal:8080/actuator/shutdown', }, ], buildVersion: '3.0.0-SNAPSHOT', tags: { 'de-service-test-6': 'A large content', 'de-service-test-4': 'A large content', 'de-service-test-5': 'A large content', 'de-service-test-2': 'A large content', 'de-service-test-3': 'A large content', environment: 'test', 'de-service-test-1': 'A large content', }, }, { id: 'af578c480d41', version: 1, registration: { name: 'spring-boot-admin-sample-servlet', managementUrl: 'http://ip-192-168-178-20.eu-central-1.compute.internal:8888/actuator', healthUrl: 'http://ip-192-168-178-20.eu-central-1.compute.internal:8888/actuator/health', serviceUrl: 'http://ip-192-168-178-20.eu-central-1.compute.internal:8888/', source: 'http-api', metadata: { 'tags.de-service-test-6': 'A large content', 'tags.de-service-test-4': 'A large content', 'tags.de-service-test-5': 'A large content', startup: '2022-12-19T11:07:19.690036+01:00', 'kubectl.kubernetes.iolast-applied-configuration': '{"name":"jvm.threads.peak","description":"The peak live thread count since the Java virtual machine started or peak was reset","baseUnit":"threads","measurements":[{"statistic":"VALUE","value":64.0}],"availableTags":[]}', 'user.name': 'user', 'tags.de-service-test-2': 'A large content', 'user.password': '******', 'tags.de-service-test-3': 'A large content', 'tags.environment': 'test', 'tags.de-service-test-1': 'A large content', }, }, registered: true, statusInfo: { status: 'UP', details: { reactiveDiscoveryClients: { description: 'Discovery Client not initialized', status: 'UNKNOWN', components: { 'Simple Reactive Discovery Client': { description: 'Discovery Client not initialized', status: 'UNKNOWN', }, }, }, clientConfigServer: { status: 'UNKNOWN', details: { error: 'no property sources located', }, }, diskSpace: { status: 'UP', details: { total: 994662584320, free: 332211322880, threshold: 10485760, path: '/Users/stekoe/workspaces/cc/spring-boot-admin/spring-boot-admin-samples/spring-boot-admin-sample-servlet/.', exists: true, }, }, ping: { status: 'UP', }, discoveryComposite: { description: 'Discovery Client not initialized', status: 'UNKNOWN', components: { discoveryClient: { description: 'Discovery Client not initialized', status: 'UNKNOWN', }, }, }, refreshScope: { status: 'UP', }, db: { status: 'UP', details: { database: 'HSQL Database Engine', validationQuery: 'isValid()', }, }, }, }, statusTimestamp: '2022-12-19T10:07:20.646905Z', info: {}, endpoints: [ { id: 'health', url: 'http://ip-192-168-178-20.eu-central-1.compute.internal:8888/actuator/health', }, ], buildVersion: null, tags: { 'de-service-test-6': 'A large content', 'de-service-test-4': 'A large content', 'de-service-test-5': 'A large content', 'de-service-test-2': 'A large content', 'de-service-test-3': 'A large content', environment: 'test', 'de-service-test-1': 'A large content', }, }, ], }; ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/mocks/fixtures/eventStream/removeInstance.ts ================================================ export const removeInstanceEvent = { name: 'spring-boot-admin-sample-servlet', buildVersion: '3.0.0-SNAPSHOT', status: 'RESTRICTED', statusTimestamp: '2022-12-19T10:12:38.430457Z', instances: [ { id: '25b07dc98984', version: 3, registration: { name: 'spring-boot-admin-sample-servlet', managementUrl: 'http://ip-192-168-178-20.eu-central-1.compute.internal:8080/actuator', healthUrl: 'http://ip-192-168-178-20.eu-central-1.compute.internal:8080/actuator/health', serviceUrl: 'http://ip-192-168-178-20.eu-central-1.compute.internal:8080/', source: 'http-api', metadata: { 'tags.de-service-test-6': 'A large content', 'tags.de-service-test-4': 'A large content', 'tags.de-service-test-5': 'A large content', startup: '2022-12-19T11:04:38.171314+01:00', 'kubectl.kubernetes.iolast-applied-configuration': '{"name":"jvm.threads.peak","description":"The peak live thread count since the Java virtual machine started or peak was reset","baseUnit":"threads","measurements":[{"statistic":"VALUE","value":64.0}],"availableTags":[]}', 'user.name': 'user', 'tags.de-service-test-2': 'A large content', 'user.password': '******', 'tags.de-service-test-3': 'A large content', 'tags.environment': 'test', 'tags.de-service-test-1': 'A large content', }, }, registered: true, statusInfo: { status: 'UP', details: { reactiveDiscoveryClients: { description: 'Discovery Client not initialized', status: 'UNKNOWN', components: { 'Simple Reactive Discovery Client': { description: 'Discovery Client not initialized', status: 'UNKNOWN', }, }, }, clientConfigServer: { status: 'UNKNOWN', details: { error: 'no property sources located', }, }, diskSpace: { status: 'UP', details: { total: 994662584320, free: 333813846016, threshold: 10485760, path: '/Users/stekoe/workspaces/cc/spring-boot-admin/spring-boot-admin-samples/spring-boot-admin-sample-servlet/.', exists: true, }, }, ping: { status: 'UP', }, discoveryComposite: { description: 'Discovery Client not initialized', status: 'UNKNOWN', components: { discoveryClient: { description: 'Discovery Client not initialized', status: 'UNKNOWN', }, }, }, refreshScope: { status: 'UP', }, db: { status: 'UP', details: { database: 'HSQL Database Engine', validationQuery: 'isValid()', }, }, }, }, statusTimestamp: '2022-12-19T10:04:39.457363Z', info: { build: { artifact: 'spring-boot-admin-sample-servlet', name: 'Spring Boot Admin Sample Servlet', time: '2022-12-16T07:23:45.732Z', version: '3.0.0-SNAPSHOT', group: 'de.codecentric', }, }, endpoints: [ { id: 'caches', url: 'http://ip-192-168-178-20.eu-central-1.compute.internal:8080/actuator/caches', }, { id: 'loggers', url: 'http://ip-192-168-178-20.eu-central-1.compute.internal:8080/actuator/loggers', }, { id: 'heapdump', url: 'http://ip-192-168-178-20.eu-central-1.compute.internal:8080/actuator/heapdump', }, { id: 'features', url: 'http://ip-192-168-178-20.eu-central-1.compute.internal:8080/actuator/features', }, { id: 'startup', url: 'http://ip-192-168-178-20.eu-central-1.compute.internal:8080/actuator/startup', }, { id: 'beans', url: 'http://ip-192-168-178-20.eu-central-1.compute.internal:8080/actuator/beans', }, { id: 'configprops', url: 'http://ip-192-168-178-20.eu-central-1.compute.internal:8080/actuator/configprops', }, { id: 'threaddump', url: 'http://ip-192-168-178-20.eu-central-1.compute.internal:8080/actuator/threaddump', }, { id: 'auditevents', url: 'http://ip-192-168-178-20.eu-central-1.compute.internal:8080/actuator/auditevents', }, { id: 'info', url: 'http://ip-192-168-178-20.eu-central-1.compute.internal:8080/actuator/info', }, { id: 'resume', url: 'http://ip-192-168-178-20.eu-central-1.compute.internal:8080/actuator/resume', }, { id: 'sessions', url: 'http://ip-192-168-178-20.eu-central-1.compute.internal:8080/actuator/sessions', }, { id: 'restart', url: 'http://ip-192-168-178-20.eu-central-1.compute.internal:8080/actuator/restart', }, { id: 'logfile', url: 'http://ip-192-168-178-20.eu-central-1.compute.internal:8080/actuator/logfile', }, { id: 'custom', url: 'http://ip-192-168-178-20.eu-central-1.compute.internal:8080/actuator/custom', }, { id: 'health', url: 'http://ip-192-168-178-20.eu-central-1.compute.internal:8080/actuator/health', }, { id: 'refresh', url: 'http://ip-192-168-178-20.eu-central-1.compute.internal:8080/actuator/refresh', }, { id: 'env', url: 'http://ip-192-168-178-20.eu-central-1.compute.internal:8080/actuator/env', }, { id: 'pause', url: 'http://ip-192-168-178-20.eu-central-1.compute.internal:8080/actuator/pause', }, { id: 'scheduledtasks', url: 'http://ip-192-168-178-20.eu-central-1.compute.internal:8080/actuator/scheduledtasks', }, { id: 'mappings', url: 'http://ip-192-168-178-20.eu-central-1.compute.internal:8080/actuator/mappings', }, { id: 'metrics', url: 'http://ip-192-168-178-20.eu-central-1.compute.internal:8080/actuator/metrics', }, { id: 'conditions', url: 'http://ip-192-168-178-20.eu-central-1.compute.internal:8080/actuator/conditions', }, { id: 'httpexchanges', url: 'http://ip-192-168-178-20.eu-central-1.compute.internal:8080/actuator/httpexchanges', }, { id: 'shutdown', url: 'http://ip-192-168-178-20.eu-central-1.compute.internal:8080/actuator/shutdown', }, ], buildVersion: '3.0.0-SNAPSHOT', tags: { 'de-service-test-6': 'A large content', 'de-service-test-4': 'A large content', 'de-service-test-5': 'A large content', 'de-service-test-2': 'A large content', 'de-service-test-3': 'A large content', environment: 'test', 'de-service-test-1': 'A large content', }, }, { id: 'af578c480d41', version: 4, registration: { name: 'spring-boot-admin-sample-servlet', managementUrl: 'http://ip-192-168-178-20.eu-central-1.compute.internal:8888/actuator', healthUrl: 'http://ip-192-168-178-20.eu-central-1.compute.internal:8888/actuator/health', serviceUrl: 'http://ip-192-168-178-20.eu-central-1.compute.internal:8888/', source: 'http-api', metadata: { 'tags.de-service-test-6': 'A large content', 'tags.de-service-test-4': 'A large content', 'tags.de-service-test-5': 'A large content', startup: '2022-12-19T11:07:19.690036+01:00', 'kubectl.kubernetes.iolast-applied-configuration': '{"name":"jvm.threads.peak","description":"The peak live thread count since the Java virtual machine started or peak was reset","baseUnit":"threads","measurements":[{"statistic":"VALUE","value":64.0}],"availableTags":[]}', 'user.name': 'user', 'tags.de-service-test-2': 'A large content', 'user.password': '******', 'tags.de-service-test-3': 'A large content', 'tags.environment': 'test', 'tags.de-service-test-1': 'A large content', }, }, registered: true, statusInfo: { status: 'OFFLINE', details: { exception: 'org.springframework.web.reactive.function.client.WebClientRequestException', message: 'Connection refused: ip-192-168-178-20.eu-central-1.compute.internal/192.168.178.20:8888', }, }, statusTimestamp: '2022-12-19T10:12:38.430457Z', info: { build: { artifact: 'spring-boot-admin-sample-servlet', name: 'Spring Boot Admin Sample Servlet', time: '2022-12-19T10:04:57.047Z', version: '3.0.0-SNAPSHOT', group: 'de.codecentric', }, }, endpoints: [ { id: 'caches', url: 'http://ip-192-168-178-20.eu-central-1.compute.internal:8888/actuator/caches', }, { id: 'loggers', url: 'http://ip-192-168-178-20.eu-central-1.compute.internal:8888/actuator/loggers', }, { id: 'heapdump', url: 'http://ip-192-168-178-20.eu-central-1.compute.internal:8888/actuator/heapdump', }, { id: 'features', url: 'http://ip-192-168-178-20.eu-central-1.compute.internal:8888/actuator/features', }, { id: 'startup', url: 'http://ip-192-168-178-20.eu-central-1.compute.internal:8888/actuator/startup', }, { id: 'beans', url: 'http://ip-192-168-178-20.eu-central-1.compute.internal:8888/actuator/beans', }, { id: 'configprops', url: 'http://ip-192-168-178-20.eu-central-1.compute.internal:8888/actuator/configprops', }, { id: 'threaddump', url: 'http://ip-192-168-178-20.eu-central-1.compute.internal:8888/actuator/threaddump', }, { id: 'auditevents', url: 'http://ip-192-168-178-20.eu-central-1.compute.internal:8888/actuator/auditevents', }, { id: 'info', url: 'http://ip-192-168-178-20.eu-central-1.compute.internal:8888/actuator/info', }, { id: 'resume', url: 'http://ip-192-168-178-20.eu-central-1.compute.internal:8888/actuator/resume', }, { id: 'sessions', url: 'http://ip-192-168-178-20.eu-central-1.compute.internal:8888/actuator/sessions', }, { id: 'restart', url: 'http://ip-192-168-178-20.eu-central-1.compute.internal:8888/actuator/restart', }, { id: 'logfile', url: 'http://ip-192-168-178-20.eu-central-1.compute.internal:8888/actuator/logfile', }, { id: 'custom', url: 'http://ip-192-168-178-20.eu-central-1.compute.internal:8888/actuator/custom', }, { id: 'health', url: 'http://ip-192-168-178-20.eu-central-1.compute.internal:8888/actuator/health', }, { id: 'refresh', url: 'http://ip-192-168-178-20.eu-central-1.compute.internal:8888/actuator/refresh', }, { id: 'env', url: 'http://ip-192-168-178-20.eu-central-1.compute.internal:8888/actuator/env', }, { id: 'pause', url: 'http://ip-192-168-178-20.eu-central-1.compute.internal:8888/actuator/pause', }, { id: 'scheduledtasks', url: 'http://ip-192-168-178-20.eu-central-1.compute.internal:8888/actuator/scheduledtasks', }, { id: 'mappings', url: 'http://ip-192-168-178-20.eu-central-1.compute.internal:8888/actuator/mappings', }, { id: 'metrics', url: 'http://ip-192-168-178-20.eu-central-1.compute.internal:8888/actuator/metrics', }, { id: 'conditions', url: 'http://ip-192-168-178-20.eu-central-1.compute.internal:8888/actuator/conditions', }, { id: 'httpexchanges', url: 'http://ip-192-168-178-20.eu-central-1.compute.internal:8888/actuator/httpexchanges', }, { id: 'shutdown', url: 'http://ip-192-168-178-20.eu-central-1.compute.internal:8888/actuator/shutdown', }, ], buildVersion: '3.0.0-SNAPSHOT', tags: { 'de-service-test-6': 'A large content', 'de-service-test-4': 'A large content', 'de-service-test-5': 'A large content', 'de-service-test-2': 'A large content', 'de-service-test-3': 'A large content', environment: 'test', 'de-service-test-1': 'A large content', }, }, ], }; ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/mocks/instance/auditevents/data.ts ================================================ const now = new Date(); const today = [ now.getFullYear(), String(now.getMonth() + 1).padStart(2, '0'), String(now.getDate()).padStart(2, '0'), ].join('-'); export const auditeventsresponse = { events: [ { timestamp: today + 'T05:03:58.546Z', principal: 'anonymous', type: 'CREATE_TEST', data: { json: '"Test"', source: 'DUM', sessionId: 'anonymous', objectId: 'Test', }, }, { timestamp: today + 'T05:03:57.435400Z', principal: 'anonymous', type: 'CREATE_TEST', data: { json: '{"id":"12345","timestamp":"2021-10-12T14:48:34.738201+08:00","gender":"M"}', source: 'DUM', sessionId: 'anonymous', objectId: '12345', }, }, { timestamp: today + 'T05:03:55.729744Z', principal: 'anonymous', type: 'CREATE_TEST', data: { json: '"Test"', source: 'DUM', sessionId: 'anonymous', objectId: 'Test', }, }, { timestamp: today + 'T05:03:55.729498Z', principal: 'anonymous', type: 'CREATE_TEST', data: { json: '{"id":"12345","timestamp":"2021-10-12T14:48:34.738201+08:00","gender":"M"}', source: 'DUM', sessionId: 'anonymous', objectId: '12345', }, }, { timestamp: today + 'T04:42:49.715651Z', principal: 'anonymous', type: 'CREATE_TEST', data: { json: '"Test"', source: 'DUM', sessionId: 'anonymous', objectId: 'Test', }, }, { timestamp: today + 'T04:42:49.715194Z', principal: 'anonymous', type: 'CREATE_TEST', data: { json: '{"id":"12345","timestamp":"2021-10-12T14:48:34.738201+08:00","gender":"M"}', source: 'DUM', sessionId: 'anonymous', objectId: '12345', }, }, { timestamp: today + 'T04:42:49.156299Z', principal: 'anonymous', type: 'CREATE_TEST', data: { json: '"Test"', source: 'DUM', sessionId: 'anonymous', objectId: 'Test', }, }, { timestamp: today + 'T04:42:49.156030Z', principal: 'anonymous', type: 'CREATE_TEST', data: { json: '{"id":"12345","timestamp":"2021-10-12T14:48:34.738201+08:00","gender":"M"}', source: 'DUM', sessionId: 'anonymous', objectId: '12345', }, }, { timestamp: today + 'T04:42:48.773277Z', principal: 'anonymous', type: 'CREATE_TEST', data: { json: '"Test"', source: 'DUM', sessionId: 'anonymous', objectId: 'Test', }, }, { timestamp: today + 'T04:42:48.773099Z', principal: 'anonymous', type: 'CREATE_TEST', data: { json: '{"id":"12345","timestamp":"2021-10-12T14:48:34.738201+08:00","gender":"M"}', source: 'DUM', sessionId: 'anonymous', objectId: '12345', }, }, { timestamp: today + 'T04:42:05.304815Z', principal: 'anonymous', type: 'CREATE_TEST', data: { json: '"Test"', source: 'DUM', sessionId: 'anonymous', objectId: 'Test', }, }, { timestamp: today + 'T04:42:05.297055Z', principal: 'anonymous', type: 'CREATE_TEST', data: { json: '{"id":"12345","timestamp":"2021-10-12T14:48:34.738201+08:00","gender":"M"}', source: 'DUM', sessionId: 'anonymous', objectId: '12345', }, }, { timestamp: today + 'T04:37:54.450217Z', principal: 'anonymous', type: 'CREATE_TEST', data: { json: '"Test"', source: 'application', sessionId: 'anonymous', objectId: 'Test', }, }, { timestamp: today + 'T04:37:54.449951Z', principal: 'anonymous', type: 'CREATE_TEST', data: { json: '{"id":"12345","timestamp":"2021-10-12T14:48:34.738201+08:00","gender":"M"}', source: 'application', sessionId: 'anonymous', objectId: '12345', }, }, { timestamp: today + 'T04:29:40.238505Z', principal: 'anonymous', type: 'CREATE_TEST', data: { json: '"Test"', source: 'application', sessionId: 'anonymous', objectId: 'Test', }, }, { timestamp: today + 'T04:29:40.238317Z', principal: 'anonymous', type: 'CREATE_TEST', data: { json: '{"id":"12345","timestamp":"2021-10-12T14:48:34.738201+08:00","gender":"M"}', source: 'application', sessionId: 'anonymous', objectId: '12345', }, }, { timestamp: today + 'T01:15:34.825846Z', principal: 'anonymous', type: 'CREATE_TEST', data: { json: '"Test"', source: 'DUM', sessionId: 'anonymous', objectId: 'Test', }, }, { timestamp: today + 'T01:15:34.825565Z', principal: 'anonymous', type: 'CREATE_TEST', data: { json: '{"id":"12345","timestamp":"2021-10-12T14:48:34.738201+08:00","gender":"M"}', source: 'DUM', sessionId: 'anonymous', objectId: '12345', }, }, { timestamp: today + 'T01:15:34.001904Z', principal: 'anonymous', type: 'CREATE_TEST', data: { json: '"Test"', source: 'DUM', sessionId: 'anonymous', objectId: 'Test', }, }, { timestamp: today + 'T01:15:34.001034Z', principal: 'anonymous', type: 'CREATE_TEST', data: { json: '{"id":"12345","timestamp":"2021-10-12T14:48:34.738201+08:00","gender":"M"}', source: 'DUM', sessionId: 'anonymous', objectId: '12345', }, }, { timestamp: today + 'T01:15:33.561225Z', principal: 'anonymous', type: 'CREATE_TEST', data: { json: '"Test"', source: 'DUM', sessionId: 'anonymous', objectId: 'Test', }, }, { timestamp: today + 'T01:15:33.560174Z', principal: 'anonymous', type: 'CREATE_TEST', data: { json: '{"id":"12345","timestamp":"2021-10-12T14:48:34.738201+08:00","gender":"M"}', source: 'DUM', sessionId: 'anonymous', objectId: '12345', }, }, { timestamp: today + 'T01:14:41.743788Z', principal: 'anonymous', type: 'CREATE_TEST', data: { json: '"Test"', source: 'DUM', sessionId: 'anonymous', objectId: 'Test', }, }, { timestamp: today + 'T01:14:41.742988Z', principal: 'anonymous', type: 'CREATE_TEST', data: { json: '{"id":"12345","timestamp":"2021-10-12T14:48:34.738201+08:00","gender":"M"}', source: 'DUM', sessionId: 'anonymous', objectId: '12345', }, }, { timestamp: today + 'T01:14:13.969356Z', principal: 'anonymous', type: 'CREATE_TEST', data: { json: '"Test"', source: 'DUM', sessionId: 'anonymous', objectId: 'Test', }, }, { timestamp: today + 'T01:14:13.954901Z', principal: 'anonymous', type: 'CREATE_TEST', data: { json: '{"id":"12345","timestamp":"2021-10-12T14:48:34.738201+08:00","gender":"M"}', source: 'DUM', sessionId: 'anonymous', objectId: '12345', }, }, ], }; ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/mocks/instance/auditevents/index.ts ================================================ import { HttpResponse, http } from 'msw'; import { auditeventsresponse } from '@/mocks/instance/auditevents/data'; const endpoints = [ http.get('/instances/:instanceId/actuator/auditevents', () => { return HttpResponse.json(auditeventsresponse); }), ]; export default endpoints; ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/mocks/instance/dependencies/data.ts ================================================ export const sbomsResponse = { ids: ['application', 'system'], }; export const applicationSbomResponse = { bomFormat: 'CycloneDX', specVersion: '1.5', serialNumber: 'urn:uuid:9f17e432-9d33-3d31-aee7-bbda6211a526', version: 1, metadata: { timestamp: '2024-04-29T14:17:19Z', lifecycles: [ { phase: 'build', }, ], tools: [ { vendor: 'OWASP Foundation', name: 'CycloneDX Maven plugin', version: '2.8.0', hashes: [ { alg: 'MD5', content: '76ffec6a7ddd46b2b24517411874eb99', }, { alg: 'SHA-1', content: '5b0d5b41975b53be4799b9621b4af0cfc41d44b6', }, { alg: 'SHA-256', content: '6852aa0f4e42a2db745bab80e384951a6a65b9215d041081d675780999027e81', }, { alg: 'SHA-512', content: '417de20fcdcb11c9713bacbd57290d8e68037fdb4553fd31b8cb08bd760ad52dc65ea88ad4be15844ad3fd5a4d3e440d2f70326f2fe1e63ec78e059c9a883f8d', }, { alg: 'SHA-384', content: '5eb755c6492e7a7385fa9a1e1f4517875bcb834b2df437808a37a2d6f5285df428741762305980315a63fcef1406597d', }, { alg: 'SHA3-384', content: '0fe16a47cf7aab0b22251dafcc39939b68e8f1778093309d8d2060b51a08df445a8b8ed5a9561669faf2e55f907c76d8', }, { alg: 'SHA3-256', content: '3e5a1eb5ab7d0797498862794709ff8eaaa071fe4cc9ec77f52db7e2f97ef487', }, { alg: 'SHA3-512', content: '59281a3e29e76270d7f44b40b5b9f05e55f1ae3ec716d80add806f360940809e3813998ac7c5758043b8e248aed73b86e37dc506cdb4cde03c16bb617d8e5a3a', }, ], }, ], component: { publisher: 'codecentric AG', group: 'de.codecentric', name: 'spring-boot-admin-sample-servlet', version: '3.2.4-SNAPSHOT', description: 'Spring Boot Admin Sample Servlet', licenses: [ { license: { id: 'Apache-2.0', }, }, ], purl: 'pkg:maven/de.codecentric/spring-boot-admin-sample-servlet@3.2.4-SNAPSHOT?type=jar', externalReferences: [ { type: 'website', url: 'https://github.com/codecentric/spring-boot-admin/spring-boot-admin-dependencies/spring-boot-admin-build/spring-boot-admin-samples/spring-boot-admin-sample-servlet/', }, { type: 'distribution-intake', url: 'https://oss.sonatype.org/service/local/staging/deploy/maven2/', }, { type: 'vcs', url: 'https://github.com/codecentric/spring-boot-admin/spring-boot-admin-dependencies/spring-boot-admin-build/spring-boot-admin-samples/spring-boot-admin-sample-servlet', }, ], type: 'library', 'bom-ref': 'pkg:maven/de.codecentric/spring-boot-admin-sample-servlet@3.2.4-SNAPSHOT?type=jar', }, properties: [ { name: 'maven.goal', value: 'makeBom', }, { name: 'maven.scopes', value: 'compile,provided,runtime,system', }, ], }, components: [ { group: 'de.codecentric', name: 'spring-boot-admin-sample-custom-ui', version: '3.2.4-SNAPSHOT', scope: 'required', purl: 'pkg:maven/de.codecentric/spring-boot-admin-sample-custom-ui@3.2.4-SNAPSHOT?type=jar', type: 'library', 'bom-ref': 'pkg:maven/de.codecentric/spring-boot-admin-sample-custom-ui@3.2.4-SNAPSHOT?type=jar', }, { group: 'de.codecentric', name: 'spring-boot-admin-starter-server', version: '3.2.4-SNAPSHOT', scope: 'required', purl: 'pkg:maven/de.codecentric/spring-boot-admin-starter-server@3.2.4-SNAPSHOT?type=jar', type: 'library', 'bom-ref': 'pkg:maven/de.codecentric/spring-boot-admin-starter-server@3.2.4-SNAPSHOT?type=jar', }, { publisher: 'VMware, Inc.', group: 'org.springframework.boot', name: 'spring-boot-starter-security', version: '3.3.0-RC1', description: 'Starter for using Spring Security', scope: 'required', hashes: [ { alg: 'MD5', content: '4dbb08add7a37e1e1f8b9a32cad03125', }, { alg: 'SHA-1', content: 'a67dc8d84c8a4562ad257d906a140b35bbe69ea3', }, { alg: 'SHA-256', content: '5142a739b5dcc1fcf0ae83cd395b35375aee5a49a0b431579ed6b8c44690a93b', }, { alg: 'SHA-512', content: 'b3565a006dcb8edd49d77be4cfb65b94c949599e3b4be3eb3291184ac9392ba78c7c9f8fd4316e0d721b92a59d99ac785536ebc8c49419e0029cc9d6f602acdf', }, { alg: 'SHA-384', content: 'bddaf7e20a489e635e28d5511e16bd902ff61eb37f1793bde86f016ec7c5ae18eae07c939af5c7e7edae9bb8fc68911b', }, { alg: 'SHA3-384', content: '630b82ecb8da9c548eab50f1e8ec923eeb6d4f3a5543ed1526eab98da7ab8b993df7687fd1d78c7df62a7b5ff8bf0a73', }, { alg: 'SHA3-256', content: '37c888764bafa9f554b4030adebc6ce342a36b9132eab3e7ffeceb97e3b04973', }, { alg: 'SHA3-512', content: '37660fe7bebb9569d62007fc89292620aeb5b860ce5eed515b5d9e0c7aa386d96b14a258d2d064910910b8c2abcc3ad9a0682d7afca76d3b04e33f53e9840924', }, ], licenses: [ { license: { id: 'Apache-2.0', }, }, ], purl: 'pkg:maven/org.springframework.boot/spring-boot-starter-security@3.3.0-RC1?type=jar', externalReferences: [ { type: 'website', url: 'https://spring.io/projects/spring-boot', }, { type: 'issue-tracker', url: 'https://github.com/spring-projects/spring-boot/issues', }, { type: 'vcs', url: 'https://github.com/spring-projects/spring-boot', }, ], type: 'library', 'bom-ref': 'pkg:maven/org.springframework.boot/spring-boot-starter-security@3.3.0-RC1?type=jar', }, { publisher: 'VMware, Inc.', group: 'org.springframework.boot', name: 'spring-boot-starter', version: '3.3.0-RC1', description: 'Core starter, including auto-configuration support, logging and YAML', scope: 'required', hashes: [ { alg: 'MD5', content: 'dcf654ba1433f982859b6164500f0cc8', }, { alg: 'SHA-1', content: '1fa34d1ded58f3464160bb4796164eb97c793acc', }, { alg: 'SHA-256', content: '161cfb61381a1569f0d0bc6f5bd61c945fc1453ae68690911052315a4aeed8fe', }, { alg: 'SHA-512', content: '8d94109934e6283ce27527c3502c78f31076047f5ac557c029f14bc6db14b0ba995ff6ca7484d4ec70d3d92e01c6ce1d7fafff5ae5b382b54ab18073755cc97d', }, { alg: 'SHA-384', content: '0cf7e45ac5e200e21c2552e8efe15f583b5482fadacf72d1726c2e0bedc57bbe759adb754ead2a6e29a693e69d4d6458', }, { alg: 'SHA3-384', content: '6a5de8f018765305adc03a0c659da62e8b24c44e4453848d5cfb0ce1cb7e90bc1924a64aded610815f6d42cd870b6e22', }, { alg: 'SHA3-256', content: '5dd5e08afa4f155445dcd7c39f64e53c92aeca801d0a39e0044d19fcfcb83ba7', }, { alg: 'SHA3-512', content: '932b5627ad1f29802a1854e78ddb35e8fbe5a41884de4943203203640a7dabdd23ace6670204496152b7c92e45441770dcee9b6be5931f9f5eee5341c9fcc5c8', }, ], licenses: [ { license: { id: 'Apache-2.0', }, }, ], purl: 'pkg:maven/org.springframework.boot/spring-boot-starter@3.3.0-RC1?type=jar', externalReferences: [ { type: 'website', url: 'https://spring.io/projects/spring-boot', }, { type: 'issue-tracker', url: 'https://github.com/spring-projects/spring-boot/issues', }, { type: 'vcs', url: 'https://github.com/spring-projects/spring-boot', }, ], type: 'library', 'bom-ref': 'pkg:maven/org.springframework.boot/spring-boot-starter@3.3.0-RC1?type=jar', }, { publisher: 'VMware, Inc.', group: 'org.springframework.boot', name: 'spring-boot', version: '3.3.0-RC1', description: 'Spring Boot', scope: 'required', hashes: [ { alg: 'MD5', content: '6670303cfb0ff56e5ac1d42a5dc1a159', }, { alg: 'SHA-1', content: 'b861466d5af7da9954661f20f851405aa5f8f5d0', }, { alg: 'SHA-256', content: 'c6197c0ca9cdc8bb1aa47b5194c79d41fb08b4a12db20954faca2b796e395265', }, { alg: 'SHA-512', content: '71932836039ea36ed5c9f00a83b83718083db69806425d49258c3b4c0047d60d7f9d6c2b416cc3fc9f4974496d5bf173fb734f1fc2ca44ba5496ade3edf85a05', }, { alg: 'SHA-384', content: '09b2a5ccea74fea8933f525c919d1294689d1f3df7bc78ef0faff1429cc94edc519b8555acfe03162b677eba17b8cbce', }, { alg: 'SHA3-384', content: '86f7da509f3f00ff3eeba112fd2427000499b850c0bcc28bd8496fd49c5bcf7e4c8b6226e1a1e21a79b43207a06a80a8', }, { alg: 'SHA3-256', content: '59aae76d129902824c07e70e811087b084f74bbd1b651d0cb5befb8c9ef37d61', }, { alg: 'SHA3-512', content: 'dcbf37d6c552fd0d2dae949e603f18181b6c58ebfaf3a51da680e44da77c79d470f237b256fbc53defcc651cd6fc319d42615af6752c735427975b3fa3827de6', }, ], licenses: [ { license: { id: 'Apache-2.0', }, }, ], purl: 'pkg:maven/org.springframework.boot/spring-boot@3.3.0-RC1?type=jar', externalReferences: [ { type: 'website', url: 'https://spring.io/projects/spring-boot', }, { type: 'issue-tracker', url: 'https://github.com/spring-projects/spring-boot/issues', }, { type: 'vcs', url: 'https://github.com/spring-projects/spring-boot', }, ], type: 'library', 'bom-ref': 'pkg:maven/org.springframework.boot/spring-boot@3.3.0-RC1?type=jar', }, { publisher: 'VMware, Inc.', group: 'org.springframework.boot', name: 'spring-boot-autoconfigure', version: '3.3.0-RC1', description: 'Spring Boot AutoConfigure', scope: 'required', hashes: [ { alg: 'MD5', content: 'c57c22c83a87833977f3a9d60f538580', }, { alg: 'SHA-1', content: '8f2eac2c0527567fbe3149dcda0d86c56fcf658e', }, { alg: 'SHA-256', content: '0c48949b7ee05da897ccb5f06180bf495372163badd4c089fbdf1b3c1a0437bd', }, { alg: 'SHA-512', content: 'ca39d678883d7c78d32eec49df582a2c9e3ce0423e22df33b6a2e19027fd180ed4f3bb3dc814e671c5d149f11fa971e77a064a327c2d4555972c66bc787f415d', }, { alg: 'SHA-384', content: '7394307a140b1344157a016c88d31512ab2794481020b928a62def6a1acc4429028846997d1114182d6381e15ef678d1', }, { alg: 'SHA3-384', content: 'be85aa9dd5b4135f5ca46e744cecfd4d7754530dbdabc0baef61fe4dc02fc44eb1f658151fdffe6ba0dcba2b5dbbca3d', }, { alg: 'SHA3-256', content: 'b65e1946b8d0d64a949ed174107973227dc00cc14d9b745b09d157472b5ceb02', }, { alg: 'SHA3-512', content: '79d79d1026d80dcdc3b967815a39b792e183666e16e55f2a86429b913e276a82c18b4bb2308786a002a68ffb2b89c706b610399d32c5d66750af949be485fad9', }, ], licenses: [ { license: { id: 'Apache-2.0', }, }, ], purl: 'pkg:maven/org.springframework.boot/spring-boot-autoconfigure@3.3.0-RC1?type=jar', externalReferences: [ { type: 'website', url: 'https://spring.io/projects/spring-boot', }, { type: 'issue-tracker', url: 'https://github.com/spring-projects/spring-boot/issues', }, { type: 'vcs', url: 'https://github.com/spring-projects/spring-boot', }, ], type: 'library', 'bom-ref': 'pkg:maven/org.springframework.boot/spring-boot-autoconfigure@3.3.0-RC1?type=jar', }, { publisher: 'VMware, Inc.', group: 'org.springframework.boot', name: 'spring-boot-starter-logging', version: '3.3.0-RC1', description: 'Starter for logging using Logback. Default logging starter', scope: 'required', hashes: [ { alg: 'MD5', content: 'e1110a14ce2877746f805cba59913bc5', }, { alg: 'SHA-1', content: 'df66b2674eca6a51aadcaf89f8a94f7d60f1e56f', }, { alg: 'SHA-256', content: 'fba9b83103dafe5885fd22244f33a8afbec8fe5f94f9f09c19889aaeb638cd6e', }, { alg: 'SHA-512', content: '8880d8536b0a322373dcc4496c00f44c3257ad5008e8f886f29ee54bcce4380bb9ba43f9a0d7d610ebe4ecd3117b1fd7fca3513895f14bed48ecabd7d7514aa0', }, { alg: 'SHA-384', content: '9429194771ea17146bb151442bf72bc3e238f382c6cb76aadfede5cfd5fbbe9f1616a6cf5676ceecb07e04373cb3f5cb', }, { alg: 'SHA3-384', content: '4db4bb42dda7fcdafa74e3543f853b4098efde28ff9d09f706932bb94826dbcf66d2afff86b7d526b8e780a292adb193', }, { alg: 'SHA3-256', content: '4ff08b5b2abd9ddf507b5586bda88f7b2bce600143421b7d215f506d85f72345', }, { alg: 'SHA3-512', content: '03a275dec748ce967180e40a31e76050ccdf83826335dc1b430b7576ebf86cc2952deff8b46a276edc60d437f373dbbdca6c3789102f7be5c8a19655fd39bf56', }, ], licenses: [ { license: { id: 'Apache-2.0', }, }, ], purl: 'pkg:maven/org.springframework.boot/spring-boot-starter-logging@3.3.0-RC1?type=jar', externalReferences: [ { type: 'website', url: 'https://spring.io/projects/spring-boot', }, { type: 'issue-tracker', url: 'https://github.com/spring-projects/spring-boot/issues', }, { type: 'vcs', url: 'https://github.com/spring-projects/spring-boot', }, ], type: 'library', 'bom-ref': 'pkg:maven/org.springframework.boot/spring-boot-starter-logging@3.3.0-RC1?type=jar', }, { publisher: 'QOS.ch', group: 'ch.qos.logback', name: 'logback-classic', version: '1.5.6', description: 'logback-classic module', scope: 'required', hashes: [ { alg: 'MD5', content: '83cff9a718cf3449f75d2bda0b9276c6', }, { alg: 'SHA-1', content: 'afc75d260d838a3bddfb8f207c2805ed7d1b34f9', }, { alg: 'SHA-256', content: '6115c6cac5ed1d9db810d14f2f7f4dd6a9f21f0acbba8016e4daaca2ba0f5eb8', }, { alg: 'SHA-512', content: '9e3e227c0effccfd4938558f374877cda7c08c3abf3960bfcb6c7eb2bfdbb49d163484f7120c176b1eaef56e83d7e8921d8c19394a91c52d5bdbcbef660d3ec1', }, { alg: 'SHA-384', content: '1c311989271a0dce0bcb38177fac80721100ed472fe4ec9746e56223e44dbfdbfbeb5b9c3e6b4ab635a5ca2635066bac', }, { alg: 'SHA3-384', content: '0951ec84907202404fdbfb8c7cf8e4ced6aaf4e6fa8850b75a3692f51403508d2150a2c510744bb007bb31350d9ad2f4', }, { alg: 'SHA3-256', content: '9d4d9f5794f39d2e2e83c8f8bae91636bc674d7d355cc450f239dea2905000cd', }, { alg: 'SHA3-512', content: 'a3e1e03e9f3def61d619f86ee1126fc6ffa66a9c60624bf120f9beddbb1095195fbc3fddbc488f5393aea19cd5f4bf5dc47939e2daba15b24aacae392969aa70', }, ], licenses: [ { license: { id: 'EPL-1.0', }, }, { license: { name: 'GNU Lesser General Public License', url: 'http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html', }, }, ], purl: 'pkg:maven/ch.qos.logback/logback-classic@1.5.6?type=jar', externalReferences: [ { type: 'website', url: 'http://logback.qos.ch/logback-classic', }, { type: 'distribution-intake', url: 'https://oss.sonatype.org/service/local/staging/deploy/maven2/', }, { type: 'vcs', url: 'https://github.com/qos-ch/logback/logback-classic', }, ], type: 'library', 'bom-ref': 'pkg:maven/ch.qos.logback/logback-classic@1.5.6?type=jar', }, { publisher: 'QOS.ch', group: 'ch.qos.logback', name: 'logback-core', version: '1.5.6', description: 'logback-core module', scope: 'required', hashes: [ { alg: 'MD5', content: 'd0634e717a5e885c6b7eeb1bcfac5b61', }, { alg: 'SHA-1', content: '41cbe874701200c5624c19e0ab50d1b88dfcc77d', }, { alg: 'SHA-256', content: '898c7d120199f37e1acc8118d97ab15a4d02b0e72e27ba9f05843cb374e160c6', }, { alg: 'SHA-512', content: '44601eea5e12b2ca4a707cb43a04d863e0c5dedaf690a4d95772de725ea4097dcf4058d6449971253487803fdb6d534f107b2c7f17c7ffca5a4811e9bac71fdf', }, { alg: 'SHA-384', content: 'becf9e457234636944263217e9aaa883eb669f5126e02038afe1e74a8e76dc90edd49b41c5d989b30bfda55f955c42f0', }, { alg: 'SHA3-384', content: '8569d0f5bc0bb3ed3b4d4734ae52a88927fa61975c82e2769c18c9a9191659ee91ba095d8fdae7028cfd72455a3d994f', }, { alg: 'SHA3-256', content: 'bde00a4a3cc9ab6e6aa4e76c85be1b13cc182a3db0bbc2fb2fe9e6ac2b2af3f3', }, { alg: 'SHA3-512', content: 'c092d071084f6b4bab96ba5863fdf1c728b81bb789cc5b5c1ca3f7245303d45f1c59be84a7d06a7590f6362ea5dfe08d52ca108a296e338917282cda63608db1', }, ], licenses: [ { license: { id: 'EPL-1.0', }, }, { license: { name: 'GNU Lesser General Public License', url: 'http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html', }, }, ], purl: 'pkg:maven/ch.qos.logback/logback-core@1.5.6?type=jar', externalReferences: [ { type: 'website', url: 'http://logback.qos.ch/logback-core', }, { type: 'distribution-intake', url: 'https://oss.sonatype.org/service/local/staging/deploy/maven2/', }, { type: 'vcs', url: 'https://github.com/qos-ch/logback/logback-core', }, ], type: 'library', 'bom-ref': 'pkg:maven/ch.qos.logback/logback-core@1.5.6?type=jar', }, { publisher: 'The Apache Software Foundation', group: 'org.apache.logging.log4j', name: 'log4j-to-slf4j', version: '2.23.1', description: 'The Apache Log4j binding between Log4j 2 API and SLF4J.', scope: 'required', hashes: [ { alg: 'MD5', content: 'd60143628bb91f9dfa0148c213388b39', }, { alg: 'SHA-1', content: '425ad1eb8a39904d2830e907a324e956fb456520', }, { alg: 'SHA-256', content: '7937a84055156910234e3b42868f55e68ff4b7becbb6ffd10146f72f5bf54dd5', }, { alg: 'SHA-512', content: '86c4dce96d5a929b3adbf2283f7188660831b02f9b04eee55010d132cb50f5677b7bf30c478b432fa2053eb11dbf6744351ce60271bb5e0da3a3f555ed50ad0c', }, { alg: 'SHA-384', content: '3d1423da6781764d19ea13c447da9ec5b9bccec4603dbd710b8e4f26fc53d3051a4d3082973a6b20b5edc024f2d4b4b4', }, { alg: 'SHA3-384', content: '9c05c76f928c4ce7b1ced6a8642257a9036c7fa66fa9655964bc7e37d98a2443da550b0b62be7d3caa357ca714b6ad3b', }, { alg: 'SHA3-256', content: '71f4969e9e3580f190e3194adc07afec56b676a4de3294600e09570497d8c573', }, { alg: 'SHA3-512', content: '483c0ea25d108c651dd80d0b694e13084ea78d64831dbd4435117c2d612f2c25d6fe5ee2e6cd5acafed65aab475890529e3b0201adf9d7e366d4449737dd6d3b', }, ], licenses: [ { license: { id: 'Apache-2.0', url: 'https://www.apache.org/licenses/LICENSE-2.0', }, }, ], purl: 'pkg:maven/org.apache.logging.log4j/log4j-to-slf4j@2.23.1?type=jar', externalReferences: [ { type: 'website', url: 'https://logging.apache.org/log4j/2.x/log4j/log4j-to-slf4j/', }, { type: 'build-system', url: 'https://github.com/apache/logging-log4j2/actions', }, { type: 'distribution', url: 'https://logging.apache.org/logging-parent/latest/#distribution', }, { type: 'distribution-intake', url: 'https://repository.apache.org/service/local/staging/deploy/maven2', }, { type: 'issue-tracker', url: 'https://github.com/apache/logging-log4j2/issues', }, { type: 'mailing-list', url: 'https://lists.apache.org/list.html?log4j-user@logging.apache.org', }, { type: 'vcs', url: 'https://github.com/apache/logging-log4j2', }, ], type: 'library', 'bom-ref': 'pkg:maven/org.apache.logging.log4j/log4j-to-slf4j@2.23.1?type=jar', }, { publisher: 'The Apache Software Foundation', group: 'org.apache.logging.log4j', name: 'log4j-api', version: '2.23.1', description: 'The Apache Log4j API', scope: 'required', hashes: [ { alg: 'MD5', content: 'bee2e2dcbeeb983bdb6b71c9c3476b6a', }, { alg: 'SHA-1', content: '9c15c29c526d9c6783049c0a77722693c66706e1', }, { alg: 'SHA-256', content: '92ec1fd36ab3bc09de6198d2d7c0914685c0f7127ea931acc32fd2ecdd82ea89', }, { alg: 'SHA-512', content: '2a296246b0059ff5fe5c26e2ba3f48aa99e38d7658d613fbd02f32c6d4262f93a67525e6cc4d767fa5c2ab0e39e70bb3c0d3966d38ea4f01608588c084af3162', }, { alg: 'SHA-384', content: '3937cb646009763a94b199a0d6c0065441b9914e2b25e3d58db523874ea760276b445ff300015948d3a813217e0ee404', }, { alg: 'SHA3-384', content: '16ea3301ca37fbede2927399209b403066621789c4f1bee531b5153f27b652458900697fb828170d541a5f3b82e77fb7', }, { alg: 'SHA3-256', content: '0a3dfffc0f362b0a86ad0cd8b36da313c7500a8bdecb0ad7e628c2637d933548', }, { alg: 'SHA3-512', content: '2e230994b8cb7442a2073d60f89c27703ffc78b613dec7891bbfa42e91c95ed684b387ed65df3ee48559ccc06e6877462748f7e2ef985082c8db0741feb576a8', }, ], licenses: [ { license: { id: 'Apache-2.0', url: 'https://www.apache.org/licenses/LICENSE-2.0', }, }, ], purl: 'pkg:maven/org.apache.logging.log4j/log4j-api@2.23.1?type=jar', externalReferences: [ { type: 'website', url: 'https://logging.apache.org/log4j/2.x/log4j/log4j-api/', }, { type: 'build-system', url: 'https://github.com/apache/logging-log4j2/actions', }, { type: 'distribution', url: 'https://logging.apache.org/logging-parent/latest/#distribution', }, { type: 'distribution-intake', url: 'https://repository.apache.org/service/local/staging/deploy/maven2', }, { type: 'issue-tracker', url: 'https://github.com/apache/logging-log4j2/issues', }, { type: 'mailing-list', url: 'https://lists.apache.org/list.html?log4j-user@logging.apache.org', }, { type: 'vcs', url: 'https://github.com/apache/logging-log4j2', }, ], type: 'library', 'bom-ref': 'pkg:maven/org.apache.logging.log4j/log4j-api@2.23.1?type=jar', }, { publisher: 'QOS.ch', group: 'org.slf4j', name: 'jul-to-slf4j', version: '2.0.13', description: 'JUL to SLF4J bridge', scope: 'required', hashes: [ { alg: 'MD5', content: 'd44cfe5a86dae2488e228cac617c6f0e', }, { alg: 'SHA-1', content: 'a3bcd9d9dd50c71ce69f06b1fd05e40fdeff6ba5', }, { alg: 'SHA-256', content: 'fa5ed8f23df2158d0d4d5c82f85cae289d36cc3cd7b7497deff5a37b0b7d7878', }, { alg: 'SHA-512', content: '0cdd6a11e82b740ac3b720e916f7abd9f081d2b0aec27962f3c2d0e7693640dc4be7cc055a4a0e64c34b5258db4483d79a7595411fe9c748fc914334e47a9b5c', }, { alg: 'SHA-384', content: '4c425ac29e0f96343aa1e388cd96f2dec2ac5ea18979f5b8e744cc444ace195ce3cc43234810cfba535151255488d3e9', }, { alg: 'SHA3-384', content: 'fba337469cb78c6764a598e6fed47dcdeeee1a04da2018c28b7108261c7331a3bd7285fafbf658823cec47377438fb24', }, { alg: 'SHA3-256', content: '6e9d4f6c4b6c3e9ce8757b32f514759761732da3ebad187abbf5aef0b6c584cf', }, { alg: 'SHA3-512', content: '566dff26f42114e7f145d8669b5afe92c8a93f027f8c1b02b5ac03716145e8638efc83a8f1d1739f25c024c365eab34183dda6c80a6f42d882db3c4b5c4e0220', }, ], licenses: [ { license: { id: 'MIT', url: 'https://opensource.org/licenses/MIT', }, }, ], purl: 'pkg:maven/org.slf4j/jul-to-slf4j@2.0.13?type=jar', externalReferences: [ { type: 'website', url: 'http://www.slf4j.org', }, { type: 'distribution-intake', url: 'https://oss.sonatype.org/service/local/staging/deploy/maven2/', }, { type: 'vcs', url: 'https://github.com/qos-ch/slf4j/slf4j-parent/jul-to-slf4j', }, ], type: 'library', 'bom-ref': 'pkg:maven/org.slf4j/jul-to-slf4j@2.0.13?type=jar', }, { publisher: 'Eclipse Foundation', group: 'jakarta.annotation', name: 'jakarta.annotation-api', version: '2.1.1', description: 'Jakarta Annotations API', scope: 'required', hashes: [ { alg: 'MD5', content: '5dac2f68e8288d0add4dc92cb161711d', }, { alg: 'SHA-1', content: '48b9bda22b091b1f48b13af03fe36db3be6e1ae3', }, { alg: 'SHA-256', content: '5f65fdaf424eee2b55e1d882ba9bb376be93fb09b37b808be6e22e8851c909fe', }, { alg: 'SHA-512', content: 'eabe8b855b735663684052ec4cc357cc737936fa57cebf144eb09f70b3b6c600db7fa6f1c93a4f36c5994b1b37dad2dfcec87a41448872e69552accfd7f52af6', }, { alg: 'SHA-384', content: '798597a6b80b423844d70609c54b00d725a357031888da7e5c3efd3914d1770be69aa7135de13ddb89a4420a5550e35b', }, { alg: 'SHA3-384', content: '9629b8ca82f61674f5573723bbb3c137060e1442062eb52fa9c90fc8f57ea7d836eb2fb765d160ec8bf300bcb6b820be', }, { alg: 'SHA3-256', content: 'f71ffc2a2c2bd1a00dfc00c4be67dbe5f374078bd50d5b24c0b29fbcc6634ecb', }, { alg: 'SHA3-512', content: 'aa4e29025a55878db6edb0d984bd3a0633f3af03fa69e1d26c97c87c6d29339714003c96e29ff0a977132ce9c2729d0e27e36e9e245a7488266138239bdba15e', }, ], licenses: [ { license: { id: 'EPL-2.0', }, }, { license: { id: 'GPL-2.0-with-classpath-exception', }, }, ], purl: 'pkg:maven/jakarta.annotation/jakarta.annotation-api@2.1.1?type=jar', externalReferences: [ { type: 'website', url: 'https://projects.eclipse.org/projects/ee4j.ca', }, { type: 'distribution-intake', url: 'https://jakarta.oss.sonatype.org/service/local/staging/deploy/maven2/', }, { type: 'issue-tracker', url: 'https://github.com/eclipse-ee4j/common-annotations-api/issues', }, { type: 'mailing-list', url: 'https://dev.eclipse.org/mhonarc/lists/ca-dev', }, { type: 'vcs', url: 'https://github.com/eclipse-ee4j/common-annotations-api', }, ], type: 'library', 'bom-ref': 'pkg:maven/jakarta.annotation/jakarta.annotation-api@2.1.1?type=jar', }, { group: 'org.yaml', name: 'snakeyaml', version: '2.2', description: 'YAML 1.1 parser and emitter for Java', scope: 'required', hashes: [ { alg: 'MD5', content: 'd78aacf5f2de5b52f1a327470efd1ad7', }, { alg: 'SHA-1', content: '3af797a25458550a16bf89acc8e4ab2b7f2bfce0', }, { alg: 'SHA-256', content: '1467931448a0817696ae2805b7b8b20bfb082652bf9c4efaed528930dc49389b', }, { alg: 'SHA-512', content: '11547e75cc80bee26f532e2598bc6e4ffa802941496dc0d8ce017f1b15e01ebbb80e91ed17d1047916e32bf2fc58da532bc71a1dfe93afccc277a296d86634ba', }, { alg: 'SHA-384', content: 'dae0cb1a7ab9ccc75413f46f18ae160e12e91dfef0c17a07ea547a365e9fb422c071aa01579f2a320f15ce6ee4c29038', }, { alg: 'SHA3-384', content: '654b418f330fa02f1111a20c27395ec5c7f463907ae44f60057c94da04f81e815cf1c3959f005026381ef79030049694', }, { alg: 'SHA3-256', content: '2c4deb8d79876b80b210ef72dc5de2b19607e50fbe3abf09a4324576ca0881fc', }, { alg: 'SHA3-512', content: '0d9be5610b2bcb6bb7562ee8bcc0d68f81d3771958ce9299c5e57e8ec952c96906d711587b7f72936328c72fb41687b4f908c4de3070b78cc1f3e257cf4b715e', }, ], licenses: [ { license: { id: 'Apache-2.0', }, }, ], purl: 'pkg:maven/org.yaml/snakeyaml@2.2?type=jar', externalReferences: [ { type: 'website', url: 'https://bitbucket.org/snakeyaml/snakeyaml', }, { type: 'distribution-intake', url: 'https://oss.sonatype.org/service/local/staging/deploy/maven2/', }, { type: 'issue-tracker', url: 'https://bitbucket.org/snakeyaml/snakeyaml/issues', }, { type: 'vcs', url: 'https://bitbucket.org/snakeyaml/snakeyaml/src', }, ], type: 'library', 'bom-ref': 'pkg:maven/org.yaml/snakeyaml@2.2?type=jar', }, { publisher: 'Spring IO', group: 'org.springframework', name: 'spring-aop', version: '6.1.6', description: 'Spring AOP', scope: 'required', hashes: [ { alg: 'MD5', content: '7dd149c85f55789d005cf7ee5e1bc666', }, { alg: 'SHA-1', content: '4958f52cb9fcb3adf7e836304550de5431a9347e', }, { alg: 'SHA-256', content: '32ec3db2653d84e5adbb4aa932c8f825d684d6f588b90a4b8674df419e2375c2', }, { alg: 'SHA-512', content: '957dfd69a39d60ca283c2a1f6a08b5ca24d2ad8fb5ef2173b07fbdc6c3b8ee0679aea8f6780e1d426d1d97555f4de27b4c7118183fea0d38b4515207885b0770', }, { alg: 'SHA-384', content: 'e9863cbc573cc4a4f990fe6d7b8d288ac358acda6ff4b33d88e15ec50e7910b64a1a1f7297665666ff2858edef852916', }, { alg: 'SHA3-384', content: 'f55459c986cf14feead7c0fa71b78893e9eb810875069d5b60623f3c63f761e344e7068a4b805b52422d8a31e72e73d1', }, { alg: 'SHA3-256', content: '922bece712b5b0617966424355a53da7fe1fff5ebd21a512e0b814e210328fd8', }, { alg: 'SHA3-512', content: 'c626119f8af6b2cd5c914629d1d29d6d4bb5f14731e2ccb54114a92486ac41d52d8469bb17e457deedebc8ab3928765e10b69e67c42ec2d74b2e5bc4028dc355', }, ], licenses: [ { license: { id: 'Apache-2.0', }, }, ], purl: 'pkg:maven/org.springframework/spring-aop@6.1.6?type=jar', externalReferences: [ { type: 'website', url: 'https://github.com/spring-projects/spring-framework', }, { type: 'issue-tracker', url: 'https://github.com/spring-projects/spring-framework/issues', }, { type: 'vcs', url: 'https://github.com/spring-projects/spring-framework', }, ], type: 'library', 'bom-ref': 'pkg:maven/org.springframework/spring-aop@6.1.6?type=jar', }, { publisher: 'Spring IO', group: 'org.springframework', name: 'spring-beans', version: '6.1.6', description: 'Spring Beans', scope: 'required', hashes: [ { alg: 'MD5', content: '70aeef1e6e39b2a0b6c9afa1bf81d4e8', }, { alg: 'SHA-1', content: '332d80ff134420db4ebf7614758e6a02a9bd3c41', }, { alg: 'SHA-256', content: 'c3040d1418ef964eb78a39204e04b6685bc840bd010818200f286159be983f30', }, { alg: 'SHA-512', content: 'c1e905c6e5bc9fa0925b14c81dd1c0c8c987b50ab4aa74de392c5fbba8cacb25f8ead28e4c01715c6719c4483f819e9338411173a11f7cad008219c9fa626f94', }, { alg: 'SHA-384', content: 'a6a0a95420b5e3068ad656c6aa14b565b0305244af4f21029590bd6187b68c2962eb33bacbc53358f1bb4df0adee54d0', }, { alg: 'SHA3-384', content: 'c85604e1ff89af420e618f8e4710465e82a3604d0fc930a3ce14171ab997f84bd5be4353b96fc4e14d0fb6764325a010', }, { alg: 'SHA3-256', content: '0b8c554ab50fa0204f3d7a7b23f717b8384aafa8dfe0a421e95fd1de0080e3a4', }, { alg: 'SHA3-512', content: '35648bafc0695efcc0c36ac7189f8b3f907a48fd7a683c9f6f77f6679b083a4531cb8dee501e468841a3cda232e68f0976e2ae9cb65f198ccde5c2028d0e56b8', }, ], licenses: [ { license: { id: 'Apache-2.0', }, }, ], purl: 'pkg:maven/org.springframework/spring-beans@6.1.6?type=jar', externalReferences: [ { type: 'website', url: 'https://github.com/spring-projects/spring-framework', }, { type: 'issue-tracker', url: 'https://github.com/spring-projects/spring-framework/issues', }, { type: 'vcs', url: 'https://github.com/spring-projects/spring-framework', }, ], type: 'library', 'bom-ref': 'pkg:maven/org.springframework/spring-beans@6.1.6?type=jar', }, { publisher: 'Pivotal Software, Inc.', group: 'org.springframework.security', name: 'spring-security-config', version: '6.3.0-RC1', description: 'Spring Security', scope: 'required', hashes: [ { alg: 'MD5', content: '6ba304080954b3d8111e68de29ac5051', }, { alg: 'SHA-1', content: 'b57a66e8644efa48846b0777038ff5f4b3e311f3', }, { alg: 'SHA-256', content: '1f345acb23cac48452b0803183026581f83828b477600d4f91c7e9422eafef23', }, { alg: 'SHA-512', content: '17d18e20affc4e785fd4a0d126ba331142f68de719fb6290345efe1212e147ccab9ac4e64a0ebf5f3f5c2da128a633a7d5935c70915db5e8d8a1eec74692afb4', }, { alg: 'SHA-384', content: '72f0a72e7a399bd62508ba2ee588028e8c5f2df40294d64f351dcecfc8e2450818b47eeef83b22c5dba64f20b0d0fb32', }, { alg: 'SHA3-384', content: '3689d2ecc77e0fa6a4a5d7206fda00b56718db7d78a2f7300845390a3728b887a53eafcb7a10213e1aeec3aa150e3323', }, { alg: 'SHA3-256', content: '0c53615a418816c5c44ec36588773ebc302aee9e3fc4ffe909cc654775ea82e7', }, { alg: 'SHA3-512', content: '1a9002e646d8e9a0860b1da43eee6fd6f0bc34fa7adc475869009cd22d363e4e29cb215e22d9d7b3da8e611d0ef01ca264f577dbf5dd44df6712b9851c92806c', }, ], licenses: [ { license: { id: 'Apache-2.0', }, }, ], purl: 'pkg:maven/org.springframework.security/spring-security-config@6.3.0-RC1?type=jar', externalReferences: [ { type: 'website', url: 'https://spring.io/projects/spring-security', }, { type: 'issue-tracker', url: 'https://github.com/spring-projects/spring-security/issues', }, { type: 'vcs', url: 'https://github.com/spring-projects/spring-security', }, ], type: 'library', 'bom-ref': 'pkg:maven/org.springframework.security/spring-security-config@6.3.0-RC1?type=jar', }, { publisher: 'Pivotal Software, Inc.', group: 'org.springframework.security', name: 'spring-security-core', version: '6.3.0-RC1', description: 'Spring Security', scope: 'required', hashes: [ { alg: 'MD5', content: 'a21081caf802eff1ae6f7df885d2c746', }, { alg: 'SHA-1', content: 'a24da460c3ad36fa03c380ba4f5a79e379c4fc88', }, { alg: 'SHA-256', content: '7e9bd0fc450dd1e6f1b02fb7b5cff14204d3171dc86f41301b2a80208c5ac95a', }, { alg: 'SHA-512', content: '71c8cded26e3408fb9e2552c96d9b3ac018169db3fde4d8a65132a36d7e711704cb476e32e70def34f2bd54dbc7d6cff52fb987c37a6e84d6409f512c5763cc5', }, { alg: 'SHA-384', content: '33aab0fd0914443b9fbbcb4ac066007c1f1a6cf2cd0dbb53fc1484c76130d3b014a92726320c7c48149a12481210bcdb', }, { alg: 'SHA3-384', content: '3265d45b944a80891195c96c07770b9fa99a4f8dbaab3ab0c39a0f1faff2bd33b2555ecf40283dbf33949e4397c89736', }, { alg: 'SHA3-256', content: 'a836031ffb32131bb88b6abc05281525c4a8797b330abb266ed0e2587d713f2b', }, { alg: 'SHA3-512', content: 'd05e3fe14397303c85362b16840a0973e9ae91d27fde9ecd0e042eafac104fa4784d3d71c71e403a04f19e98dce4db0d99d950c28ff1330a3a59c6eb76f5cfde', }, ], licenses: [ { license: { id: 'Apache-2.0', }, }, ], purl: 'pkg:maven/org.springframework.security/spring-security-core@6.3.0-RC1?type=jar', externalReferences: [ { type: 'website', url: 'https://spring.io/projects/spring-security', }, { type: 'issue-tracker', url: 'https://github.com/spring-projects/spring-security/issues', }, { type: 'vcs', url: 'https://github.com/spring-projects/spring-security', }, ], type: 'library', 'bom-ref': 'pkg:maven/org.springframework.security/spring-security-core@6.3.0-RC1?type=jar', }, { publisher: 'Pivotal Software, Inc.', group: 'org.springframework.security', name: 'spring-security-web', version: '6.3.0-RC1', description: 'Spring Security', scope: 'required', hashes: [ { alg: 'MD5', content: 'e6bafd90bc08d472ccccc904f91f336f', }, { alg: 'SHA-1', content: '7ab833ae6faad0a6ae971c8e919ed80a8fb8bf9c', }, { alg: 'SHA-256', content: '0d71b6a6c2fbdfcdab3b1e8d0b7e67d2313bbe807012ba6f88f8f4245d7c2e53', }, { alg: 'SHA-512', content: '65534406625518c123fd61e4e6543aa732010ea552879a6fa39997941a2913cc58a81b5fa6bc0d2eb825198719d890960639c9079586f11e7febaa775a98efcd', }, { alg: 'SHA-384', content: '31dd5f20a0225419cd0378d7e3a0823bdeb6ebe801a186eeb7bea82eb3f0eeb34252ac3b48ba9dafb498499d80b52762', }, { alg: 'SHA3-384', content: '6d4398f5cea31a754bf916082df2d946d4b3db1495acbfc64c9f5a53a8653dba8d28bcdc88dbf896bf4efc4b47f3ec02', }, { alg: 'SHA3-256', content: '09082e1b2b1238f8a8f4e6a2ce343c1fd5aaeeba3acbb97fcefb3aeb64a0e75f', }, { alg: 'SHA3-512', content: '391534ae0377951c76099f264667655748480a9b1afcf5febbea98f786b6dffe4a88753b697bf08d4db4e49b7416278dd4e469313202985e4cf59305bc55f6c8', }, ], licenses: [ { license: { id: 'Apache-2.0', }, }, ], purl: 'pkg:maven/org.springframework.security/spring-security-web@6.3.0-RC1?type=jar', externalReferences: [ { type: 'website', url: 'https://spring.io/projects/spring-security', }, { type: 'issue-tracker', url: 'https://github.com/spring-projects/spring-security/issues', }, { type: 'vcs', url: 'https://github.com/spring-projects/spring-security', }, ], type: 'library', 'bom-ref': 'pkg:maven/org.springframework.security/spring-security-web@6.3.0-RC1?type=jar', }, { publisher: 'Spring IO', group: 'org.springframework', name: 'spring-expression', version: '6.1.6', description: 'Spring Expression Language (SpEL)', scope: 'required', hashes: [ { alg: 'MD5', content: '80acfe831814a3712ae046de5baa2fe9', }, { alg: 'SHA-1', content: '9c3d7f0e17a919a4ea9f087e4e2140ad39776bc8', }, { alg: 'SHA-256', content: '6e53929ab7abda1a43b7d81cefcc441d187eb41aab493d2f61cc6161512c2d97', }, { alg: 'SHA-512', content: '96c4712e5673fb30c55aea7cc7a5d29027780f71b7c0db8d539d95ff7a371cbe16013f59dda4163f5ceda2d09897b5486871cdb3bb22d11fa4858aad0c5aa8b2', }, { alg: 'SHA-384', content: 'da97153d3e4d5665240206d2d93ac2b2edef8be11fe1da0504a41345d58fe3072779922b283c28f62865453269a0b488', }, { alg: 'SHA3-384', content: 'ea503cc1a439b0eef0dbf2e131eb3a29ce2f8414afe515211ad08fb233089c051e2a2dd1fd0dfa8ff5d6d5210ee27673', }, { alg: 'SHA3-256', content: 'e28bb3971e4e14716bac8c0ff013d2ac3aa66feb357569e0b746d4752d82d175', }, { alg: 'SHA3-512', content: '3de81987e28908779b9d7d5714b2c98b905bd045724452c8b84c1814d2d3f0d0ba086cca6564476db41f200182e26332e45a4b278a78fd1b545d1a02a235ca93', }, ], licenses: [ { license: { id: 'Apache-2.0', }, }, ], purl: 'pkg:maven/org.springframework/spring-expression@6.1.6?type=jar', externalReferences: [ { type: 'website', url: 'https://github.com/spring-projects/spring-framework', }, { type: 'issue-tracker', url: 'https://github.com/spring-projects/spring-framework/issues', }, { type: 'vcs', url: 'https://github.com/spring-projects/spring-framework', }, ], type: 'library', 'bom-ref': 'pkg:maven/org.springframework/spring-expression@6.1.6?type=jar', }, { publisher: 'VMware, Inc.', group: 'org.springframework.boot', name: 'spring-boot-starter-webmvc', version: '3.3.0-RC1', description: 'Starter for building web, including RESTful, applications using Spring MVC. Uses Tomcat as the default embedded container', scope: 'required', hashes: [ { alg: 'MD5', content: '45bcad7dddc72b1cb3f89ec71022ed39', }, { alg: 'SHA-1', content: '8be23edf4e71e1d631d8c406e08d06ad9744e7fe', }, { alg: 'SHA-256', content: 'e589ae916b15a648f3ad157d669b8d509ad4c128832adf71c31760b46235c3aa', }, { alg: 'SHA-512', content: '846d72082be7e8fd2062769604cae15f92b94a274b9056bd74e6303c20d5531c427148954a68bc0479e4b126e970eb03545c5acbf229bce90fda99a7cfdb9414', }, { alg: 'SHA-384', content: 'd3ade9fdbeb94be500f41ba447f5d5854af1bd0135297fca735b8f085ae2513668da1a366e5c9e1d236bf9b0328e73fe', }, { alg: 'SHA3-384', content: '5a03502b89c530e1399618afc2f0b6d619c7cf1a2a59d55d1148bdec518ebb3e85bb92f8bc4552a7db991083be8ac8df', }, { alg: 'SHA3-256', content: '20570b3c35f0f5a7126d21fa4a8ddb0ec42f94834f6866ce94925d43931ee85b', }, { alg: 'SHA3-512', content: '74ba8db1c56f0f36314ccfb5399395988450151f6b9cb4131506bcbb0038aa05445db441a96afe5c1e1131fde5e1c299d09ca55758f805bc0fa39f438d33713b', }, ], licenses: [ { license: { id: 'Apache-2.0', }, }, ], purl: 'pkg:maven/org.springframework.boot/spring-boot-starter-webmvc@3.3.0-RC1?type=jar', externalReferences: [ { type: 'website', url: 'https://spring.io/projects/spring-boot', }, { type: 'issue-tracker', url: 'https://github.com/spring-projects/spring-boot/issues', }, { type: 'vcs', url: 'https://github.com/spring-projects/spring-boot', }, ], type: 'library', 'bom-ref': 'pkg:maven/org.springframework.boot/spring-boot-starter-webmvc@3.3.0-RC1?type=jar', }, { publisher: 'VMware, Inc.', group: 'org.springframework.boot', name: 'spring-boot-starter-json', version: '3.3.0-RC1', description: 'Starter for reading and writing json', scope: 'required', hashes: [ { alg: 'MD5', content: '1eda6c496208f03d0cf14805de435376', }, { alg: 'SHA-1', content: 'c063d6d99f359b1e16596dee791800d51298b27f', }, { alg: 'SHA-256', content: '1065123cf78e26eb8c94e1cb145f045b9fc35bfd97fc2dac947fb62aecbf15e8', }, { alg: 'SHA-512', content: 'e1019534961d270d89217180e761c86330c3f85412168ae084cdd1ae25139e6a558be89002ffd263a99ffb5ddb3074efd921f1f5755f35d6cb47cdd1ccdfebe9', }, { alg: 'SHA-384', content: '74ceda16372f1eb59c16e3a26d0887d2493f148f0ac9601f6636354fb62def879461a72f17d5af62cc365a7d64201446', }, { alg: 'SHA3-384', content: 'e7e34d626727b3d4cef1599d49f71e3930dfba60abb9b8b33caabc38c962f4f9f3694b759e0854769a624ce5c4654db8', }, { alg: 'SHA3-256', content: 'ac8220d453e7752437d04bffb7b0750567e99860f1d93f2fbe8a3c50798df24e', }, { alg: 'SHA3-512', content: 'dfecf210a3b18c555b3e6e8b2287a91c6c0c7771a430a31a72c798f8a867c5601e0eeeb8d2bec17f71ee59711c5534b41d7c6b03eda280ae311294636da8e26a', }, ], licenses: [ { license: { id: 'Apache-2.0', }, }, ], purl: 'pkg:maven/org.springframework.boot/spring-boot-starter-json@3.3.0-RC1?type=jar', externalReferences: [ { type: 'website', url: 'https://spring.io/projects/spring-boot', }, { type: 'issue-tracker', url: 'https://github.com/spring-projects/spring-boot/issues', }, { type: 'vcs', url: 'https://github.com/spring-projects/spring-boot', }, ], type: 'library', 'bom-ref': 'pkg:maven/org.springframework.boot/spring-boot-starter-json@3.3.0-RC1?type=jar', }, { publisher: 'FasterXML', group: 'com.fasterxml.jackson.datatype', name: 'jackson-datatype-jdk8', version: '2.17.0', description: 'Add-on module for Jackson (https://github.com/FasterXML/jackson) to support JDK 8 data types.', scope: 'required', hashes: [ { alg: 'MD5', content: '65e3b1936136e16cefbe9059c01c4505', }, { alg: 'SHA-1', content: '95519a116d909faec29da76cf6b944b4a84c2c26', }, { alg: 'SHA-256', content: 'b090239968a0ae5a172472f4014dbd97133af9426d91bf4805a6ba5fd90d80f1', }, { alg: 'SHA-512', content: '6d7ee0d139fd9f7c24f14cb4bf231c1d9070c785d607b9a7be2f46297985ee8a7f184f9bf0b3a150d6b4a168175352cf8479c0e411342393af6bb259fdf0ec42', }, { alg: 'SHA-384', content: 'd734ba8f8892dd41f63a2faec072cd3b57abf6a8e461c3e04880c285fc13103b50adbade060b387659a49f8d380f4b9c', }, { alg: 'SHA3-384', content: 'bb64b906356ef4839cd988be6de66eb7fe1f89e6e6fb1cee3f11097eab26532dbdd791cf99ebf4f9ebc6a3ff975183d7', }, { alg: 'SHA3-256', content: '4ae3e4ad6652e7c2c363ca3d9e6c41871d31531aac7f2a4f50b8d62bff4b8b94', }, { alg: 'SHA3-512', content: 'b368861aa6108fc6cc6138863d901d9aca49b16baebeb20db8df7e4451f971f1debae8751c4df27c83565d8e6e7dea21a9209b1c9c07a535b888bb965492ac56', }, ], licenses: [ { license: { id: 'Apache-2.0', }, }, ], purl: 'pkg:maven/com.fasterxml.jackson.datatype/jackson-datatype-jdk8@2.17.0?type=jar', externalReferences: [ { type: 'website', url: 'https://github.com/FasterXML/jackson-modules-java8/jackson-datatype-jdk8', }, { type: 'distribution-intake', url: 'https://oss.sonatype.org/service/local/staging/deploy/maven2/', }, { type: 'issue-tracker', url: 'https://github.com/FasterXML/jackson-modules-java8/issues', }, { type: 'vcs', url: 'http://github.com/FasterXML/jackson-modules-java8/jackson-datatype-jdk8', }, ], type: 'library', 'bom-ref': 'pkg:maven/com.fasterxml.jackson.datatype/jackson-datatype-jdk8@2.17.0?type=jar', }, { publisher: 'FasterXML', group: 'com.fasterxml.jackson.datatype', name: 'jackson-datatype-jsr310', version: '2.17.0', description: 'Add-on module to support JSR-310 (Java 8 Date & Time API) data types.', scope: 'required', hashes: [ { alg: 'MD5', content: 'b60f65312afa00f61e0950c3b5fbff88', }, { alg: 'SHA-1', content: '3fab507bba9d477e52ed2302dc3ddbd23cbae339', }, { alg: 'SHA-256', content: '94ea2f224e36632c02db1e668127c3018cacb859afc15ddf6f4c585917a93396', }, { alg: 'SHA-512', content: 'bda1467594001aa22d7d622a5dcbb27a8aea54427d6e77dd7c03fb34ec8f4051b976f92c425d047045a0a1f48e23853b81d01a6a25ab0bf9fd479c05e91b5594', }, { alg: 'SHA-384', content: 'a94642eed5de82b126672562f03c00e8e1668b8a0df388b8f52e19cfa79e5d2665f2160737026acd1c5d1d7fb7bf2423', }, { alg: 'SHA3-384', content: 'f6ca2b5923378c65b91e9f6b5a7c8268f1c19413cb3803355272dc63a7092ae5c453a84b7041d83423dae4fec96f11da', }, { alg: 'SHA3-256', content: '8703a4132f8ae6f48e37eb55da1bacff6c7e098d5e75c93cf19d72080b5afad7', }, { alg: 'SHA3-512', content: '69cc3cfccc7f130cdb67a14e2448c288cf8917e599c046fe9427176135f8e337706032dc313c06ab3ae8548f6a83ab60f6a79d9ec1bc753dc9606302f76e0aef', }, ], licenses: [ { license: { id: 'Apache-2.0', }, }, ], purl: 'pkg:maven/com.fasterxml.jackson.datatype/jackson-datatype-jsr310@2.17.0?type=jar', externalReferences: [ { type: 'website', url: 'https://github.com/FasterXML/jackson-modules-java8/jackson-datatype-jsr310', }, { type: 'distribution-intake', url: 'https://oss.sonatype.org/service/local/staging/deploy/maven2/', }, { type: 'issue-tracker', url: 'https://github.com/FasterXML/jackson-modules-java8/issues', }, { type: 'vcs', url: 'http://github.com/FasterXML/jackson-modules-java8/jackson-datatype-jsr310', }, ], type: 'library', 'bom-ref': 'pkg:maven/com.fasterxml.jackson.datatype/jackson-datatype-jsr310@2.17.0?type=jar', }, { publisher: 'FasterXML', group: 'com.fasterxml.jackson.module', name: 'jackson-module-parameter-names', version: '2.17.0', description: 'Add-on module for Jackson (https://github.com/FasterXML/jackson) to support introspection of method/constructor parameter names, without having to add explicit property name annotation.', scope: 'required', hashes: [ { alg: 'MD5', content: '9f22e80510b61baa23689159ec2293cd', }, { alg: 'SHA-1', content: '59340d6d12c15bcc465a91a4b9a2a93a920c4212', }, { alg: 'SHA-256', content: '1fd79421bb95c74cc4c44c1ae4910e9253f255f248d34c3f9e5b2abeb2145b6a', }, { alg: 'SHA-512', content: 'c6d6efabc8e7212ecfe5c3c77b1dd72b8bdae0aa62f08b2a6179aec1bfb56e910240933db0a2bbf62fdc5bfb54ea52b709327e1e1b472c34a1615cdf0c2a350a', }, { alg: 'SHA-384', content: 'bb1466be83197443274697476b338f2878325651e8a6799cfd0f491ca3764713dec2b321a22f61346d256d859498792a', }, { alg: 'SHA3-384', content: 'c39bfb70ed88a7a3f45697491fb7e4b3da7bd906008ea00f80b113bc6ebb667fc0982388c6964512bfbf85591354d207', }, { alg: 'SHA3-256', content: 'cb3a1f00581b629b760e5af30bb0343c09c80d8eb98e409eaa31555a50782af1', }, { alg: 'SHA3-512', content: 'd93aea5c736b4f2062e1175c203aa641443824f77923a8a8a4f65a47cb47cb1aab079c06c2f460ad85d908a51e008d1db45d3947866d401e4f20ef63a4aa1882', }, ], licenses: [ { license: { id: 'Apache-2.0', }, }, ], purl: 'pkg:maven/com.fasterxml.jackson.module/jackson-module-parameter-names@2.17.0?type=jar', externalReferences: [ { type: 'website', url: 'https://github.com/FasterXML/jackson-modules-java8/jackson-module-parameter-names', }, { type: 'distribution-intake', url: 'https://oss.sonatype.org/service/local/staging/deploy/maven2/', }, { type: 'issue-tracker', url: 'https://github.com/FasterXML/jackson-modules-java8/issues', }, { type: 'vcs', url: 'http://github.com/FasterXML/jackson-modules-java8/jackson-module-parameter-names', }, ], type: 'library', 'bom-ref': 'pkg:maven/com.fasterxml.jackson.module/jackson-module-parameter-names@2.17.0?type=jar', }, { publisher: 'VMware, Inc.', group: 'org.springframework.boot', name: 'spring-boot-starter-tomcat', version: '3.3.0-RC1', description: 'Starter for using Tomcat as the embedded servlet container. Default servlet container starter used by spring-boot-starter-webmvc', scope: 'required', hashes: [ { alg: 'MD5', content: 'aa98610a3e8b6a74c67467bbbef82875', }, { alg: 'SHA-1', content: 'cda596da92cdf96bbb042f6c7c93a6d803c3a6d9', }, { alg: 'SHA-256', content: '7a08fb133c7ce64d845fbe3c602757fcb482bc71f90198990af7ed5f452c9a10', }, { alg: 'SHA-512', content: '37c5ecbb41156a72db314065f3473fbc2c72a87b1d5afe114bfba446c66d64c9b9f9e8013bdc141cb1c0f1bcc5a56908620566cbed6c3650f4c18b775e8f2e5c', }, { alg: 'SHA-384', content: '848b8eed45c458edc89dee5faa8219ecedbe5abf78be3094a0955caa013db5409c33ff14960e044c8fe2bd4d57cf2323', }, { alg: 'SHA3-384', content: '2d05d05a755042668cff311be9391c53333695936094cb958cc01be39e767383e7f2402b12a9bc5540b49119e496ab0f', }, { alg: 'SHA3-256', content: '2453b3c286f8e1e7a9e72c596fa80884bc5047b0a7a869ed31dab3583da54593', }, { alg: 'SHA3-512', content: 'd746199f73100208ebfafb6094ab7a2548d5554f89cd16747b69ec200cfca9582eebfc7d93f74c1582fb8b8cd35c0aa13e2f3cc67da79c79c6e78614eae43939', }, ], licenses: [ { license: { id: 'Apache-2.0', }, }, ], purl: 'pkg:maven/org.springframework.boot/spring-boot-starter-tomcat@3.3.0-RC1?type=jar', externalReferences: [ { type: 'website', url: 'https://spring.io/projects/spring-boot', }, { type: 'issue-tracker', url: 'https://github.com/spring-projects/spring-boot/issues', }, { type: 'vcs', url: 'https://github.com/spring-projects/spring-boot', }, ], type: 'library', 'bom-ref': 'pkg:maven/org.springframework.boot/spring-boot-starter-tomcat@3.3.0-RC1?type=jar', }, { group: 'org.apache.tomcat.embed', name: 'tomcat-embed-core', version: '10.1.20', description: 'Core Tomcat implementation', scope: 'required', hashes: [ { alg: 'MD5', content: '27154be1577cf6e837e8537d359d5da1', }, { alg: 'SHA-1', content: 'ba0dc784e12086f83d8e1d5a10443b166abf5780', }, { alg: 'SHA-256', content: 'd4b04888ede3a3232dc1798f3ae2ed7265f9b0dcf631b4bf16f50b7ed90ba361', }, { alg: 'SHA-512', content: '2b0cf032cbe0195e7621b4e0e97766382cefb5b4185eb89ec1818823c9ced75e94a321e9451f929784734d32b2ddc93b75a9ba635d88d3d3a26a349b2921f098', }, { alg: 'SHA-384', content: 'f64c3a561b496481ba1c26307f4b44f5f0742f954fbda9528bcd0fcb9385b86c6fb0107fceb319218a2cd842e492ffb0', }, { alg: 'SHA3-384', content: 'ce4860079353fcbe5ba07028179ee84272118233d94a91f78a8bb5b85bf277788221d52bff0206ba778b60dd7146250e', }, { alg: 'SHA3-256', content: '265a4f17f7c3854f907d81905780a5a3e704a96b05e7fec5b9ee4c0a2ccd2f39', }, { alg: 'SHA3-512', content: '60e4b7f62dcc53d7fce264e2f56eae21eeb1442076b20da6a2617e84657726625e12f10223c20127509cc8a7ecd95da338989e530cb09e315c8cd7846006a23d', }, ], licenses: [ { license: { id: 'Apache-2.0', }, }, ], purl: 'pkg:maven/org.apache.tomcat.embed/tomcat-embed-core@10.1.20?type=jar', externalReferences: [ { type: 'website', url: 'https://tomcat.apache.org/', }, ], type: 'library', 'bom-ref': 'pkg:maven/org.apache.tomcat.embed/tomcat-embed-core@10.1.20?type=jar', }, { group: 'org.apache.tomcat.embed', name: 'tomcat-embed-el', version: '10.1.20', description: 'Core Tomcat implementation', scope: 'required', hashes: [ { alg: 'MD5', content: '818fdd8c396fedf7117842cd10169d23', }, { alg: 'SHA-1', content: 'cc1a42b8228699e92c8eba0187eccf54bf892802', }, { alg: 'SHA-256', content: 'cac3dd6fa016dd85f5751438274c2f35955ce3024fec59d6d2fb0fe7c847c2d2', }, { alg: 'SHA-512', content: 'd17c25d7884a9286843eb809dbff11212dea47946f57d41eae48af2c89f20c21426680b1ba962e2260a0e5fbd5dc6543ff4d90df574a1be12789f5a42eb2ac96', }, { alg: 'SHA-384', content: '20f108b7e2fa1d03df021578cbaefb7f41dadd91cbc2a4c8c0f9ab149fa89e040bc7242c46bf6528d9fe119bce979273', }, { alg: 'SHA3-384', content: '8824efde0b5c6f5c619f2e96f1c4b2e37975d05f77bbc9ea51389f5be31869257251d098b60c26ddf5099e5c02762a6a', }, { alg: 'SHA3-256', content: '8b9caed961e5b913bad0bb6b73c98bd8b24670eaa1fa9915eb79c5b57686f8a2', }, { alg: 'SHA3-512', content: 'e444662720754ad71ac02b03a6e943985fa6ee671c2e9bb5042e465fd03f479860a791994af3d195838f66db9859fae1319910a3e8969d59b862bcd94567bb12', }, ], licenses: [ { license: { id: 'Apache-2.0', }, }, ], purl: 'pkg:maven/org.apache.tomcat.embed/tomcat-embed-el@10.1.20?type=jar', externalReferences: [ { type: 'website', url: 'https://tomcat.apache.org/', }, ], type: 'library', 'bom-ref': 'pkg:maven/org.apache.tomcat.embed/tomcat-embed-el@10.1.20?type=jar', }, { group: 'org.apache.tomcat.embed', name: 'tomcat-embed-websocket', version: '10.1.20', description: 'Core Tomcat implementation', scope: 'required', hashes: [ { alg: 'MD5', content: 'fca3c1e1675701e2dfb01f4e3b8bd2c3', }, { alg: 'SHA-1', content: '21502adffaf9e6e4bc2b63a557e348d7f6c0faf7', }, { alg: 'SHA-256', content: 'e2ec1b5f17c8ae01dcf9b487221f50217c9a03293c0fa98eecdcecd702b4617a', }, { alg: 'SHA-512', content: 'a9bd9b831058a46faa808e6aa5625a4b3a43c287de5d7be96b831f57f2cfa08dbca357b1a3edf54f94555e2d9e526ae69dcb172c1cb5fd713c67f50230ee5f03', }, { alg: 'SHA-384', content: '7eb531e48659fe2c79a27087fb62636946c1b06f2996f146791c2d7160e94735da031112c6b76e536ecf4e47c06a74a2', }, { alg: 'SHA3-384', content: '9b62ab366557ac14e52d31dcb81d242ec7c577c9db3ca5203718784d11cea74604be66b8ae88e69ff4927d3504695d82', }, { alg: 'SHA3-256', content: '186766a966f4f58833760027095108ba435094664417d3ea93e77e11bbfa753a', }, { alg: 'SHA3-512', content: '0cc6608af58570bbbe8f86b9a24a5a2c1eb1f7953a8e96ed0389f24b4c81327e5e3973c39d95032d3b36fc8b10ea9ac032117b7a5a1fabd97dcc9c4a7ab9e784', }, ], licenses: [ { license: { id: 'Apache-2.0', }, }, ], purl: 'pkg:maven/org.apache.tomcat.embed/tomcat-embed-websocket@10.1.20?type=jar', externalReferences: [ { type: 'website', url: 'https://tomcat.apache.org/', }, ], type: 'library', 'bom-ref': 'pkg:maven/org.apache.tomcat.embed/tomcat-embed-websocket@10.1.20?type=jar', }, { publisher: 'Spring IO', group: 'org.springframework', name: 'spring-web', version: '6.1.6', description: 'Spring Web', scope: 'required', hashes: [ { alg: 'MD5', content: 'bea30302bcb6ef493a8123e4a40ae6a2', }, { alg: 'SHA-1', content: '49a32e3497fe39550da3b280bda5d9933ae2d51d', }, { alg: 'SHA-256', content: '0f33f5530ef848063958b4b437e3df3119c04a92aea58f9e37fc46948cbbde8e', }, { alg: 'SHA-512', content: '7dcd359d76d3d924305130f9cb9f0758f9a7b5574fa737cccd44629bb6bb946bebec3a9323d6a84b4ca4bb5083ce1f99bd7ba78682f8751ad4185d32cda604c2', }, { alg: 'SHA-384', content: '549fef67d6afd82c177548429390d959986064c051103edc063d691e10ff663d5743fab4fd1cfdeaf203cb925acf2c3a', }, { alg: 'SHA3-384', content: 'aaf3913b5df31048b841c305b29ec6be978c615846f36eaa4f5e8e278c4af6db96c7e441c7eeb9828c3728b40e5fe5e7', }, { alg: 'SHA3-256', content: '387f0d730eff538a56c7e0da1c75e512e888f73a1373c91e24ab5019ee969902', }, { alg: 'SHA3-512', content: '01856c9595cdc7aa2501f82f7482a710aad3050098114e185c2a05a7fcdc2a06a6ca932540ae0a4ff84c084a621b913b50dd29eb894376707554134be9cacdff', }, ], licenses: [ { license: { id: 'Apache-2.0', }, }, ], purl: 'pkg:maven/org.springframework/spring-web@6.1.6?type=jar', externalReferences: [ { type: 'website', url: 'https://github.com/spring-projects/spring-framework', }, { type: 'issue-tracker', url: 'https://github.com/spring-projects/spring-framework/issues', }, { type: 'vcs', url: 'https://github.com/spring-projects/spring-framework', }, ], type: 'library', 'bom-ref': 'pkg:maven/org.springframework/spring-web@6.1.6?type=jar', }, { group: 'io.micrometer', name: 'micrometer-observation', version: '1.13.0-RC1', description: 'Module containing Observation related code', scope: 'required', hashes: [ { alg: 'MD5', content: '465af4c7b39389b4b64ee821c58391c4', }, { alg: 'SHA-1', content: 'a9977cbdfad0d4271377c197e4273260e7d05200', }, { alg: 'SHA-256', content: '30cf97a63e36e1016e6e482ecf7569abf0711f34991e30c8a6180b7c27961890', }, { alg: 'SHA-512', content: 'ebbca3cfe52d16124a079d95db429e3a82fe5ae7d0b09380db1d47fdec2207848ac4192afb06852e13d08cf042b10e2ab2da179eb0f3b0e382e32db3b5487bbc', }, { alg: 'SHA-384', content: '1c06ea65a6e4e7fa4226db67817769aa347be244d268941351b1972dd7e92efa3b9d78f357a06e8a7f940fb59cbab34c', }, { alg: 'SHA3-384', content: '7aad6d20027ff90e9d02e1840387a72716e7ee1264a08a0cfe95bd6ea4da038d49577821b1823cd2e26145330a37c2df', }, { alg: 'SHA3-256', content: 'db7a863b261f3b4eed015bcde2989acd4ea71de29e0ace7049e1de43a0c9ffad', }, { alg: 'SHA3-512', content: '49aeec650a29421aeffb7233f1da579b9cb35d362130a52f19366cbdbb790ba8edf9309b4bf508bd96d69caa9ae094a7eb9c5a934607b437b73c7635188b6d81', }, ], licenses: [ { license: { id: 'Apache-2.0', }, }, ], purl: 'pkg:maven/io.micrometer/micrometer-observation@1.13.0-RC1?type=jar', externalReferences: [ { type: 'website', url: 'https://github.com/micrometer-metrics/micrometer', }, ], type: 'library', 'bom-ref': 'pkg:maven/io.micrometer/micrometer-observation@1.13.0-RC1?type=jar', }, { group: 'io.micrometer', name: 'micrometer-commons', version: '1.13.0-RC1', description: 'Module containing common code', scope: 'required', hashes: [ { alg: 'MD5', content: 'e4d7efe7ad5ffd085e174a976307c86f', }, { alg: 'SHA-1', content: '63f08fc273b44773c417014a8f1897502137bcbe', }, { alg: 'SHA-256', content: '3cfe50d4f7cb3d223def27f83397d879ed66214a40fadb3d1da1970faf083e16', }, { alg: 'SHA-512', content: 'b011d2ed89193195554bf08199c5f06cb74207c1be3916cc2783050ef259710049fc571720f113439fd26f00a648ebf3ce70114c21e6a97422daef0e33981777', }, { alg: 'SHA-384', content: '7987407064defaf0218c13217fd2d4573161e25fc5b2dee1f1d020dd15c34c82eddebb533ca45a91c53cb3fa0c39d9e2', }, { alg: 'SHA3-384', content: 'b66eda2bcf16c13c3d7a54975e0dccbb9c27bf891b170db953e1229db145753fc9a63222fce7abdfab3540b684c56198', }, { alg: 'SHA3-256', content: 'e5fe43477ea8e5b35feca18e3c52337f6bba9439af00bc055fe75201c4491275', }, { alg: 'SHA3-512', content: '881fc90acd90b72db2d8cd6fe13ac0d8018e8450d739178a58c56c923fc74430ff7baad68e3dcea2b72563e00d0f350315f3e52202b876fc9438b3c8b20f648e', }, ], licenses: [ { license: { id: 'Apache-2.0', }, }, ], purl: 'pkg:maven/io.micrometer/micrometer-commons@1.13.0-RC1?type=jar', externalReferences: [ { type: 'website', url: 'https://github.com/micrometer-metrics/micrometer', }, ], type: 'library', 'bom-ref': 'pkg:maven/io.micrometer/micrometer-commons@1.13.0-RC1?type=jar', }, { publisher: 'Spring IO', group: 'org.springframework', name: 'spring-webmvc', version: '6.1.6', description: 'Spring Web MVC', scope: 'required', hashes: [ { alg: 'MD5', content: '9a174b724dac50617689536e658be1e4', }, { alg: 'SHA-1', content: 'ef1f76db6d94bac428839cb91fa59235c8356e56', }, { alg: 'SHA-256', content: '22f21b895c1d85d54d2357498b7aa2ea4e0e05646a360b98a1b67515f17666a9', }, { alg: 'SHA-512', content: 'f5dad19b989c885241365657a20ab68c1ccef9a827d28f0594dc0e7502cb1a8f52673d68c85e9e241ca5d349ca853606033fc6ae68a37a848d53be154c20960f', }, { alg: 'SHA-384', content: '7ab7a42380d3635158a20bea9702f4f6c32259a95611627956638fd220a163cbc8234dd1e7d2a9c6c61cd9897594cd0f', }, { alg: 'SHA3-384', content: 'f924555166988fb9c980bfffea0b7b3c1fc55b309aa2de8492302eae281632de76c616e35e5cd6e3b7d029220aa1b896', }, { alg: 'SHA3-256', content: '5fd7c65d3a4a7476af53d09e8948d9ffbd64bdc4684ced7b1aba62031caa1a46', }, { alg: 'SHA3-512', content: 'dd705a6dfa4ad993fa4734da16522c9e5eb590ec210cdb14df179026193e631afd38ced4b122803441e5bb483fcb238dd316f45d4ac81f20b042ab700003c7da', }, ], licenses: [ { license: { id: 'Apache-2.0', }, }, ], purl: 'pkg:maven/org.springframework/spring-webmvc@6.1.6?type=jar', externalReferences: [ { type: 'website', url: 'https://github.com/spring-projects/spring-framework', }, { type: 'issue-tracker', url: 'https://github.com/spring-projects/spring-framework/issues', }, { type: 'vcs', url: 'https://github.com/spring-projects/spring-framework', }, ], type: 'library', 'bom-ref': 'pkg:maven/org.springframework/spring-webmvc@6.1.6?type=jar', }, { publisher: 'Pivotal Software, Inc.', group: 'org.springframework.cloud', name: 'spring-cloud-starter', version: '4.1.2', description: 'Spring Cloud Starter', scope: 'required', hashes: [ { alg: 'MD5', content: '61f6ed7c42e186cd32941cb34e886eac', }, { alg: 'SHA-1', content: '757a6f0ecdb191fb04c0aed2055e91f50f89231d', }, { alg: 'SHA-256', content: '4d154b87d2601eceefd7eddfce08092c3e6c7cdf8ecef49809d56f14aaae7686', }, { alg: 'SHA-512', content: '0f618660887c0da46552ff99ff48a80609d6ec2df03c9a9db3c28a85c65eabe020bc810f6753e3f0c036a13975746905e4684250b48dddaa08e3ea039e219e56', }, { alg: 'SHA-384', content: 'a17cb39994d1bee8f0e58feb3f451c71640991f8946decb018b22dd0ba9371e82d4828fc1a0fcd85fc96e6f60b86bf6e', }, { alg: 'SHA3-384', content: '6ff22a32d93e94f994f7da8aaa6db6eb06bc5f468ce6b1b8f8783435e34b86d6960e06ea69fb774f9057cf98544be0a1', }, { alg: 'SHA3-256', content: '5b01491c4ce024e79378fe7c6f7be0dbb81128a2d2e2083bfedd330a8fab9e6f', }, { alg: 'SHA3-512', content: '7e71ec57c00b33e0934c4f40c23434a5a3b0ac4571c796cb441aa04b51c6267cb8a4096d8932466e7b6556c359dd4a5cca3abdaaffe4bbfd7424dd9b2bcc2557', }, ], licenses: [ { license: { id: 'Apache-2.0', }, }, ], purl: 'pkg:maven/org.springframework.cloud/spring-cloud-starter@4.1.2?type=jar', externalReferences: [ { type: 'website', url: 'https://projects.spring.io/spring-cloud', }, { type: 'distribution', url: 'https://github.com/spring-cloud', }, { type: 'distribution-intake', url: 'https://s01.oss.sonatype.org/service/local/staging/deploy/maven2/', }, { type: 'vcs', url: 'https://github.com/spring-cloud/spring-cloud-commons/spring-cloud-starter', }, ], type: 'library', 'bom-ref': 'pkg:maven/org.springframework.cloud/spring-cloud-starter@4.1.2?type=jar', }, { publisher: 'Pivotal Software, Inc.', group: 'org.springframework.cloud', name: 'spring-cloud-context', version: '4.1.2', description: 'Spring Cloud Context', scope: 'required', hashes: [ { alg: 'MD5', content: '791fc874f57106f112882318c640db8c', }, { alg: 'SHA-1', content: '069d9edfe8c4b4037653d28b29f3184afc573603', }, { alg: 'SHA-256', content: 'c8594e855931216df95433735750b7a229bc1d52f60299041b38f9998f57b06c', }, { alg: 'SHA-512', content: '5f234a5a762287e64c951269a37aca9c1cb5dea90c5d0cd84b0e49beb5d8ad87fd83480f854371ab136195354f953dc4eb6646da4186f17e54ceec58fbb1e07a', }, { alg: 'SHA-384', content: 'd4b15bf701af1a7614c433aa792f9945541eb6089fef2933b23271867bbaca479396e1643a08ea4b37c332dda98f9354', }, { alg: 'SHA3-384', content: '6bcda9d7275df6d7fdbf7c2aada404b13696bfa3430d98e9dc61ae2700691cd89f727a48d5a121fc607819425df86d9b', }, { alg: 'SHA3-256', content: '00fcba56a744c1ded7a5e67caae7d8ef02af961fb4da0b8ca1b2f6e91af7a169', }, { alg: 'SHA3-512', content: '32fb2b12b806c016e2a52b04ea207fe6310f54be28cfd9e0b1f608cccf78ae0bea2baf1995b7632d322c8e89aa134d9b2d9f99a7729f68c8a755e09f16d3b8a2', }, ], licenses: [ { license: { id: 'Apache-2.0', }, }, ], purl: 'pkg:maven/org.springframework.cloud/spring-cloud-context@4.1.2?type=jar', externalReferences: [ { type: 'website', url: 'https://projects.spring.io/spring-cloud/spring-cloud-context/', }, { type: 'distribution', url: 'https://github.com/spring-cloud', }, { type: 'distribution-intake', url: 'https://s01.oss.sonatype.org/service/local/staging/deploy/maven2/', }, { type: 'vcs', url: 'https://github.com/spring-cloud/spring-cloud-commons/spring-cloud-context', }, ], type: 'library', 'bom-ref': 'pkg:maven/org.springframework.cloud/spring-cloud-context@4.1.2?type=jar', }, { publisher: 'Pivotal Software, Inc.', group: 'org.springframework.security', name: 'spring-security-crypto', version: '6.3.0-RC1', description: 'Spring Security', scope: 'required', hashes: [ { alg: 'MD5', content: '313c83d23c769416f746767f03484b3e', }, { alg: 'SHA-1', content: '13b842fc45bf78c54f42151774789f63a4c7395f', }, { alg: 'SHA-256', content: '4961c7f1da2c362444a9a047bd4d26513f5b9d90b623cc9e6d9c5a95052e6ded', }, { alg: 'SHA-512', content: 'ed03b0027d869f4aa0381f01c5826a9385500ee081b91a844b7b72e80e83bf5a4106327a348f6809c005bec578c7dbe1ba22e29c6320dc0896d08547b9c42b3b', }, { alg: 'SHA-384', content: '6008c6d0a0bb6ac403484d01ea7b99c88d437c8f3033d7caedfe9e63eca8ffc204ca0813c9e03e7fa5c1317bc4fed979', }, { alg: 'SHA3-384', content: 'c8d8dc5a5097bc9d9a301eeabdbf54c73640e7e4d2c390257c1eeb0aace8793f71ef4c6cc715590f55dc745f322af61d', }, { alg: 'SHA3-256', content: '1ba8a3ca028e574f4ae1455c05c5b12401ae129dcce8d2770736c9f0091d08d3', }, { alg: 'SHA3-512', content: '019d9a59c2e0bd995367417662210e061c6f5540859ae32fdcd5602ff334b446afb7b71d537302577500182a912130c600657c3bebf1c313ad8b795ef05b3f98', }, ], licenses: [ { license: { id: 'Apache-2.0', }, }, ], purl: 'pkg:maven/org.springframework.security/spring-security-crypto@6.3.0-RC1?type=jar', externalReferences: [ { type: 'website', url: 'https://spring.io/projects/spring-security', }, { type: 'issue-tracker', url: 'https://github.com/spring-projects/spring-security/issues', }, { type: 'vcs', url: 'https://github.com/spring-projects/spring-security', }, ], type: 'library', 'bom-ref': 'pkg:maven/org.springframework.security/spring-security-crypto@6.3.0-RC1?type=jar', }, { publisher: 'Pivotal Software, Inc.', group: 'org.springframework.cloud', name: 'spring-cloud-commons', version: '4.1.2', description: 'Spring Cloud Commons', scope: 'required', hashes: [ { alg: 'MD5', content: 'e8b1acf9de6507b9f6900dfe6ee6a137', }, { alg: 'SHA-1', content: '84377482af72a3ef008b6c981e77897a04ae20aa', }, { alg: 'SHA-256', content: 'e4e1e4d511422b9d79cb26cb7597fbee0a2d25e28a34527fc7a24f5e7900f084', }, { alg: 'SHA-512', content: 'edf6ee48a22e532ceeff37e9e7536dca0f0fd30dd136c844fa39308929c9cb87a66259c5fe86d89c5e91bbdbcd5c7d7b8d23e0dae12510fa040363d3e1f97810', }, { alg: 'SHA-384', content: '8674cd5c6931066ec77972f0176fb1b16be8a3607092286fd0f1183ea11abdead5d80ef31666a07679e042d2fdf1ba15', }, { alg: 'SHA3-384', content: 'f60ce002a0d45e08b87854c95ccd67b1c4e6d7297da5f7f28072b903f73038bb24c0a06c6012002e4a8fc5b81ce54a51', }, { alg: 'SHA3-256', content: 'ef84cc527220b23fea91cce6b1c6619a0d8d1d89a65bf5f6fd453cb408e64e39', }, { alg: 'SHA3-512', content: 'd1fe7229c4a75eccc49ea12ee61e7f5105949f26aa8604ddfda8549b2ca2f4b0ec034f54fc7051ff705c15f324ca7f10016d4514fd0fac32067f334be472efa7', }, ], licenses: [ { license: { id: 'Apache-2.0', }, }, ], purl: 'pkg:maven/org.springframework.cloud/spring-cloud-commons@4.1.2?type=jar', externalReferences: [ { type: 'website', url: 'https://projects.spring.io/spring-cloud/spring-cloud-commons/', }, { type: 'distribution', url: 'https://github.com/spring-cloud', }, { type: 'distribution-intake', url: 'https://s01.oss.sonatype.org/service/local/staging/deploy/maven2/', }, { type: 'vcs', url: 'https://github.com/spring-cloud/spring-cloud-commons/spring-cloud-commons', }, ], type: 'library', 'bom-ref': 'pkg:maven/org.springframework.cloud/spring-cloud-commons@4.1.2?type=jar', }, { publisher: 'SpringSource', group: 'org.springframework.security', name: 'spring-security-rsa', version: '1.1.2', description: 'Spring Security RSA is a small utility library for RSA ciphers. It belongs to the family of Spring Security crypto libraries that handle encoding and decoding text as a general, useful thing to be able to do.', scope: 'required', hashes: [ { alg: 'MD5', content: '44755711c5e1d2bd12ea7bd669aff3a5', }, { alg: 'SHA-1', content: 'ca388d615a60199186ec248ac2a9806a76db4014', }, { alg: 'SHA-256', content: '6483d1ece7049e58c85b2904c1030d653840516e7b80bb4d4c00dbbb95a2c564', }, { alg: 'SHA-512', content: 'b204ac9aac553d1243889305d600a6ee79737e482e7c8b51833183a555a03c37c1631635a39b4ab442a74097881b9d7f1618517dce1d9a99318b2699cf16047b', }, { alg: 'SHA-384', content: 'f7be923a5b035df4f15caa1e8a9fa7dee58bbb14a9c93f348349b0c798ea9c0f907bde5654a9de5b84ff6afa9c355d0f', }, { alg: 'SHA3-384', content: 'cd9271f4c75cb2c9da22cf4322205fab4dba6f17e114dba7f2b4d7cb53545b5e8ca2f38db8a378d4b0107fc573ebd1d2', }, { alg: 'SHA3-256', content: '830f5c790d73c1390b1ab2f64a453e44a5ad20217aae0ed3b2f9c23cc8209463', }, { alg: 'SHA3-512', content: '2d1c4292f43052e08a0783dc1f9b723cfbc56662f67da5fba38f9b579aab60ba6ce7a871e19c3aa732cd28135e22be20c90c251aedb2b1cbaca3be2ec47ed684', }, ], licenses: [ { license: { id: 'Apache-2.0', }, }, ], purl: 'pkg:maven/org.springframework.security/spring-security-rsa@1.1.2?type=jar', externalReferences: [ { type: 'website', url: 'http://github.com/spring-projects/spring-security-oauth', }, { type: 'distribution-intake', url: 'https://repo.spring.io/libs-release-local', }, { type: 'vcs', url: 'http://github.com/spring-projects/spring-security-rsa', }, ], type: 'library', 'bom-ref': 'pkg:maven/org.springframework.security/spring-security-rsa@1.1.2?type=jar', }, { group: 'org.bouncycastle', name: 'bcprov-jdk18on', version: '1.77', description: 'The Bouncy Castle Crypto package is a Java implementation of cryptographic algorithms. This jar contains JCE provider and lightweight API for the Bouncy Castle Cryptography APIs for JDK 1.8 and up.', scope: 'required', hashes: [ { alg: 'MD5', content: 'ca01387064e08db12e1345b474521ff1', }, { alg: 'SHA-1', content: '2cc971b6c20949c1ff98d1a4bc741ee848a09523', }, { alg: 'SHA-256', content: 'dabb98c24d72c9b9f585633d1df9c5cd58d9ad373d0cd681367e6a603a495d58', }, { alg: 'SHA-512', content: '56c359f1370131f91eaeae3ec1d44884358f4296defd8d7516c7b81b9b66158454a667eb1c859d8ad919faee074ae3ecb4ceba2a39a3dd799bef9ada2d8c3da3', }, { alg: 'SHA-384', content: '2951d9bb941e960287cc4a8947a0239ccfb9bd5058002bd5b9fe045b0bb22e4b23f31357f65211c191384cedf3ef3555', }, { alg: 'SHA3-384', content: 'b02af7de4704cf8f93fcd876055595bd9d117afd5eecf0fa883c43e30a285cbbd71473dd9197d6bb41f2b7702bc2620f', }, { alg: 'SHA3-256', content: '6e69119cc95e642da12dcb0043589137bc7b36ba11ff3299598aaa510b8f0c03', }, { alg: 'SHA3-512', content: 'da87498233675c659ed554261a641aeb2eecc83df76864f199fee9d5c63564c2fe9465baf86d9ac9e409bba74a4de1e7197eda8736852f4f4a729301ea8c9233', }, ], licenses: [ { license: { name: 'Bouncy Castle Licence', url: 'https://www.bouncycastle.org/licence.html', }, }, ], purl: 'pkg:maven/org.bouncycastle/bcprov-jdk18on@1.77?type=jar', externalReferences: [ { type: 'website', url: 'https://www.bouncycastle.org/java.html', }, { type: 'issue-tracker', url: 'https://github.com/bcgit/bc-java/issues', }, { type: 'vcs', url: 'https://github.com/bcgit/bc-java', }, ], type: 'library', 'bom-ref': 'pkg:maven/org.bouncycastle/bcprov-jdk18on@1.77?type=jar', }, { publisher: 'VMware, Inc.', group: 'org.springframework.boot', name: 'spring-boot-starter-mail', version: '3.3.0-RC1', description: "Starter for using Java Mail and Spring Framework's email sending support", scope: 'required', hashes: [ { alg: 'MD5', content: '408a1076991aedc862f183cf7cd072ec', }, { alg: 'SHA-1', content: 'b93d26b0a94995568fdf7a81616d8cf6238f7a76', }, { alg: 'SHA-256', content: '77698db1dafe474e9fc430f6f8532bf80ae7d74f6b8d3d26a28f112875f3dd88', }, { alg: 'SHA-512', content: '521352975f69713d274e94d8adf7e327a48c48047243839e31b5a5767f9a67cb7f725e00dc4b390732bcf82f8f0470a0051e431563046cc9f861e8ffc94e6808', }, { alg: 'SHA-384', content: '511c84f78c10532dfe1ed1fbfc2ec7fd4e263fcd349e6d4c37d67b16cf811ca25192b30c4cef2bad5f4935cc0f0bff3e', }, { alg: 'SHA3-384', content: 'c5e2c1b5a331d1a1032b6674fd76d86e7de43e2611c40ba1bb7c1cf668439725216b553d51d7467c8ac6899bf44af875', }, { alg: 'SHA3-256', content: '7ceb912921ce9f446be9eb0ac542a253690570dbce25fecc8ad85afb445faf96', }, { alg: 'SHA3-512', content: '8fc7aa1eddeacbc592018db332c3be52ecfc13bb793f930903e009197082ce4dd4ae2546ad9a3597d50552d28aa23b766aa837f684afe9b02208908967ce5f8f', }, ], licenses: [ { license: { id: 'Apache-2.0', }, }, ], purl: 'pkg:maven/org.springframework.boot/spring-boot-starter-mail@3.3.0-RC1?type=jar', externalReferences: [ { type: 'website', url: 'https://spring.io/projects/spring-boot', }, { type: 'issue-tracker', url: 'https://github.com/spring-projects/spring-boot/issues', }, { type: 'vcs', url: 'https://github.com/spring-projects/spring-boot', }, ], type: 'library', 'bom-ref': 'pkg:maven/org.springframework.boot/spring-boot-starter-mail@3.3.0-RC1?type=jar', }, { publisher: 'Spring IO', group: 'org.springframework', name: 'spring-context-support', version: '6.1.6', description: 'Spring Context Support', scope: 'required', hashes: [ { alg: 'MD5', content: '8d5862cfaec6d2d9b6292992317859cb', }, { alg: 'SHA-1', content: '7cc404d7f0c6e1b1ecfa30080fe194b52867d6b2', }, { alg: 'SHA-256', content: '0f62f0fef682b9ca10e83a292d01e934f0cb38207b07521bcb57e71050523fef', }, { alg: 'SHA-512', content: '82f8fe4eb3bd776c0ea866eb5b11a3b5a1fbbc83348abe6d7c22fb5767fcac1ef4955969378d7a63059df4445fac059724e6f175099932cae95c381fa28bc3b0', }, { alg: 'SHA-384', content: '0b7e95883e480c6bac9358517568c0bfc1f1fd1abf329f4b6c870ab0c7a24810d3f6c2beda6a19e8830393cc6dc8ec58', }, { alg: 'SHA3-384', content: '79310e3758ea66e467fbb1908c8018bdb4eeec3ec52b3274965808fd252803b466214199e9aa357048aec0b958276853', }, { alg: 'SHA3-256', content: '450c4fb5ae0ac74f111274395746f8eaf7ee68a7bdcc6c58582ead738653bda9', }, { alg: 'SHA3-512', content: '14ff0935a90a396cf9f8597a7fb60a51441ae8773fc23acff541cf8cd758e1d364880b0cf1b543ce8f67d9c8b309f991484a7fa8c581eb69784ac137e0c1a71e', }, ], licenses: [ { license: { id: 'Apache-2.0', }, }, ], purl: 'pkg:maven/org.springframework/spring-context-support@6.1.6?type=jar', externalReferences: [ { type: 'website', url: 'https://github.com/spring-projects/spring-framework', }, { type: 'issue-tracker', url: 'https://github.com/spring-projects/spring-framework/issues', }, { type: 'vcs', url: 'https://github.com/spring-projects/spring-framework', }, ], type: 'library', 'bom-ref': 'pkg:maven/org.springframework/spring-context-support@6.1.6?type=jar', }, { publisher: 'Eclipse Foundation', group: 'org.eclipse.angus', name: 'jakarta.mail', version: '2.0.3', description: 'Angus Mail default provider', scope: 'required', hashes: [ { alg: 'MD5', content: '33043478b24ab3845a3bf702c18f9226', }, { alg: 'SHA-1', content: '3dea6aeee9603f573687b0d4da5dc1316d921bb7', }, { alg: 'SHA-256', content: 'efb946424933806bc6f8136752d22fdb3ba887ea0527ff849c474e51f7b3715e', }, { alg: 'SHA-512', content: '6e4abfa2efb985e2bb77e248c8788ea8dfd1fa2a3f88337399a0e4d0c748777100fb6b17c4d1f1e25ce2595fbbec78625f527b21a4986d4d16ea2132847fea51', }, { alg: 'SHA-384', content: '9ab6e99b3d90c3ce031923dbed8ba018c6ae951d2b82d83b9918bc97da06b2096a74a47ae72f4be65bc60471745566b6', }, { alg: 'SHA3-384', content: 'c408a694e3c7db7e500f361ce0f999c905d27ba98d1fc4a1597904af2e5ae83689d5792b1a21369c440ef81f262a3c63', }, { alg: 'SHA3-256', content: '8106e97a462f4fd311b9d5a1a3dbd1fa0e6df2ae3d1944240d8015bec1264c9a', }, { alg: 'SHA3-512', content: '03a3e514d879d6533181f6e99cb234bc4c39cc67e19053ad33fd92547c3e41b8a744293b4bd90d6e89aa129c410b5fe819e6c852b360cb8d9f2a1bbe92126916', }, ], licenses: [ { license: { id: 'EPL-2.0', }, }, { license: { id: 'GPL-2.0-with-classpath-exception', }, }, { license: { id: 'BSD-3-Clause', }, }, ], purl: 'pkg:maven/org.eclipse.angus/jakarta.mail@2.0.3?type=jar', externalReferences: [ { type: 'website', url: 'http://eclipse-ee4j.github.io/angus-mail/jakarta.mail', }, { type: 'distribution-intake', url: 'https://jakarta.oss.sonatype.org/service/local/staging/deploy/maven2/', }, { type: 'issue-tracker', url: 'https://github.com/eclipse-ee4j/angus-mail/issues', }, { type: 'mailing-list', url: 'https://dev.eclipse.org/mhonarc/lists/jakarta.ee-community/', }, { type: 'vcs', url: 'https://github.com/eclipse-ee4j/angus-mail/jakarta.mail', }, ], type: 'library', 'bom-ref': 'pkg:maven/org.eclipse.angus/jakarta.mail@2.0.3?type=jar', }, { publisher: 'Eclipse Foundation', group: 'jakarta.activation', name: 'jakarta.activation-api', version: '2.1.3', description: 'Jakarta Activation API 2.1 Specification', scope: 'required', hashes: [ { alg: 'MD5', content: '76e7b680375ea9f40f3ddbd702efcd25', }, { alg: 'SHA-1', content: 'fa165bd70cda600368eee31555222776a46b881f', }, { alg: 'SHA-256', content: '01b176d718a169263e78290691fc479977186bcc6b333487325084d6586f4627', }, { alg: 'SHA-512', content: 'aaabd4d6085a07035eaaae7b5a81aef429fea76e7fe1c8d29971e6595f0adad6bcf1088cff8a1c8936d739b0e3fce4b845323032f046b7edab2eaebd0e10a2ad', }, { alg: 'SHA-384', content: '4c4e73f59bf09342ca7691fd4855b41d3466da80618a5b7df059a2d89cf6d9779a4af751a6c4a9c48e5025c3ff75f42e', }, { alg: 'SHA3-384', content: '20be816700c87778e9453d41b6d8cb9dc992a092a308a9b7f2dfbf72e2393940a7d666c46625f130a2b57bc414df85ca', }, { alg: 'SHA3-256', content: '8a574b0a249842ea1b397d4cdef9b6d00b34ce8a849ea34184cdf45ac5aafe67', }, { alg: 'SHA3-512', content: '69cfb7dddda70ac1fca272ace0a3d5551b85dd60a6dbaf987ee777fbf573b420d13f06b8990ae70e8fe063f92b78c8a447cf9309ba516a5e993ba2d49cca8d23', }, ], licenses: [ { license: { id: 'BSD-3-Clause', }, }, ], purl: 'pkg:maven/jakarta.activation/jakarta.activation-api@2.1.3?type=jar', externalReferences: [ { type: 'website', url: 'https://github.com/jakartaee/jaf-api', }, { type: 'distribution-intake', url: 'https://jakarta.oss.sonatype.org/service/local/staging/deploy/maven2/', }, { type: 'issue-tracker', url: 'https://github.com/jakartaee/jaf-api/issues/', }, { type: 'mailing-list', url: 'https://dev.eclipse.org/mhonarc/lists/jakarta.ee-community/', }, { type: 'vcs', url: 'https://github.com/jakartaee/jaf-api', }, ], type: 'library', 'bom-ref': 'pkg:maven/jakarta.activation/jakarta.activation-api@2.1.3?type=jar', }, { publisher: 'Eclipse Foundation', group: 'org.eclipse.angus', name: 'angus-activation', version: '2.0.2', description: 'Angus Activation Registries Implementation', scope: 'required', hashes: [ { alg: 'MD5', content: '42bba74155dc773eca277ee7a16f74be', }, { alg: 'SHA-1', content: '41f1e0ddd157c856926ed149ab837d110955a9fc', }, { alg: 'SHA-256', content: '6dd3bcffc22bce83b07376a0e2e094e4964a3195d4118fb43e380ef35436cc1e', }, { alg: 'SHA-512', content: '1482c759843c23e0343ca554194862d53ac18a04ab4691b3bf05145abb77283617022a895c5ba2e33f62b77c2cfb906b90d0cb690623621b11f35194b54b1180', }, { alg: 'SHA-384', content: '0263b0f42e56f9cbf4a2446c26a29d6397477561c2149f7b7d0e62fb28ab4315d50faf4e96aff088d3ac204b16f90892', }, { alg: 'SHA3-384', content: 'e77e5bf8be9f98ed06a652e2317253bb29e8f79b26910075332823987b2e1bd3dfbb2d7aeb5a57a454c8632241abcc0a', }, { alg: 'SHA3-256', content: '41d7d300d1399e4706a0ead464e13702d85023598a0a81899e40ee8eed847826', }, { alg: 'SHA3-512', content: 'dbdcb824069f0dcf9f9d362b8db7c2efa77f28d77e07c204a28e56b79ebfc478d9c5f9e5f01c7269d3afc0db0e6126d74237cc5a51b5e9ec6b6664580a06de8c', }, ], licenses: [ { license: { id: 'BSD-3-Clause', }, }, ], purl: 'pkg:maven/org.eclipse.angus/angus-activation@2.0.2?type=jar', externalReferences: [ { type: 'website', url: 'https://github.com/eclipse-ee4j/angus-activation/angus-activation', }, { type: 'distribution-intake', url: 'https://jakarta.oss.sonatype.org/service/local/staging/deploy/maven2/', }, { type: 'issue-tracker', url: 'https://github.com/eclipse-ee4j/angus-activation/issues/', }, { type: 'mailing-list', url: 'https://dev.eclipse.org/mhonarc/lists/jakarta.ee-community/', }, { type: 'vcs', url: 'https://github.com/eclipse-ee4j/angus-activation/angus-activation', }, ], type: 'library', 'bom-ref': 'pkg:maven/org.eclipse.angus/angus-activation@2.0.2?type=jar', }, { group: 'de.codecentric', name: 'spring-boot-admin-starter-client', version: '3.2.4-SNAPSHOT', scope: 'required', purl: 'pkg:maven/de.codecentric/spring-boot-admin-starter-client@3.2.4-SNAPSHOT?type=jar', type: 'library', 'bom-ref': 'pkg:maven/de.codecentric/spring-boot-admin-starter-client@3.2.4-SNAPSHOT?type=jar', }, { publisher: 'Pivotal Software, Inc.', group: 'org.springframework.session', name: 'spring-session-core', version: '3.3.0-RC1', description: 'Spring Session', scope: 'required', hashes: [ { alg: 'MD5', content: 'ccb0d954843dc820edeeeaf5152bbd2a', }, { alg: 'SHA-1', content: '7a795db51bf3380c327f945a5addaf949e5edfa8', }, { alg: 'SHA-256', content: '4a63d762c211e49ae21a13cc56acd1c25ac10882f481a261323c9f0b7397fc0a', }, { alg: 'SHA-512', content: '809bc5e693b3bf2db798ec040dd0cc5b9c5d33ae43cf536333c2efadd33a4559b7d01aec8d2b939113ce4e18f7e4c548e0ee346236a1999fdcac5d2ae448ca2b', }, { alg: 'SHA-384', content: 'f5ba3c2d09289e1a6dd773eea4aa75ba2cf60209db4304eb3f05508b596e6ca5e18fcab02dbd5621a272fd1781d30f11', }, { alg: 'SHA3-384', content: 'a827feca6a8b8bc82f9bed8485c9cf3a1130e3d404bd4ba9798ad6a6ab15409f495b4bdf8cab95274151a6e9d8092f92', }, { alg: 'SHA3-256', content: '903ab26a5e3269afbf5a94456db29bc01afda1273b29abcafd8b88f9bff6e17d', }, { alg: 'SHA3-512', content: '63efe99ccbb959d5a66a177108949e4c07b329af11fdcfe270f01fe42ef221b33e8d44af12e88c606616bfe7c46d67b04c137a6bcda8ae00c497200d777abe3e', }, ], licenses: [ { license: { id: 'Apache-2.0', }, }, ], purl: 'pkg:maven/org.springframework.session/spring-session-core@3.3.0-RC1?type=jar', externalReferences: [ { type: 'website', url: 'https://spring.io/projects/spring-session', }, { type: 'issue-tracker', url: 'https://github.com/spring-projects/spring-session/issues', }, { type: 'vcs', url: 'https://github.com/spring-projects/spring-session', }, ], type: 'library', 'bom-ref': 'pkg:maven/org.springframework.session/spring-session-core@3.3.0-RC1?type=jar', }, { publisher: 'Spring IO', group: 'org.springframework', name: 'spring-jcl', version: '6.1.6', description: 'Spring Commons Logging Bridge', scope: 'required', hashes: [ { alg: 'MD5', content: 'acf8ba19c939bace96969efc2c5a6c2b', }, { alg: 'SHA-1', content: '84cb19b30b22feca73c2ac005ca849c5890935a3', }, { alg: 'SHA-256', content: 'bfbd972fbd94dfb40cc2b19de21b769e8157497cf55555523a0e01b468b8e9b8', }, { alg: 'SHA-512', content: 'ae26ff2bdbb1928eb4349a3b0375376c64ba1e528006f826cf37b92af1552532cf5efaa49a115665dd2e426ea3cb2ee12e7120615766c41c2c7aab9f510240dc', }, { alg: 'SHA-384', content: 'db3928758d60ffd564c54a9ca173405910166e30e1dd799499019b85fac437aae515b50da9cd8f6c17eea94078c88c18', }, { alg: 'SHA3-384', content: '9423a4259723be7647a237a150d22521dc412b3519f5d607e2a83dfbda4c26ff98e34b386ddbfcdc15aa5dd31375da4d', }, { alg: 'SHA3-256', content: '9921f099221d6690b3bab7582b53d254c17560922c591f70e608d144fce564fb', }, { alg: 'SHA3-512', content: '8aff52cc2ced97617ae8df8597045eee432a92a07cf34ccf03c7d3b8ecba455c86b16752a38a841a3cdaf050b19c0eebba447bc854caa92824564f67a304397f', }, ], licenses: [ { license: { id: 'Apache-2.0', }, }, ], purl: 'pkg:maven/org.springframework/spring-jcl@6.1.6?type=jar', externalReferences: [ { type: 'website', url: 'https://github.com/spring-projects/spring-framework', }, { type: 'issue-tracker', url: 'https://github.com/spring-projects/spring-framework/issues', }, { type: 'vcs', url: 'https://github.com/spring-projects/spring-framework', }, ], type: 'library', 'bom-ref': 'pkg:maven/org.springframework/spring-jcl@6.1.6?type=jar', }, { publisher: 'Pivotal Software, Inc.', group: 'org.springframework.session', name: 'spring-session-jdbc', version: '3.3.0-RC1', description: 'Spring Session and Spring JDBC integration', scope: 'required', hashes: [ { alg: 'MD5', content: 'd950e3c7212c0994d66c4f6dabe6df06', }, { alg: 'SHA-1', content: '1f633f053c2005b540513a1295ea4d38a57fb1cb', }, { alg: 'SHA-256', content: 'd2f3a9caa0f5812d412d20423852d50208292a0fe7e56777f8332f1b8b989201', }, { alg: 'SHA-512', content: '4593eb3c20d6fe41b3c5566bfbb989c0c563dd81b0a796428c7e598da972d7a15aa9e55dd774c5b6e74e004e4c4bac9df03e9306be3dc20894df2eccbf09b600', }, { alg: 'SHA-384', content: '708c0bdc7f191aaa9f005d29dfcbc53635666b7ac670082a270b77bff878ea2c90f874c02af8b0e2f86638df1febf579', }, { alg: 'SHA3-384', content: '3e563a19f6bde339bff361595b2d3ac6ed6e627a384068ad5cf3636f2b1b77b8cc8d00453e980f5afc3da31977ad373c', }, { alg: 'SHA3-256', content: '1de39e1781ec3f371b49acdee594256ec48e2a05d8fa5d71642a74427a5d23fe', }, { alg: 'SHA3-512', content: '74699f35b8d1275420b989b1fa0b191e4013247d6a2e4309e6f1051692ea335643648142f63e390265c6d3807aef0ae0bdccaba97d50d56819423e31b9a6d910', }, ], licenses: [ { license: { id: 'Apache-2.0', }, }, ], purl: 'pkg:maven/org.springframework.session/spring-session-jdbc@3.3.0-RC1?type=jar', externalReferences: [ { type: 'website', url: 'https://spring.io/projects/spring-session', }, { type: 'issue-tracker', url: 'https://github.com/spring-projects/spring-session/issues', }, { type: 'vcs', url: 'https://github.com/spring-projects/spring-session', }, ], type: 'library', 'bom-ref': 'pkg:maven/org.springframework.session/spring-session-jdbc@3.3.0-RC1?type=jar', }, { publisher: 'Spring IO', group: 'org.springframework', name: 'spring-context', version: '6.1.6', description: 'Spring Context', scope: 'required', hashes: [ { alg: 'MD5', content: 'bff5b9db23e0dfe995e1f4a4a160c5d0', }, { alg: 'SHA-1', content: '2be30298638975efaf7fff22f1570d79b2679814', }, { alg: 'SHA-256', content: '452f82d693ada09ebd54666de9c1ad561cb77a1e9574e2076637c08d0b1393ce', }, { alg: 'SHA-512', content: '70edce09aad5fe76ce0d98a6562a14d77d12c7f7fdedcdf3ca1f6fdc356f05de87bc3310268caca1ff7afef8b759a9a40c1ef78e88aa699b9f1eb76acbc968f5', }, { alg: 'SHA-384', content: 'f4072a357ee4e1c403cb0f6d4638f93ba85b611a0d27eafb481dc21933060116a20723dd9cc584559782c5ac0fc9ff37', }, { alg: 'SHA3-384', content: '41424e53a13405ec20d5e21e32c078960bf456030bf268d8484c4a5390784b083b93b4524b86fe497e6af3c9c598d260', }, { alg: 'SHA3-256', content: 'b2fcf0269d3a9309ed817775b81ad748338dd071a1b51e77fe095f41806a0aa7', }, { alg: 'SHA3-512', content: '563b6d0faff0662d57c1619e83135facc819abfb97d91f2cda542a1b034e6d521950fc09457b3b897ea8b20f96cc83379e0479837553a8434a9d7a3c01ed03ba', }, ], licenses: [ { license: { id: 'Apache-2.0', }, }, ], purl: 'pkg:maven/org.springframework/spring-context@6.1.6?type=jar', externalReferences: [ { type: 'website', url: 'https://github.com/spring-projects/spring-framework', }, { type: 'issue-tracker', url: 'https://github.com/spring-projects/spring-framework/issues', }, { type: 'vcs', url: 'https://github.com/spring-projects/spring-framework', }, ], type: 'library', 'bom-ref': 'pkg:maven/org.springframework/spring-context@6.1.6?type=jar', }, { publisher: 'Spring IO', group: 'org.springframework', name: 'spring-jdbc', version: '6.1.6', description: 'Spring JDBC', scope: 'required', hashes: [ { alg: 'MD5', content: 'c8c19b4161ee251c20a0b6eba94ed825', }, { alg: 'SHA-1', content: '3f8a440a49c15264ff438598b715bd00c5a88109', }, { alg: 'SHA-256', content: '4f575ff3515214853590f07ccbdac48947a4bd1246596017fb048ab77d0290ae', }, { alg: 'SHA-512', content: '3c513ec606ab67e69c1bb4b49b07a31c41cda60a064b0b83e30f0b2cb40ee10ade84f8e18f2c37d8c10d45a5fa4c0054d827219ad3aa0efb2f158d32928c3d4b', }, { alg: 'SHA-384', content: 'f414896caf82ba89de3dff44b78c9e43b9de9e625c75e4d696c20a8ddf35941d0bb52dbd268ba0896ca87f8b7007e032', }, { alg: 'SHA3-384', content: '13a4c9fc471326b80243ce2d737f9cee633b3798e29888ba24447734f4b5ecbd4a8725e4198956aa7aeae052477ec5ac', }, { alg: 'SHA3-256', content: 'fe64c52eed719a2be7a11b7f35c115f68ee6e3d653212c909e1d417fcea16f46', }, { alg: 'SHA3-512', content: 'fab938f5c5734258958c30f9a4876d84dc94b1771b5d9ba63c8911176eca110a4b1ffc86ed73f1e5c98671f4fce14798adf387e330b01d3fd8e0c8c8430ad22d', }, ], licenses: [ { license: { id: 'Apache-2.0', }, }, ], purl: 'pkg:maven/org.springframework/spring-jdbc@6.1.6?type=jar', externalReferences: [ { type: 'website', url: 'https://github.com/spring-projects/spring-framework', }, { type: 'issue-tracker', url: 'https://github.com/spring-projects/spring-framework/issues', }, { type: 'vcs', url: 'https://github.com/spring-projects/spring-framework', }, ], type: 'library', 'bom-ref': 'pkg:maven/org.springframework/spring-jdbc@6.1.6?type=jar', }, { publisher: 'Spring IO', group: 'org.springframework', name: 'spring-tx', version: '6.1.6', description: 'Spring Transaction', scope: 'required', hashes: [ { alg: 'MD5', content: '290de6a7f55216181b65e1f682fd9bc4', }, { alg: 'SHA-1', content: '4e18554fb6669f266108cc838a4619bbc8f7db8d', }, { alg: 'SHA-256', content: 'dc76ab1629b986555fc83a0f83803aa591b91af46e9d187ac9eb999a99898b8f', }, { alg: 'SHA-512', content: '5456bf6a0bada70a25bb477534749f39a27e607d7b55144908c076882da9dc9a8a05e95051675ef635b6a285a91e51860a14d3dba94db110ac3309a040fe5f85', }, { alg: 'SHA-384', content: '72cdfa7450e9706dcfccb735c8dbc7af2588cc566d88e3deb65f3f1c9db889e5d8520cd49a21daee503da3193f058435', }, { alg: 'SHA3-384', content: 'cd7807ba85d96ecfb8f019372dd658fc777990578b3e088e059bf37a5518ed126808009ac73d1e41d4030fc3b9524041', }, { alg: 'SHA3-256', content: 'd920b7548652e6a43344eb289e5f47b701ce1746f426c09d08d1960b3d6e831c', }, { alg: 'SHA3-512', content: '5099e4245f5172cad173f62506596f09b5096f7777715527b07d4e88005ce4f48d16be35c4ebb4f3629f3a3ec3ff406d2a36e39ec9464e7364751b56ebb3b5fe', }, ], licenses: [ { license: { id: 'Apache-2.0', }, }, ], purl: 'pkg:maven/org.springframework/spring-tx@6.1.6?type=jar', externalReferences: [ { type: 'website', url: 'https://github.com/spring-projects/spring-framework', }, { type: 'issue-tracker', url: 'https://github.com/spring-projects/spring-framework/issues', }, { type: 'vcs', url: 'https://github.com/spring-projects/spring-framework', }, ], type: 'library', 'bom-ref': 'pkg:maven/org.springframework/spring-tx@6.1.6?type=jar', }, { publisher: 'Pivotal Software, Inc.', group: 'org.springframework.cloud', name: 'spring-cloud-starter-config', version: '4.1.1', description: 'Spring Cloud Starter', scope: 'required', hashes: [ { alg: 'MD5', content: 'f1e90f5c16f40b9c6aa52273d42a36a5', }, { alg: 'SHA-1', content: 'c90d7c91c5b422fc416f20a994b303074aa37388', }, { alg: 'SHA-256', content: 'b9b6d56cac78c59467001cd0fa904bc9e6dcc3e26e02872e0ac3d29b2c8031c2', }, { alg: 'SHA-512', content: '5ce61142994ba355a8ee8cfe37e0b07a0b2b4be5ff3c39ab430d253a10f5f37cc910eb0eb4bc7f982e64f379012fa7d339d400a3c96e969b03ebc65cee2c0279', }, { alg: 'SHA-384', content: 'c3159a313b718fbb558d393768d7aecfd3bd73b89ec246435c6de75b152a27889cd946ef58632155a964732b733f07c4', }, { alg: 'SHA3-384', content: '747a95ac4d7a2f237861a2cdb068fc42092e3a2aecd660e33cf075392360b1043c4919de8fb57cee82634178ede0d0c3', }, { alg: 'SHA3-256', content: '72bab7c132b389f0ec733a0e29a4a0532b8cbddd49e8a923cf02f762fb6fed24', }, { alg: 'SHA3-512', content: 'c177c7c5073b1621b93220e790d1402d44ba9339c7468f9515930e96be26672c3e02703de1a61c636b9edee6207466b5c37250a73890290099126a237345decd', }, ], licenses: [ { license: { id: 'Apache-2.0', }, }, ], purl: 'pkg:maven/org.springframework.cloud/spring-cloud-starter-config@4.1.1?type=jar', externalReferences: [ { type: 'website', url: 'https://projects.spring.io/spring-cloud', }, { type: 'distribution', url: 'https://github.com/spring-cloud', }, { type: 'distribution-intake', url: 'https://s01.oss.sonatype.org/service/local/staging/deploy/maven2/', }, { type: 'vcs', url: 'https://github.com/spring-cloud/spring-cloud-config/spring-cloud-starter-config', }, ], type: 'library', 'bom-ref': 'pkg:maven/org.springframework.cloud/spring-cloud-starter-config@4.1.1?type=jar', }, { publisher: 'Pivotal Software, Inc.', group: 'org.springframework.cloud', name: 'spring-cloud-config-client', version: '4.1.1', description: 'This project is a Spring configuration client.', scope: 'required', hashes: [ { alg: 'MD5', content: '02567f0e59035f8b9cf4916468533939', }, { alg: 'SHA-1', content: 'eb8e3991ab2a7c944a22544dc2b87763554b8409', }, { alg: 'SHA-256', content: '556349cf937fbaf14428f4ee69976cf13c70af7701abb97489395572e5221031', }, { alg: 'SHA-512', content: 'f0e9ae214fca3739ec110ad32878de64acf9e45069b58f66a7c31694b932607f5b973f6332e4560a84de5ad8b0d6e1d537662b990aa967ed365f72dab38fb12d', }, { alg: 'SHA-384', content: 'cdbf51bcbdc08d8a9fcca1bdcab3c201be85053613fb12b2a866aefea6cf479d8c16aa7ae1b384c15f70f06ab7ad606b', }, { alg: 'SHA3-384', content: '5fd2d1526a1e487bfbaa51143043bfd864854d8317f9d51d861047305c2d4ea9e143cc0d24fad9369ea89a4c2170cade', }, { alg: 'SHA3-256', content: 'db1ddb5759a6889da3fc99405529daf4556795b7ea6deac63fca9633f9fe69fc', }, { alg: 'SHA3-512', content: '529441dfcf47e687640b432ad2dff0b365b3bfdfcf2090a8e64d650bca9b618b4b7c97c8f45b04087d711bb09d5477141ca4c53803ebb35ab2bb9ac2d6ac82ca', }, ], licenses: [ { license: { id: 'Apache-2.0', }, }, ], purl: 'pkg:maven/org.springframework.cloud/spring-cloud-config-client@4.1.1?type=jar', externalReferences: [ { type: 'website', url: 'https://spring.io', }, { type: 'distribution', url: 'https://github.com/spring-cloud', }, { type: 'distribution-intake', url: 'https://s01.oss.sonatype.org/service/local/staging/deploy/maven2/', }, { type: 'vcs', url: 'https://github.com/spring-cloud/spring-cloud-config/spring-cloud-config-client', }, ], type: 'library', 'bom-ref': 'pkg:maven/org.springframework.cloud/spring-cloud-config-client@4.1.1?type=jar', }, { publisher: 'FasterXML', group: 'com.fasterxml.jackson.core', name: 'jackson-annotations', version: '2.17.0', description: 'Core annotations used for value types, used by Jackson data binding package.', scope: 'required', hashes: [ { alg: 'MD5', content: '7529e022796db72bc17288e950c24b3f', }, { alg: 'SHA-1', content: '880a742337010da4c851f843d8cac150e22dff9f', }, { alg: 'SHA-256', content: '8562569a001d46e84ea23802257e33c8f68b24eb47c1e0efd133a0372c512959', }, { alg: 'SHA-512', content: '20104840da168653b27ffcbef6600d29d04b7f315934531f6521b30cfc0438893ac5e3b2476ba03a6a47f3aed8882cc7d5a57b66163ad19aac217a258826e51b', }, { alg: 'SHA-384', content: 'c597370368f411e8f63500537a94f503f44f3bbd653c77d39871eb65745ee2a3d8d83bb8c303790c1e26f30e76219000', }, { alg: 'SHA3-384', content: '6c446264fac7209fc435be283dbb6d578ed7328e84756d7e987a0871a9119bf9bffbfe40827e84324ea7924f83aad770', }, { alg: 'SHA3-256', content: '437fa185a964c155377819fed79558491f31a7ee20a60c4624d252f6c6bf75bc', }, { alg: 'SHA3-512', content: 'ad18bc120cfafe0ee6c961c5422242361de7b3154d3252a2ffaecf5d981865c141a25fa8706709eabc351d42f3593dd2832219657671f21d9672c2488e5d1bf4', }, ], licenses: [ { license: { id: 'Apache-2.0', }, }, ], purl: 'pkg:maven/com.fasterxml.jackson.core/jackson-annotations@2.17.0?type=jar', externalReferences: [ { type: 'website', url: 'https://github.com/FasterXML/jackson', }, { type: 'distribution-intake', url: 'https://oss.sonatype.org/service/local/staging/deploy/maven2/', }, { type: 'issue-tracker', url: 'https://github.com/FasterXML/jackson-annotations/issues', }, { type: 'vcs', url: 'https://github.com/FasterXML/jackson-annotations', }, ], type: 'library', 'bom-ref': 'pkg:maven/com.fasterxml.jackson.core/jackson-annotations@2.17.0?type=jar', }, { publisher: 'The Apache Software Foundation', group: 'org.apache.httpcomponents.client5', name: 'httpclient5', version: '5.3.1', description: 'Apache HttpComponents Client', scope: 'required', hashes: [ { alg: 'MD5', content: 'de1810a606b27192cbf5bbad9c25a648', }, { alg: 'SHA-1', content: '56b53c8f4bcdaada801d311cf2ff8a24d6d96883', }, { alg: 'SHA-256', content: '08346a757c617f6ecc66af9f099260adde1f3a1351fa81cb22fc17482b31f823', }, { alg: 'SHA-512', content: '4c2d75106af8470789f0e08305e64ad86528f2f737da230e561892d33dbca0b6e2dbced2a075f0744cee7801c06ef174481540661b3c9a1bec6d6f93938b05bc', }, { alg: 'SHA-384', content: '27470f74660b89f8a0af562a4edbd244afff4947b0fa7364c61e53ca49713efbca49e661214590f532c4acf9cfd66eac', }, { alg: 'SHA3-384', content: 'd25be0f1c5e0c02de0adf7113e591f10bd7fab20c168a20b7d15c859b252a6dc3ae3a24098e838d95c179ab3107f07b6', }, { alg: 'SHA3-256', content: '9e22ce6935e71d12d1be70ef0b7cea9a87191c767de2904cb82fcb6e58d0e9b2', }, { alg: 'SHA3-512', content: '9d36e201e469dd357ef715bba7beba62dbea98daefcea3b793fd285c2ffade97d72b35a07f05015fbc2d5b4fa5db58ff5ecf40e1269582a6c3e53ed62cbf97f4', }, ], licenses: [ { license: { id: 'Apache-2.0', }, }, ], purl: 'pkg:maven/org.apache.httpcomponents.client5/httpclient5@5.3.1?type=jar', externalReferences: [ { type: 'website', url: 'https://hc.apache.org/httpcomponents-client-5.0.x/5.3.1/httpclient5/', }, { type: 'distribution-intake', url: 'https://repository.apache.org/service/local/staging/deploy/maven2', }, { type: 'issue-tracker', url: 'https://issues.apache.org/jira/browse/HTTPCLIENT', }, { type: 'mailing-list', url: 'https://lists.apache.org/list.html?httpclient-users@hc.apache.org', }, { type: 'vcs', url: 'https://github.com/apache/httpcomponents-client/tree/5.3.1/httpclient5', }, ], type: 'library', 'bom-ref': 'pkg:maven/org.apache.httpcomponents.client5/httpclient5@5.3.1?type=jar', }, { publisher: 'The Apache Software Foundation', group: 'org.apache.httpcomponents.core5', name: 'httpcore5', version: '5.2.4', description: 'Apache HttpComponents HTTP/1.1 core components', scope: 'required', hashes: [ { alg: 'MD5', content: '5a3d417ea4e65e0f74194263dc5c6c43', }, { alg: 'SHA-1', content: '34d8332b975f9e9a8298efe4c883ec43d45b7059', }, { alg: 'SHA-256', content: 'a7f62496113f66f9e27c26b84c44f5ce4555c6270083cdf2d45f255336cd52af', }, { alg: 'SHA-512', content: '9fb4134d85e665e15410af005b21cd2f9b5e60d75112945d37b879f96f769a70be034557526ea7d05f8b83dda91c56d00f946763c44a183d7aea2857549b4481', }, { alg: 'SHA-384', content: '8ec2da2fd22a23e9f740589947398a907795ab310d4ed166ecc1448ceea7035a50090cf645dad28f3c84c08599f1e57e', }, { alg: 'SHA3-384', content: 'e52f4f1c073ec9d893cffa353a27939054a0b537fe49e4aacfdd6c265dfba037309913a001c025d85ccbb06a1e9e72b0', }, { alg: 'SHA3-256', content: '23fb185f22dac603ba579c4f707671f43b3b08ab049ae519fb492ea0232c5ba9', }, { alg: 'SHA3-512', content: '8371cd7e6f94ccb1590bd90d5868a90a3344123b0e6a4f7113d76b816c16d58efc6f2cab124a45ffd93739990fed05201f294f890aa989345f52e736383b4261', }, ], licenses: [ { license: { id: 'Apache-2.0', }, }, ], purl: 'pkg:maven/org.apache.httpcomponents.core5/httpcore5@5.2.4?type=jar', externalReferences: [ { type: 'website', url: 'https://hc.apache.org/httpcomponents-core-5.2.x/5.2.4/httpcore5/', }, { type: 'distribution-intake', url: 'https://repository.apache.org/service/local/staging/deploy/maven2', }, { type: 'issue-tracker', url: 'https://issues.apache.org/jira/browse/HTTPCORE', }, { type: 'mailing-list', url: 'https://lists.apache.org/list.html?httpclient-users@hc.apache.org', }, { type: 'vcs', url: 'https://github.com/apache/httpcomponents-core/tree/5.2.4/httpcore5', }, ], type: 'library', 'bom-ref': 'pkg:maven/org.apache.httpcomponents.core5/httpcore5@5.2.4?type=jar', }, { publisher: 'The Apache Software Foundation', group: 'org.apache.httpcomponents.core5', name: 'httpcore5-h2', version: '5.2.4', description: 'Apache HttpComponents HTTP/2 Core Components', scope: 'required', hashes: [ { alg: 'MD5', content: 'd407b8144029db656ac5ba3d54ef801f', }, { alg: 'SHA-1', content: '2872764df7b4857549e2880dd32a6f9009166289', }, { alg: 'SHA-256', content: 'dc1a95e73eb04db93451533d390ce02c53b301a10dc343d08c862f2934b3d30e', }, { alg: 'SHA-512', content: '72fbee55f173c43d9ffc0cc5a83d59e60be1002c06ab81de39ba700cc30b04e84fdfed73d3a8985d561a1aa8ac3ca905f9259d01b431e1ff14da6fae622f787d', }, { alg: 'SHA-384', content: '2f96537af2866fa96aae46138febe3009dca97cc9b4284cf18510c12d159ad3f5d34c3c9bafc8026215da81520331660', }, { alg: 'SHA3-384', content: '9900a3aeaf434d7f32a7500e29e16d354857ef34e6af3fb7de9e1ab7683b6a1c4bfa9b9f70bb779a8ec8d8be82b6bca4', }, { alg: 'SHA3-256', content: 'da34ed59342e368229b74245d2268a457588adea9e276a1ac2fb57419c605f31', }, { alg: 'SHA3-512', content: 'ca5b03cf34c7e344fd0b809c582e60f0eaea796372cf68e2e95087ac5943154e51472595f6554b810a5ac4789ba6f7c06cae46437badecbf31c57907123a49fc', }, ], licenses: [ { license: { id: 'Apache-2.0', }, }, ], purl: 'pkg:maven/org.apache.httpcomponents.core5/httpcore5-h2@5.2.4?type=jar', externalReferences: [ { type: 'website', url: 'https://hc.apache.org/httpcomponents-core-5.2.x/5.2.4/httpcore5-h2/', }, { type: 'distribution-intake', url: 'https://repository.apache.org/service/local/staging/deploy/maven2', }, { type: 'issue-tracker', url: 'https://issues.apache.org/jira/browse/HTTPCORE', }, { type: 'mailing-list', url: 'https://lists.apache.org/list.html?httpclient-users@hc.apache.org', }, { type: 'vcs', url: 'https://github.com/apache/httpcomponents-core/tree/5.2.4/httpcore5-h2', }, ], type: 'library', 'bom-ref': 'pkg:maven/org.apache.httpcomponents.core5/httpcore5-h2@5.2.4?type=jar', }, { publisher: 'FasterXML', group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: '2.17.0', description: 'General data-binding functionality for Jackson: works on core streaming API', scope: 'required', hashes: [ { alg: 'MD5', content: '09dd83868b44c6a3dc48911f4b3bbbc1', }, { alg: 'SHA-1', content: '7173e9e1d4bc6d7ca03bc4eeedcd548b8b580b34', }, { alg: 'SHA-256', content: 'd0ed5b54cb1b0bbb0828e24ce752a43a006dc188b34e3a4ae3238acc7b637418', }, { alg: 'SHA-512', content: 'c6b06d4b20941d9e32b462552031e6c98378e5edce57693e55adcc73cf7d5088af5b3a666a59e94a7f0b57066ac694863919f398f28ee0d7ceb362c8c05f7491', }, { alg: 'SHA-384', content: '02875865ef42573114755ab7147d64f8e5a791f2a2b8debe51dda22886370ef34af8c159d8efa8b90735f33f90089187', }, { alg: 'SHA3-384', content: 'ee93411dc73337c11d48609fbf79ae606ccd0ab712e3d2c12c91103964182910a810b9fa062a0afc47d19c720c97c5e7', }, { alg: 'SHA3-256', content: 'eb5b5dfb8afb2538a2c31caab47d909970bb647763a0eccabaae8e6f0a9ad988', }, { alg: 'SHA3-512', content: '09ce7a8d928d42b221e0f6151653248f78eb68b6228cfc36fabd48ae48a3890f235fb33b82cc2faa24069efdf5a14d7433014e3bfb0d5173047d1911e5a55fe4', }, ], licenses: [ { license: { id: 'Apache-2.0', }, }, ], purl: 'pkg:maven/com.fasterxml.jackson.core/jackson-databind@2.17.0?type=jar', externalReferences: [ { type: 'website', url: 'https://github.com/FasterXML/jackson', }, { type: 'distribution-intake', url: 'https://oss.sonatype.org/service/local/staging/deploy/maven2/', }, { type: 'issue-tracker', url: 'https://github.com/FasterXML/jackson-databind/issues', }, { type: 'vcs', url: 'https://github.com/FasterXML/jackson-databind', }, ], type: 'library', 'bom-ref': 'pkg:maven/com.fasterxml.jackson.core/jackson-databind@2.17.0?type=jar', }, { publisher: 'FasterXML', group: 'com.fasterxml.jackson.core', name: 'jackson-core', version: '2.17.0', description: 'Core Jackson processing abstractions (aka Streaming API), implementation for JSON', scope: 'required', hashes: [ { alg: 'MD5', content: '3e4b82b6e29693927dd289a344c35e46', }, { alg: 'SHA-1', content: 'a6e5058ef9720623c517252d17162f845306ff3a', }, { alg: 'SHA-256', content: '55be130f6a68038088a261856c4e383ce79957a0fc1a29ecb213a9efd6ef4389', }, { alg: 'SHA-512', content: '85611fb7687450eb6078855c46d94dabf1cebcf179e23455cd1069aaded3b169112ec5d3d8d8510a7076166dc146e2f684f8527c5ef5b9ed99a7ec91f0825523', }, { alg: 'SHA-384', content: '12bbfe5721ecd374a77ede24cca8a39f1415fd50dd95938c5e3365c703a02b5f6c0e7b10c7b44b7c9c5a874dd0b971c7', }, { alg: 'SHA3-384', content: 'd1b1f9c3e53603ccc690d76ac1a90dd1ddf07723f4eb53f58acfa266f17a675b311b0f29b94d9f1bb0ab32ad1a8aea4a', }, { alg: 'SHA3-256', content: '62a76265cdc48c8a7f80c9d5566a179bd796c646b25c5cb937fd0a10cfffff1f', }, { alg: 'SHA3-512', content: '87c42bea365905e9d877bda162c9d79d962a969a53a46861c350b9a9a87d09f4986c6ff67c4f00eba6df8c86f0621cfb52359a91321ace7eec3bb5d7c82feeb9', }, ], licenses: [ { license: { id: 'Apache-2.0', }, }, ], purl: 'pkg:maven/com.fasterxml.jackson.core/jackson-core@2.17.0?type=jar', externalReferences: [ { type: 'website', url: 'https://github.com/FasterXML/jackson-core', }, { type: 'distribution-intake', url: 'https://oss.sonatype.org/service/local/staging/deploy/maven2/', }, { type: 'issue-tracker', url: 'https://github.com/FasterXML/jackson-core/issues', }, { type: 'vcs', url: 'https://github.com/FasterXML/jackson-core', }, ], type: 'library', 'bom-ref': 'pkg:maven/com.fasterxml.jackson.core/jackson-core@2.17.0?type=jar', }, { group: 'net.bytebuddy', name: 'byte-buddy', version: '1.14.13', description: 'Byte Buddy is a Java library for creating Java classes at run time. This artifact is a build of Byte Buddy with all ASM dependencies repackaged into its own name space.', scope: 'required', hashes: [ { alg: 'MD5', content: '7f4df0c9277f4c1c418a742cc3178ac9', }, { alg: 'SHA-1', content: '45cf516d9a23485200950549ff72b204c307fc9d', }, { alg: 'SHA-256', content: 'ba8254ff6d612af49acee4cac1108453ce3a417efa548b24f2f4f268cd2b441a', }, { alg: 'SHA-512', content: 'c7f76ce1bf108c98af398c3b2df01d8bd0a81a8eb6efe669fb23ef3cfb33419fe7f975c2523579f4d48567da7786751a31ef79f41babc391be250da485c93b0e', }, { alg: 'SHA-384', content: '31ea6c6cc36495936761edee2ce2a3ba61edf66e5e2de78d7ae243db03e84cf57081e8434b5c4cafbf042a5ab15799ec', }, { alg: 'SHA3-384', content: '0528e8facb1eb96e7c2f825bbda814432ca7b269de046c9d49eac047762fc5dc0c3d6b91cca08a9853d8744497e28e62', }, { alg: 'SHA3-256', content: 'a1da765e093a8b14bb5c2e3eb7012ced676840f8201a3ffaf47c3da4784018fd', }, { alg: 'SHA3-512', content: '05581ee76ccce6fb141e55863db63e03a9885381a185a3413046bf5f40302d8314141ff093f396ef99a3b0948fc58346b0b5c7c139ccae2294cd3040bd0493cd', }, ], licenses: [ { license: { id: 'Apache-2.0', }, }, ], purl: 'pkg:maven/net.bytebuddy/byte-buddy@1.14.13?type=jar', externalReferences: [ { type: 'website', url: 'https://bytebuddy.net/byte-buddy', }, { type: 'distribution-intake', url: 'https://s01.oss.sonatype.org/service/local/staging/deploy/maven2', }, { type: 'issue-tracker', url: 'https://github.com/raphw/byte-buddy/issues', }, ], type: 'library', 'bom-ref': 'pkg:maven/net.bytebuddy/byte-buddy@1.14.13?type=jar', }, { publisher: 'The HSQL Development Group', group: 'org.hsqldb', name: 'hsqldb', version: '2.7.2', description: 'HSQLDB - Lightweight 100% Java SQL Database Engine', scope: 'required', hashes: [ { alg: 'MD5', content: 'dab42304e10a7983af59ce89a8ccee12', }, { alg: 'SHA-1', content: 'd92d4d2aa515714da2165c9d640d584c2896c9df', }, { alg: 'SHA-256', content: 'aa455133e664f6a7e6f30cd0cd4f8ad83dfbd94eb717c438548e446784614a92', }, { alg: 'SHA-512', content: '0b997354bae288f84925cfb0aca0525257225a150a84ecbc687b3c7e6ef38cddbcdf2cf24404ac2bf4990a5d8baad2394c38bb5b299a7cfcbd2982741cd35b20', }, { alg: 'SHA-384', content: 'f2d2e3fa488ffc041cef71acb951a9475177974c98a9f6b9f711d0ea67bd6c344dd0601856423fd96e63b080d9a41dfc', }, { alg: 'SHA3-384', content: '84b769ff1bb3ace9e00b9e499405b445304bc55892d093e8306a9f94e100c2fb3aefa569290e74f42b207c980f0baaef', }, { alg: 'SHA3-256', content: 'b981fa576356765be24044e6bcc3d273891a8e53a2caa5a4541a6039cfc4ae67', }, { alg: 'SHA3-512', content: '042bfe7d7d30f16c23bb2305e37c526999553c2ba1df39ab7d1108943a4699b84fc9515e5ab272733e39882aac22d59fb1691c95e0d1d99f8bb012ce8caf8aa3', }, ], licenses: [ { license: { name: 'HSQLDB License, a BSD open source license', url: 'http://hsqldb.org/web/hsqlLicense.html', }, }, ], purl: 'pkg:maven/org.hsqldb/hsqldb@2.7.2?type=jar', externalReferences: [ { type: 'website', url: 'http://hsqldb.org', }, { type: 'vcs', url: 'http://sourceforge.net/p/hsqldb/svn/HEAD/tree/base/tags/2.7.2', }, ], type: 'library', 'bom-ref': 'pkg:maven/org.hsqldb/hsqldb@2.7.2?type=jar', }, { publisher: 'QOS.ch', group: 'org.slf4j', name: 'slf4j-api', version: '2.0.13', description: 'The slf4j API', scope: 'required', hashes: [ { alg: 'MD5', content: '7f4028aa04f75427327f3f30cd62ba4e', }, { alg: 'SHA-1', content: '80229737f704b121a318bba5d5deacbcf395bc77', }, { alg: 'SHA-256', content: 'e7c2a48e8515ba1f49fa637d57b4e2f590b3f5bd97407ac699c3aa5efb1204a9', }, { alg: 'SHA-512', content: 'b4eeb5757118e264ec7f107d879270784357380d6f53471b7874dd7e0166fdf5686a95eb66bab867abbe9536da032ab052e207165211391c293cbf6178431fb6', }, { alg: 'SHA-384', content: 'b67cbb4ef32141423000dd4e067bf32e0c1dd2c4689c611522b9fedfc1744513175a22f4b1276f2cec4721c9467cf882', }, { alg: 'SHA3-384', content: '817fc9641f4fc52bfd76006886c6eba975f6f09b2a7cc59334729a8cc033807c8e89be9ec4309acfc16ed65ff6eee018', }, { alg: 'SHA3-256', content: 'f26080cceb5a2e605f3844d6dc8dd3f14c543cb14510765d841d71a64fa454dc', }, { alg: 'SHA3-512', content: '00646c78d65ec854e157638f40735f1888aa585ede59915d58386c599c2fe54ec8c1da73284aeff00ce3142165e33c4c995ad39d08843c31e9e4d7e32c746836', }, ], licenses: [ { license: { id: 'MIT', url: 'https://opensource.org/licenses/MIT', }, }, ], purl: 'pkg:maven/org.slf4j/slf4j-api@2.0.13?type=jar', externalReferences: [ { type: 'website', url: 'http://www.slf4j.org', }, { type: 'distribution-intake', url: 'https://oss.sonatype.org/service/local/staging/deploy/maven2/', }, { type: 'vcs', url: 'https://github.com/qos-ch/slf4j/slf4j-parent/slf4j-api', }, ], type: 'library', 'bom-ref': 'pkg:maven/org.slf4j/slf4j-api@2.0.13?type=jar', }, { publisher: 'Spring IO', group: 'org.springframework', name: 'spring-core', version: '6.1.6', description: 'Spring Core', scope: 'required', hashes: [ { alg: 'MD5', content: '852be6055a31d2ce17b5d231b17f732e', }, { alg: 'SHA-1', content: 'dea4b8e110b7b54a02a4907e32dbb0adee8a7168', }, { alg: 'SHA-256', content: 'caf51f3d51c5d95e931f411027688f1dde3986d5f2aad67ff1096ddddac36ac5', }, { alg: 'SHA-512', content: '893d9c5956c3005717dc7f09b31908dfed3588f9c81fb6180781ca687f305157cf3481f246d1a493fa348991d41a660b54e5db7ff5a4e4676570062b8c22b38b', }, { alg: 'SHA-384', content: '984ff65f605a97d0ecea2a30986fb6c443deb83c7f58450bc661d9060563e1f199f9d472794520106ccbd23b29de0531', }, { alg: 'SHA3-384', content: 'edf8fafbef9d85a15d226dcc85d4fd71c6eaca2cd885867b8a81c37424c9ce024dc16a8f8169a2b2d1214b8b6532d278', }, { alg: 'SHA3-256', content: '71f60a76d6b31290bb2024f17e82e5f920d2dc576a2649c054767ea574baf685', }, { alg: 'SHA3-512', content: 'cf4ab0e13b212c22f9cbea6afdf4aac815c62bd904d2f0d91198e29098988495b4038b6b3c56b6cb48d5b26efc39dec92283e067a969be1d75db817949d73cc4', }, ], licenses: [ { license: { id: 'Apache-2.0', }, }, ], purl: 'pkg:maven/org.springframework/spring-core@6.1.6?type=jar', externalReferences: [ { type: 'website', url: 'https://github.com/spring-projects/spring-framework', }, { type: 'issue-tracker', url: 'https://github.com/spring-projects/spring-framework/issues', }, { type: 'vcs', url: 'https://github.com/spring-projects/spring-framework', }, ], type: 'library', 'bom-ref': 'pkg:maven/org.springframework/spring-core@6.1.6?type=jar', }, ], dependencies: [ { ref: 'pkg:maven/de.codecentric/spring-boot-admin-sample-servlet@3.2.4-SNAPSHOT?type=jar', dependsOn: [ 'pkg:maven/de.codecentric/spring-boot-admin-sample-custom-ui@3.2.4-SNAPSHOT?type=jar', 'pkg:maven/de.codecentric/spring-boot-admin-starter-server@3.2.4-SNAPSHOT?type=jar', 'pkg:maven/org.springframework.boot/spring-boot-starter-security@3.3.0-RC1?type=jar', 'pkg:maven/org.springframework.boot/spring-boot-starter-webmvc@3.3.0-RC1?type=jar', 'pkg:maven/org.springframework.cloud/spring-cloud-starter@4.1.2?type=jar', 'pkg:maven/org.springframework.boot/spring-boot-starter-mail@3.3.0-RC1?type=jar', 'pkg:maven/de.codecentric/spring-boot-admin-starter-client@3.2.4-SNAPSHOT?type=jar', 'pkg:maven/org.springframework.session/spring-session-core@3.3.0-RC1?type=jar', 'pkg:maven/org.springframework.session/spring-session-jdbc@3.3.0-RC1?type=jar', 'pkg:maven/org.springframework.cloud/spring-cloud-starter-config@4.1.1?type=jar', 'pkg:maven/org.hsqldb/hsqldb@2.7.2?type=jar', ], }, { ref: 'pkg:maven/de.codecentric/spring-boot-admin-sample-custom-ui@3.2.4-SNAPSHOT?type=jar', dependsOn: [], }, { ref: 'pkg:maven/de.codecentric/spring-boot-admin-starter-server@3.2.4-SNAPSHOT?type=jar', dependsOn: [], }, { ref: 'pkg:maven/org.springframework.boot/spring-boot-starter-security@3.3.0-RC1?type=jar', dependsOn: [ 'pkg:maven/org.springframework.boot/spring-boot-starter@3.3.0-RC1?type=jar', 'pkg:maven/org.springframework/spring-aop@6.1.6?type=jar', 'pkg:maven/org.springframework.security/spring-security-config@6.3.0-RC1?type=jar', 'pkg:maven/org.springframework.security/spring-security-web@6.3.0-RC1?type=jar', ], }, { ref: 'pkg:maven/org.springframework.boot/spring-boot-starter@3.3.0-RC1?type=jar', dependsOn: [ 'pkg:maven/org.springframework.boot/spring-boot@3.3.0-RC1?type=jar', 'pkg:maven/org.springframework.boot/spring-boot-autoconfigure@3.3.0-RC1?type=jar', 'pkg:maven/org.springframework.boot/spring-boot-starter-logging@3.3.0-RC1?type=jar', 'pkg:maven/jakarta.annotation/jakarta.annotation-api@2.1.1?type=jar', 'pkg:maven/org.springframework/spring-core@6.1.6?type=jar', 'pkg:maven/org.yaml/snakeyaml@2.2?type=jar', ], }, { ref: 'pkg:maven/org.springframework.boot/spring-boot@3.3.0-RC1?type=jar', dependsOn: [ 'pkg:maven/org.springframework/spring-core@6.1.6?type=jar', 'pkg:maven/org.springframework/spring-context@6.1.6?type=jar', ], }, { ref: 'pkg:maven/org.springframework/spring-core@6.1.6?type=jar', dependsOn: ['pkg:maven/org.springframework/spring-jcl@6.1.6?type=jar'], }, { ref: 'pkg:maven/org.springframework/spring-jcl@6.1.6?type=jar', dependsOn: [], }, { ref: 'pkg:maven/org.springframework/spring-context@6.1.6?type=jar', dependsOn: [ 'pkg:maven/org.springframework/spring-aop@6.1.6?type=jar', 'pkg:maven/org.springframework/spring-beans@6.1.6?type=jar', 'pkg:maven/org.springframework/spring-core@6.1.6?type=jar', 'pkg:maven/org.springframework/spring-expression@6.1.6?type=jar', 'pkg:maven/io.micrometer/micrometer-observation@1.13.0-RC1?type=jar', ], }, { ref: 'pkg:maven/org.springframework/spring-aop@6.1.6?type=jar', dependsOn: [ 'pkg:maven/org.springframework/spring-beans@6.1.6?type=jar', 'pkg:maven/org.springframework/spring-core@6.1.6?type=jar', ], }, { ref: 'pkg:maven/org.springframework/spring-beans@6.1.6?type=jar', dependsOn: ['pkg:maven/org.springframework/spring-core@6.1.6?type=jar'], }, { ref: 'pkg:maven/org.springframework/spring-expression@6.1.6?type=jar', dependsOn: ['pkg:maven/org.springframework/spring-core@6.1.6?type=jar'], }, { ref: 'pkg:maven/io.micrometer/micrometer-observation@1.13.0-RC1?type=jar', dependsOn: [ 'pkg:maven/io.micrometer/micrometer-commons@1.13.0-RC1?type=jar', ], }, { ref: 'pkg:maven/io.micrometer/micrometer-commons@1.13.0-RC1?type=jar', dependsOn: [], }, { ref: 'pkg:maven/org.springframework.boot/spring-boot-autoconfigure@3.3.0-RC1?type=jar', dependsOn: [ 'pkg:maven/org.springframework.boot/spring-boot@3.3.0-RC1?type=jar', ], }, { ref: 'pkg:maven/org.springframework.boot/spring-boot-starter-logging@3.3.0-RC1?type=jar', dependsOn: [ 'pkg:maven/ch.qos.logback/logback-classic@1.5.6?type=jar', 'pkg:maven/org.apache.logging.log4j/log4j-to-slf4j@2.23.1?type=jar', 'pkg:maven/org.slf4j/jul-to-slf4j@2.0.13?type=jar', ], }, { ref: 'pkg:maven/ch.qos.logback/logback-classic@1.5.6?type=jar', dependsOn: [ 'pkg:maven/ch.qos.logback/logback-core@1.5.6?type=jar', 'pkg:maven/org.slf4j/slf4j-api@2.0.13?type=jar', ], }, { ref: 'pkg:maven/ch.qos.logback/logback-core@1.5.6?type=jar', dependsOn: [], }, { ref: 'pkg:maven/org.slf4j/slf4j-api@2.0.13?type=jar', dependsOn: [], }, { ref: 'pkg:maven/org.apache.logging.log4j/log4j-to-slf4j@2.23.1?type=jar', dependsOn: [ 'pkg:maven/org.apache.logging.log4j/log4j-api@2.23.1?type=jar', 'pkg:maven/org.slf4j/slf4j-api@2.0.13?type=jar', ], }, { ref: 'pkg:maven/org.apache.logging.log4j/log4j-api@2.23.1?type=jar', dependsOn: [], }, { ref: 'pkg:maven/org.slf4j/jul-to-slf4j@2.0.13?type=jar', dependsOn: ['pkg:maven/org.slf4j/slf4j-api@2.0.13?type=jar'], }, { ref: 'pkg:maven/jakarta.annotation/jakarta.annotation-api@2.1.1?type=jar', dependsOn: [], }, { ref: 'pkg:maven/org.yaml/snakeyaml@2.2?type=jar', dependsOn: [], }, { ref: 'pkg:maven/org.springframework.security/spring-security-config@6.3.0-RC1?type=jar', dependsOn: [ 'pkg:maven/org.springframework.security/spring-security-core@6.3.0-RC1?type=jar', 'pkg:maven/org.springframework/spring-aop@6.1.6?type=jar', 'pkg:maven/org.springframework/spring-beans@6.1.6?type=jar', 'pkg:maven/org.springframework/spring-context@6.1.6?type=jar', 'pkg:maven/org.springframework/spring-core@6.1.6?type=jar', ], }, { ref: 'pkg:maven/org.springframework.security/spring-security-core@6.3.0-RC1?type=jar', dependsOn: [ 'pkg:maven/org.springframework.security/spring-security-crypto@6.3.0-RC1?type=jar', 'pkg:maven/org.springframework/spring-aop@6.1.6?type=jar', 'pkg:maven/org.springframework/spring-beans@6.1.6?type=jar', 'pkg:maven/org.springframework/spring-context@6.1.6?type=jar', 'pkg:maven/org.springframework/spring-core@6.1.6?type=jar', 'pkg:maven/org.springframework/spring-expression@6.1.6?type=jar', 'pkg:maven/io.micrometer/micrometer-observation@1.13.0-RC1?type=jar', ], }, { ref: 'pkg:maven/org.springframework.security/spring-security-crypto@6.3.0-RC1?type=jar', dependsOn: [], }, { ref: 'pkg:maven/org.springframework.security/spring-security-web@6.3.0-RC1?type=jar', dependsOn: [ 'pkg:maven/org.springframework.security/spring-security-core@6.3.0-RC1?type=jar', 'pkg:maven/org.springframework/spring-core@6.1.6?type=jar', 'pkg:maven/org.springframework/spring-aop@6.1.6?type=jar', 'pkg:maven/org.springframework/spring-beans@6.1.6?type=jar', 'pkg:maven/org.springframework/spring-context@6.1.6?type=jar', 'pkg:maven/org.springframework/spring-expression@6.1.6?type=jar', 'pkg:maven/org.springframework/spring-web@6.1.6?type=jar', ], }, { ref: 'pkg:maven/org.springframework/spring-web@6.1.6?type=jar', dependsOn: [ 'pkg:maven/org.springframework/spring-beans@6.1.6?type=jar', 'pkg:maven/org.springframework/spring-core@6.1.6?type=jar', 'pkg:maven/io.micrometer/micrometer-observation@1.13.0-RC1?type=jar', ], }, { ref: 'pkg:maven/org.springframework.boot/spring-boot-starter-webmvc@3.3.0-RC1?type=jar', dependsOn: [ 'pkg:maven/org.springframework.boot/spring-boot-starter@3.3.0-RC1?type=jar', 'pkg:maven/org.springframework.boot/spring-boot-starter-json@3.3.0-RC1?type=jar', 'pkg:maven/org.springframework.boot/spring-boot-starter-tomcat@3.3.0-RC1?type=jar', 'pkg:maven/org.springframework/spring-web@6.1.6?type=jar', 'pkg:maven/org.springframework/spring-webmvc@6.1.6?type=jar', ], }, { ref: 'pkg:maven/org.springframework.boot/spring-boot-starter-json@3.3.0-RC1?type=jar', dependsOn: [ 'pkg:maven/org.springframework.boot/spring-boot-starter@3.3.0-RC1?type=jar', 'pkg:maven/org.springframework/spring-web@6.1.6?type=jar', 'pkg:maven/com.fasterxml.jackson.core/jackson-databind@2.17.0?type=jar', 'pkg:maven/com.fasterxml.jackson.datatype/jackson-datatype-jdk8@2.17.0?type=jar', 'pkg:maven/com.fasterxml.jackson.datatype/jackson-datatype-jsr310@2.17.0?type=jar', 'pkg:maven/com.fasterxml.jackson.module/jackson-module-parameter-names@2.17.0?type=jar', ], }, { ref: 'pkg:maven/com.fasterxml.jackson.core/jackson-databind@2.17.0?type=jar', dependsOn: [ 'pkg:maven/com.fasterxml.jackson.core/jackson-annotations@2.17.0?type=jar', 'pkg:maven/com.fasterxml.jackson.core/jackson-core@2.17.0?type=jar', 'pkg:maven/net.bytebuddy/byte-buddy@1.14.13?type=jar', ], }, { ref: 'pkg:maven/com.fasterxml.jackson.core/jackson-annotations@2.17.0?type=jar', dependsOn: [], }, { ref: 'pkg:maven/com.fasterxml.jackson.core/jackson-core@2.17.0?type=jar', dependsOn: [], }, { ref: 'pkg:maven/net.bytebuddy/byte-buddy@1.14.13?type=jar', dependsOn: [], }, { ref: 'pkg:maven/com.fasterxml.jackson.datatype/jackson-datatype-jdk8@2.17.0?type=jar', dependsOn: [ 'pkg:maven/com.fasterxml.jackson.core/jackson-core@2.17.0?type=jar', 'pkg:maven/com.fasterxml.jackson.core/jackson-databind@2.17.0?type=jar', ], }, { ref: 'pkg:maven/com.fasterxml.jackson.datatype/jackson-datatype-jsr310@2.17.0?type=jar', dependsOn: [ 'pkg:maven/com.fasterxml.jackson.core/jackson-annotations@2.17.0?type=jar', 'pkg:maven/com.fasterxml.jackson.core/jackson-core@2.17.0?type=jar', 'pkg:maven/com.fasterxml.jackson.core/jackson-databind@2.17.0?type=jar', ], }, { ref: 'pkg:maven/com.fasterxml.jackson.module/jackson-module-parameter-names@2.17.0?type=jar', dependsOn: [ 'pkg:maven/com.fasterxml.jackson.core/jackson-core@2.17.0?type=jar', 'pkg:maven/com.fasterxml.jackson.core/jackson-databind@2.17.0?type=jar', ], }, { ref: 'pkg:maven/org.springframework.boot/spring-boot-starter-tomcat@3.3.0-RC1?type=jar', dependsOn: [ 'pkg:maven/jakarta.annotation/jakarta.annotation-api@2.1.1?type=jar', 'pkg:maven/org.apache.tomcat.embed/tomcat-embed-core@10.1.20?type=jar', 'pkg:maven/org.apache.tomcat.embed/tomcat-embed-el@10.1.20?type=jar', 'pkg:maven/org.apache.tomcat.embed/tomcat-embed-websocket@10.1.20?type=jar', ], }, { ref: 'pkg:maven/org.apache.tomcat.embed/tomcat-embed-core@10.1.20?type=jar', dependsOn: [], }, { ref: 'pkg:maven/org.apache.tomcat.embed/tomcat-embed-el@10.1.20?type=jar', dependsOn: [], }, { ref: 'pkg:maven/org.apache.tomcat.embed/tomcat-embed-websocket@10.1.20?type=jar', dependsOn: [ 'pkg:maven/org.apache.tomcat.embed/tomcat-embed-core@10.1.20?type=jar', ], }, { ref: 'pkg:maven/org.springframework/spring-webmvc@6.1.6?type=jar', dependsOn: [ 'pkg:maven/org.springframework/spring-aop@6.1.6?type=jar', 'pkg:maven/org.springframework/spring-beans@6.1.6?type=jar', 'pkg:maven/org.springframework/spring-context@6.1.6?type=jar', 'pkg:maven/org.springframework/spring-core@6.1.6?type=jar', 'pkg:maven/org.springframework/spring-expression@6.1.6?type=jar', 'pkg:maven/org.springframework/spring-web@6.1.6?type=jar', ], }, { ref: 'pkg:maven/org.springframework.cloud/spring-cloud-starter@4.1.2?type=jar', dependsOn: [ 'pkg:maven/org.springframework.boot/spring-boot-starter@3.3.0-RC1?type=jar', 'pkg:maven/org.springframework.cloud/spring-cloud-context@4.1.2?type=jar', 'pkg:maven/org.springframework.cloud/spring-cloud-commons@4.1.2?type=jar', 'pkg:maven/org.springframework.security/spring-security-rsa@1.1.2?type=jar', ], }, { ref: 'pkg:maven/org.springframework.cloud/spring-cloud-context@4.1.2?type=jar', dependsOn: [ 'pkg:maven/org.springframework.security/spring-security-crypto@6.3.0-RC1?type=jar', ], }, { ref: 'pkg:maven/org.springframework.cloud/spring-cloud-commons@4.1.2?type=jar', dependsOn: [ 'pkg:maven/org.springframework.security/spring-security-crypto@6.3.0-RC1?type=jar', ], }, { ref: 'pkg:maven/org.springframework.security/spring-security-rsa@1.1.2?type=jar', dependsOn: ['pkg:maven/org.bouncycastle/bcprov-jdk18on@1.77?type=jar'], }, { ref: 'pkg:maven/org.bouncycastle/bcprov-jdk18on@1.77?type=jar', dependsOn: [], }, { ref: 'pkg:maven/org.springframework.boot/spring-boot-starter-mail@3.3.0-RC1?type=jar', dependsOn: [ 'pkg:maven/org.springframework.boot/spring-boot-starter@3.3.0-RC1?type=jar', 'pkg:maven/org.springframework/spring-context-support@6.1.6?type=jar', 'pkg:maven/org.eclipse.angus/jakarta.mail@2.0.3?type=jar', ], }, { ref: 'pkg:maven/org.springframework/spring-context-support@6.1.6?type=jar', dependsOn: [ 'pkg:maven/org.springframework/spring-beans@6.1.6?type=jar', 'pkg:maven/org.springframework/spring-context@6.1.6?type=jar', 'pkg:maven/org.springframework/spring-core@6.1.6?type=jar', ], }, { ref: 'pkg:maven/org.eclipse.angus/jakarta.mail@2.0.3?type=jar', dependsOn: [ 'pkg:maven/jakarta.activation/jakarta.activation-api@2.1.3?type=jar', 'pkg:maven/org.eclipse.angus/angus-activation@2.0.2?type=jar', ], }, { ref: 'pkg:maven/jakarta.activation/jakarta.activation-api@2.1.3?type=jar', dependsOn: [], }, { ref: 'pkg:maven/org.eclipse.angus/angus-activation@2.0.2?type=jar', dependsOn: [ 'pkg:maven/jakarta.activation/jakarta.activation-api@2.1.3?type=jar', ], }, { ref: 'pkg:maven/de.codecentric/spring-boot-admin-starter-client@3.2.4-SNAPSHOT?type=jar', dependsOn: [], }, { ref: 'pkg:maven/org.springframework.session/spring-session-core@3.3.0-RC1?type=jar', dependsOn: ['pkg:maven/org.springframework/spring-jcl@6.1.6?type=jar'], }, { ref: 'pkg:maven/org.springframework.session/spring-session-jdbc@3.3.0-RC1?type=jar', dependsOn: [ 'pkg:maven/org.springframework.session/spring-session-core@3.3.0-RC1?type=jar', 'pkg:maven/org.springframework/spring-context@6.1.6?type=jar', 'pkg:maven/org.springframework/spring-jdbc@6.1.6?type=jar', ], }, { ref: 'pkg:maven/org.springframework/spring-jdbc@6.1.6?type=jar', dependsOn: [ 'pkg:maven/org.springframework/spring-beans@6.1.6?type=jar', 'pkg:maven/org.springframework/spring-core@6.1.6?type=jar', 'pkg:maven/org.springframework/spring-tx@6.1.6?type=jar', ], }, { ref: 'pkg:maven/org.springframework/spring-tx@6.1.6?type=jar', dependsOn: [ 'pkg:maven/org.springframework/spring-beans@6.1.6?type=jar', 'pkg:maven/org.springframework/spring-core@6.1.6?type=jar', ], }, { ref: 'pkg:maven/org.springframework.cloud/spring-cloud-starter-config@4.1.1?type=jar', dependsOn: [ 'pkg:maven/org.springframework.cloud/spring-cloud-starter@4.1.2?type=jar', 'pkg:maven/org.springframework.cloud/spring-cloud-config-client@4.1.1?type=jar', 'pkg:maven/com.fasterxml.jackson.core/jackson-databind@2.17.0?type=jar', ], }, { ref: 'pkg:maven/org.springframework.cloud/spring-cloud-config-client@4.1.1?type=jar', dependsOn: [ 'pkg:maven/org.springframework.boot/spring-boot-autoconfigure@3.3.0-RC1?type=jar', 'pkg:maven/org.springframework.cloud/spring-cloud-commons@4.1.2?type=jar', 'pkg:maven/org.springframework.cloud/spring-cloud-context@4.1.2?type=jar', 'pkg:maven/org.springframework/spring-web@6.1.6?type=jar', 'pkg:maven/com.fasterxml.jackson.core/jackson-annotations@2.17.0?type=jar', 'pkg:maven/com.fasterxml.jackson.core/jackson-databind@2.17.0?type=jar', 'pkg:maven/org.apache.httpcomponents.client5/httpclient5@5.3.1?type=jar', ], }, { ref: 'pkg:maven/org.apache.httpcomponents.client5/httpclient5@5.3.1?type=jar', dependsOn: [ 'pkg:maven/org.apache.httpcomponents.core5/httpcore5@5.2.4?type=jar', 'pkg:maven/org.apache.httpcomponents.core5/httpcore5-h2@5.2.4?type=jar', 'pkg:maven/org.slf4j/slf4j-api@2.0.13?type=jar', ], }, { ref: 'pkg:maven/org.apache.httpcomponents.core5/httpcore5@5.2.4?type=jar', dependsOn: [], }, { ref: 'pkg:maven/org.apache.httpcomponents.core5/httpcore5-h2@5.2.4?type=jar', dependsOn: [ 'pkg:maven/org.apache.httpcomponents.core5/httpcore5@5.2.4?type=jar', ], }, { ref: 'pkg:maven/org.hsqldb/hsqldb@2.7.2?type=jar', dependsOn: [], }, ], }; export const systemSbomResponse = applicationSbomResponse; ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/mocks/instance/dependencies/index.ts ================================================ import { HttpResponse, http } from 'msw'; import { applicationSbomResponse, sbomsResponse, systemSbomResponse, } from '@/mocks/instance/dependencies/data'; const dependenciesEndpoints = [ http.get('/instances/:instanceId/actuator/sbom', () => { return HttpResponse.json(sbomsResponse); }), http.get('/instances/:instanceId/actuator/sbom/application', () => { return HttpResponse.json(applicationSbomResponse); }), http.get('/instances/:instanceId/actuator/sbom/system', () => { return HttpResponse.json(systemSbomResponse); }), ]; export default dependenciesEndpoints; ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/mocks/instance/flyway/data.ts ================================================ export const flyway = { contexts: { application: { flywayBeans: { flyway: { migrations: [ { type: 'SQL', checksum: -156244537, version: '1', description: 'init', script: 'V1__init.sql', state: 'SUCCESS', installedBy: 'SA', installedOn: '2022-04-21T08:50:41.580Z', installedRank: 1, executionTime: 3, }, ], }, }, }, }, }; ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/mocks/instance/flyway/index.ts ================================================ import { HttpResponse, http } from 'msw'; import { flyway } from './data'; const flywayEndpoints = [ http.get('/instances/:instanceId/actuator/flyway', () => { return HttpResponse.json(flyway); }), ]; export default flywayEndpoints; ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/mocks/instance/health/index.ts ================================================ import { HttpResponse, http } from 'msw'; const healthEndpoint = [ http.get('/instances/:instanceId/actuator/health', () => { return HttpResponse.json({ status: 'UP', details: { clientConfigServer: { status: 'UNKNOWN', details: { error: 'no property sources located' }, }, db: { status: 'UP', details: { database: 'HSQL Database Engine', validationQuery: 'isValid()', }, }, discoveryComposite: { description: 'Discovery Client not initialized', status: 'UNKNOWN', details: { discoveryClient: { description: 'Discovery Client not initialized', status: 'UNKNOWN', }, }, }, diskSpace: { status: 'UP', details: { total: 994662584320, free: 300063879168, threshold: 10485760, exists: true, }, }, ping: { status: 'UP' }, reactiveDiscoveryClients: { description: 'Discovery Client not initialized', status: 'UNKNOWN', details: { 'Simple Reactive Discovery Client': { description: 'Discovery Client not initialized', status: 'UNKNOWN', }, }, }, refreshScope: { status: 'UP' }, }, }); }), ]; export default healthEndpoint; ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/mocks/instance/httptrace/data.ts ================================================ const now = new Date(); const today = [ now.getFullYear(), String(now.getMonth() + 1).padStart(2, '0'), String(now.getDate()).padStart(2, '0'), ].join('-'); export const httptraceresponse = { traces: [ { timestamp: today + 'T09:12:34.567Z', principal: 'admin', session: 'D43F6A32C1E34A7B', request: { method: 'GET', uri: 'http://localhost:8080/api/users', headers: { accept: ['application/json'], 'user-agent': [ 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36', ], authorization: ['Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...'], }, remoteAddress: '192.168.1.105', }, response: { status: 200, headers: { 'content-type': ['application/json'], 'content-length': ['1024'], 'cache-control': ['no-cache, no-store, max-age=0, must-revalidate'], }, timeTaken: 125, }, }, { timestamp: today + 'T09:13:45.678Z', principal: 'user123', session: 'E54G7B43D2F45B8C', request: { method: 'POST', uri: 'http://localhost:8080/api/orders', headers: { 'content-type': ['application/json'], 'user-agent': ['PostmanRuntime/7.29.2'], authorization: ['Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...'], }, remoteAddress: '192.168.1.110', }, response: { status: 201, headers: { 'content-type': ['application/json'], location: ['/api/orders/12345'], 'content-length': ['256'], }, timeTaken: 187, }, }, { timestamp: today + today + 'T09:14:56.789Z', principal: null, session: null, request: { method: 'GET', uri: 'http://localhost:8080/api/products', headers: { accept: ['application/json'], 'user-agent': ['curl/7.68.0'], }, remoteAddress: '192.168.1.115', }, response: { status: 200, headers: { 'content-type': ['application/json'], 'content-length': ['2048'], 'cache-control': ['max-age=3600'], }, timeTaken: 95, }, }, { timestamp: today + 'T09:15:23.456Z', principal: 'user456', session: 'F65H8C54E3G56D9E', request: { method: 'PUT', uri: 'http://localhost:8080/api/users/profile', headers: { 'content-type': ['application/json'], 'user-agent': ['Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)'], authorization: ['Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...'], }, remoteAddress: '192.168.1.120', }, response: { status: 200, headers: { 'content-type': ['application/json'], 'content-length': ['512'], }, timeTaken: 143, }, }, { timestamp: today + 'T09:16:34.567Z', principal: null, session: null, request: { method: 'GET', uri: 'http://localhost:8080/actuator/health', headers: { accept: ['application/json'], 'user-agent': ['Prometheus/2.40.0'], }, remoteAddress: '192.168.1.200', }, response: { status: 200, headers: { 'content-type': ['application/json'], 'content-length': ['15'], }, timeTaken: 12, }, }, { timestamp: today + 'T09:17:45.678Z', principal: 'admin', session: 'D43F6A32C1E34A7B', request: { method: 'DELETE', uri: 'http://localhost:8080/api/products/54321', headers: { 'user-agent': [ 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36', ], authorization: ['Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...'], }, remoteAddress: '192.168.1.105', }, response: { status: 204, headers: { 'cache-control': ['no-cache, no-store, max-age=0, must-revalidate'], }, timeTaken: 78, }, }, { timestamp: today + 'T09:18:56.789Z', principal: null, session: null, request: { method: 'OPTIONS', uri: 'http://localhost:8080/api/users', headers: { origin: ['https://example.com'], 'access-control-request-method': ['GET'], 'user-agent': [ 'Mozilla/5.0 (iPhone; CPU iPhone OS 15_0 like Mac OS X)', ], }, remoteAddress: '192.168.1.130', }, response: { status: 200, headers: { 'access-control-allow-origin': ['https://example.com'], 'access-control-allow-methods': ['GET, POST, PUT, DELETE'], 'access-control-allow-headers': ['Authorization, Content-Type'], 'access-control-max-age': ['3600'], }, timeTaken: 5, }, }, { timestamp: today + 'T09:19:23.456Z', principal: 'user789', session: 'G76I9D65F4H67E0F', request: { method: 'GET', uri: 'http://localhost:8080/api/orders/history?page=0&size=10', headers: { accept: ['application/json'], 'user-agent': ['Mozilla/5.0 (Android 12; Mobile)'], authorization: ['Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...'], }, remoteAddress: '192.168.1.140', }, response: { status: 200, headers: { 'content-type': ['application/json'], 'content-length': ['1536'], 'x-total-count': ['45'], }, timeTaken: 167, }, }, { timestamp: today + 'T09:20:34.567Z', principal: null, session: null, request: { method: 'GET', uri: 'http://localhost:8080/api/nonexistent', headers: { accept: ['application/json'], 'user-agent': ['curl/7.68.0'], }, remoteAddress: '192.168.1.150', }, response: { status: 404, headers: { 'content-type': ['application/json'], 'content-length': ['64'], }, timeTaken: 23, }, }, { timestamp: today + 'T09:21:45.678Z', principal: 'user123', session: 'E54G7B43D2F45B8C', request: { method: 'POST', uri: 'http://localhost:8080/api/checkout', headers: { 'content-type': ['application/json'], 'user-agent': ['PostmanRuntime/7.29.2'], authorization: ['Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...'], }, remoteAddress: '192.168.1.110', }, response: { status: 400, headers: { 'content-type': ['application/json'], 'content-length': ['128'], }, timeTaken: 45, }, }, ], }; ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/mocks/instance/httptrace/index.ts ================================================ import { HttpResponse, http } from 'msw'; import { httptraceresponse } from '@/mocks/instance/httptrace/data'; const endpoints = [ http.get('/instances/:instanceId/actuator/httptrace', () => { return HttpResponse.json(httptraceresponse); }), ]; export default endpoints; ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/mocks/instance/info/index.ts ================================================ import { HttpResponse, http } from 'msw'; const infoEndpoint = [ http.get('/instances/:instanceId/actuator/info', () => { return HttpResponse.json({}); }), ]; export default infoEndpoint; ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/mocks/instance/jolokia/data.read.ts ================================================ export const jolokiaRead = { request: { mbean: 'com.codecentric.boot.sample:name=stringMapManagedBean,type=StringMapManagedBean', type: 'read', }, value: { Test: 0, Size: 0 }, timestamp: 1673727499, status: 200, }; ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/mocks/instance/jolokia/data.ts ================================================ export const jolokiaList = { request: { type: 'list' }, value: { 'jdk.management.jfr': { 'type=FlightRecorder': { op: { getRecordingOptions: { args: [{ name: 'p0', type: 'long', desc: 'p0' }], ret: 'javax.management.openmbean.TabularData', desc: 'getRecordingOptions', }, takeSnapshot: { args: [], ret: 'long', desc: 'takeSnapshot' }, closeRecording: { args: [{ name: 'p0', type: 'long', desc: 'p0' }], ret: 'void', desc: 'closeRecording', }, newRecording: { args: [], ret: 'long', desc: 'newRecording' }, setRecordingSettings: { args: [ { name: 'p0', type: 'long', desc: 'p0' }, { name: 'p1', type: 'javax.management.openmbean.TabularData', desc: 'p1', }, ], ret: 'void', desc: 'setRecordingSettings', }, openStream: { args: [ { name: 'p0', type: 'long', desc: 'p0' }, { name: 'p1', type: 'javax.management.openmbean.TabularData', desc: 'p1', }, ], ret: 'long', desc: 'openStream', }, cloneRecording: { args: [ { name: 'p0', type: 'long', desc: 'p0' }, { name: 'p1', type: 'boolean', desc: 'p1', }, ], ret: 'long', desc: 'cloneRecording', }, setRecordingOptions: { args: [ { name: 'p0', type: 'long', desc: 'p0' }, { name: 'p1', type: 'javax.management.openmbean.TabularData', desc: 'p1', }, ], ret: 'void', desc: 'setRecordingOptions', }, copyTo: { args: [ { name: 'p0', type: 'long', desc: 'p0' }, { name: 'p1', type: 'java.lang.String', desc: 'p1', }, ], ret: 'void', desc: 'copyTo', }, startRecording: { args: [{ name: 'p0', type: 'long', desc: 'p0' }], ret: 'void', desc: 'startRecording', }, closeStream: { args: [{ name: 'p0', type: 'long', desc: 'p0' }], ret: 'void', desc: 'closeStream', }, getRecordingSettings: { args: [{ name: 'p0', type: 'long', desc: 'p0' }], ret: 'javax.management.openmbean.TabularData', desc: 'getRecordingSettings', }, setPredefinedConfiguration: { args: [ { name: 'p0', type: 'long', desc: 'p0' }, { name: 'p1', type: 'java.lang.String', desc: 'p1', }, ], ret: 'void', desc: 'setPredefinedConfiguration', }, readStream: { args: [{ name: 'p0', type: 'long', desc: 'p0' }], ret: '[B', desc: 'readStream', }, setConfiguration: { args: [ { name: 'p0', type: 'long', desc: 'p0' }, { name: 'p1', type: 'java.lang.String', desc: 'p1', }, ], ret: 'void', desc: 'setConfiguration', }, stopRecording: { args: [{ name: 'p0', type: 'long', desc: 'p0' }], ret: 'boolean', desc: 'stopRecording', }, }, attr: { EventTypes: { rw: false, type: '[Ljavax.management.openmbean.CompositeData;', desc: 'EventTypes', }, Recordings: { rw: false, type: '[Ljavax.management.openmbean.CompositeData;', desc: 'Recordings', }, Configurations: { rw: false, type: '[Ljavax.management.openmbean.CompositeData;', desc: 'Configurations', }, ObjectName: { rw: false, type: 'javax.management.ObjectName', desc: 'ObjectName', }, }, class: 'jdk.management.jfr.FlightRecorderMXBeanImpl', desc: 'Information on the management interface of the MBean', }, }, 'java.util.logging': { 'type=Logging': { op: { getLoggerLevel: { args: [ { name: 'p0', type: 'java.lang.String', desc: 'p0', }, ], ret: 'java.lang.String', desc: 'getLoggerLevel', }, getParentLoggerName: { args: [{ name: 'p0', type: 'java.lang.String', desc: 'p0' }], ret: 'java.lang.String', desc: 'getParentLoggerName', }, setLoggerLevel: { args: [ { name: 'p0', type: 'java.lang.String', desc: 'p0' }, { name: 'p1', type: 'java.lang.String', desc: 'p1', }, ], ret: 'void', desc: 'setLoggerLevel', }, }, attr: { LoggerNames: { rw: false, type: '[Ljava.lang.String;', desc: 'LoggerNames', }, ObjectName: { rw: false, type: 'javax.management.ObjectName', desc: 'ObjectName', }, }, class: 'sun.management.ManagementFactoryHelper$PlatformLoggingImpl', desc: 'Information on the management interface of the MBean', }, }, 'java.nio': { 'name=direct,type=BufferPool': { attr: { TotalCapacity: { rw: false, type: 'long', desc: 'TotalCapacity', }, MemoryUsed: { rw: false, type: 'long', desc: 'MemoryUsed' }, Count: { rw: false, type: 'long', desc: 'Count' }, Name: { rw: false, type: 'java.lang.String', desc: 'Name' }, ObjectName: { rw: false, type: 'javax.management.ObjectName', desc: 'ObjectName', }, }, class: 'sun.management.ManagementFactoryHelper$1', desc: 'Information on the management interface of the MBean', }, 'name=mapped,type=BufferPool': { attr: { TotalCapacity: { rw: false, type: 'long', desc: 'TotalCapacity', }, MemoryUsed: { rw: false, type: 'long', desc: 'MemoryUsed' }, Count: { rw: false, type: 'long', desc: 'Count' }, Name: { rw: false, type: 'java.lang.String', desc: 'Name' }, ObjectName: { rw: false, type: 'javax.management.ObjectName', desc: 'ObjectName', }, }, class: 'sun.management.ManagementFactoryHelper$1', desc: 'Information on the management interface of the MBean', }, "name=mapped - 'non-volatile memory',type=BufferPool": { attr: { TotalCapacity: { rw: false, type: 'long', desc: 'TotalCapacity', }, MemoryUsed: { rw: false, type: 'long', desc: 'MemoryUsed' }, Count: { rw: false, type: 'long', desc: 'Count' }, Name: { rw: false, type: 'java.lang.String', desc: 'Name' }, ObjectName: { rw: false, type: 'javax.management.ObjectName', desc: 'ObjectName', }, }, class: 'sun.management.ManagementFactoryHelper$1', desc: 'Information on the management interface of the MBean', }, }, DefaultDomain: { 'application=': { attr: { SnapshotAsJson: { rw: false, type: 'java.lang.String', desc: 'Attribute exposed for management', }, }, class: 'org.springframework.context.support.LiveBeansView', desc: 'Information on the management interface of the MBean', }, }, 'org.springframework.boot': { 'type=Endpoint,name=Scheduledtasks': { op: { scheduledTasks: { args: [], ret: 'java.util.Map', desc: 'Invoke scheduledTasks for endpoint scheduledtasks', }, }, class: 'org.springframework.boot.actuate.endpoint.jmx.EndpointMBean', desc: 'MBean operations for endpoint scheduledtasks', }, 'type=Endpoint,name=Loggers': { op: { configureLogLevel: { args: [ { name: 'name', type: 'java.lang.String', desc: null, }, { name: 'configuredLevel', type: 'java.lang.String', desc: null }, ], ret: 'java.util.Map', desc: 'Invoke configureLogLevel for endpoint loggers', }, loggers: { args: [], ret: 'java.util.Map', desc: 'Invoke loggers for endpoint loggers', }, loggerLevels: { args: [{ name: 'name', type: 'java.lang.String', desc: null }], ret: 'java.util.Map', desc: 'Invoke loggerLevels for endpoint loggers', }, }, class: 'org.springframework.boot.actuate.endpoint.jmx.EndpointMBean', desc: 'MBean operations for endpoint loggers', }, 'type=Endpoint,name=Mappings': { op: { mappings: { args: [], ret: 'java.util.Map', desc: 'Invoke mappings for endpoint mappings', }, }, class: 'org.springframework.boot.actuate.endpoint.jmx.EndpointMBean', desc: 'MBean operations for endpoint mappings', }, 'type=Endpoint,name=Features': { op: { features: { args: [], ret: 'java.util.Map', desc: 'Invoke features for endpoint features', }, }, class: 'org.springframework.boot.actuate.endpoint.jmx.EndpointMBean', desc: 'MBean operations for endpoint features', }, 'type=Endpoint,name=Info': { op: { info: { args: [], ret: 'java.util.Map', desc: 'Invoke info for endpoint info', }, }, class: 'org.springframework.boot.actuate.endpoint.jmx.EndpointMBean', desc: 'MBean operations for endpoint info', }, 'type=Endpoint,name=Env': { op: { environment: { args: [ { name: 'pattern', type: 'java.lang.String', desc: null, }, ], ret: 'java.util.Map', desc: 'Invoke environment for endpoint env', }, environmentEntry: { args: [{ name: 'toMatch', type: 'java.lang.String', desc: null }], ret: 'java.util.Map', desc: 'Invoke environmentEntry for endpoint env', }, }, class: 'org.springframework.boot.actuate.endpoint.jmx.EndpointMBean', desc: 'MBean operations for endpoint env', }, 'type=Endpoint,name=Caches': { op: { clearCaches: { args: [], ret: 'java.util.Map', desc: 'Invoke clearCaches for endpoint caches', }, cache: { args: [ { name: 'cache', type: 'java.lang.String', desc: null }, { name: 'cacheManager', type: 'java.lang.String', desc: null, }, ], ret: 'java.util.Map', desc: 'Invoke cache for endpoint caches', }, caches: { args: [], ret: 'java.util.Map', desc: 'Invoke caches for endpoint caches', }, clearCache: { args: [ { name: 'cache', type: 'java.lang.String', desc: null, }, { name: 'cacheManager', type: 'java.lang.String', desc: null }, ], ret: 'java.util.Map', desc: 'Invoke clearCache for endpoint caches', }, }, class: 'org.springframework.boot.actuate.endpoint.jmx.EndpointMBean', desc: 'MBean operations for endpoint caches', }, 'type=Endpoint,name=Beans': { op: { beans: { args: [], ret: 'java.util.Map', desc: 'Invoke beans for endpoint beans', }, }, class: 'org.springframework.boot.actuate.endpoint.jmx.EndpointMBean', desc: 'MBean operations for endpoint beans', }, 'type=Endpoint,name=Refresh': { op: { refresh: { args: [], ret: 'java.util.List', desc: 'Invoke refresh for endpoint refresh', }, }, class: 'org.springframework.boot.actuate.endpoint.jmx.EndpointMBean', desc: 'MBean operations for endpoint refresh', }, 'type=Endpoint,name=Flyway': { op: { flywayBeans: { args: [], ret: 'java.util.Map', desc: 'Invoke flywayBeans for endpoint flyway', }, }, class: 'org.springframework.boot.actuate.endpoint.jmx.EndpointMBean', desc: 'MBean operations for endpoint flyway', }, 'type=Endpoint,name=Threaddump': { op: { threadDump: { args: [], ret: 'java.util.Map', desc: 'Invoke threadDump for endpoint threaddump', }, textThreadDump: { args: [], ret: 'java.lang.String', desc: 'Invoke textThreadDump for endpoint threaddump', }, }, class: 'org.springframework.boot.actuate.endpoint.jmx.EndpointMBean', desc: 'MBean operations for endpoint threaddump', }, 'type=Endpoint,name=Metrics': { op: { metric: { args: [ { name: 'requiredMetricName', type: 'java.lang.String', desc: null, }, { name: 'tag', type: 'java.util.List', desc: null }, ], ret: 'java.util.Map', desc: 'Invoke metric for endpoint metrics', }, listNames: { args: [], ret: 'java.util.Map', desc: 'Invoke listNames for endpoint metrics', }, }, class: 'org.springframework.boot.actuate.endpoint.jmx.EndpointMBean', desc: 'MBean operations for endpoint metrics', }, 'type=Endpoint,name=Configprops': { op: { configurationPropertiesWithPrefix: { args: [ { name: 'prefix', type: 'java.lang.String', desc: null, }, ], ret: 'java.util.Map', desc: 'Invoke configurationPropertiesWithPrefix for endpoint configprops', }, configurationProperties: { args: [], ret: 'java.util.Map', desc: 'Invoke configurationProperties for endpoint configprops', }, }, class: 'org.springframework.boot.actuate.endpoint.jmx.EndpointMBean', desc: 'MBean operations for endpoint configprops', }, 'type=Endpoint,name=Startup': { op: { startup: { args: [], ret: 'java.util.Map', desc: 'Invoke startup for endpoint startup', }, startupSnapshot: { args: [], ret: 'java.util.Map', desc: 'Invoke startupSnapshot for endpoint startup', }, }, class: 'org.springframework.boot.actuate.endpoint.jmx.EndpointMBean', desc: 'MBean operations for endpoint startup', }, 'type=Admin,name=SpringApplication': { op: { getProperty: { args: [ { name: 'p0', type: 'java.lang.String', desc: 'p0', }, ], ret: 'java.lang.String', desc: 'getProperty', }, shutdown: { args: [], ret: 'void', desc: 'shutdown' }, }, attr: { Ready: { rw: false, type: 'boolean', desc: 'Ready' }, EmbeddedWebApplication: { rw: false, type: 'boolean', desc: 'EmbeddedWebApplication', }, }, class: 'org.springframework.boot.admin.SpringApplicationAdminMXBeanRegistrar$SpringApplicationAdmin', desc: 'Information on the management interface of the MBean', }, 'type=Endpoint,name=Liquibase': { op: { liquibaseBeans: { args: [], ret: 'java.util.Map', desc: 'Invoke liquibaseBeans for endpoint liquibase', }, }, class: 'org.springframework.boot.actuate.endpoint.jmx.EndpointMBean', desc: 'MBean operations for endpoint liquibase', }, 'type=Endpoint,name=Conditions': { op: { applicationConditionEvaluation: { args: [], ret: 'java.util.Map', desc: 'Invoke applicationConditionEvaluation for endpoint conditions', }, }, class: 'org.springframework.boot.actuate.endpoint.jmx.EndpointMBean', desc: 'MBean operations for endpoint conditions', }, 'type=Endpoint,name=Health': { op: { health: { args: [], ret: 'java.util.Map', desc: 'Invoke health for endpoint health', }, healthForPath: { args: [{ name: 'path', type: 'java.lang.Object', desc: null }], ret: 'java.util.Map', desc: 'Invoke healthForPath for endpoint health', }, }, class: 'org.springframework.boot.actuate.endpoint.jmx.EndpointMBean', desc: 'MBean operations for endpoint health', }, }, JMImplementation: { 'type=MBeanServerDelegate': { attr: { ImplementationName: { rw: false, type: 'java.lang.String', desc: 'The JMX implementation name (the name of this product)', }, MBeanServerId: { rw: false, type: 'java.lang.String', desc: 'The MBean server agent identification', }, ImplementationVersion: { rw: false, type: 'java.lang.String', desc: 'The JMX implementation version (the version of this product).', }, SpecificationVersion: { rw: false, type: 'java.lang.String', desc: 'The version of the JMX specification implemented by this product.', }, SpecificationVendor: { rw: false, type: 'java.lang.String', desc: 'The vendor of the JMX specification implemented by this product.', }, SpecificationName: { rw: false, type: 'java.lang.String', desc: 'The full name of the JMX specification implemented by this product.', }, ImplementationVendor: { rw: false, type: 'java.lang.String', desc: 'the JMX implementation vendor (the vendor of this product).', }, }, class: 'javax.management.MBeanServerDelegate', desc: 'Represents the MBean server from the management point of view.', }, }, 'java.lang': { 'name=G1 Survivor Space,type=MemoryPool': { op: { resetPeakUsage: { args: [], ret: 'void', desc: 'resetPeakUsage' }, }, attr: { Usage: { rw: false, type: 'javax.management.openmbean.CompositeData', desc: 'Usage', }, UsageThresholdCount: { rw: false, type: 'long', desc: 'UsageThresholdCount', }, MemoryManagerNames: { rw: false, type: '[Ljava.lang.String;', desc: 'MemoryManagerNames', }, UsageThresholdSupported: { rw: false, type: 'boolean', desc: 'UsageThresholdSupported', }, UsageThreshold: { rw: true, type: 'long', desc: 'UsageThreshold' }, CollectionUsageThresholdCount: { rw: false, type: 'long', desc: 'CollectionUsageThresholdCount', }, PeakUsage: { rw: false, type: 'javax.management.openmbean.CompositeData', desc: 'PeakUsage', }, UsageThresholdExceeded: { rw: false, type: 'boolean', desc: 'UsageThresholdExceeded', }, CollectionUsageThreshold: { rw: true, type: 'long', desc: 'CollectionUsageThreshold', }, Name: { rw: false, type: 'java.lang.String', desc: 'Name' }, ObjectName: { rw: false, type: 'javax.management.ObjectName', desc: 'ObjectName', }, Type: { rw: false, type: 'java.lang.String', desc: 'Type' }, CollectionUsageThresholdSupported: { rw: false, type: 'boolean', desc: 'CollectionUsageThresholdSupported', }, Valid: { rw: false, type: 'boolean', desc: 'Valid' }, CollectionUsage: { rw: false, type: 'javax.management.openmbean.CompositeData', desc: 'CollectionUsage', }, CollectionUsageThresholdExceeded: { rw: false, type: 'boolean', desc: 'CollectionUsageThresholdExceeded', }, }, class: 'sun.management.MemoryPoolImpl', desc: 'Information on the management interface of the MBean', }, 'type=Threading': { op: { getThreadCpuTime: [ { args: [{ name: 'p0', type: '[J', desc: 'p0' }], ret: '[J', desc: 'getThreadCpuTime', }, { args: [{ name: 'p0', type: 'long', desc: 'p0' }], ret: 'long', desc: 'getThreadCpuTime', }, ], getThreadInfo: [ { args: [{ name: 'p0', type: 'long', desc: 'p0' }], ret: 'javax.management.openmbean.CompositeData', desc: 'getThreadInfo', }, { args: [{ name: 'p0', type: '[J', desc: 'p0' }], ret: '[Ljavax.management.openmbean.CompositeData;', desc: 'getThreadInfo', }, { args: [ { name: 'p0', type: '[J', desc: 'p0' }, { name: 'p1', type: 'boolean', desc: 'p1', }, { name: 'p2', type: 'boolean', desc: 'p2' }, { name: 'p3', type: 'int', desc: 'p3' }, ], ret: '[Ljavax.management.openmbean.CompositeData;', desc: 'getThreadInfo', }, { args: [ { name: 'p0', type: '[J', desc: 'p0' }, { name: 'p1', type: 'boolean', desc: 'p1', }, { name: 'p2', type: 'boolean', desc: 'p2' }, ], ret: '[Ljavax.management.openmbean.CompositeData;', desc: 'getThreadInfo', }, { args: [ { name: 'p0', type: '[J', desc: 'p0' }, { name: 'p1', type: 'int', desc: 'p1' }, ], ret: '[Ljavax.management.openmbean.CompositeData;', desc: 'getThreadInfo', }, { args: [ { name: 'p0', type: 'long', desc: 'p0' }, { name: 'p1', type: 'int', desc: 'p1' }, ], ret: 'javax.management.openmbean.CompositeData', desc: 'getThreadInfo', }, ], findDeadlockedThreads: { args: [], ret: '[J', desc: 'findDeadlockedThreads', }, getThreadAllocatedBytes: [ { args: [{ name: 'p0', type: '[J', desc: 'p0' }], ret: '[J', desc: 'getThreadAllocatedBytes', }, { args: [{ name: 'p0', type: 'long', desc: 'p0' }], ret: 'long', desc: 'getThreadAllocatedBytes', }, ], getThreadUserTime: [ { args: [{ name: 'p0', type: '[J', desc: 'p0' }], ret: '[J', desc: 'getThreadUserTime', }, { args: [{ name: 'p0', type: 'long', desc: 'p0' }], ret: 'long', desc: 'getThreadUserTime', }, ], findMonitorDeadlockedThreads: { args: [], ret: '[J', desc: 'findMonitorDeadlockedThreads', }, resetPeakThreadCount: { args: [], ret: 'void', desc: 'resetPeakThreadCount', }, dumpAllThreads: [ { args: [ { name: 'p0', type: 'boolean', desc: 'p0' }, { name: 'p1', type: 'boolean', desc: 'p1', }, ], ret: '[Ljavax.management.openmbean.CompositeData;', desc: 'dumpAllThreads', }, { args: [ { name: 'p0', type: 'boolean', desc: 'p0' }, { name: 'p1', type: 'boolean', desc: 'p1', }, { name: 'p2', type: 'int', desc: 'p2' }, ], ret: '[Ljavax.management.openmbean.CompositeData;', desc: 'dumpAllThreads', }, ], }, attr: { ThreadAllocatedMemorySupported: { rw: false, type: 'boolean', desc: 'ThreadAllocatedMemorySupported', }, ThreadContentionMonitoringEnabled: { rw: true, type: 'boolean', desc: 'ThreadContentionMonitoringEnabled', }, CurrentThreadAllocatedBytes: { rw: false, type: 'long', desc: 'CurrentThreadAllocatedBytes', }, TotalStartedThreadCount: { rw: false, type: 'long', desc: 'TotalStartedThreadCount', }, CurrentThreadCpuTimeSupported: { rw: false, type: 'boolean', desc: 'CurrentThreadCpuTimeSupported', }, CurrentThreadUserTime: { rw: false, type: 'long', desc: 'CurrentThreadUserTime', }, PeakThreadCount: { rw: false, type: 'int', desc: 'PeakThreadCount' }, AllThreadIds: { rw: false, type: '[J', desc: 'AllThreadIds' }, ThreadAllocatedMemoryEnabled: { rw: true, type: 'boolean', desc: 'ThreadAllocatedMemoryEnabled', }, CurrentThreadCpuTime: { rw: false, type: 'long', desc: 'CurrentThreadCpuTime', }, ObjectName: { rw: false, type: 'javax.management.ObjectName', desc: 'ObjectName', }, ThreadContentionMonitoringSupported: { rw: false, type: 'boolean', desc: 'ThreadContentionMonitoringSupported', }, ThreadCpuTimeSupported: { rw: false, type: 'boolean', desc: 'ThreadCpuTimeSupported', }, ThreadCount: { rw: false, type: 'int', desc: 'ThreadCount' }, ThreadCpuTimeEnabled: { rw: true, type: 'boolean', desc: 'ThreadCpuTimeEnabled', }, ObjectMonitorUsageSupported: { rw: false, type: 'boolean', desc: 'ObjectMonitorUsageSupported', }, SynchronizerUsageSupported: { rw: false, type: 'boolean', desc: 'SynchronizerUsageSupported', }, DaemonThreadCount: { rw: false, type: 'int', desc: 'DaemonThreadCount', }, }, class: 'com.sun.management.internal.HotSpotThreadImpl', desc: 'Information on the management interface of the MBean', }, 'name=CodeCache,type=MemoryPool': { op: { resetPeakUsage: { args: [], ret: 'void', desc: 'resetPeakUsage' }, }, attr: { Usage: { rw: false, type: 'javax.management.openmbean.CompositeData', desc: 'Usage', }, UsageThresholdCount: { rw: false, type: 'long', desc: 'UsageThresholdCount', }, MemoryManagerNames: { rw: false, type: '[Ljava.lang.String;', desc: 'MemoryManagerNames', }, UsageThresholdSupported: { rw: false, type: 'boolean', desc: 'UsageThresholdSupported', }, UsageThreshold: { rw: true, type: 'long', desc: 'UsageThreshold' }, CollectionUsageThresholdCount: { rw: false, type: 'long', desc: 'CollectionUsageThresholdCount', }, PeakUsage: { rw: false, type: 'javax.management.openmbean.CompositeData', desc: 'PeakUsage', }, UsageThresholdExceeded: { rw: false, type: 'boolean', desc: 'UsageThresholdExceeded', }, CollectionUsageThreshold: { rw: true, type: 'long', desc: 'CollectionUsageThreshold', }, Name: { rw: false, type: 'java.lang.String', desc: 'Name' }, ObjectName: { rw: false, type: 'javax.management.ObjectName', desc: 'ObjectName', }, Type: { rw: false, type: 'java.lang.String', desc: 'Type' }, CollectionUsageThresholdSupported: { rw: false, type: 'boolean', desc: 'CollectionUsageThresholdSupported', }, Valid: { rw: false, type: 'boolean', desc: 'Valid' }, CollectionUsage: { rw: false, type: 'javax.management.openmbean.CompositeData', desc: 'CollectionUsage', }, CollectionUsageThresholdExceeded: { rw: false, type: 'boolean', desc: 'CollectionUsageThresholdExceeded', }, }, class: 'sun.management.MemoryPoolImpl', desc: 'Information on the management interface of the MBean', }, 'type=Memory': { op: { gc: { args: [], ret: 'void', desc: 'gc' } }, attr: { ObjectPendingFinalizationCount: { rw: false, type: 'int', desc: 'ObjectPendingFinalizationCount', }, Verbose: { rw: true, type: 'boolean', desc: 'Verbose' }, HeapMemoryUsage: { rw: false, type: 'javax.management.openmbean.CompositeData', desc: 'HeapMemoryUsage', }, NonHeapMemoryUsage: { rw: false, type: 'javax.management.openmbean.CompositeData', desc: 'NonHeapMemoryUsage', }, ObjectName: { rw: false, type: 'javax.management.ObjectName', desc: 'ObjectName', }, }, class: 'sun.management.MemoryImpl', desc: 'Information on the management interface of the MBean', }, 'name=Metaspace,type=MemoryPool': { op: { resetPeakUsage: { args: [], ret: 'void', desc: 'resetPeakUsage' }, }, attr: { Usage: { rw: false, type: 'javax.management.openmbean.CompositeData', desc: 'Usage', }, UsageThresholdCount: { rw: false, type: 'long', desc: 'UsageThresholdCount', }, MemoryManagerNames: { rw: false, type: '[Ljava.lang.String;', desc: 'MemoryManagerNames', }, UsageThresholdSupported: { rw: false, type: 'boolean', desc: 'UsageThresholdSupported', }, UsageThreshold: { rw: true, type: 'long', desc: 'UsageThreshold' }, CollectionUsageThresholdCount: { rw: false, type: 'long', desc: 'CollectionUsageThresholdCount', }, PeakUsage: { rw: false, type: 'javax.management.openmbean.CompositeData', desc: 'PeakUsage', }, UsageThresholdExceeded: { rw: false, type: 'boolean', desc: 'UsageThresholdExceeded', }, CollectionUsageThreshold: { rw: true, type: 'long', desc: 'CollectionUsageThreshold', }, Name: { rw: false, type: 'java.lang.String', desc: 'Name' }, ObjectName: { rw: false, type: 'javax.management.ObjectName', desc: 'ObjectName', }, Type: { rw: false, type: 'java.lang.String', desc: 'Type' }, CollectionUsageThresholdSupported: { rw: false, type: 'boolean', desc: 'CollectionUsageThresholdSupported', }, Valid: { rw: false, type: 'boolean', desc: 'Valid' }, CollectionUsage: { rw: false, type: 'javax.management.openmbean.CompositeData', desc: 'CollectionUsage', }, CollectionUsageThresholdExceeded: { rw: false, type: 'boolean', desc: 'CollectionUsageThresholdExceeded', }, }, class: 'sun.management.MemoryPoolImpl', desc: 'Information on the management interface of the MBean', }, 'name=G1 Eden Space,type=MemoryPool': { op: { resetPeakUsage: { args: [], ret: 'void', desc: 'resetPeakUsage' }, }, attr: { Usage: { rw: false, type: 'javax.management.openmbean.CompositeData', desc: 'Usage', }, UsageThresholdCount: { rw: false, type: 'long', desc: 'UsageThresholdCount', }, MemoryManagerNames: { rw: false, type: '[Ljava.lang.String;', desc: 'MemoryManagerNames', }, UsageThresholdSupported: { rw: false, type: 'boolean', desc: 'UsageThresholdSupported', }, UsageThreshold: { rw: true, type: 'long', desc: 'UsageThreshold' }, CollectionUsageThresholdCount: { rw: false, type: 'long', desc: 'CollectionUsageThresholdCount', }, PeakUsage: { rw: false, type: 'javax.management.openmbean.CompositeData', desc: 'PeakUsage', }, UsageThresholdExceeded: { rw: false, type: 'boolean', desc: 'UsageThresholdExceeded', }, CollectionUsageThreshold: { rw: true, type: 'long', desc: 'CollectionUsageThreshold', }, Name: { rw: false, type: 'java.lang.String', desc: 'Name' }, ObjectName: { rw: false, type: 'javax.management.ObjectName', desc: 'ObjectName', }, Type: { rw: false, type: 'java.lang.String', desc: 'Type' }, CollectionUsageThresholdSupported: { rw: false, type: 'boolean', desc: 'CollectionUsageThresholdSupported', }, Valid: { rw: false, type: 'boolean', desc: 'Valid' }, CollectionUsage: { rw: false, type: 'javax.management.openmbean.CompositeData', desc: 'CollectionUsage', }, CollectionUsageThresholdExceeded: { rw: false, type: 'boolean', desc: 'CollectionUsageThresholdExceeded', }, }, class: 'sun.management.MemoryPoolImpl', desc: 'Information on the management interface of the MBean', }, 'type=OperatingSystem': { attr: { OpenFileDescriptorCount: { rw: false, type: 'long', desc: 'OpenFileDescriptorCount', }, CommittedVirtualMemorySize: { rw: false, type: 'long', desc: 'CommittedVirtualMemorySize', }, FreePhysicalMemorySize: { rw: false, type: 'long', desc: 'FreePhysicalMemorySize', }, SystemLoadAverage: { rw: false, type: 'double', desc: 'SystemLoadAverage', }, Arch: { rw: false, type: 'java.lang.String', desc: 'Arch' }, ProcessCpuLoad: { rw: false, type: 'double', desc: 'ProcessCpuLoad' }, FreeSwapSpaceSize: { rw: false, type: 'long', desc: 'FreeSwapSpaceSize', }, TotalPhysicalMemorySize: { rw: false, type: 'long', desc: 'TotalPhysicalMemorySize', }, Name: { rw: false, type: 'java.lang.String', desc: 'Name' }, ObjectName: { rw: false, type: 'javax.management.ObjectName', desc: 'ObjectName', }, TotalSwapSpaceSize: { rw: false, type: 'long', desc: 'TotalSwapSpaceSize', }, TotalMemorySize: { rw: false, type: 'long', desc: 'TotalMemorySize' }, ProcessCpuTime: { rw: false, type: 'long', desc: 'ProcessCpuTime' }, MaxFileDescriptorCount: { rw: false, type: 'long', desc: 'MaxFileDescriptorCount', }, SystemCpuLoad: { rw: false, type: 'double', desc: 'SystemCpuLoad' }, Version: { rw: false, type: 'java.lang.String', desc: 'Version' }, AvailableProcessors: { rw: false, type: 'int', desc: 'AvailableProcessors', }, CpuLoad: { rw: false, type: 'double', desc: 'CpuLoad' }, FreeMemorySize: { rw: false, type: 'long', desc: 'FreeMemorySize' }, }, class: 'com.sun.management.internal.OperatingSystemImpl', desc: 'Information on the management interface of the MBean', }, 'name=CodeCacheManager,type=MemoryManager': { attr: { MemoryPoolNames: { rw: false, type: '[Ljava.lang.String;', desc: 'MemoryPoolNames', }, Valid: { rw: false, type: 'boolean', desc: 'Valid' }, Name: { rw: false, type: 'java.lang.String', desc: 'Name' }, ObjectName: { rw: false, type: 'javax.management.ObjectName', desc: 'ObjectName', }, }, class: 'sun.management.MemoryManagerImpl', desc: 'Information on the management interface of the MBean', }, 'name=G1 Old Gen,type=MemoryPool': { op: { resetPeakUsage: { args: [], ret: 'void', desc: 'resetPeakUsage' }, }, attr: { Usage: { rw: false, type: 'javax.management.openmbean.CompositeData', desc: 'Usage', }, UsageThresholdCount: { rw: false, type: 'long', desc: 'UsageThresholdCount', }, MemoryManagerNames: { rw: false, type: '[Ljava.lang.String;', desc: 'MemoryManagerNames', }, UsageThresholdSupported: { rw: false, type: 'boolean', desc: 'UsageThresholdSupported', }, UsageThreshold: { rw: true, type: 'long', desc: 'UsageThreshold' }, CollectionUsageThresholdCount: { rw: false, type: 'long', desc: 'CollectionUsageThresholdCount', }, PeakUsage: { rw: false, type: 'javax.management.openmbean.CompositeData', desc: 'PeakUsage', }, UsageThresholdExceeded: { rw: false, type: 'boolean', desc: 'UsageThresholdExceeded', }, CollectionUsageThreshold: { rw: true, type: 'long', desc: 'CollectionUsageThreshold', }, Name: { rw: false, type: 'java.lang.String', desc: 'Name' }, ObjectName: { rw: false, type: 'javax.management.ObjectName', desc: 'ObjectName', }, Type: { rw: false, type: 'java.lang.String', desc: 'Type' }, CollectionUsageThresholdSupported: { rw: false, type: 'boolean', desc: 'CollectionUsageThresholdSupported', }, Valid: { rw: false, type: 'boolean', desc: 'Valid' }, CollectionUsage: { rw: false, type: 'javax.management.openmbean.CompositeData', desc: 'CollectionUsage', }, CollectionUsageThresholdExceeded: { rw: false, type: 'boolean', desc: 'CollectionUsageThresholdExceeded', }, }, class: 'sun.management.MemoryPoolImpl', desc: 'Information on the management interface of the MBean', }, 'name=Compressed Class Space,type=MemoryPool': { op: { resetPeakUsage: { args: [], ret: 'void', desc: 'resetPeakUsage' }, }, attr: { Usage: { rw: false, type: 'javax.management.openmbean.CompositeData', desc: 'Usage', }, UsageThresholdCount: { rw: false, type: 'long', desc: 'UsageThresholdCount', }, MemoryManagerNames: { rw: false, type: '[Ljava.lang.String;', desc: 'MemoryManagerNames', }, UsageThresholdSupported: { rw: false, type: 'boolean', desc: 'UsageThresholdSupported', }, UsageThreshold: { rw: true, type: 'long', desc: 'UsageThreshold' }, CollectionUsageThresholdCount: { rw: false, type: 'long', desc: 'CollectionUsageThresholdCount', }, PeakUsage: { rw: false, type: 'javax.management.openmbean.CompositeData', desc: 'PeakUsage', }, UsageThresholdExceeded: { rw: false, type: 'boolean', desc: 'UsageThresholdExceeded', }, CollectionUsageThreshold: { rw: true, type: 'long', desc: 'CollectionUsageThreshold', }, Name: { rw: false, type: 'java.lang.String', desc: 'Name' }, ObjectName: { rw: false, type: 'javax.management.ObjectName', desc: 'ObjectName', }, Type: { rw: false, type: 'java.lang.String', desc: 'Type' }, CollectionUsageThresholdSupported: { rw: false, type: 'boolean', desc: 'CollectionUsageThresholdSupported', }, Valid: { rw: false, type: 'boolean', desc: 'Valid' }, CollectionUsage: { rw: false, type: 'javax.management.openmbean.CompositeData', desc: 'CollectionUsage', }, CollectionUsageThresholdExceeded: { rw: false, type: 'boolean', desc: 'CollectionUsageThresholdExceeded', }, }, class: 'sun.management.MemoryPoolImpl', desc: 'Information on the management interface of the MBean', }, 'name=G1 Old Generation,type=GarbageCollector': { attr: { MemoryPoolNames: { rw: false, type: '[Ljava.lang.String;', desc: 'MemoryPoolNames', }, LastGcInfo: { rw: false, type: 'javax.management.openmbean.CompositeData', desc: 'LastGcInfo', }, CollectionTime: { rw: false, type: 'long', desc: 'CollectionTime' }, Valid: { rw: false, type: 'boolean', desc: 'Valid' }, CollectionCount: { rw: false, type: 'long', desc: 'CollectionCount' }, Name: { rw: false, type: 'java.lang.String', desc: 'Name' }, ObjectName: { rw: false, type: 'javax.management.ObjectName', desc: 'ObjectName', }, }, class: 'com.sun.management.internal.GarbageCollectorExtImpl', desc: 'Information on the management interface of the MBean', }, 'type=ClassLoading': { attr: { UnloadedClassCount: { rw: false, type: 'long', desc: 'UnloadedClassCount', }, LoadedClassCount: { rw: false, type: 'int', desc: 'LoadedClassCount', }, Verbose: { rw: true, type: 'boolean', desc: 'Verbose' }, TotalLoadedClassCount: { rw: false, type: 'long', desc: 'TotalLoadedClassCount', }, ObjectName: { rw: false, type: 'javax.management.ObjectName', desc: 'ObjectName', }, }, class: 'sun.management.ClassLoadingImpl', desc: 'Information on the management interface of the MBean', }, 'name=G1 Young Generation,type=GarbageCollector': { attr: { MemoryPoolNames: { rw: false, type: '[Ljava.lang.String;', desc: 'MemoryPoolNames', }, LastGcInfo: { rw: false, type: 'javax.management.openmbean.CompositeData', desc: 'LastGcInfo', }, CollectionTime: { rw: false, type: 'long', desc: 'CollectionTime' }, Valid: { rw: false, type: 'boolean', desc: 'Valid' }, CollectionCount: { rw: false, type: 'long', desc: 'CollectionCount' }, Name: { rw: false, type: 'java.lang.String', desc: 'Name' }, ObjectName: { rw: false, type: 'javax.management.ObjectName', desc: 'ObjectName', }, }, class: 'com.sun.management.internal.GarbageCollectorExtImpl', desc: 'Information on the management interface of the MBean', }, 'type=Compilation': { attr: { CompilationTimeMonitoringSupported: { rw: false, type: 'boolean', desc: 'CompilationTimeMonitoringSupported', }, TotalCompilationTime: { rw: false, type: 'long', desc: 'TotalCompilationTime', }, Name: { rw: false, type: 'java.lang.String', desc: 'Name' }, ObjectName: { rw: false, type: 'javax.management.ObjectName', desc: 'ObjectName', }, }, class: 'sun.management.CompilationImpl', desc: 'Information on the management interface of the MBean', }, 'name=Metaspace Manager,type=MemoryManager': { attr: { MemoryPoolNames: { rw: false, type: '[Ljava.lang.String;', desc: 'MemoryPoolNames', }, Valid: { rw: false, type: 'boolean', desc: 'Valid' }, Name: { rw: false, type: 'java.lang.String', desc: 'Name' }, ObjectName: { rw: false, type: 'javax.management.ObjectName', desc: 'ObjectName', }, }, class: 'sun.management.MemoryManagerImpl', desc: 'Information on the management interface of the MBean', }, 'type=Runtime': { attr: { SpecVendor: { rw: false, type: 'java.lang.String', desc: 'SpecVendor', }, ClassPath: { rw: false, type: 'java.lang.String', desc: 'ClassPath' }, InputArguments: { rw: false, type: '[Ljava.lang.String;', desc: 'InputArguments', }, Uptime: { rw: false, type: 'long', desc: 'Uptime' }, VmName: { rw: false, type: 'java.lang.String', desc: 'VmName' }, StartTime: { rw: false, type: 'long', desc: 'StartTime' }, VmVersion: { rw: false, type: 'java.lang.String', desc: 'VmVersion' }, SpecName: { rw: false, type: 'java.lang.String', desc: 'SpecName' }, Pid: { rw: false, type: 'long', desc: 'Pid' }, ManagementSpecVersion: { rw: false, type: 'java.lang.String', desc: 'ManagementSpecVersion', }, Name: { rw: false, type: 'java.lang.String', desc: 'Name' }, ObjectName: { rw: false, type: 'javax.management.ObjectName', desc: 'ObjectName', }, VmVendor: { rw: false, type: 'java.lang.String', desc: 'VmVendor' }, LibraryPath: { rw: false, type: 'java.lang.String', desc: 'LibraryPath', }, BootClassPath: { rw: false, type: 'java.lang.String', desc: 'BootClassPath', }, SpecVersion: { rw: false, type: 'java.lang.String', desc: 'SpecVersion', }, SystemProperties: { rw: false, type: 'javax.management.openmbean.TabularData', desc: 'SystemProperties', }, BootClassPathSupported: { rw: false, type: 'boolean', desc: 'BootClassPathSupported', }, }, class: 'sun.management.RuntimeImpl', desc: 'Information on the management interface of the MBean', }, }, 'org.springframework.cloud.context.properties': { 'name=configurationPropertiesRebinder,type=ConfigurationPropertiesRebinder': { op: { getNeverRefreshable: { args: [], ret: 'java.util.Set', desc: 'getNeverRefreshable', }, getBeanNames: { args: [], ret: 'java.util.Set', desc: 'getBeanNames', }, rebind: [ { args: [ { name: 'name', type: 'java.lang.String', desc: 'name' }, ], ret: 'boolean', desc: 'rebind', }, { args: [], ret: 'void', desc: 'rebind' }, ], }, attr: { NeverRefreshable: { rw: false, type: 'java.util.Set', desc: 'neverRefreshable', }, BeanNames: { rw: false, type: 'java.util.Set', desc: 'beanNames' }, }, class: 'org.springframework.cloud.context.properties.ConfigurationPropertiesRebinder', desc: '', }, }, 'org.springframework.cloud.context.scope.refresh': { 'name=refreshScope,type=RefreshScope': { op: { refreshAll: { args: [], ret: 'void', desc: 'Dispose of the current instance of all beans in this scope and force a refresh on next method execution.', }, refresh: { args: [{ name: 'name', type: 'java.lang.String', desc: 'name' }], ret: 'boolean', desc: 'Dispose of the current instance of bean name provided and force a refresh on next method execution.', }, }, class: 'org.springframework.cloud.context.scope.refresh.RefreshScope', desc: '', }, }, 'com.sun.management': { 'type=HotSpotDiagnostic': { op: { setVMOption: { args: [ { name: 'p0', type: 'java.lang.String', desc: 'p0', }, { name: 'p1', type: 'java.lang.String', desc: 'p1' }, ], ret: 'void', desc: 'setVMOption', }, getVMOption: { args: [{ name: 'p0', type: 'java.lang.String', desc: 'p0' }], ret: 'javax.management.openmbean.CompositeData', desc: 'getVMOption', }, dumpHeap: { args: [ { name: 'p0', type: 'java.lang.String', desc: 'p0' }, { name: 'p1', type: 'boolean', desc: 'p1', }, ], ret: 'void', desc: 'dumpHeap', }, }, attr: { DiagnosticOptions: { rw: false, type: '[Ljavax.management.openmbean.CompositeData;', desc: 'DiagnosticOptions', }, ObjectName: { rw: false, type: 'javax.management.ObjectName', desc: 'ObjectName', }, }, class: 'com.sun.management.internal.HotSpotDiagnostic', desc: 'Information on the management interface of the MBean', }, 'type=DiagnosticCommand': { op: { vmUptime: { args: [ { name: 'arguments', type: '[Ljava.lang.String;', desc: 'Array of Diagnostic Commands Arguments and Options', }, ], ret: 'java.lang.String', desc: 'Print VM uptime.', }, jfrDump: { args: [ { name: 'arguments', type: '[Ljava.lang.String;', desc: 'Array of Diagnostic Commands Arguments and Options', }, ], ret: 'java.lang.String', desc: 'Copies contents of a JFR recording to file. Either the name or the recording id must be specified.', }, jfrStart: { args: [ { name: 'arguments', type: '[Ljava.lang.String;', desc: 'Array of Diagnostic Commands Arguments and Options', }, ], ret: 'java.lang.String', desc: 'Starts a new JFR recording', }, threadPrint: { args: [ { name: 'arguments', type: '[Ljava.lang.String;', desc: 'Array of Diagnostic Commands Arguments and Options', }, ], ret: 'java.lang.String', desc: 'Print all threads with stacktraces.', }, jfrStop: { args: [ { name: 'arguments', type: '[Ljava.lang.String;', desc: 'Array of Diagnostic Commands Arguments and Options', }, ], ret: 'java.lang.String', desc: 'Stops a JFR recording', }, vmCds: { args: [ { name: 'arguments', type: '[Ljava.lang.String;', desc: 'Array of Diagnostic Commands Arguments and Options', }, ], ret: 'java.lang.String', desc: 'Dump a static or dynamic shared archive including all shareable classes', }, compilerCodelist: { args: [], ret: 'java.lang.String', desc: 'Print all compiled methods in code cache that are alive', }, vmEvents: { args: [ { name: 'arguments', type: '[Ljava.lang.String;', desc: 'Array of Diagnostic Commands Arguments and Options', }, ], ret: 'java.lang.String', desc: 'Print VM event logs', }, jfrCheck: { args: [ { name: 'arguments', type: '[Ljava.lang.String;', desc: 'Array of Diagnostic Commands Arguments and Options', }, ], ret: 'java.lang.String', desc: 'Checks running JFR recording(s)', }, vmSymboltable: { args: [ { name: 'arguments', type: '[Ljava.lang.String;', desc: 'Array of Diagnostic Commands Arguments and Options', }, ], ret: 'java.lang.String', desc: 'Dump symbol table.', }, gcRun: { args: [], ret: 'java.lang.String', desc: 'Call java.lang.System.gc().', }, vmClassloaders: { args: [ { name: 'arguments', type: '[Ljava.lang.String;', desc: 'Array of Diagnostic Commands Arguments and Options', }, ], ret: 'java.lang.String', desc: 'Prints classloader hierarchy.', }, vmMetaspace: { args: [ { name: 'arguments', type: '[Ljava.lang.String;', desc: 'Array of Diagnostic Commands Arguments and Options', }, ], ret: 'java.lang.String', desc: 'Prints the statistics for the metaspace', }, compilerDirectivesPrint: { args: [], ret: 'java.lang.String', desc: 'Print all active compiler directives.', }, vmSetFlag: { args: [ { name: 'arguments', type: '[Ljava.lang.String;', desc: 'Array of Diagnostic Commands Arguments and Options', }, ], ret: 'java.lang.String', desc: 'Sets VM flag option using the provided value.', }, compilerDirectivesAdd: { args: [ { name: 'arguments', type: '[Ljava.lang.String;', desc: 'Array of Diagnostic Commands Arguments and Options', }, ], ret: 'java.lang.String', desc: 'Add compiler directives from file.', }, vmDynlibs: { args: [], ret: 'java.lang.String', desc: 'Print loaded dynamic libraries.', }, vmPrintTouchedMethods: { args: [], ret: 'java.lang.String', desc: 'Print all methods that have ever been touched during the lifetime of this JVM.', }, compilerCodecache: { args: [], ret: 'java.lang.String', desc: 'Print code cache layout and bounds.', }, vmNativeMemory: { args: [ { name: 'arguments', type: '[Ljava.lang.String;', desc: 'Array of Diagnostic Commands Arguments and Options', }, ], ret: 'java.lang.String', desc: 'Print native memory usage', }, gcClassHistogram: { args: [ { name: 'arguments', type: '[Ljava.lang.String;', desc: 'Array of Diagnostic Commands Arguments and Options', }, ], ret: 'java.lang.String', desc: 'Provide statistics about the Java heap usage.', }, gcRunFinalization: { args: [], ret: 'java.lang.String', desc: 'Call java.lang.System.runFinalization().', }, jvmtiDataDump: { args: [], ret: 'java.lang.String', desc: 'Signal the JVM to do a data-dump request for JVMTI.', }, gcFinalizerInfo: { args: [], ret: 'java.lang.String', desc: 'Provide information about Java finalization queue.', }, vmStringtable: { args: [ { name: 'arguments', type: '[Ljava.lang.String;', desc: 'Array of Diagnostic Commands Arguments and Options', }, ], ret: 'java.lang.String', desc: 'Dump string table.', }, help: { args: [ { name: 'arguments', type: '[Ljava.lang.String;', desc: 'Array of Diagnostic Commands Arguments and Options', }, ], ret: 'java.lang.String', desc: "For more information about a specific command use 'help '. With no argument this will show a list of available commands. 'help all' will show help for all commands.", }, jfrConfigure: { args: [ { name: 'arguments', type: '[Ljava.lang.String;', desc: 'Array of Diagnostic Commands Arguments and Options', }, ], ret: 'java.lang.String', desc: 'Configure JFR', }, vmSystemProperties: { args: [], ret: 'java.lang.String', desc: 'Print system properties.', }, compilerDirectivesClear: { args: [], ret: 'java.lang.String', desc: 'Remove all compiler directives.', }, vmSystemdictionary: { args: [ { name: 'arguments', type: '[Ljava.lang.String;', desc: 'Array of Diagnostic Commands Arguments and Options', }, ], ret: 'java.lang.String', desc: 'Prints the statistics for dictionary hashtable sizes and bucket length', }, vmClassloaderStats: { args: [], ret: 'java.lang.String', desc: 'Print statistics about all ClassLoaders.', }, compilerDirectivesRemove: { args: [], ret: 'java.lang.String', desc: 'Remove latest added compiler directive.', }, gcHeapInfo: { args: [], ret: 'java.lang.String', desc: 'Provide generic Java heap information.', }, compilerCodeHeapAnalytics: { args: [ { name: 'arguments', type: '[Ljava.lang.String;', desc: 'Array of Diagnostic Commands Arguments and Options', }, ], ret: 'java.lang.String', desc: 'Print CodeHeap analytics', }, vmVersion: { args: [], ret: 'java.lang.String', desc: 'Print JVM version information.', }, vmInfo: { args: [], ret: 'java.lang.String', desc: 'Print information about JVM environment and status.', }, compilerQueue: { args: [], ret: 'java.lang.String', desc: 'Print methods queued for compilation.', }, vmFlags: { args: [ { name: 'arguments', type: '[Ljava.lang.String;', desc: 'Array of Diagnostic Commands Arguments and Options', }, ], ret: 'java.lang.String', desc: 'Print VM flag options and their current values.', }, vmLog: { args: [ { name: 'arguments', type: '[Ljava.lang.String;', desc: 'Array of Diagnostic Commands Arguments and Options', }, ], ret: 'java.lang.String', desc: 'Lists current log configuration, enables/disables/configures a log output, or rotates all logs.', }, jvmtiAgentLoad: { args: [ { name: 'arguments', type: '[Ljava.lang.String;', desc: 'Array of Diagnostic Commands Arguments and Options', }, ], ret: 'java.lang.String', desc: 'Load JVMTI native agent.', }, vmClassHierarchy: { args: [ { name: 'arguments', type: '[Ljava.lang.String;', desc: 'Array of Diagnostic Commands Arguments and Options', }, ], ret: 'java.lang.String', desc: 'Print a list of all loaded classes, indented to show the class hiearchy. The name of each class is followed by the ClassLoaderData* of its ClassLoader, or "null" if loaded by the bootstrap class loader.', }, vmCommandLine: { args: [], ret: 'java.lang.String', desc: 'Print the command line used to start this VM instance.', }, }, class: 'com.sun.management.internal.DiagnosticCommandImpl', desc: 'Diagnostic Commands', }, }, jmx4perl: { 'type=Config': { op: { setHistoryEntriesForOperation: { args: [ { name: 'p1', type: 'java.lang.String', desc: '', }, { name: 'p2', type: 'java.lang.String', desc: '' }, { name: 'p3', type: 'java.lang.String', desc: '', }, { name: 'p4', type: 'int', desc: '' }, ], ret: 'void', desc: 'Operation exposed for management', }, setHistoryLimitForOperation: { args: [ { name: 'p1', type: 'java.lang.String', desc: '', }, { name: 'p2', type: 'java.lang.String', desc: '' }, { name: 'p3', type: 'java.lang.String', desc: '', }, { name: 'p4', type: 'int', desc: '' }, { name: 'p5', type: 'long', desc: '' }, ], ret: 'void', desc: 'Operation exposed for management', }, resetDebugInfo: { args: [], ret: 'void', desc: 'Operation exposed for management', }, resetHistoryEntries: { args: [], ret: 'void', desc: 'Operation exposed for management', }, setHistoryEntriesForAttribute: { args: [ { name: 'p1', type: 'java.lang.String', desc: '', }, { name: 'p2', type: 'java.lang.String', desc: '' }, { name: 'p3', type: 'java.lang.String', desc: '', }, { name: 'p4', type: 'java.lang.String', desc: '' }, { name: 'p5', type: 'int', desc: '' }, ], ret: 'void', desc: 'Operation exposed for management', }, setHistoryLimitForAttribute: { args: [ { name: 'p1', type: 'java.lang.String', desc: '', }, { name: 'p2', type: 'java.lang.String', desc: '' }, { name: 'p3', type: 'java.lang.String', desc: '', }, { name: 'p4', type: 'java.lang.String', desc: '' }, { name: 'p5', type: 'int', desc: '', }, { name: 'p6', type: 'long', desc: '' }, ], ret: 'void', desc: 'Operation exposed for management', }, debugInfo: { args: [], ret: 'java.lang.String', desc: 'Operation exposed for management', }, }, attr: { HistorySize: { rw: false, type: 'int', desc: 'Attribute exposed for management', }, MaxDebugEntries: { rw: true, type: 'int', desc: 'Attribute exposed for management', }, HistoryMaxEntries: { rw: true, type: 'int', desc: 'Attribute exposed for management', }, Debug: { rw: true, type: 'boolean', desc: 'Attribute exposed for management', }, }, class: 'org.jolokia.backend.Config', desc: 'Information on the management interface of the MBean', }, }, 'com.zaxxer.hikari': { 'name=dataSource,type=HikariDataSource': { attr: { MaxLifetime: { rw: true, type: 'long', desc: 'MaxLifetime', }, ConnectionTimeout: { rw: true, type: 'long', desc: 'ConnectionTimeout', }, MaximumPoolSize: { rw: true, type: 'int', desc: 'MaximumPoolSize' }, PoolName: { rw: false, type: 'java.lang.String', desc: 'PoolName' }, Username: { rw: false, type: 'java.lang.String', desc: 'Username' }, IdleTimeout: { rw: true, type: 'long', desc: 'IdleTimeout' }, LeakDetectionThreshold: { rw: true, type: 'long', desc: 'LeakDetectionThreshold', }, ValidationTimeout: { rw: true, type: 'long', desc: 'ValidationTimeout', }, Catalog: { rw: true, type: 'java.lang.String', desc: 'Catalog' }, Password: { rw: false, type: 'java.lang.String', desc: 'Password' }, MinimumIdle: { rw: true, type: 'int', desc: 'MinimumIdle' }, }, class: 'com.zaxxer.hikari.HikariDataSource', desc: 'Information on the management interface of the MBean', }, }, 'com.codecentric.boot.sample': { 'name=stringMapManagedBean.StringSetter,type=StringMapManagedBean.StringSetter': { op: { getConfigKeys: { args: [], ret: 'java.util.List', desc: 'getConfigKeys', }, setTest: { args: [{ name: 'test', type: 'java.lang.String', desc: 'test' }], ret: 'void', desc: 'Set the value', }, getTest: { args: [], ret: 'java.lang.String', desc: 'Get the value', }, }, attr: { Test: { rw: true, type: 'java.lang.String', desc: 'Get the value' }, ConfigKeys: { rw: false, type: 'java.util.List', desc: 'configKeys', }, }, class: 'com.codecentric.boot.sample.StringMapManagedBean$StringSetter', desc: 'String Setter MBean inside other MBEAN', }, 'name=stringMapManagedBean,type=StringMapManagedBean': { op: { getSize: { args: [], ret: 'int', desc: 'getSize', }, setTest: { args: [{ name: 'test', type: 'int', desc: 'test' }], ret: 'void', desc: 'Set the value of the test instance variable', }, get: { args: [{ name: 'key', type: 'java.lang.String', desc: 'key' }], ret: 'java.lang.String', desc: 'get', }, getTest: { args: [], ret: 'int', desc: 'Get the value of the test instance variable', }, put: { args: [ { name: 'key', type: 'java.lang.String', desc: 'key' }, { name: 'value', type: 'java.lang.String', desc: 'value', }, ], ret: 'java.lang.String', desc: 'PUT DESCRIPTION', }, }, attr: { Test: { rw: true, type: 'int', desc: 'Get the value of the test instance variable', }, Size: { rw: false, type: 'int', desc: 'size' }, }, class: 'com.codecentric.boot.sample.StringMapManagedBean', desc: '', }, }, 'org.springframework.cloud.context.environment': { 'name=environmentManager,type=EnvironmentManager': { op: { getProperty: { args: [ { name: 'name', type: 'java.lang.String', desc: 'name', }, ], ret: 'java.lang.Object', desc: 'getProperty', }, setProperty: { args: [ { name: 'name', type: 'java.lang.String', desc: 'name' }, { name: 'value', type: 'java.lang.String', desc: 'value', }, ], ret: 'void', desc: 'setProperty', }, reset: { args: [], ret: 'java.util.Map', desc: 'reset' }, }, class: 'org.springframework.cloud.context.environment.EnvironmentManager', desc: '', }, }, jolokia: { 'type=Discovery': { op: { lookupAgentsWithTimeout: { args: [{ name: 'p1', type: 'int', desc: '' }], ret: 'java.util.List', desc: 'Operation exposed for management', }, lookupAgentsWithTimeoutAndMulticastAddress: { args: [ { name: 'p1', type: 'int', desc: '', }, { name: 'p2', type: 'java.lang.String', desc: '' }, { name: 'p3', type: 'int', desc: '' }, ], ret: 'java.util.List', desc: 'Operation exposed for management', }, lookupAgents: { args: [], ret: 'java.util.List', desc: 'Operation exposed for management', }, }, class: 'org.jolokia.discovery.JolokiaDiscovery', desc: 'Information on the management interface of the MBean', }, 'type=ServerHandler': { op: { mBeanServersInfo: { args: [], ret: 'java.lang.String', desc: 'Operation exposed for management', }, }, class: 'org.jolokia.backend.MBeanServerHandler', desc: 'Information on the management interface of the MBean', }, 'type=Config': { op: { setHistoryEntriesForOperation: { args: [ { name: 'p1', type: 'java.lang.String', desc: '', }, { name: 'p2', type: 'java.lang.String', desc: '' }, { name: 'p3', type: 'java.lang.String', desc: '', }, { name: 'p4', type: 'int', desc: '' }, ], ret: 'void', desc: 'Operation exposed for management', }, setHistoryLimitForOperation: { args: [ { name: 'p1', type: 'java.lang.String', desc: '', }, { name: 'p2', type: 'java.lang.String', desc: '' }, { name: 'p3', type: 'java.lang.String', desc: '', }, { name: 'p4', type: 'int', desc: '' }, { name: 'p5', type: 'long', desc: '' }, ], ret: 'void', desc: 'Operation exposed for management', }, resetDebugInfo: { args: [], ret: 'void', desc: 'Operation exposed for management', }, resetHistoryEntries: { args: [], ret: 'void', desc: 'Operation exposed for management', }, setHistoryEntriesForAttribute: { args: [ { name: 'p1', type: 'java.lang.String', desc: '', }, { name: 'p2', type: 'java.lang.String', desc: '' }, { name: 'p3', type: 'java.lang.String', desc: '', }, { name: 'p4', type: 'java.lang.String', desc: '' }, { name: 'p5', type: 'int', desc: '' }, ], ret: 'void', desc: 'Operation exposed for management', }, setHistoryLimitForAttribute: { args: [ { name: 'p1', type: 'java.lang.String', desc: '', }, { name: 'p2', type: 'java.lang.String', desc: '' }, { name: 'p3', type: 'java.lang.String', desc: '', }, { name: 'p4', type: 'java.lang.String', desc: '' }, { name: 'p5', type: 'int', desc: '', }, { name: 'p6', type: 'long', desc: '' }, ], ret: 'void', desc: 'Operation exposed for management', }, debugInfo: { args: [], ret: 'java.lang.String', desc: 'Operation exposed for management', }, }, attr: { HistorySize: { rw: false, type: 'int', desc: 'Attribute exposed for management', }, MaxDebugEntries: { rw: true, type: 'int', desc: 'Attribute exposed for management', }, HistoryMaxEntries: { rw: true, type: 'int', desc: 'Attribute exposed for management', }, Debug: { rw: true, type: 'boolean', desc: 'Attribute exposed for management', }, }, class: 'org.jolokia.backend.Config', desc: 'Information on the management interface of the MBean', }, }, }, timestamp: 1673711911, status: 200, }; ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/mocks/instance/jolokia/index.ts ================================================ import { HttpResponse, http } from 'msw'; import { jolokiaList } from './data'; import { jolokiaRead } from '@/mocks/instance/jolokia/data.read'; const jolokiaEndpoint = [ http.get('/instances/:instanceId/actuator/jolokia/list', () => { return HttpResponse.json(jolokiaList); }), http.post('/instances/:instanceId/actuator/jolokia', async () => { try { const body = { type: 'read' }; if (body.type === 'read') { return HttpResponse.json(jolokiaRead); } } catch (e) { console.error(e); } }), ]; export default jolokiaEndpoint; ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/mocks/instance/liquibase/data.ts ================================================ export const liquibase = { contexts: { application: { liquibaseBeans: { liquibase: { changeSets: [ { author: 'marceloverdijk', changeLog: 'db/changelog/db.changelog-master.yaml', comments: '', contexts: ['context1'], dateExecuted: '2022-04-21T08:50:32.817Z', deploymentId: '0531032536', description: 'createTable tableName=customer', execType: 'EXECUTED', id: '1', labels: ['label1', 'label2'], checksum: '8:46debf252cce6d7b25e28ddeb9fc4bf6', orderExecuted: 1, }, ], }, }, }, }, }; ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/mocks/instance/liquibase/index.ts ================================================ import { HttpResponse, http } from 'msw'; import { liquibase } from './data'; const liquibaseEndpoints = [ http.get('/instances/:instanceId/actuator/liquibase', () => { return HttpResponse.json(liquibase); }), ]; export default liquibaseEndpoints; ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/mocks/instance/mappings/data.ts ================================================ export const mappings = { contexts: { 'spring-boot-admin-sample-servlet': { mappings: { dispatcherServlets: { dispatcherServlet: [ { handler: 'Actuator root web endpoint', predicate: '{GET [/actuator], produces [application/vnd.spring-boot.actuator.v3+json || application/vnd.spring-boot.actuator.v2+json || application/json]}', details: { handlerMethod: { className: 'org.springframework.boot.actuate.endpoint.web.servlet.WebMvcEndpointHandlerMapping.WebMvcLinksHandler', name: 'links', descriptor: '(Ljavax/servlet/http/HttpServletRequest;Ljavax/servlet/http/HttpServletResponse;)Ljava/lang/Object;', }, requestMappingConditions: { consumes: [], headers: [], methods: ['GET'], params: [], patterns: ['/actuator'], produces: [ { mediaType: 'application/vnd.spring-boot.actuator.v3+json', negated: false, }, { mediaType: 'application/vnd.spring-boot.actuator.v2+json', negated: false, }, { mediaType: 'application/json', negated: false }, ], }, }, }, { handler: "Actuator web endpoint 'caches'", predicate: '{GET [/actuator/caches], produces [application/vnd.spring-boot.actuator.v3+json || application/vnd.spring-boot.actuator.v2+json || application/json]}', details: { handlerMethod: { className: 'org.springframework.boot.actuate.endpoint.web.servlet.AbstractWebMvcEndpointHandlerMapping.OperationHandler', name: 'handle', descriptor: '(Ljavax/servlet/http/HttpServletRequest;Ljava/util/Map;)Ljava/lang/Object;', }, requestMappingConditions: { consumes: [], headers: [], methods: ['GET'], params: [], patterns: ['/actuator/caches'], produces: [ { mediaType: 'application/vnd.spring-boot.actuator.v3+json', negated: false, }, { mediaType: 'application/vnd.spring-boot.actuator.v2+json', negated: false, }, { mediaType: 'application/json', negated: false }, ], }, }, }, { handler: "Actuator web endpoint 'httptrace'", predicate: '{GET [/actuator/httptrace], produces [application/vnd.spring-boot.actuator.v3+json || application/vnd.spring-boot.actuator.v2+json || application/json]}', details: { handlerMethod: { className: 'org.springframework.boot.actuate.endpoint.web.servlet.AbstractWebMvcEndpointHandlerMapping.OperationHandler', name: 'handle', descriptor: '(Ljavax/servlet/http/HttpServletRequest;Ljava/util/Map;)Ljava/lang/Object;', }, requestMappingConditions: { consumes: [], headers: [], methods: ['GET'], params: [], patterns: ['/actuator/httptrace'], produces: [ { mediaType: 'application/vnd.spring-boot.actuator.v3+json', negated: false, }, { mediaType: 'application/vnd.spring-boot.actuator.v2+json', negated: false, }, { mediaType: 'application/json', negated: false }, ], }, }, }, { handler: "Actuator web endpoint 'metrics'", predicate: '{GET [/actuator/metrics], produces [application/vnd.spring-boot.actuator.v3+json || application/vnd.spring-boot.actuator.v2+json || application/json]}', details: { handlerMethod: { className: 'org.springframework.boot.actuate.endpoint.web.servlet.AbstractWebMvcEndpointHandlerMapping.OperationHandler', name: 'handle', descriptor: '(Ljavax/servlet/http/HttpServletRequest;Ljava/util/Map;)Ljava/lang/Object;', }, requestMappingConditions: { consumes: [], headers: [], methods: ['GET'], params: [], patterns: ['/actuator/metrics'], produces: [ { mediaType: 'application/vnd.spring-boot.actuator.v3+json', negated: false, }, { mediaType: 'application/vnd.spring-boot.actuator.v2+json', negated: false, }, { mediaType: 'application/json', negated: false }, ], }, }, }, { handler: "Actuator web endpoint 'auditevents'", predicate: '{GET [/actuator/auditevents], produces [application/vnd.spring-boot.actuator.v3+json || application/vnd.spring-boot.actuator.v2+json || application/json]}', details: { handlerMethod: { className: 'org.springframework.boot.actuate.endpoint.web.servlet.AbstractWebMvcEndpointHandlerMapping.OperationHandler', name: 'handle', descriptor: '(Ljavax/servlet/http/HttpServletRequest;Ljava/util/Map;)Ljava/lang/Object;', }, requestMappingConditions: { consumes: [], headers: [], methods: ['GET'], params: [], patterns: ['/actuator/auditevents'], produces: [ { mediaType: 'application/vnd.spring-boot.actuator.v3+json', negated: false, }, { mediaType: 'application/vnd.spring-boot.actuator.v2+json', negated: false, }, { mediaType: 'application/json', negated: false }, ], }, }, }, { handler: "Actuator web endpoint 'loggers-name'", predicate: '{POST [/actuator/loggers/{name}], consumes [application/vnd.spring-boot.actuator.v3+json || application/vnd.spring-boot.actuator.v2+json || application/json]}', details: { handlerMethod: { className: 'org.springframework.boot.actuate.endpoint.web.servlet.AbstractWebMvcEndpointHandlerMapping.OperationHandler', name: 'handle', descriptor: '(Ljavax/servlet/http/HttpServletRequest;Ljava/util/Map;)Ljava/lang/Object;', }, requestMappingConditions: { consumes: [ { mediaType: 'application/vnd.spring-boot.actuator.v3+json', negated: false, }, { mediaType: 'application/vnd.spring-boot.actuator.v2+json', negated: false, }, { mediaType: 'application/json', negated: false }, ], headers: [], methods: ['POST'], params: [], patterns: ['/actuator/loggers/{name}'], produces: [], }, }, }, { handler: "Actuator web endpoint 'custom'", predicate: '{GET [/actuator/custom], produces [application/vnd.spring-boot.actuator.v3+json || application/vnd.spring-boot.actuator.v2+json || application/json]}', details: { handlerMethod: { className: 'org.springframework.boot.actuate.endpoint.web.servlet.AbstractWebMvcEndpointHandlerMapping.OperationHandler', name: 'handle', descriptor: '(Ljavax/servlet/http/HttpServletRequest;Ljava/util/Map;)Ljava/lang/Object;', }, requestMappingConditions: { consumes: [], headers: [], methods: ['GET'], params: [], patterns: ['/actuator/custom'], produces: [ { mediaType: 'application/vnd.spring-boot.actuator.v3+json', negated: false, }, { mediaType: 'application/vnd.spring-boot.actuator.v2+json', negated: false, }, { mediaType: 'application/json', negated: false }, ], }, }, }, { handler: "Actuator web endpoint 'logfile'", predicate: '{GET [/actuator/logfile], produces [text/plain;charset=UTF-8]}', details: { handlerMethod: { className: 'org.springframework.boot.actuate.endpoint.web.servlet.AbstractWebMvcEndpointHandlerMapping.OperationHandler', name: 'handle', descriptor: '(Ljavax/servlet/http/HttpServletRequest;Ljava/util/Map;)Ljava/lang/Object;', }, requestMappingConditions: { consumes: [], headers: [], methods: ['GET'], params: [], patterns: ['/actuator/logfile'], produces: [ { mediaType: 'text/plain;charset=UTF-8', negated: false }, ], }, }, }, { handler: "Actuator web endpoint 'metrics-requiredMetricName'", predicate: '{GET [/actuator/metrics/{requiredMetricName}], produces [application/vnd.spring-boot.actuator.v3+json || application/vnd.spring-boot.actuator.v2+json || application/json]}', details: { handlerMethod: { className: 'org.springframework.boot.actuate.endpoint.web.servlet.AbstractWebMvcEndpointHandlerMapping.OperationHandler', name: 'handle', descriptor: '(Ljavax/servlet/http/HttpServletRequest;Ljava/util/Map;)Ljava/lang/Object;', }, requestMappingConditions: { consumes: [], headers: [], methods: ['GET'], params: [], patterns: ['/actuator/metrics/{requiredMetricName}'], produces: [ { mediaType: 'application/vnd.spring-boot.actuator.v3+json', negated: false, }, { mediaType: 'application/vnd.spring-boot.actuator.v2+json', negated: false, }, { mediaType: 'application/json', negated: false }, ], }, }, }, { handler: "Actuator web endpoint 'loggers'", predicate: '{GET [/actuator/loggers], produces [application/vnd.spring-boot.actuator.v3+json || application/vnd.spring-boot.actuator.v2+json || application/json]}', details: { handlerMethod: { className: 'org.springframework.boot.actuate.endpoint.web.servlet.AbstractWebMvcEndpointHandlerMapping.OperationHandler', name: 'handle', descriptor: '(Ljavax/servlet/http/HttpServletRequest;Ljava/util/Map;)Ljava/lang/Object;', }, requestMappingConditions: { consumes: [], headers: [], methods: ['GET'], params: [], patterns: ['/actuator/loggers'], produces: [ { mediaType: 'application/vnd.spring-boot.actuator.v3+json', negated: false, }, { mediaType: 'application/vnd.spring-boot.actuator.v2+json', negated: false, }, { mediaType: 'application/json', negated: false }, ], }, }, }, { handler: "Actuator web endpoint 'threaddump'", predicate: '{GET [/actuator/threaddump], produces [application/vnd.spring-boot.actuator.v3+json || application/vnd.spring-boot.actuator.v2+json || application/json]}', details: { handlerMethod: { className: 'org.springframework.boot.actuate.endpoint.web.servlet.AbstractWebMvcEndpointHandlerMapping.OperationHandler', name: 'handle', descriptor: '(Ljavax/servlet/http/HttpServletRequest;Ljava/util/Map;)Ljava/lang/Object;', }, requestMappingConditions: { consumes: [], headers: [], methods: ['GET'], params: [], patterns: ['/actuator/threaddump'], produces: [ { mediaType: 'application/vnd.spring-boot.actuator.v3+json', negated: false, }, { mediaType: 'application/vnd.spring-boot.actuator.v2+json', negated: false, }, { mediaType: 'application/json', negated: false }, ], }, }, }, { handler: "Actuator web endpoint 'env'", predicate: '{GET [/actuator/env], produces [application/vnd.spring-boot.actuator.v3+json || application/vnd.spring-boot.actuator.v2+json || application/json]}', details: { handlerMethod: { className: 'org.springframework.boot.actuate.endpoint.web.servlet.AbstractWebMvcEndpointHandlerMapping.OperationHandler', name: 'handle', descriptor: '(Ljavax/servlet/http/HttpServletRequest;Ljava/util/Map;)Ljava/lang/Object;', }, requestMappingConditions: { consumes: [], headers: [], methods: ['GET'], params: [], patterns: ['/actuator/env'], produces: [ { mediaType: 'application/vnd.spring-boot.actuator.v3+json', negated: false, }, { mediaType: 'application/vnd.spring-boot.actuator.v2+json', negated: false, }, { mediaType: 'application/json', negated: false }, ], }, }, }, { handler: "Actuator web endpoint 'startup'", predicate: '{GET [/actuator/startup], produces [application/vnd.spring-boot.actuator.v3+json || application/vnd.spring-boot.actuator.v2+json || application/json]}', details: { handlerMethod: { className: 'org.springframework.boot.actuate.endpoint.web.servlet.AbstractWebMvcEndpointHandlerMapping.OperationHandler', name: 'handle', descriptor: '(Ljavax/servlet/http/HttpServletRequest;Ljava/util/Map;)Ljava/lang/Object;', }, requestMappingConditions: { consumes: [], headers: [], methods: ['GET'], params: [], patterns: ['/actuator/startup'], produces: [ { mediaType: 'application/vnd.spring-boot.actuator.v3+json', negated: false, }, { mediaType: 'application/vnd.spring-boot.actuator.v2+json', negated: false, }, { mediaType: 'application/json', negated: false }, ], }, }, }, { handler: "Actuator web endpoint 'threaddump'", predicate: '{GET [/actuator/threaddump], produces [text/plain;charset=UTF-8]}', details: { handlerMethod: { className: 'org.springframework.boot.actuate.endpoint.web.servlet.AbstractWebMvcEndpointHandlerMapping.OperationHandler', name: 'handle', descriptor: '(Ljavax/servlet/http/HttpServletRequest;Ljava/util/Map;)Ljava/lang/Object;', }, requestMappingConditions: { consumes: [], headers: [], methods: ['GET'], params: [], patterns: ['/actuator/threaddump'], produces: [ { mediaType: 'text/plain;charset=UTF-8', negated: false }, ], }, }, }, { handler: "Actuator web endpoint 'info'", predicate: '{GET [/actuator/info], produces [application/vnd.spring-boot.actuator.v3+json || application/vnd.spring-boot.actuator.v2+json || application/json]}', details: { handlerMethod: { className: 'org.springframework.boot.actuate.endpoint.web.servlet.AbstractWebMvcEndpointHandlerMapping.OperationHandler', name: 'handle', descriptor: '(Ljavax/servlet/http/HttpServletRequest;Ljava/util/Map;)Ljava/lang/Object;', }, requestMappingConditions: { consumes: [], headers: [], methods: ['GET'], params: [], patterns: ['/actuator/info'], produces: [ { mediaType: 'application/vnd.spring-boot.actuator.v3+json', negated: false, }, { mediaType: 'application/vnd.spring-boot.actuator.v2+json', negated: false, }, { mediaType: 'application/json', negated: false }, ], }, }, }, { handler: "Actuator web endpoint 'sessions-sessionId'", predicate: '{DELETE [/actuator/sessions/{sessionId}]}', details: { handlerMethod: { className: 'org.springframework.boot.actuate.endpoint.web.servlet.AbstractWebMvcEndpointHandlerMapping.OperationHandler', name: 'handle', descriptor: '(Ljavax/servlet/http/HttpServletRequest;Ljava/util/Map;)Ljava/lang/Object;', }, requestMappingConditions: { consumes: [], headers: [], methods: ['DELETE'], params: [], patterns: ['/actuator/sessions/{sessionId}'], produces: [], }, }, }, { handler: "Actuator web endpoint 'caches-cache'", predicate: '{DELETE [/actuator/caches/{cache}], produces [application/vnd.spring-boot.actuator.v3+json || application/vnd.spring-boot.actuator.v2+json || application/json]}', details: { handlerMethod: { className: 'org.springframework.boot.actuate.endpoint.web.servlet.AbstractWebMvcEndpointHandlerMapping.OperationHandler', name: 'handle', descriptor: '(Ljavax/servlet/http/HttpServletRequest;Ljava/util/Map;)Ljava/lang/Object;', }, requestMappingConditions: { consumes: [], headers: [], methods: ['DELETE'], params: [], patterns: ['/actuator/caches/{cache}'], produces: [ { mediaType: 'application/vnd.spring-boot.actuator.v3+json', negated: false, }, { mediaType: 'application/vnd.spring-boot.actuator.v2+json', negated: false, }, { mediaType: 'application/json', negated: false }, ], }, }, }, { handler: "Actuator web endpoint 'scheduledtasks'", predicate: '{GET [/actuator/scheduledtasks], produces [application/vnd.spring-boot.actuator.v3+json || application/vnd.spring-boot.actuator.v2+json || application/json]}', details: { handlerMethod: { className: 'org.springframework.boot.actuate.endpoint.web.servlet.AbstractWebMvcEndpointHandlerMapping.OperationHandler', name: 'handle', descriptor: '(Ljavax/servlet/http/HttpServletRequest;Ljava/util/Map;)Ljava/lang/Object;', }, requestMappingConditions: { consumes: [], headers: [], methods: ['GET'], params: [], patterns: ['/actuator/scheduledtasks'], produces: [ { mediaType: 'application/vnd.spring-boot.actuator.v3+json', negated: false, }, { mediaType: 'application/vnd.spring-boot.actuator.v2+json', negated: false, }, { mediaType: 'application/json', negated: false }, ], }, }, }, { handler: "Actuator web endpoint 'configprops'", predicate: '{GET [/actuator/configprops], produces [application/vnd.spring-boot.actuator.v3+json || application/vnd.spring-boot.actuator.v2+json || application/json]}', details: { handlerMethod: { className: 'org.springframework.boot.actuate.endpoint.web.servlet.AbstractWebMvcEndpointHandlerMapping.OperationHandler', name: 'handle', descriptor: '(Ljavax/servlet/http/HttpServletRequest;Ljava/util/Map;)Ljava/lang/Object;', }, requestMappingConditions: { consumes: [], headers: [], methods: ['GET'], params: [], patterns: ['/actuator/configprops'], produces: [ { mediaType: 'application/vnd.spring-boot.actuator.v3+json', negated: false, }, { mediaType: 'application/vnd.spring-boot.actuator.v2+json', negated: false, }, { mediaType: 'application/json', negated: false }, ], }, }, }, { handler: "Actuator web endpoint 'configprops-prefix'", predicate: '{GET [/actuator/configprops/{prefix}], produces [application/vnd.spring-boot.actuator.v3+json || application/vnd.spring-boot.actuator.v2+json || application/json]}', details: { handlerMethod: { className: 'org.springframework.boot.actuate.endpoint.web.servlet.AbstractWebMvcEndpointHandlerMapping.OperationHandler', name: 'handle', descriptor: '(Ljavax/servlet/http/HttpServletRequest;Ljava/util/Map;)Ljava/lang/Object;', }, requestMappingConditions: { consumes: [], headers: [], methods: ['GET'], params: [], patterns: ['/actuator/configprops/{prefix}'], produces: [ { mediaType: 'application/vnd.spring-boot.actuator.v3+json', negated: false, }, { mediaType: 'application/vnd.spring-boot.actuator.v2+json', negated: false, }, { mediaType: 'application/json', negated: false }, ], }, }, }, { handler: "Actuator web endpoint 'env-toMatch'", predicate: '{GET [/actuator/env/{toMatch}], produces [application/vnd.spring-boot.actuator.v3+json || application/vnd.spring-boot.actuator.v2+json || application/json]}', details: { handlerMethod: { className: 'org.springframework.boot.actuate.endpoint.web.servlet.AbstractWebMvcEndpointHandlerMapping.OperationHandler', name: 'handle', descriptor: '(Ljavax/servlet/http/HttpServletRequest;Ljava/util/Map;)Ljava/lang/Object;', }, requestMappingConditions: { consumes: [], headers: [], methods: ['GET'], params: [], patterns: ['/actuator/env/{toMatch}'], produces: [ { mediaType: 'application/vnd.spring-boot.actuator.v3+json', negated: false, }, { mediaType: 'application/vnd.spring-boot.actuator.v2+json', negated: false, }, { mediaType: 'application/json', negated: false }, ], }, }, }, { handler: "Actuator web endpoint 'startup'", predicate: '{POST [/actuator/startup], produces [application/vnd.spring-boot.actuator.v3+json || application/vnd.spring-boot.actuator.v2+json || application/json]}', details: { handlerMethod: { className: 'org.springframework.boot.actuate.endpoint.web.servlet.AbstractWebMvcEndpointHandlerMapping.OperationHandler', name: 'handle', descriptor: '(Ljavax/servlet/http/HttpServletRequest;Ljava/util/Map;)Ljava/lang/Object;', }, requestMappingConditions: { consumes: [], headers: [], methods: ['POST'], params: [], patterns: ['/actuator/startup'], produces: [ { mediaType: 'application/vnd.spring-boot.actuator.v3+json', negated: false, }, { mediaType: 'application/vnd.spring-boot.actuator.v2+json', negated: false, }, { mediaType: 'application/json', negated: false }, ], }, }, }, { handler: "Actuator web endpoint 'conditions'", predicate: '{GET [/actuator/conditions], produces [application/vnd.spring-boot.actuator.v3+json || application/vnd.spring-boot.actuator.v2+json || application/json]}', details: { handlerMethod: { className: 'org.springframework.boot.actuate.endpoint.web.servlet.AbstractWebMvcEndpointHandlerMapping.OperationHandler', name: 'handle', descriptor: '(Ljavax/servlet/http/HttpServletRequest;Ljava/util/Map;)Ljava/lang/Object;', }, requestMappingConditions: { consumes: [], headers: [], methods: ['GET'], params: [], patterns: ['/actuator/conditions'], produces: [ { mediaType: 'application/vnd.spring-boot.actuator.v3+json', negated: false, }, { mediaType: 'application/vnd.spring-boot.actuator.v2+json', negated: false, }, { mediaType: 'application/json', negated: false }, ], }, }, }, { handler: "Actuator web endpoint 'sessions-sessionId'", predicate: '{GET [/actuator/sessions/{sessionId}], produces [application/vnd.spring-boot.actuator.v3+json || application/vnd.spring-boot.actuator.v2+json || application/json]}', details: { handlerMethod: { className: 'org.springframework.boot.actuate.endpoint.web.servlet.AbstractWebMvcEndpointHandlerMapping.OperationHandler', name: 'handle', descriptor: '(Ljavax/servlet/http/HttpServletRequest;Ljava/util/Map;)Ljava/lang/Object;', }, requestMappingConditions: { consumes: [], headers: [], methods: ['GET'], params: [], patterns: ['/actuator/sessions/{sessionId}'], produces: [ { mediaType: 'application/vnd.spring-boot.actuator.v3+json', negated: false, }, { mediaType: 'application/vnd.spring-boot.actuator.v2+json', negated: false, }, { mediaType: 'application/json', negated: false }, ], }, }, }, { handler: "Actuator web endpoint 'mappings'", predicate: '{GET [/actuator/mappings], produces [application/vnd.spring-boot.actuator.v3+json || application/vnd.spring-boot.actuator.v2+json || application/json]}', details: { handlerMethod: { className: 'org.springframework.boot.actuate.endpoint.web.servlet.AbstractWebMvcEndpointHandlerMapping.OperationHandler', name: 'handle', descriptor: '(Ljavax/servlet/http/HttpServletRequest;Ljava/util/Map;)Ljava/lang/Object;', }, requestMappingConditions: { consumes: [], headers: [], methods: ['GET'], params: [], patterns: ['/actuator/mappings'], produces: [ { mediaType: 'application/vnd.spring-boot.actuator.v3+json', negated: false, }, { mediaType: 'application/vnd.spring-boot.actuator.v2+json', negated: false, }, { mediaType: 'application/json', negated: false }, ], }, }, }, { handler: "Actuator web endpoint 'caches-cache'", predicate: '{GET [/actuator/caches/{cache}], produces [application/vnd.spring-boot.actuator.v3+json || application/vnd.spring-boot.actuator.v2+json || application/json]}', details: { handlerMethod: { className: 'org.springframework.boot.actuate.endpoint.web.servlet.AbstractWebMvcEndpointHandlerMapping.OperationHandler', name: 'handle', descriptor: '(Ljavax/servlet/http/HttpServletRequest;Ljava/util/Map;)Ljava/lang/Object;', }, requestMappingConditions: { consumes: [], headers: [], methods: ['GET'], params: [], patterns: ['/actuator/caches/{cache}'], produces: [ { mediaType: 'application/vnd.spring-boot.actuator.v3+json', negated: false, }, { mediaType: 'application/vnd.spring-boot.actuator.v2+json', negated: false, }, { mediaType: 'application/json', negated: false }, ], }, }, }, { handler: "Actuator web endpoint 'beans'", predicate: '{GET [/actuator/beans], produces [application/vnd.spring-boot.actuator.v3+json || application/vnd.spring-boot.actuator.v2+json || application/json]}', details: { handlerMethod: { className: 'org.springframework.boot.actuate.endpoint.web.servlet.AbstractWebMvcEndpointHandlerMapping.OperationHandler', name: 'handle', descriptor: '(Ljavax/servlet/http/HttpServletRequest;Ljava/util/Map;)Ljava/lang/Object;', }, requestMappingConditions: { consumes: [], headers: [], methods: ['GET'], params: [], patterns: ['/actuator/beans'], produces: [ { mediaType: 'application/vnd.spring-boot.actuator.v3+json', negated: false, }, { mediaType: 'application/vnd.spring-boot.actuator.v2+json', negated: false, }, { mediaType: 'application/json', negated: false }, ], }, }, }, { handler: "Actuator web endpoint 'health'", predicate: '{GET [/actuator/health], produces [application/vnd.spring-boot.actuator.v3+json || application/vnd.spring-boot.actuator.v2+json || application/json]}', details: { handlerMethod: { className: 'org.springframework.boot.actuate.endpoint.web.servlet.AbstractWebMvcEndpointHandlerMapping.OperationHandler', name: 'handle', descriptor: '(Ljavax/servlet/http/HttpServletRequest;Ljava/util/Map;)Ljava/lang/Object;', }, requestMappingConditions: { consumes: [], headers: [], methods: ['GET'], params: [], patterns: ['/actuator/health'], produces: [ { mediaType: 'application/vnd.spring-boot.actuator.v3+json', negated: false, }, { mediaType: 'application/vnd.spring-boot.actuator.v2+json', negated: false, }, { mediaType: 'application/json', negated: false }, ], }, }, }, { handler: "Actuator web endpoint 'health-path'", predicate: '{GET [/actuator/health/**], produces [application/vnd.spring-boot.actuator.v3+json || application/vnd.spring-boot.actuator.v2+json || application/json]}', details: { handlerMethod: { className: 'org.springframework.boot.actuate.endpoint.web.servlet.AbstractWebMvcEndpointHandlerMapping.OperationHandler', name: 'handle', descriptor: '(Ljavax/servlet/http/HttpServletRequest;Ljava/util/Map;)Ljava/lang/Object;', }, requestMappingConditions: { consumes: [], headers: [], methods: ['GET'], params: [], patterns: ['/actuator/health/**'], produces: [ { mediaType: 'application/vnd.spring-boot.actuator.v3+json', negated: false, }, { mediaType: 'application/vnd.spring-boot.actuator.v2+json', negated: false, }, { mediaType: 'application/json', negated: false }, ], }, }, }, { handler: "Actuator web endpoint 'caches'", predicate: '{DELETE [/actuator/caches]}', details: { handlerMethod: { className: 'org.springframework.boot.actuate.endpoint.web.servlet.AbstractWebMvcEndpointHandlerMapping.OperationHandler', name: 'handle', descriptor: '(Ljavax/servlet/http/HttpServletRequest;Ljava/util/Map;)Ljava/lang/Object;', }, requestMappingConditions: { consumes: [], headers: [], methods: ['DELETE'], params: [], patterns: ['/actuator/caches'], produces: [], }, }, }, { handler: "Actuator web endpoint 'heapdump'", predicate: '{GET [/actuator/heapdump], produces [application/octet-stream]}', details: { handlerMethod: { className: 'org.springframework.boot.actuate.endpoint.web.servlet.AbstractWebMvcEndpointHandlerMapping.OperationHandler', name: 'handle', descriptor: '(Ljavax/servlet/http/HttpServletRequest;Ljava/util/Map;)Ljava/lang/Object;', }, requestMappingConditions: { consumes: [], headers: [], methods: ['GET'], params: [], patterns: ['/actuator/heapdump'], produces: [ { mediaType: 'application/octet-stream', negated: false }, ], }, }, }, { handler: "Actuator web endpoint 'loggers-name'", predicate: '{GET [/actuator/loggers/{name}], produces [application/vnd.spring-boot.actuator.v3+json || application/vnd.spring-boot.actuator.v2+json || application/json]}', details: { handlerMethod: { className: 'org.springframework.boot.actuate.endpoint.web.servlet.AbstractWebMvcEndpointHandlerMapping.OperationHandler', name: 'handle', descriptor: '(Ljavax/servlet/http/HttpServletRequest;Ljava/util/Map;)Ljava/lang/Object;', }, requestMappingConditions: { consumes: [], headers: [], methods: ['GET'], params: [], patterns: ['/actuator/loggers/{name}'], produces: [ { mediaType: 'application/vnd.spring-boot.actuator.v3+json', negated: false, }, { mediaType: 'application/vnd.spring-boot.actuator.v2+json', negated: false, }, { mediaType: 'application/json', negated: false }, ], }, }, }, { handler: "Actuator web endpoint 'sessions'", predicate: '{GET [/actuator/sessions], produces [application/vnd.spring-boot.actuator.v3+json || application/vnd.spring-boot.actuator.v2+json || application/json]}', details: { handlerMethod: { className: 'org.springframework.boot.actuate.endpoint.web.servlet.AbstractWebMvcEndpointHandlerMapping.OperationHandler', name: 'handle', descriptor: '(Ljavax/servlet/http/HttpServletRequest;Ljava/util/Map;)Ljava/lang/Object;', }, requestMappingConditions: { consumes: [], headers: [], methods: ['GET'], params: [], patterns: ['/actuator/sessions'], produces: [ { mediaType: 'application/vnd.spring-boot.actuator.v3+json', negated: false, }, { mediaType: 'application/vnd.spring-boot.actuator.v2+json', negated: false, }, { mediaType: 'application/json', negated: false }, ], }, }, }, { handler: 'org.springframework.boot.autoconfigure.web.servlet.error.BasicErrorController#error(HttpServletRequest)', predicate: '{ [/error]}', details: { handlerMethod: { className: 'org.springframework.boot.autoconfigure.web.servlet.error.BasicErrorController', name: 'error', descriptor: '(Ljavax/servlet/http/HttpServletRequest;)Lorg/springframework/http/ResponseEntity;', }, requestMappingConditions: { consumes: [], headers: [], methods: [], params: [], patterns: ['/error'], produces: [], }, }, }, { handler: 'org.springframework.boot.autoconfigure.web.servlet.error.BasicErrorController#errorHtml(HttpServletRequest, HttpServletResponse)', predicate: '{ [/error], produces [text/html]}', details: { handlerMethod: { className: 'org.springframework.boot.autoconfigure.web.servlet.error.BasicErrorController', name: 'errorHtml', descriptor: '(Ljavax/servlet/http/HttpServletRequest;Ljavax/servlet/http/HttpServletResponse;)Lorg/springframework/web/servlet/ModelAndView;', }, requestMappingConditions: { consumes: [], headers: [], methods: [], params: [], patterns: ['/error'], produces: [{ mediaType: 'text/html', negated: false }], }, }, }, { handler: 'de.codecentric.boot.admin.server.web.InstancesController#instances()', predicate: '{GET [/instances], produces [application/json]}', details: { handlerMethod: { className: 'de.codecentric.boot.admin.server.web.InstancesController', name: 'instances', descriptor: '()Lreactor/core/publisher/Flux;', }, requestMappingConditions: { consumes: [], headers: [], methods: ['GET'], params: [], patterns: ['/instances'], produces: [{ mediaType: 'application/json', negated: false }], }, }, }, { handler: 'de.codecentric.boot.admin.server.web.InstancesController#instance(String)', predicate: '{GET [/instances/{id}], produces [application/json]}', details: { handlerMethod: { className: 'de.codecentric.boot.admin.server.web.InstancesController', name: 'instance', descriptor: '(Ljava/lang/String;)Lreactor/core/publisher/Mono;', }, requestMappingConditions: { consumes: [], headers: [], methods: ['GET'], params: [], patterns: ['/instances/{id}'], produces: [{ mediaType: 'application/json', negated: false }], }, }, }, { handler: 'de.codecentric.boot.admin.server.web.ApplicationsController#unregister(String)', predicate: '{DELETE [/applications/{name}]}', details: { handlerMethod: { className: 'de.codecentric.boot.admin.server.web.ApplicationsController', name: 'unregister', descriptor: '(Ljava/lang/String;)Lreactor/core/publisher/Mono;', }, requestMappingConditions: { consumes: [], headers: [], methods: ['DELETE'], params: [], patterns: ['/applications/{name}'], produces: [], }, }, }, { handler: 'de.codecentric.boot.admin.server.notify.filter.web.NotificationFilterController#getFilters()', predicate: '{GET [/notifications/filters], produces [application/json]}', details: { handlerMethod: { className: 'de.codecentric.boot.admin.server.notify.filter.web.NotificationFilterController', name: 'getFilters', descriptor: '()Ljava/util/Collection;', }, requestMappingConditions: { consumes: [], headers: [], methods: ['GET'], params: [], patterns: ['/notifications/filters'], produces: [{ mediaType: 'application/json', negated: false }], }, }, }, { handler: 'de.codecentric.boot.admin.server.notify.filter.web.NotificationFilterController#deleteFilter(String)', predicate: '{DELETE [/notifications/filters/{id}]}', details: { handlerMethod: { className: 'de.codecentric.boot.admin.server.notify.filter.web.NotificationFilterController', name: 'deleteFilter', descriptor: '(Ljava/lang/String;)Lorg/springframework/http/ResponseEntity;', }, requestMappingConditions: { consumes: [], headers: [], methods: ['DELETE'], params: [], patterns: ['/notifications/filters/{id}'], produces: [], }, }, }, { handler: 'de.codecentric.boot.admin.server.ui.web.UiController#sbaSettings()', predicate: '{GET [/sba-settings.js], produces [application/javascript]}', details: { handlerMethod: { className: 'de.codecentric.boot.admin.server.ui.web.UiController', name: 'sbaSettings', descriptor: '()Ljava/lang/String;', }, requestMappingConditions: { consumes: [], headers: [], methods: ['GET'], params: [], patterns: ['/sba-settings'], produces: [ { mediaType: 'application/javascript', negated: false }, ], }, }, }, { handler: 'de.codecentric.boot.admin.server.web.ApplicationsController#applicationsStream()', predicate: '{GET [/applications], produces [text/event-stream]}', details: { handlerMethod: { className: 'de.codecentric.boot.admin.server.web.ApplicationsController', name: 'applicationsStream', descriptor: '()Lreactor/core/publisher/Flux;', }, requestMappingConditions: { consumes: [], headers: [], methods: ['GET'], params: [], patterns: ['/applications'], produces: [ { mediaType: 'text/event-stream', negated: false }, ], }, }, }, { handler: 'de.codecentric.boot.admin.server.web.InstancesController#instanceStream(String)', predicate: '{GET [/instances/{id}], produces [text/event-stream]}', details: { handlerMethod: { className: 'de.codecentric.boot.admin.server.web.InstancesController', name: 'instanceStream', descriptor: '(Ljava/lang/String;)Lreactor/core/publisher/Flux;', }, requestMappingConditions: { consumes: [], headers: [], methods: ['GET'], params: [], patterns: ['/instances/{id}'], produces: [ { mediaType: 'text/event-stream', negated: false }, ], }, }, }, { handler: 'de.codecentric.boot.admin.server.web.InstancesController#unregister(String)', predicate: '{DELETE [/instances/{id}]}', details: { handlerMethod: { className: 'de.codecentric.boot.admin.server.web.InstancesController', name: 'unregister', descriptor: '(Ljava/lang/String;)Lreactor/core/publisher/Mono;', }, requestMappingConditions: { consumes: [], headers: [], methods: ['DELETE'], params: [], patterns: ['/instances/{id}'], produces: [], }, }, }, { handler: 'de.codecentric.boot.admin.server.web.InstancesController#register(Registration, UriComponentsBuilder)', predicate: '{POST [/instances], consumes [application/json]}', details: { handlerMethod: { className: 'de.codecentric.boot.admin.server.web.InstancesController', name: 'register', descriptor: '(Lde/codecentric/boot/admin/server/domain/values/Registration;Lorg/springframework/web/util/UriComponentsBuilder;)Lreactor/core/publisher/Mono;', }, requestMappingConditions: { consumes: [{ mediaType: 'application/json', negated: false }], headers: [], methods: ['POST'], params: [], patterns: ['/instances'], produces: [], }, }, }, { handler: 'de.codecentric.boot.admin.server.web.servlet.InstancesProxyController#endpointProxy(String, HttpServletRequest)', predicate: '{[GET, HEAD, POST, PUT, PATCH, DELETE, OPTIONS] [/applications/{applicationName}/actuator/**]}', details: { handlerMethod: { className: 'de.codecentric.boot.admin.server.web.servlet.InstancesProxyController', name: 'endpointProxy', descriptor: '(Ljava/lang/String;Ljavax/servlet/http/HttpServletRequest;)Lreactor/core/publisher/Flux;', }, requestMappingConditions: { consumes: [], headers: [], methods: [ 'GET', 'HEAD', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS', ], params: [], patterns: ['/applications/{applicationName}/actuator/**'], produces: [], }, }, }, { handler: 'de.codecentric.boot.admin.server.notify.filter.web.NotificationFilterController#addFilter(String, String, Long)', predicate: '{POST [/notifications/filters], produces [application/json]}', details: { handlerMethod: { className: 'de.codecentric.boot.admin.server.notify.filter.web.NotificationFilterController', name: 'addFilter', descriptor: '(Ljava/lang/String;Ljava/lang/String;Ljava/lang/Long;)Lorg/springframework/http/ResponseEntity;', }, requestMappingConditions: { consumes: [], headers: [], methods: ['POST'], params: [], patterns: ['/notifications/filters'], produces: [{ mediaType: 'application/json', negated: false }], }, }, }, { handler: 'de.codecentric.boot.admin.server.ui.web.UiController#index()', predicate: '{GET [/], produces [text/html]}', details: { handlerMethod: { className: 'de.codecentric.boot.admin.server.ui.web.UiController', name: 'index', descriptor: '()Ljava/lang/String;', }, requestMappingConditions: { consumes: [], headers: [], methods: ['GET'], params: [], patterns: ['/'], produces: [{ mediaType: 'text/html', negated: false }], }, }, }, { handler: 'de.codecentric.boot.admin.server.web.ApplicationsController#application(String)', predicate: '{GET [/applications/{name}], produces [application/json]}', details: { handlerMethod: { className: 'de.codecentric.boot.admin.server.web.ApplicationsController', name: 'application', descriptor: '(Ljava/lang/String;)Lreactor/core/publisher/Mono;', }, requestMappingConditions: { consumes: [], headers: [], methods: ['GET'], params: [], patterns: ['/applications/{name}'], produces: [{ mediaType: 'application/json', negated: false }], }, }, }, { handler: 'de.codecentric.boot.admin.server.web.servlet.InstancesProxyController#endpointProxy(String, HttpServletRequest, HttpServletResponse)', predicate: '{[GET, HEAD, POST, PUT, PATCH, DELETE, OPTIONS] [/instances/{instanceId}/actuator/**]}', details: { handlerMethod: { className: 'de.codecentric.boot.admin.server.web.servlet.InstancesProxyController', name: 'endpointProxy', descriptor: '(Ljava/lang/String;Ljavax/servlet/http/HttpServletRequest;Ljavax/servlet/http/HttpServletResponse;)V', }, requestMappingConditions: { consumes: [], headers: [], methods: [ 'GET', 'HEAD', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS', ], params: [], patterns: ['/instances/{instanceId}/actuator/**'], produces: [], }, }, }, { handler: 'de.codecentric.boot.admin.server.web.InstancesController#events()', predicate: '{GET [/instances/events], produces [application/json]}', details: { handlerMethod: { className: 'de.codecentric.boot.admin.server.web.InstancesController', name: 'events', descriptor: '()Lreactor/core/publisher/Flux;', }, requestMappingConditions: { consumes: [], headers: [], methods: ['GET'], params: [], patterns: ['/instances/events'], produces: [{ mediaType: 'application/json', negated: false }], }, }, }, { handler: 'de.codecentric.boot.admin.server.web.InstancesController#instances(String)', predicate: '{GET [/instances], params [name], produces [application/json]}', details: { handlerMethod: { className: 'de.codecentric.boot.admin.server.web.InstancesController', name: 'instances', descriptor: '(Ljava/lang/String;)Lreactor/core/publisher/Flux;', }, requestMappingConditions: { consumes: [], headers: [], methods: ['GET'], params: [{ name: 'name', value: null, negated: false }], patterns: ['/instances'], produces: [{ mediaType: 'application/json', negated: false }], }, }, }, { handler: 'de.codecentric.boot.admin.server.ui.web.UiController#login()', predicate: '{GET [/login], produces [text/html]}', details: { handlerMethod: { className: 'de.codecentric.boot.admin.server.ui.web.UiController', name: 'login', descriptor: '()Ljava/lang/String;', }, requestMappingConditions: { consumes: [], headers: [], methods: ['GET'], params: [], patterns: ['/login'], produces: [{ mediaType: 'text/html', negated: false }], }, }, }, { handler: 'de.codecentric.boot.admin.server.web.InstancesController#eventStream()', predicate: '{GET [/instances/events], produces [text/event-stream]}', details: { handlerMethod: { className: 'de.codecentric.boot.admin.server.web.InstancesController', name: 'eventStream', descriptor: '()Lreactor/core/publisher/Flux;', }, requestMappingConditions: { consumes: [], headers: [], methods: ['GET'], params: [], patterns: ['/instances/events'], produces: [ { mediaType: 'text/event-stream', negated: false }, ], }, }, }, { handler: 'de.codecentric.boot.admin.server.web.ApplicationsController#applications()', predicate: '{GET [/applications], produces [application/json]}', details: { handlerMethod: { className: 'de.codecentric.boot.admin.server.web.ApplicationsController', name: 'applications', descriptor: '()Lreactor/core/publisher/Flux;', }, requestMappingConditions: { consumes: [], headers: [], methods: ['GET'], params: [], patterns: ['/applications'], produces: [{ mediaType: 'application/json', negated: false }], }, }, }, { handler: 'ResourceHttpRequestHandler [Classpath [META-INF/resources/webjars/]]', predicate: '/webjars/**', details: null, }, { handler: 'ResourceHttpRequestHandler', predicate: '/**', details: null, }, { handler: 'ResourceHttpRequestHandler', predicate: '/extensions/**', details: null, }, ], }, servletFilters: [ { servletNameMappings: [], urlPatternMappings: ['/*'], name: 'webMvcMetricsFilter', className: 'org.springframework.boot.actuate.metrics.web.servlet.WebMvcMetricsFilter', }, { servletNameMappings: [], urlPatternMappings: ['/*'], name: 'requestContextFilter', className: 'org.springframework.boot.web.servlet.filter.OrderedRequestContextFilter', }, { servletNameMappings: [], urlPatternMappings: ['/*'], name: 'homepageForwardFilter', className: 'de.codecentric.boot.admin.server.ui.web.servlet.HomepageForwardingFilter', }, { servletNameMappings: [], urlPatternMappings: ['/*'], name: 'sessionRepositoryFilter', className: 'org.springframework.session.web.http.SessionRepositoryFilter', }, { servletNameMappings: [], urlPatternMappings: ['/*'], name: 'Tomcat WebSocket (JSR356) Filter', className: 'org.apache.tomcat.websocket.server.WsFilter', }, { servletNameMappings: [], urlPatternMappings: ['/*'], name: 'characterEncodingFilter', className: 'org.springframework.boot.web.servlet.filter.OrderedCharacterEncodingFilter', }, { servletNameMappings: [], urlPatternMappings: ['/*'], name: 'httpTraceFilter', className: 'org.springframework.boot.actuate.web.trace.servlet.HttpTraceFilter', }, { servletNameMappings: [], urlPatternMappings: ['/*'], name: 'springSecurityFilterChain', className: 'org.springframework.boot.web.servlet.DelegatingFilterProxyRegistrationBean$1', }, { servletNameMappings: [], urlPatternMappings: ['/*'], name: 'formContentFilter', className: 'org.springframework.boot.web.servlet.filter.OrderedFormContentFilter', }, ], servlets: [ { mappings: ['/'], name: 'dispatcherServlet', className: 'org.springframework.web.servlet.DispatcherServlet', }, ], }, parentId: null, }, }, }; ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/mocks/instance/mappings/index.ts ================================================ import { HttpResponse, http } from 'msw'; import { mappings } from './data'; const mappingsEndpoint = [ http.get('/instances/:instanceId/actuator/mappings', () => { return HttpResponse.json(mappings); }), ]; export default mappingsEndpoint; ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/mocks/instance/metrics/data.ts ================================================ export const memoryMaxResponse = { name: 'jvm.memory.max', description: 'The maximum amount of memory in bytes that can be used for memory management', baseUnit: 'bytes', measurements: [ { statistic: 'VALUE', value: 8589934590, }, ], availableTags: [ { tag: 'id', values: ['G1 Old Gen', 'G1 Survivor Space', 'G1 Eden Space'], }, ], }; export const memoryUsedResponse = { name: 'jvm.memory.used', description: 'The amount of used memory', baseUnit: 'bytes', measurements: [ { statistic: 'VALUE', value: 115390832, }, ], availableTags: [ { tag: 'id', values: ['G1 Survivor Space', 'G1 Old Gen', 'G1 Eden Space', 'Metaspace'], }, ], }; export const memoryCommittedResponse = { name: 'jvm.memory.committed', description: 'The amount of memory in bytes that is committed for the Java virtual machine to use', baseUnit: 'bytes', measurements: [ { statistic: 'VALUE', value: 197132288, }, ], availableTags: [ { tag: 'id', values: ['G1 Survivor Space', 'G1 Old Gen', 'G1 Eden Space'], }, ], }; ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/mocks/instance/metrics/index.ts ================================================ import { HttpResponse, http } from 'msw'; import { memoryCommittedResponse, memoryMaxResponse, memoryUsedResponse, } from '@/mocks/instance/metrics/data'; const metricsEntpoints = [ http.get('/instances/:instanceId/actuator/metrics/jvm.memory.max', () => { return HttpResponse.json(memoryMaxResponse); }), http.get('/instances/:instanceId/actuator/metrics/jvm.memory.used', () => { return HttpResponse.json(memoryUsedResponse); }), http.get( '/instances/:instanceId/actuator/metrics/jvm.memory.committed', () => { return HttpResponse.json(memoryCommittedResponse); }, ), http.get('/instances/:instanceId/actuator/metrics', () => { return HttpResponse.json({ names: ['cache.gets', 'jdbc.connections.active'], }); }), http.get('/instances/:instanceId/actuator/metrics/cache.gets', () => { return HttpResponse.json({ name: 'cache.gets', description: 'The number of cache gets', baseUnit: 'none', measurements: [ { statistic: 'COUNT', value: 150, }, { statistic: 'TOTAL_TIME', value: 120.5, }, { statistic: 'MAX', value: 5.2, }, ], availableTags: [ { tag: 'name', values: ['myCache'], }, { tag: 'cache', values: ['myCache'], }, { tag: 'result', values: ['hit', 'miss'], }, ], }); }), http.get( '/instances/:instanceId/actuator/metrics/jdbc.connections.active', () => { return HttpResponse.json({ name: 'jdbc.connections.active', description: 'The number of active JDBC connections', baseUnit: 'connections', measurements: [ { statistic: 'VALUE', value: 5, }, ], availableTags: [ { tag: 'name', values: ['HikariPool-1'], }, { tag: 'pool', values: ['HikariPool-1'], }, { tag: 'min', values: 1, }, ], }); }, ), http.get('/instances/:instanceId/actuator/metrics/jdbc.connections.min', () => HttpResponse.json({ name: 'jdbc.connections.min', description: 'The minimum number of idle JDBC connections in the pool', baseUnit: 'connections', measurements: [ { statistic: 'VALUE', value: 10, }, ], availableTags: [ { tag: 'pool', values: ['HikariPool-1'], }, ], }), ), http.get('/instances/:instanceId/actuator/metrics/jdbc.connections.max', () => HttpResponse.json({ name: 'jdbc.connections.max', description: 'The minimum number of idle JDBC connections in the pool', baseUnit: 'connections', measurements: [ { statistic: 'VALUE', value: 10, }, ], availableTags: [ { tag: 'pool', values: ['HikariPool-1'], }, ], }), ), ]; export default metricsEntpoints; ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/mocks/instance/scheduledtasks/data.ts ================================================ export const scheduledtasksResponse = { cron: [ { runnable: { target: 'com.example.Processor.processOrders', }, expression: '0 0 0/3 1/1 * ?', nextExecution: { time: '2025-05-22T20:59:59.999087966Z', }, }, ], fixedDelay: [ { runnable: { target: 'com.example.Processor.purge', }, initialDelay: 0, interval: 5000, nextExecution: { time: '2025-05-22T20:03:39.767908889Z', }, lastExecution: { time: '2025-05-22T20:03:34.761508282Z', status: 'SUCCESS', }, }, ], fixedRate: [ { runnable: { target: 'com.example.Processor.retrieveIssues', }, initialDelay: 10000, interval: 3000, nextExecution: { time: '2025-05-22T20:03:44.755151650Z', }, }, ], custom: [ { runnable: { target: 'com.example.Processor$CustomTriggeredRunnable@6e5b7446', }, trigger: 'com.example.Processor$CustomTrigger@513ad29e', lastExecution: { exception: { message: 'Failed while running custom task', type: 'java.lang.IllegalStateException', }, time: '2025-05-22T20:03:34.807437342Z', status: 'ERROR', }, }, ], }; ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/mocks/instance/scheduledtasks/index.ts ================================================ import { HttpResponse, http } from 'msw'; import { scheduledtasksResponse } from '@/mocks/instance/sessions/data'; const endpoints = [ http.get('/instances/:instanceId/actuator/sessions', () => { return HttpResponse.json(scheduledtasksResponse); }), ]; export default endpoints; ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/mocks/instance/sessions/data.ts ================================================ const now = new Date(); const today = [ now.getFullYear(), String(now.getMonth() + 1).padStart(2, '0'), String(now.getDate()).padStart(2, '0'), ].join('-'); export const scheduledtasksResponse = { sessions: [ { id: 'D43F6A32C1E34A7B', creationTime: today + 'T08:23:45.123Z', lastAccessedTime: today + 'T09:12:34.567Z', maxInactiveInterval: 1800, expired: false, principal: 'admin', attributes: { SPRING_SECURITY_CONTEXT: { authentication: { name: 'admin', authorities: ['ROLE_ADMIN', 'ROLE_USER'], authenticated: true, }, }, sessionTrackingId: 'web-session-01', userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36', remoteAddress: '192.168.1.105', }, }, { id: 'E54G7B43D2F45B8C', creationTime: today + 'T08:26:45.234Z', lastAccessedTime: today + 'T09:21:45.678Z', maxInactiveInterval: 1800, expired: false, principal: 'user123', attributes: { SPRING_SECURITY_CONTEXT: { authentication: { name: 'user123', authorities: ['ROLE_USER'], authenticated: true, }, }, sessionTrackingId: 'web-session-02', userAgent: 'PostmanRuntime/7.29.2', remoteAddress: '192.168.1.110', }, }, { id: 'F65H8C54E3G56D9E', creationTime: today + 'T08:32:11.678Z', lastAccessedTime: today + 'T09:15:23.456Z', maxInactiveInterval: 1800, expired: false, principal: 'user456', attributes: { SPRING_SECURITY_CONTEXT: { authentication: { name: 'user456', authorities: ['ROLE_USER'], authenticated: true, }, }, sessionTrackingId: 'web-session-03', userAgent: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)', remoteAddress: '192.168.1.120', }, }, { id: 'G76I9D65F4H67E0F', creationTime: today + 'T09:05:22.345Z', lastAccessedTime: today + 'T09:19:23.456Z', maxInactiveInterval: 1800, expired: false, principal: 'user789', attributes: { SPRING_SECURITY_CONTEXT: { authentication: { name: 'user789', authorities: ['ROLE_USER'], authenticated: true, }, }, sessionTrackingId: 'mobile-session-01', userAgent: 'Mozilla/5.0 (Android 12; Mobile)', remoteAddress: '192.168.1.140', }, }, { id: 'H87J0E76G5I78F1G', creationTime: today + 'T08:45:33.456Z', lastAccessedTime: today + 'T09:10:12.345Z', maxInactiveInterval: 1800, expired: false, principal: 'manager1', attributes: { SPRING_SECURITY_CONTEXT: { authentication: { name: 'manager1', authorities: ['ROLE_MANAGER', 'ROLE_USER'], authenticated: true, }, }, sessionTrackingId: 'web-session-04', userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/96.0.4664.110', remoteAddress: '192.168.1.160', }, }, { id: 'I98K1F87H6J89G2H', creationTime: today + 'T08:50:44.567Z', lastAccessedTime: today + 'T08:55:22.678Z', maxInactiveInterval: 1800, expired: true, principal: 'user321', attributes: { SPRING_SECURITY_CONTEXT: { authentication: { name: 'user321', authorities: ['ROLE_USER'], authenticated: true, }, }, sessionTrackingId: 'web-session-05', userAgent: 'Mozilla/5.0 (iPad; CPU OS 15_0 like Mac OS X)', remoteAddress: '192.168.1.170', }, }, { id: 'J09L2G98I7K90H3I', creationTime: today + 'T09:00:55.678Z', lastAccessedTime: today + 'T09:22:33.789Z', maxInactiveInterval: 1800, expired: false, principal: 'analyst1', attributes: { SPRING_SECURITY_CONTEXT: { authentication: { name: 'analyst1', authorities: ['ROLE_ANALYST', 'ROLE_USER'], authenticated: true, }, }, sessionTrackingId: 'web-session-06', userAgent: 'Mozilla/5.0 (X11; Linux x86_64) Firefox/95.0', remoteAddress: '192.168.1.180', }, }, { id: 'K10M3H09J8L01I4J', creationTime: today + 'T09:05:06.789Z', lastAccessedTime: today + 'T09:20:44.890Z', maxInactiveInterval: 1800, expired: false, principal: 'support1', attributes: { SPRING_SECURITY_CONTEXT: { authentication: { name: 'support1', authorities: ['ROLE_SUPPORT', 'ROLE_USER'], authenticated: true, }, }, sessionTrackingId: 'web-session-07', userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) Edge/96.0.1054.62', remoteAddress: '192.168.1.190', }, }, { id: 'L21N4I10K9M12J5K', creationTime: today + 'T09:10:17.890Z', lastAccessedTime: today + 'T09:10:17.890Z', maxInactiveInterval: 1800, expired: false, principal: 'guest', attributes: { SPRING_SECURITY_CONTEXT: { authentication: { name: 'guest', authorities: ['ROLE_GUEST'], authenticated: true, }, }, sessionTrackingId: 'web-session-08', userAgent: 'Mozilla/5.0 (iPhone; CPU iPhone OS 15_1 like Mac OS X) Safari/605.1.15', remoteAddress: '192.168.1.200', }, }, { id: 'M32O5J21L0N23K6L', creationTime: today + 'T09:15:28.901Z', lastAccessedTime: today + 'T09:18:55.012Z', maxInactiveInterval: 1800, expired: false, principal: 'developer1', attributes: { SPRING_SECURITY_CONTEXT: { authentication: { name: 'developer1', authorities: ['ROLE_DEVELOPER', 'ROLE_USER'], authenticated: true, }, }, sessionTrackingId: 'web-session-09', userAgent: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) Chrome/96.0.4664.110', remoteAddress: '192.168.1.210', }, }, ], sessionCount: 10, activeSessionCount: 9, expiredSessionCount: 1, }; ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/mocks/instance/sessions/index.ts ================================================ import { HttpResponse, http } from 'msw'; import { scheduledtasksResponse } from '@/mocks/instance/scheduledtasks/data'; const endpoints = [ http.get('/instances/:instanceId/actuator/scheduledtasks', () => { return HttpResponse.json(scheduledtasksResponse); }), ]; export default endpoints; ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/mocks/server.ts ================================================ import { setupServer } from 'msw/node'; import auditEventsEndpoint from '@/mocks/instance/auditevents'; import dependenciesEndpoints from '@/mocks/instance/dependencies'; import flywayEndpoints from '@/mocks/instance/flyway'; import healthEndpoint from '@/mocks/instance/health'; import infoEndpoint from '@/mocks/instance/info'; import jolokiaEndpoint from '@/mocks/instance/jolokia'; import liquibaseEndpoints from '@/mocks/instance/liquibase'; import mappingsEndpoint from '@/mocks/instance/mappings'; import metricsEntpoints from '@/mocks/instance/metrics'; const handler = [ ...infoEndpoint, ...healthEndpoint, ...mappingsEndpoint, ...metricsEntpoints, ...liquibaseEndpoints, ...flywayEndpoints, ...auditEventsEndpoint, ...jolokiaEndpoint, ...dependenciesEndpoints, ]; export const server = setupServer(...handler); ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/notificationcenter.d.ts ================================================ declare module '@stekoe/vue-toast-notificationcenter'; ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/notifications.ts ================================================ /* * Copyright 2014-2019 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 { groupBy, values } from 'lodash-es'; import { Subject, bufferTime, filter } from 'rxjs'; import { HealthStatus } from './HealthStatus'; import sbaConfig from './sba-config'; let granted = false; const requestPermissions = async () => { if ('Notification' in window) { granted = window.Notification.permission === 'granted'; if (!granted && window.Notification.permission !== 'denied') { const permission = await window.Notification.requestPermission(); granted = permission === 'granted'; } } }; const notifyForSingleChange = (application, oldApplication) => { return createNotification( `${application.name} is now ${application.status}`, { tag: `${application.name}-${application.status}`, lang: 'en', body: `was ${oldApplication.status}.`, icon: application.status === HealthStatus.UP ? sbaConfig.uiSettings.favicon : sbaConfig.uiSettings.faviconDanger, renotify: true, timeout: 5000, }, ); }; const notifyForBulkChange = ({ count, status, oldStatus }) => { return createNotification(`${count} applications are now ${status}`, { lang: 'en', body: `was ${oldStatus}.`, icon: status === HealthStatus.UP ? sbaConfig.uiSettings.favicon : sbaConfig.uiSettings.faviconDanger, timeout: 5000, }); }; const createNotification = (title, options) => { if (granted) { const notification = new window.Notification(title, options); if (options.url !== null) { notification.onclick = () => { window.focus(); window.open(options.url, '_self'); }; } if (options.timeout > 0) { notification.onshow = () => setTimeout(() => notification.close(), options.timeout); } } }; export default { install: ({ applicationStore }) => { requestPermissions(); const queue = new Subject(); queue .pipe( bufferTime(1000), filter((n) => n.length > 0), ) .subscribe({ next: (events) => { const groupedByChange = groupBy( events, (event) => `${event.oldApplication.status}-${event.application.status}`, ); for (const eventsPerChange of values(groupedByChange)) { if (eventsPerChange.length <= 5) { eventsPerChange.forEach((event) => { notifyForSingleChange(event.application, event.oldApplication); }); } else { notifyForBulkChange({ status: eventsPerChange[0].application.status, oldStatus: eventsPerChange[0].oldApplication.status, count: events.length, }); } } }, }); applicationStore.addEventListener( 'updated', (application, oldApplication) => { if (application.status !== oldApplication.status) { queue.next({ application, oldApplication }); } }, ); }, }; ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/plugins/modal/ConfirmButtons.vue ================================================ ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/plugins/modal/Modal.vue ================================================ ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/plugins/modal/api.ts ================================================ import { h } from 'vue'; import ConfirmButtons from './ConfirmButtons'; import Modal from './Modal'; import { createComponent } from './helpers'; import eventBus from '@/services/bus'; export const useModal = (globalProps = {}) => { const t = globalProps.i18n?.global.t || function (value) { return value; }; return { open(options, slots = {}) { let title = null; if (typeof options === 'string') title = options; const defaultProps = { title, }; const propsData = Object.assign({}, defaultProps, globalProps, options); return createComponent(Modal, propsData, document.body, slots); }, async confirm(title, body) { const bodyFn = () => h( 'span', { innerHTML: body, }, [], ); // eslint-disable-next-line @typescript-eslint/no-unused-vars const { vNode, destroy } = this.open( { title }, { buttons: () => h(ConfirmButtons, { labelOk: t('term.ok'), labelCancel: t('term.cancel'), }), body: bodyFn, }, ); return new Promise((resolve) => { const handler = (result) => { eventBus.off('sba-modal-close', handler); destroy(); resolve(result); }; eventBus.on('sba-modal-close', handler); }); }, }; }; ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/plugins/modal/helpers.ts ================================================ import { h, render } from 'vue'; export function removeElement(el) { if (typeof el.remove !== 'undefined') { el.remove(); } else { el.parentNode?.removeChild(el); } } export function createComponent(component, props, parentContainer, slots = {}) { let vNode = h(component, props, slots); let container = parentContainer.querySelector('.sba-modal--wrapper'); container = container || document.createElement('div'); container.classList.add('sba-modal--wrapper'); parentContainer.appendChild(container); render(vNode, container); const destroy = () => { if (container) render(null, container); container = null; vNode = null; }; return { vNode, destroy, }; } ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/plugins/modal/index.ts ================================================ import { useModal } from './api'; const SbaModalPlugin = { install: (app, options = {}) => { const instance = useModal(options); app.config.globalProperties.$sbaModal = instance; app.provide('$sbaModal', instance); }, }; export default SbaModalPlugin; ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/public/variables.css ================================================ :root { --main-50: /*[(${@cssColorUtils.hexToRgb(uiSettings.theme.palette.getShade50())})]*/ #e8fbef; --main-100: /*[(${@cssColorUtils.hexToRgb(uiSettings.theme.palette.getShade100())})]*/ #d0f7df; --main-200: /*[(${@cssColorUtils.hexToRgb(uiSettings.theme.palette.getShade200())})]*/ #a1efbd; --main-300: /*[(${@cssColorUtils.hexToRgb(uiSettings.theme.palette.getShade300())})]*/ #71e69c; --main-400: /*[(${@cssColorUtils.hexToRgb(uiSettings.theme.palette.getShade400())})]*/ #41de7b; --main-500: /*[(${@cssColorUtils.hexToRgb(uiSettings.theme.palette.getShade500())})]*/ #22c55e; --main-600: /*[(${@cssColorUtils.hexToRgb(uiSettings.theme.palette.getShade600())})]*/ #1a9547; --main-700: /*[(${@cssColorUtils.hexToRgb(uiSettings.theme.palette.getShade700())})]*/ #116530; --main-800: /*[(${@cssColorUtils.hexToRgb(uiSettings.theme.palette.getShade800())})]*/ #09351a; --main-900: /*[(${@cssColorUtils.hexToRgb(uiSettings.theme.palette.getShade900())})]*/ #010603; --bg-color-start: /*[(${uiSettings.theme.palette.getShade300()})]*/ #71e69c; --bg-color-stop: /*[(${uiSettings.theme.palette.getShade700()})]*/ #09351a; } .bg-color-start { transition: 0.4s ease; stop-color: var(--bg-color-start); } .bg-color-stop { transition: 0.4s ease; stop-color: var(--bg-color-stop); } ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/sba-config.ts ================================================ /* * Copyright 2014-2024 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 { merge } from 'lodash-es'; const brand = 'Spring Boot Admin'; const DEFAULT_CONFIG: SBASettings = { uiSettings: { title: 'Spring Boot Admin', brand, theme: { backgroundEnabled: true, color: '#42d3a5', }, rememberMeEnabled: true, enableToasts: false, externalViews: [] as ExternalView[], favicon: 'assets/img/favicon.png', faviconDanger: 'assets/img/favicon-danger.png', notificationFilterEnabled: false, routes: [], availableLanguages: [], viewSettings: [], pollTimer: { cache: 2500, datasource: 2500, gc: 2500, process: 2500, memory: 2500, threads: 2500, logfile: 1000, }, hideInstanceUrl: false, }, user: null, extensions: {}, csrf: { parameterName: '_csrf', headerName: 'X-XSRF-TOKEN', }, use: function (ext) { this.extensions.push(ext); }, }; const mergedConfig = merge(DEFAULT_CONFIG, window.SBA) as SBASettings; export const getCurrentUser = () => { return mergedConfig.user; }; export default mergedConfig; export const useSbaConfig = () => { return mergedConfig; }; ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/sba-settings.js ================================================ /* * Copyright 2014-2019 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. */ //This is a Thymleaf template whill will be rendered by the backend // eslint-disable-next-line @typescript-eslint/no-unused-vars var SBA = { uiSettings: /*[[${uiSettings}]]*/ {}, user: /*[[${user}]]*/ null, extensions: { js: /*[[${jsExtensions}]]*/ [], css: /*[[${cssExtensions}]]*/ [], }, csrf: { parameterName: /*[[${_csrf} ? ${_csrf.parameterName} : 'null']]*/ null, headerName: /*[[${_csrf} ? ${_csrf.headerName} : 'null']]*/ null, }, }; ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/services/application.spec.ts ================================================ /* * Copyright 2014-2020 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 { describe, expect, it } from 'vitest'; import { convertBody, hasMatchingContentType } from './application'; describe('hasMatchingContentType', () => { it('should match content-type', () => { const matches = hasMatchingContentType('application/json;charset=UTF-8', [ 'application/vnd.spring-boot.actuator.v1+json', 'application/json', ]); expect(matches).toBe(true); }); it('should not match content-types', () => { const matches = hasMatchingContentType('application/html;charset=UTF-8', [ 'application/vnd.spring-boot.actuator.v1+json', 'application/json', ]); expect(matches).toBe(false); }); it('should not match undefined', () => { const matches = hasMatchingContentType(undefined, [ 'application/vnd.spring-boot.actuator.v1+json', 'application/json', ]); expect(matches).toBe(false); }); }); describe('convertBody', () => { it('should not convert empty responses', () => { expect(convertBody([])).toEqual([]); }); it('should not convert empty body', () => { expect(convertBody([{}])).toEqual([{}]); }); it('should not convert non-json body', () => { expect( convertBody([{ body: 'foobar', contentType: 'text/plain' }]), ).toEqual([{ body: 'foobar', contentType: 'text/plain' }]); }); it('should convert json body', () => { expect( convertBody([ { body: '{"foo": "bar"}', contentType: 'application/json' }, ]), ).toEqual([{ body: { foo: 'bar' }, contentType: 'application/json' }]); }); }); ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/services/application.ts ================================================ /* * Copyright 2014-2020 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 { AxiosInstance } from 'axios'; import { sortBy } from 'lodash-es'; import { Observable, concat, from, ignoreElements } from 'rxjs'; import axios, { redirectOn401 } from '../utils/axios'; import waitForPolyfill from '../utils/eventsource-polyfill'; import uri from '../utils/uri'; import Instance, { DOWN_STATES, UNKNOWN_STATES, UP_STATES } from './instance'; import { actuatorMimeTypes } from '@/services/spring-mime-types'; export const hasMatchingContentType = ( contentType: string, compatibleContentTypes: Array, ) => Boolean(contentType) && compatibleContentTypes.includes(contentType.replace(/;.*$/, '')); export const convertBody = (responses: any[]) => responses.map((res) => { if ( res.body && hasMatchingContentType(res.contentType, actuatorMimeTypes) ) { return { ...res, body: JSON.parse(res.body), }; } return res; }); export const getStatusInfo = (applications: Application[]) => { const instances = applications.flatMap( (application) => application.instances, ); const upCount = instances.filter((instance) => UP_STATES.includes(instance.statusInfo.status), ).length; const downCount = instances.filter((instance) => DOWN_STATES.includes(instance.statusInfo.status), ).length; const unknownCount = instances.filter((instance) => UNKNOWN_STATES.includes(instance.statusInfo.status), ).length; return { upCount, downCount, unknownCount, allUp: upCount === instances.length, allDown: downCount === instances.length, allUnknown: unknownCount === instances.length, someUnknown: unknownCount > 0 && unknownCount < instances.length, someDown: downCount > 0 && downCount < instances.length, }; }; class Application { public readonly name: string; public readonly instances: Instance[]; public readonly buildVersion? = {} as { value: string }; public readonly status: string; public readonly statusTimestamp: string; private readonly axios: AxiosInstance; constructor({ name, instances, ...application }: { name: string; instances: any[]; [key: string]: any; }) { Object.assign(this, application); this.name = name; this.axios = axios.create({ baseURL: uri`applications/${this.name}`, headers: { 'X-SBA-REQUEST': true, }, }); this.axios.interceptors.response.use( (response) => response, redirectOn401(), ); this.instances = sortBy( instances.map( (i) => new Instance(i), [(instance) => instance.registration.healthUrl], ), ); } get isUnregisterable() { return this.instances.some((i) => i.isUnregisterable); } get hasShutdownEndpoint() { return this.hasEndpoint('shutdown'); } get hasRestartEndpoint() { return this.hasEndpoint('restart'); } static async list() { return axios.get('applications', { headers: { Accept: 'application/json', 'X-SBA-REQUEST': true }, transformResponse: Application._transformResponse, }); } static getStream(): Observable { return concat( from(waitForPolyfill()).pipe(ignoreElements()), Observable.create((observer) => { const eventSource = new EventSource('applications'); eventSource.onmessage = (message) => observer.next({ ...message, data: Application._transformResponse(message.data), } as ApplicationStream); eventSource.onerror = (err) => observer.error(err); return () => eventSource.close(); }), ); } static _transformResponse(data: string) { if (!data) { return data; } const json = JSON.parse(data); if (json instanceof Array) { const applications = json.map((j) => new Application(j)); return sortBy(applications, [(item) => item.name]); } return new Application(json); } filterInstances(predicate: (instance: Instance) => boolean) { return new Application({ ...this, instances: this.instances.filter(predicate), }); } hasEndpoint(endpointId: string): boolean { return this.instances.some((i) => i.hasEndpoint(endpointId)); } findInstance(instanceId: string): Instance | undefined { return this.instances.find((instance) => instance.id === instanceId); } async unregister() { return this.axios.delete('', { headers: { Accept: 'application/json' }, }); } async fetchLoggers() { const responses = convertBody( ( await this.axios.get(uri`actuator/loggers`, { headers: { Accept: actuatorMimeTypes.join(',') }, }) ).data, ); return { responses }; } async configureLogger(name: string, level: string | null) { const responses = ( await this.axios.post( uri`actuator/loggers/${name}`, level === null ? {} : { configuredLevel: level }, { headers: { 'Content-Type': 'application/json' } }, ) ).data; return { responses }; } async setEnv(name: string, value: string) { return this.axios.post( uri`actuator/env`, { name, value }, { headers: { 'Content-Type': 'application/json' }, }, ); } async resetEnv() { return this.axios.delete(uri`actuator/env`); } async refreshContext() { return this.axios.post(uri`actuator/refresh`); } async clearCaches() { return this.axios.delete(uri`actuator/caches`); } async clearCache(name: string, cacheManager?: string) { return this.axios.delete(uri`actuator/caches/${name}`, { params: { cacheManager: cacheManager }, }); } shutdown() { return this.axios.post(uri`actuator/shutdown`); } restart() { return this.axios.post(uri`actuator/restart`); } async writeMBeanAttribute( domain: string, mBean: string, attribute: string, value: any, ) { const body = { type: 'write', mbean: `${domain}:${mBean}`, attribute, value, }; return this.axios.post(uri`actuator/jolokia`, body, { headers: { Accept: 'application/json', 'Content-Type': 'application/json', }, }); } async invokeMBeanOperation( domain: string, mBean: string, operation: string, args: any[], ) { const body = { type: 'exec', mbean: `${domain}:${mBean}`, operation, arguments: args, }; return this.axios.post(uri`actuator/jolokia`, body, { headers: { Accept: 'application/json', 'Content-Type': 'application/json', }, }); } } export default Application; ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/services/bus.ts ================================================ import mitt from 'mitt'; const eventBus = mitt(); export default eventBus; ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/services/instance.spec.ts ================================================ import { describe, expect, test, vi } from 'vitest'; import Instance from '@/services/instance'; const { useSbaConfig } = vi.hoisted(() => ({ useSbaConfig: vi.fn().mockReturnValue(true), })); vi.mock('@/sba-config', async (importOriginal) => ({ ...(await importOriginal()), useSbaConfig, })); describe('Instance', () => { test.each` hideInstanceUrl | metadataHideUrl | expectUrlToBeShownOnUI ${false} | ${'true'} | ${false} ${false} | ${'false'} | ${true} ${false} | ${undefined} | ${true} ${true} | ${'true'} | ${false} ${true} | ${'false'} | ${false} `( 'showUrl when hideInstanceUrl=$hideInstanceUrl and metadataHideUrl=$metadataHideUrl should return $expectUrlToBeShownOnUI', ({ hideInstanceUrl, metadataHideUrl, expectUrlToBeShownOnUI }) => { useSbaConfig.mockReturnValue({ uiSettings: { hideInstanceUrl, }, }); const instance = new Instance({ id: 'id', registration: { metadata: { ['hide-url']: metadataHideUrl, }, }, }); expect(instance.showUrl()).toEqual(expectUrlToBeShownOnUI); }, ); }); ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/services/instance.ts ================================================ /* * Copyright 2014-2019 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 { AxiosInstance } from 'axios'; import saveAs from 'file-saver'; import { Observable, concat, from, ignoreElements } from 'rxjs'; import axios, { redirectOn401, registerErrorToastInterceptor, } from '../utils/axios'; import waitForPolyfill from '../utils/eventsource-polyfill'; import logtail from '../utils/logtail'; import uri from '../utils/uri'; import { useSbaConfig } from '@/sba-config'; import { actuatorMimeTypes } from '@/services/spring-mime-types'; import { transformToJSON } from '@/utils/transformToJSON'; const isInstanceActuatorRequest = (url: string) => url.match(/^instances[/][^/]+[/]actuator([/].*)?$/); class Instance { public readonly id: string; private readonly axios: AxiosInstance; public registration: Registration; public endpoints: Endpoint[] = []; public availableMetrics: string[] = []; public tags: { [key: string]: string }[]; public statusTimestamp: string; public buildVersion: string; public statusInfo: StatusInfo; constructor({ id, ...instance }: InstanceData) { Object.assign(this, instance); this.id = id; this.axios = axios.create({ withCredentials: true, baseURL: uri`instances/${this.id}`, headers: { Accept: actuatorMimeTypes.join(',') }, }); this.axios.interceptors.response.use( (response) => response, redirectOn401( (error) => !isInstanceActuatorRequest(error.config.baseURL + error.config.url), ), ); registerErrorToastInterceptor(this.axios); } get metadata() { return this.registration.metadata; } get metadataParsed() { const metadata = this.registration.metadata || {}; return transformToJSON(metadata, 'LAX'); } get isUnregisterable() { return this.registration.source === 'http-api'; } static async fetchEvents() { return axios.get(uri`instances/events`, { headers: { Accept: 'application/json' }, }); } static getEventStream() { return concat( from(waitForPolyfill()).pipe(ignoreElements()), Observable.create((observer) => { const eventSource = new EventSource('instances/events'); eventSource.onmessage = (message) => observer.next({ ...message, data: JSON.parse(message.data), }); eventSource.onerror = (err) => observer.error(err); return () => { eventSource.close(); }; }), ); } static async get(id: string) { return axios.get(uri`instances/${id}`, { headers: { Accept: 'application/json' }, transformResponse(data: string) { if (!data) { return data; } const instance = JSON.parse(data); return new Instance(instance); }, }); } private static _toMBeans(data: string) { if (!data) { return data; } const raw = JSON.parse(data); return Object.entries(raw.value).map(([domain, mBeans]) => ({ domain, mBeans: Object.entries(mBeans as Record).map( ([descriptor, mBean]) => ({ descriptor: descriptor, ...mBean, }), ), })); } showUrl() { const sbaConfig = useSbaConfig(); if (sbaConfig.uiSettings.hideInstanceUrl) { return false; } const hideUrlMetadata = this.registration.metadata?.['hide-url']; return hideUrlMetadata !== 'true'; } isUrlDisabled() { const sbaConfig = useSbaConfig(); if (sbaConfig.uiSettings.disableInstanceUrl) { return true; } const disableUrl = this.registration.metadata?.['disable-url']; return disableUrl === 'true'; } hasEndpoint(endpointId: string): boolean { return this.endpoints.some((endpoint) => endpoint.id === endpointId); } async unregister() { return this.axios.delete('', { headers: { Accept: 'application/json' }, }); } async fetchInfo() { return this.axios.get(uri`actuator/info`); } async fetchMetrics() { const response = await this.axios.get(uri`actuator/metrics`); this.availableMetrics = response?.data?.names ?? []; return response; } async fetchMetric(metric: string, tags: Record) { if (this.availableMetrics.length === 0) { try { await this.fetchMetrics(); } catch (e) { console.error('Available metrics could not be determined.', e); } } if (!this.availableMetrics.includes(metric)) { console.warn( `Metric '${metric}' seems not to be available on instance '${this.id}'.`, ); return; } const params = new URLSearchParams(); if (tags) { let firstElementDuplicated = false; Object.entries(tags) .filter(([, value]) => typeof value !== 'undefined' && value !== null) .forEach(([name, value]) => { params.append('tag', `${name}:${value}`); if (!firstElementDuplicated) { // workaround for tags that contains comma // take a look at https://github.com/spring-projects/spring-framework/issues/23820#issuecomment-543087878 // If there is single tag specified and name or value contains comma then it will be incorrectly split into several parts // To bypass it we duplicate first tag. params.append('tag', `${name}:${value}`); firstElementDuplicated = true; } }); } return this.axios.get(uri`actuator/metrics/${metric}`, { params, }); } async fetchHealth() { return await this.axios.get(uri`actuator/health`, { validateStatus: null, }); } async fetchHealthGroup(groupName: string) { return await this.axios.get(uri`actuator/health/${groupName}`, { validateStatus: null, }); } async fetchEnv(name?: string) { return this.axios.get(uri`actuator/env/${name || ''}`); } async fetchConfigprops() { return this.axios.get(uri`actuator/configprops`); } async hasEnvManagerSupport() { const response = await this.axios.options(uri`actuator/env`); return ( response.headers['allow'] && response.headers['allow'].includes('POST') ); } async resetEnv() { return this.axios.delete(uri`actuator/env`); } async setEnv(name: string, value: string) { return this.axios.post( uri`actuator/env`, { name, value }, { headers: { 'Content-Type': 'application/json' }, }, ); } async refreshContext() { return this.axios.post(uri`actuator/refresh`); } async busRefreshContext() { return this.axios.post(uri`actuator/busrefresh`); } async fetchLiquibase() { return this.axios.get(uri`actuator/liquibase`); } async fetchScheduledTasks() { return this.axios.get(uri`actuator/scheduledtasks`); } async fetchGatewayGlobalFilters() { return this.axios.get(uri`actuator/gateway/globalfilters`); } async addGatewayRoute(route: { id: string; [key: string]: any }) { return this.axios.post(uri`actuator/gateway/routes/${route.id}`, route, { headers: { 'Content-Type': 'application/json' }, }); } async fetchGatewayRoutes() { return this.axios.get(uri`actuator/gateway/routes`); } async deleteGatewayRoute(routeId: string) { return this.axios.delete(uri`actuator/gateway/routes/${routeId}`); } async refreshGatewayRoutesCache() { return this.axios.post(uri`actuator/gateway/refresh`); } async fetchCaches() { return this.axios.get(uri`actuator/caches`); } async clearCaches() { return this.axios.delete(uri`actuator/caches`); } async clearCache(name: string, cacheManager?: string) { return this.axios.delete(uri`actuator/caches/${name}`, { params: { cacheManager: cacheManager }, }); } async fetchFlyway() { return this.axios.get(uri`actuator/flyway`); } async fetchLoggers() { return this.axios.get(uri`actuator/loggers`); } async configureLogger(name: string, level: string | null) { await this.axios.post( uri`actuator/loggers/${name}`, level === null ? {} : { configuredLevel: level }, { headers: { 'Content-Type': 'application/json' }, }, ); } async fetchHttptrace() { return this.axios.get(uri`actuator/httptrace`); } async fetchHttpExchanges() { return this.axios.get(uri`actuator/httpexchanges`); } async fetchBeans() { return this.axios.get(uri`actuator/beans`); } async fetchConditions() { return this.axios.get(uri`actuator/conditions`); } async fetchThreaddump() { return this.axios.get(uri`actuator/threaddump`); } async downloadThreaddump() { const res = await this.axios.get(uri`actuator/threaddump`, { headers: { Accept: 'text/plain' }, }); const blob = new Blob([res.data], { type: 'text/plain;charset=utf-8' }); saveAs(blob, this.registration.name + '-threaddump.txt'); } async fetchAuditevents({ after, type, principal, }: { after: Date; type?: string; principal?: string; }) { return this.axios.get(uri`actuator/auditevents`, { params: { after: after.toISOString(), type: type, principal: principal, }, }); } async fetchSessionsByUsername(username?: string) { return this.axios.get(uri`actuator/sessions`, { params: { username: username, }, }); } async fetchSession(sessionId: string) { return this.axios.get(uri`actuator/sessions/${sessionId}`); } async deleteSession(sessionId: string) { return this.axios.delete(uri`actuator/sessions/${sessionId}`); } async fetchStartup() { const optionsResponse = await this.axios.options(uri`actuator/startup`); if ( optionsResponse.headers.allow && optionsResponse.headers.allow.includes('GET') ) { return this.axios.get(uri`actuator/startup`); } return this.axios.post(uri`actuator/startup`); } streamLogfile(interval: number) { return logtail( (opt) => this.axios.get(uri`actuator/logfile`, opt), interval, ); } async listMBeans() { return this.axios.get(uri`actuator/jolokia/list`, { headers: { Accept: 'application/json' }, params: { canonicalNaming: false }, transformResponse: Instance._toMBeans, }); } async readMBeanAttributes(domain: string, mBean: string) { const body = { type: 'read', mbean: `${domain}:${mBean}`, config: { ignoreErrors: true }, }; return this.axios.post(uri`actuator/jolokia`, body, { headers: { Accept: 'application/json', 'Content-Type': 'application/json', }, }); } async writeMBeanAttribute( domain: string, mBean: string, attribute: string, value: any, ) { const body = { type: 'write', mbean: `${domain}:${mBean}`, attribute, value, }; return this.axios.post(uri`actuator/jolokia`, body, { headers: { Accept: 'application/json', 'Content-Type': 'application/json', }, }); } async invokeMBeanOperation( domain: string, mBean: string, operation: string, args: any[], ) { const body = { type: 'exec', mbean: `${domain}:${mBean}`, operation, arguments: args, }; return this.axios.post(uri`actuator/jolokia`, body, { headers: { Accept: 'application/json', 'Content-Type': 'application/json', }, }); } async fetchMappings() { return this.axios.get(uri`actuator/mappings`); } async fetchQuartzJobs() { return this.axios.get(uri`actuator/quartz/jobs`, { headers: { Accept: 'application/json' }, }); } async fetchQuartzJob(group, name) { return this.axios.get(uri`actuator/quartz/jobs/${group}/${name}`, { headers: { Accept: 'application/json' }, }); } async fetchQuartzTriggers() { return this.axios.get(uri`actuator/quartz/triggers`, { headers: { Accept: 'application/json' }, }); } async fetchQuartzTrigger(group, name) { return this.axios.get(uri`actuator/quartz/triggers/${group}/${name}`, { headers: { Accept: 'application/json' }, }); } async fetchSbomIds() { return this.axios.get(uri`actuator/sbom`, { headers: { Accept: 'application/json' }, }); } async fetchSbom(id: string) { return this.axios.get(uri`actuator/sbom/${id}`, { headers: { Accept: '*/*' }, }); } shutdown() { return this.axios.post(uri`actuator/shutdown`); } restart() { return this.axios.post(uri`actuator/restart`); } } export default Instance; export type Registration = { name: string; managementUrl?: string; healthUrl: string; serviceUrl?: string; source: string; metadata?: { [key: string]: string }; }; type StatusInfo = { status: | 'UNKNOWN' | 'OUT_OF_SERVICE' | 'UP' | 'DOWN' | 'OFFLINE' | 'RESTRICTED' | string; details: { [key: string]: string }; }; type InstanceData = { id: string; registration: Registration; endpoints?: Endpoint[]; availableMetrics?: string[]; tags?: { [key: string]: string }[]; statusTimestamp?: string; buildVersion?: string; statusInfo?: StatusInfo; }; type Endpoint = { id: string; url: string; }; export const DOWN_STATES = ['OUT_OF_SERVICE', 'DOWN', 'OFFLINE', 'RESTRICTED']; export const UP_STATES = ['UP']; export const UNKNOWN_STATES = ['UNKNOWN']; ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/services/notification-filter.ts ================================================ /* * Copyright 2014-2019 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 moment from 'moment'; import sbaConfig from '@/sba-config'; import axios from '@/utils/axios'; import uri from '@/utils/uri'; class NotificationFilter { private id: string; private applicationName: string; private instanceId: string; private expiry: moment.Moment | null; constructor({ expiry, ...filter }) { Object.assign(this, filter); this.expiry = expiry ? moment(expiry) : null; } affects(obj) { if (!obj) { return false; } if (this.isApplicationFilter) { return this.applicationName === obj.name; } if (this.isInstanceFilter) { return this.instanceId === obj.id; } return false; } get isApplicationFilter() { return this.applicationName != null; } get isInstanceFilter() { return this.instanceId != null; } async delete() { return axios.delete(uri`notifications/filters/${this.id}`); } static isSupported() { return Boolean(sbaConfig.uiSettings.notificationFilterEnabled); } static async getFilters() { return axios.get('notifications/filters', { transformResponse: NotificationFilter._transformResponse, }); } static async addFilter(object, ttl) { const params = { ttl }; if ('name' in object) { params.applicationName = object.name; } else if ('id' in object) { params.instanceId = object.id; } return axios.post('notifications/filters', null, { params, transformResponse: NotificationFilter._transformResponse, }); } static _transformResponse(data) { if (!data) { return data; } const json = JSON.parse(data); if (json instanceof Array) { return json .map(NotificationFilter._toNotificationFilters) .filter((f) => !f.expired); } return NotificationFilter._toNotificationFilters(json); } static _toNotificationFilters(notificationFilter) { return new NotificationFilter(notificationFilter); } } export default NotificationFilter; ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/services/spring-mime-types.ts ================================================ export const actuatorMimeTypes = [ 'application/vnd.spring-boot.actuator.v3+json', 'application/vnd.spring-boot.actuator.v2+json', 'application/vnd.spring-boot.actuator.v1+json', 'application/json', ]; ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/services/startup-activator-tree.ts ================================================ /* * Copyright 2014-2020 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. */ export class StartupActuatorEventTree { constructor(events) { this.events = events; } getEvents() { return this.events || []; } getRoots() { return this.getByDepth(0); } getByDepth(depth) { return this.getEvents().filter( (event) => event.startupStep.depth === depth, ); } getById(id) { return this.getEvents().find((event) => event.startupStep.id === id); } getByParentId(parentId) { return this.getEvents().filter( (event) => event.startupStep.parentId === parentId, ); } getStartTime() { return this.getEvents() .map((e) => Date.parse(e.startTime)) .reduce((a, b) => Math.min(a, b), Number.MAX_VALUE); } getEndTime() { return this.getEvents() .map((e) => Date.parse(e.endTime)) .reduce((a, b) => Math.max(a, b), Number.MIN_VALUE); } getPath(id) { const event = this.getById(id); if (!event) { return []; } const path = [id]; let parent = event.startupStep.parent; while (parent !== null && parent !== undefined) { path.push(parent.startupStep.id); parent = parent.startupStep.parent; } return path; } getPeriod(event) { const eventStartTime = Date.parse(event.startTime); const eventEndTime = Date.parse(event.endTime); const treeStartTime = this.getStartTime(); const treeEndTime = this.getEndTime(); const treeTimeSpan = treeEndTime - treeStartTime; const relativeEventStartTime = eventStartTime - treeStartTime; const relativeEventEndTime = eventEndTime - treeStartTime; const relativeStart = relativeEventStartTime > 0 ? relativeEventStartTime / treeTimeSpan : 0; const relativeEnd = relativeEventEndTime > 0 ? relativeEventEndTime / treeTimeSpan : 0; return { start: relativeStart, end: relativeEnd, }; } } ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/services/startup-actuator.fixture.spec.json ================================================ { "springBootVersion": "2.4.0", "timeline": { "startTime": "2020-12-10T21:53:41.801353Z", "events": [ { "startupStep": { "name": "spring.boot.application.starting", "id": 1, "parentId": 0, "tags": [ { "key": "mainApplicationClass", "value": "de.codecentric.boot.admin.SpringBootAdminServletApplication" } ] }, "startTime": "2020-12-10T21:53:41.836728041Z", "endTime": "2020-12-10T21:53:41.882007902Z", "duration": "PT0.045279861S" }, { "startupStep": { "name": "spring.boot.application.environment-prepared", "id": 2, "parentId": 0, "tags": [] }, "startTime": "2020-12-10T21:53:42.077870978Z", "endTime": "2020-12-10T21:53:42.678761997Z", "duration": 0.600891019 }, { "startupStep": { "name": "spring.boot.application.context-prepared", "id": 3, "parentId": 0, "tags": [] }, "startTime": "2020-12-10T21:53:42.770730378Z", "endTime": "2020-12-10T21:53:42.772108879Z", "duration": "PT0.001378501S" }, { "startupStep": { "name": "spring.boot.application.context-loaded", "id": 4, "parentId": 0, "tags": [] }, "startTime": "2020-12-10T21:53:42.899752225Z", "endTime": "2020-12-10T21:53:42.905630362Z", "duration": "PT0.005878137S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 8, "parentId": 7, "tags": [ { "key": "beanName", "value": "org.springframework.boot.autoconfigure.internalCachingMetadataReaderFactory" } ] }, "startTime": "2020-12-10T21:53:42.958550077Z", "endTime": "2020-12-10T21:53:42.960040652Z", "duration": "PT0.001490575S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 7, "parentId": 6, "tags": [ { "key": "beanName", "value": "org.springframework.context.annotation.internalConfigurationAnnotationProcessor" }, { "key": "beanType", "value": "interface org.springframework.beans.factory.support.BeanDefinitionRegistryPostProcessor" } ] }, "startTime": "2020-12-10T21:53:42.938902628Z", "endTime": "2020-12-10T21:53:42.993399929Z", "duration": "PT0.054497301S" }, { "startupStep": { "name": "spring.context.config-classes.parse", "id": 10, "parentId": 9, "tags": [ { "key": "classCount", "value": "186" } ] }, "startTime": "2020-12-10T21:53:43.007880681Z", "endTime": "2020-12-10T21:53:44.822142600Z", "duration": "PT1.814261919S" }, { "startupStep": { "name": "spring.context.beandef-registry.post-process", "id": 9, "parentId": 6, "tags": [ { "key": "postProcessor", "value": "org.springframework.context.annotation.ConfigurationClassPostProcessor@1d572e62" } ] }, "startTime": "2020-12-10T21:53:42.993465304Z", "endTime": "2020-12-10T21:53:44.836797924Z", "duration": "PT1.84333262S" }, { "startupStep": { "name": "spring.context.bean-factory.post-process", "id": 11, "parentId": 6, "tags": [ { "key": "postProcessor", "value": "org.springframework.boot.autoconfigure.SharedMetadataReaderFactoryContextInitializer$CachingMetadataReaderFactoryPostProcessor@7a8b9166" } ] }, "startTime": "2020-12-10T21:53:44.838366162Z", "endTime": "2020-12-10T21:53:44.838499998Z", "duration": "PT0.000133836S" }, { "startupStep": { "name": "spring.context.bean-factory.post-process", "id": 12, "parentId": 6, "tags": [ { "key": "postProcessor", "value": "org.springframework.boot.context.ConfigurationWarningsApplicationContextInitializer$ConfigurationWarningsPostProcessor@4acc5dff" } ] }, "startTime": "2020-12-10T21:53:44.838507052Z", "endTime": "2020-12-10T21:53:44.838514129Z", "duration": "PT0.000007077S" }, { "startupStep": { "name": "spring.context.config-classes.enhance", "id": 14, "parentId": 13, "tags": [] }, "startTime": "2020-12-10T21:53:44.838529850Z", "endTime": "2020-12-10T21:53:44.839152967Z", "duration": "PT0.000623117S" }, { "startupStep": { "name": "spring.context.bean-factory.post-process", "id": 13, "parentId": 6, "tags": [ { "key": "postProcessor", "value": "org.springframework.context.annotation.ConfigurationClassPostProcessor@1d572e62" } ] }, "startTime": "2020-12-10T21:53:44.838516975Z", "endTime": "2020-12-10T21:53:44.839430759Z", "duration": "PT0.000913784S" }, { "startupStep": { "name": "spring.context.bean-factory.post-process", "id": 15, "parentId": 6, "tags": [ { "key": "postProcessor", "value": "org.springframework.boot.LazyInitializationBeanFactoryPostProcessor@7e446d92" } ] }, "startTime": "2020-12-10T21:53:44.839436254Z", "endTime": "2020-12-10T21:53:44.840566795Z", "duration": "PT0.001130541S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 16, "parentId": 6, "tags": [ { "key": "beanName", "value": "propertySourcesPlaceholderConfigurer" }, { "key": "beanType", "value": "interface org.springframework.beans.factory.config.BeanFactoryPostProcessor" } ] }, "startTime": "2020-12-10T21:53:44.840830662Z", "endTime": "2020-12-10T21:53:44.842673263Z", "duration": "PT0.001842601S" }, { "startupStep": { "name": "spring.context.bean-factory.post-process", "id": 17, "parentId": 6, "tags": [ { "key": "postProcessor", "value": "org.springframework.context.support.PropertySourcesPlaceholderConfigurer@2100d047" } ] }, "startTime": "2020-12-10T21:53:44.842686283Z", "endTime": "2020-12-10T21:53:44.844940434Z", "duration": "PT0.002254151S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 18, "parentId": 6, "tags": [ { "key": "beanName", "value": "org.springframework.context.event.internalEventListenerProcessor" }, { "key": "beanType", "value": "interface org.springframework.beans.factory.config.BeanFactoryPostProcessor" } ] }, "startTime": "2020-12-10T21:53:44.844971919Z", "endTime": "2020-12-10T21:53:44.845821402Z", "duration": "PT0.000849483S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 19, "parentId": 6, "tags": [ { "key": "beanName", "value": "preserveErrorControllerTargetClassPostProcessor" }, { "key": "beanType", "value": "interface org.springframework.beans.factory.config.BeanFactoryPostProcessor" } ] }, "startTime": "2020-12-10T21:53:44.845832086Z", "endTime": "2020-12-10T21:53:44.845957179Z", "duration": "PT0.000125093S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 20, "parentId": 6, "tags": [ { "key": "beanName", "value": "conversionServicePostProcessor" }, { "key": "beanType", "value": "interface org.springframework.beans.factory.config.BeanFactoryPostProcessor" } ] }, "startTime": "2020-12-10T21:53:44.845964548Z", "endTime": "2020-12-10T21:53:44.846387495Z", "duration": "PT0.000422947S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 22, "parentId": 21, "tags": [ { "key": "beanName", "value": "org.springframework.context.event.internalEventListenerFactory" } ] }, "startTime": "2020-12-10T21:53:44.846856708Z", "endTime": "2020-12-10T21:53:44.846921706Z", "duration": "PT0.000064998S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 23, "parentId": 21, "tags": [ { "key": "beanName", "value": "org.springframework.transaction.config.internalTransactionalEventListenerFactory" } ] }, "startTime": "2020-12-10T21:53:44.846928712Z", "endTime": "2020-12-10T21:53:44.847038664Z", "duration": "PT0.000109952S" }, { "startupStep": { "name": "spring.context.bean-factory.post-process", "id": 21, "parentId": 6, "tags": [ { "key": "postProcessor", "value": "org.springframework.context.event.EventListenerMethodProcessor@1f53481b" } ] }, "startTime": "2020-12-10T21:53:44.846393630Z", "endTime": "2020-12-10T21:53:44.847076446Z", "duration": "PT0.000682816S" }, { "startupStep": { "name": "spring.context.bean-factory.post-process", "id": 24, "parentId": 6, "tags": [ { "key": "postProcessor", "value": "org.springframework.boot.autoconfigure.web.servlet.error.ErrorMvcAutoConfiguration$PreserveErrorControllerTargetClassPostProcessor@27e7c77f" } ] }, "startTime": "2020-12-10T21:53:44.847080458Z", "endTime": "2020-12-10T21:53:44.847649741Z", "duration": "PT0.000569283S" }, { "startupStep": { "name": "spring.context.bean-factory.post-process", "id": 25, "parentId": 6, "tags": [ { "key": "postProcessor", "value": "org.springframework.security.config.crypto.RsaKeyConversionServicePostProcessor@37c36608" } ] }, "startTime": "2020-12-10T21:53:44.847655132Z", "endTime": "2020-12-10T21:53:44.850160232Z", "duration": "PT0.0025051S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 26, "parentId": 6, "tags": [ { "key": "beanName", "value": "org.springframework.context.annotation.internalAutowiredAnnotationProcessor" }, { "key": "beanType", "value": "interface org.springframework.beans.factory.config.BeanPostProcessor" } ] }, "startTime": "2020-12-10T21:53:44.851920181Z", "endTime": "2020-12-10T21:53:44.852304028Z", "duration": "PT0.000383847S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 27, "parentId": 6, "tags": [ { "key": "beanName", "value": "org.springframework.context.annotation.internalCommonAnnotationProcessor" }, { "key": "beanType", "value": "interface org.springframework.beans.factory.config.BeanPostProcessor" } ] }, "startTime": "2020-12-10T21:53:44.852315102Z", "endTime": "2020-12-10T21:53:44.854369560Z", "duration": "PT0.002054458S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 30, "parentId": 29, "tags": [ { "key": "beanName", "value": "org.springframework.boot.context.internalConfigurationPropertiesBinderFactory" }, { "key": "beanType", "value": "class org.springframework.boot.context.properties.ConfigurationPropertiesBinder$Factory" } ] }, "startTime": "2020-12-10T21:53:44.854490772Z", "endTime": "2020-12-10T21:53:44.854559976Z", "duration": "PT0.000069204S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 29, "parentId": 28, "tags": [ { "key": "beanName", "value": "org.springframework.boot.context.internalConfigurationPropertiesBinder" }, { "key": "beanType", "value": "class org.springframework.boot.context.properties.ConfigurationPropertiesBinder" } ] }, "startTime": "2020-12-10T21:53:44.854460593Z", "endTime": "2020-12-10T21:53:44.855712643Z", "duration": "PT0.00125205S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 28, "parentId": 6, "tags": [ { "key": "beanName", "value": "org.springframework.boot.context.properties.ConfigurationPropertiesBindingPostProcessor" }, { "key": "beanType", "value": "interface org.springframework.beans.factory.config.BeanPostProcessor" } ] }, "startTime": "2020-12-10T21:53:44.854382719Z", "endTime": "2020-12-10T21:53:44.855721551Z", "duration": "PT0.001338832S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 31, "parentId": 6, "tags": [ { "key": "beanName", "value": "dataSourceInitializerPostProcessor" }, { "key": "beanType", "value": "interface org.springframework.beans.factory.config.BeanPostProcessor" } ] }, "startTime": "2020-12-10T21:53:44.855918113Z", "endTime": "2020-12-10T21:53:44.857439105Z", "duration": "PT0.001520992S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 33, "parentId": 32, "tags": [ { "key": "beanName", "value": "org.springframework.scheduling.annotation.SchedulingConfiguration" } ] }, "startTime": "2020-12-10T21:53:44.857489672Z", "endTime": "2020-12-10T21:53:44.858026207Z", "duration": "PT0.000536535S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 32, "parentId": 6, "tags": [ { "key": "beanName", "value": "org.springframework.context.annotation.internalScheduledAnnotationProcessor" }, { "key": "beanType", "value": "interface org.springframework.beans.factory.config.BeanPostProcessor" } ] }, "startTime": "2020-12-10T21:53:44.857446720Z", "endTime": "2020-12-10T21:53:44.859566422Z", "duration": "PT0.002119702S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 34, "parentId": 6, "tags": [ { "key": "beanName", "value": "persistenceExceptionTranslationPostProcessor" }, { "key": "beanType", "value": "interface org.springframework.beans.factory.config.BeanPostProcessor" } ] }, "startTime": "2020-12-10T21:53:44.859575037Z", "endTime": "2020-12-10T21:53:44.868684966Z", "duration": "PT0.009109929S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 35, "parentId": 6, "tags": [ { "key": "beanName", "value": "org.springframework.aop.config.internalAutoProxyCreator" }, { "key": "beanType", "value": "interface org.springframework.beans.factory.config.BeanPostProcessor" } ] }, "startTime": "2020-12-10T21:53:44.868701999Z", "endTime": "2020-12-10T21:53:44.884744010Z", "duration": "PT0.016042011S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 36, "parentId": 6, "tags": [ { "key": "beanName", "value": "webServerFactoryCustomizerBeanPostProcessor" }, { "key": "beanType", "value": "interface org.springframework.beans.factory.config.BeanPostProcessor" } ] }, "startTime": "2020-12-10T21:53:44.884781597Z", "endTime": "2020-12-10T21:53:44.885065574Z", "duration": "PT0.000283977S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 37, "parentId": 6, "tags": [ { "key": "beanName", "value": "errorPageRegistrarBeanPostProcessor" }, { "key": "beanType", "value": "interface org.springframework.beans.factory.config.BeanPostProcessor" } ] }, "startTime": "2020-12-10T21:53:44.885074022Z", "endTime": "2020-12-10T21:53:44.885241629Z", "duration": "PT0.000167607S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 40, "parentId": 39, "tags": [ { "key": "beanName", "value": "org.springframework.transaction.annotation.ProxyTransactionManagementConfiguration" } ] }, "startTime": "2020-12-10T21:53:44.887124534Z", "endTime": "2020-12-10T21:53:44.904479489Z", "duration": "PT0.017354955S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 41, "parentId": 39, "tags": [ { "key": "beanName", "value": "transactionAttributeSource" } ] }, "startTime": "2020-12-10T21:53:44.905815649Z", "endTime": "2020-12-10T21:53:44.914842149Z", "duration": "PT0.0090265S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 42, "parentId": 39, "tags": [ { "key": "beanName", "value": "transactionInterceptor" } ] }, "startTime": "2020-12-10T21:53:44.915755794Z", "endTime": "2020-12-10T21:53:44.958518037Z", "duration": "PT0.042762243S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 39, "parentId": 38, "tags": [ { "key": "beanName", "value": "org.springframework.transaction.config.internalTransactionAdvisor" }, { "key": "beanType", "value": "interface org.springframework.aop.Advisor" } ] }, "startTime": "2020-12-10T21:53:44.887064010Z", "endTime": "2020-12-10T21:53:44.967011446Z", "duration": "PT0.079947436S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 38, "parentId": 6, "tags": [ { "key": "beanName", "value": "healthEndpointGroupsBeanPostProcessor" }, { "key": "beanType", "value": "interface org.springframework.beans.factory.config.BeanPostProcessor" } ] }, "startTime": "2020-12-10T21:53:44.885249353Z", "endTime": "2020-12-10T21:53:44.971580308Z", "duration": "PT0.086330955S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 43, "parentId": 6, "tags": [ { "key": "beanName", "value": "meterRegistryPostProcessor" }, { "key": "beanType", "value": "interface org.springframework.beans.factory.config.BeanPostProcessor" } ] }, "startTime": "2020-12-10T21:53:44.971632203Z", "endTime": "2020-12-10T21:53:44.977955438Z", "duration": "PT0.006323235S" }, { "startupStep": { "name": "spring.context.beans.post-process", "id": 6, "parentId": 5, "tags": [] }, "startTime": "2020-12-10T21:53:42.926508051Z", "endTime": "2020-12-10T21:53:44.978094536Z", "duration": "PT2.051586485S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 46, "parentId": 45, "tags": [ { "key": "beanName", "value": "org.springframework.boot.autoconfigure.web.servlet.ServletWebServerFactoryConfiguration$EmbeddedTomcat" } ] }, "startTime": "2020-12-10T21:53:44.985087873Z", "endTime": "2020-12-10T21:53:44.987186025Z", "duration": "PT0.002098152S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 48, "parentId": 47, "tags": [ { "key": "beanName", "value": "org.springframework.boot.autoconfigure.websocket.servlet.WebSocketServletAutoConfiguration$TomcatWebSocketConfiguration" } ] }, "startTime": "2020-12-10T21:53:45.067147987Z", "endTime": "2020-12-10T21:53:45.067973061Z", "duration": "PT0.000825074S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 47, "parentId": 45, "tags": [ { "key": "beanName", "value": "websocketServletWebServerCustomizer" } ] }, "startTime": "2020-12-10T21:53:45.066861556Z", "endTime": "2020-12-10T21:53:45.069323508Z", "duration": "PT0.002461952S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 50, "parentId": 49, "tags": [ { "key": "beanName", "value": "org.springframework.boot.autoconfigure.web.servlet.ServletWebServerFactoryAutoConfiguration" } ] }, "startTime": "2020-12-10T21:53:45.069563112Z", "endTime": "2020-12-10T21:53:45.074283657Z", "duration": "PT0.004720545S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 52, "parentId": 51, "tags": [ { "key": "beanName", "value": "org.springframework.boot.context.properties.BoundConfigurationProperties" }, { "key": "beanType", "value": "class org.springframework.boot.context.properties.BoundConfigurationProperties" } ] }, "startTime": "2020-12-10T21:53:45.094576558Z", "endTime": "2020-12-10T21:53:45.095119136Z", "duration": "PT0.000542578S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 53, "parentId": 51, "tags": [ { "key": "beanName", "value": "conversionService" }, { "key": "beanType", "value": "interface org.springframework.core.convert.ConversionService" }, { "key": "exception", "value": "class org.springframework.beans.factory.NoSuchBeanDefinitionException" }, { "key": "message", "value": "No bean named 'conversionService' available" } ] }, "startTime": "2020-12-10T21:53:45.104717348Z", "endTime": "2020-12-10T21:53:45.107373959Z", "duration": "PT0.002656611S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 51, "parentId": 49, "tags": [ { "key": "beanName", "value": "server-org.springframework.boot.autoconfigure.web.ServerProperties" } ] }, "startTime": "2020-12-10T21:53:45.076947344Z", "endTime": "2020-12-10T21:53:45.117598728Z", "duration": "PT0.040651384S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 49, "parentId": 45, "tags": [ { "key": "beanName", "value": "servletWebServerFactoryCustomizer" } ] }, "startTime": "2020-12-10T21:53:45.069373628Z", "endTime": "2020-12-10T21:53:45.119065020Z", "duration": "PT0.049691392S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 54, "parentId": 45, "tags": [ { "key": "beanName", "value": "tomcatServletWebServerFactoryCustomizer" } ] }, "startTime": "2020-12-10T21:53:45.119082527Z", "endTime": "2020-12-10T21:53:45.120267864Z", "duration": "PT0.001185337S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 56, "parentId": 55, "tags": [ { "key": "beanName", "value": "org.springframework.boot.autoconfigure.web.embedded.EmbeddedWebServerFactoryCustomizerAutoConfiguration$NettyWebServerFactoryCustomizerConfiguration" } ] }, "startTime": "2020-12-10T21:53:45.120324756Z", "endTime": "2020-12-10T21:53:45.120684988Z", "duration": "PT0.000360232S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 55, "parentId": 45, "tags": [ { "key": "beanName", "value": "nettyWebServerFactoryCustomizer" } ] }, "startTime": "2020-12-10T21:53:45.120279417Z", "endTime": "2020-12-10T21:53:45.124121682Z", "duration": "PT0.003842265S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 58, "parentId": 57, "tags": [ { "key": "beanName", "value": "org.springframework.boot.autoconfigure.web.embedded.EmbeddedWebServerFactoryCustomizerAutoConfiguration$TomcatWebServerFactoryCustomizerConfiguration" } ] }, "startTime": "2020-12-10T21:53:45.124180858Z", "endTime": "2020-12-10T21:53:45.124549261Z", "duration": "PT0.000368403S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 57, "parentId": 45, "tags": [ { "key": "beanName", "value": "tomcatWebServerFactoryCustomizer" } ] }, "startTime": "2020-12-10T21:53:45.124134393Z", "endTime": "2020-12-10T21:53:45.126687053Z", "duration": "PT0.00255266S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 60, "parentId": 59, "tags": [ { "key": "beanName", "value": "org.springframework.boot.autoconfigure.web.servlet.HttpEncodingAutoConfiguration" } ] }, "startTime": "2020-12-10T21:53:45.126740133Z", "endTime": "2020-12-10T21:53:45.128824942Z", "duration": "PT0.002084809S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 59, "parentId": 45, "tags": [ { "key": "beanName", "value": "localeCharsetMappingsCustomizer" } ] }, "startTime": "2020-12-10T21:53:45.126698425Z", "endTime": "2020-12-10T21:53:45.129202358Z", "duration": "PT0.002503933S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 62, "parentId": 61, "tags": [ { "key": "beanName", "value": "org.springframework.boot.autoconfigure.web.servlet.error.ErrorMvcAutoConfiguration" } ] }, "startTime": "2020-12-10T21:53:45.157395915Z", "endTime": "2020-12-10T21:53:45.161004523Z", "duration": "PT0.003608608S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 64, "parentId": 63, "tags": [ { "key": "beanName", "value": "org.springframework.boot.autoconfigure.web.servlet.DispatcherServletAutoConfiguration$DispatcherServletRegistrationConfiguration" } ] }, "startTime": "2020-12-10T21:53:45.161752769Z", "endTime": "2020-12-10T21:53:45.163238078Z", "duration": "PT0.001485309S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 66, "parentId": 65, "tags": [ { "key": "beanName", "value": "org.springframework.boot.autoconfigure.web.servlet.DispatcherServletAutoConfiguration$DispatcherServletConfiguration" } ] }, "startTime": "2020-12-10T21:53:45.165349747Z", "endTime": "2020-12-10T21:53:45.166886925Z", "duration": "PT0.001537178S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 67, "parentId": 65, "tags": [ { "key": "beanName", "value": "spring.mvc-org.springframework.boot.autoconfigure.web.servlet.WebMvcProperties" } ] }, "startTime": "2020-12-10T21:53:45.168108671Z", "endTime": "2020-12-10T21:53:45.173566740Z", "duration": "PT0.005458069S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 65, "parentId": 63, "tags": [ { "key": "beanName", "value": "dispatcherServlet" } ] }, "startTime": "2020-12-10T21:53:45.165083780Z", "endTime": "2020-12-10T21:53:45.185159391Z", "duration": "PT0.020075611S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 70, "parentId": 69, "tags": [ { "key": "beanName", "value": "spring.servlet.multipart-org.springframework.boot.autoconfigure.web.servlet.MultipartProperties" } ] }, "startTime": "2020-12-10T21:53:45.187937179Z", "endTime": "2020-12-10T21:53:45.191376574Z", "duration": "PT0.003439395S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 69, "parentId": 68, "tags": [ { "key": "beanName", "value": "org.springframework.boot.autoconfigure.web.servlet.MultipartAutoConfiguration" } ] }, "startTime": "2020-12-10T21:53:45.187348147Z", "endTime": "2020-12-10T21:53:45.192068984Z", "duration": "PT0.004720837S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 68, "parentId": 63, "tags": [ { "key": "beanName", "value": "multipartConfigElement" } ] }, "startTime": "2020-12-10T21:53:45.187286256Z", "endTime": "2020-12-10T21:53:45.193228092Z", "duration": "PT0.005941836S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 63, "parentId": 61, "tags": [ { "key": "beanName", "value": "dispatcherServletRegistration" } ] }, "startTime": "2020-12-10T21:53:45.161697628Z", "endTime": "2020-12-10T21:53:45.196438877Z", "duration": "PT0.034741249S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 61, "parentId": 45, "tags": [ { "key": "beanName", "value": "errorPageCustomizer" } ] }, "startTime": "2020-12-10T21:53:45.157275571Z", "endTime": "2020-12-10T21:53:45.196828278Z", "duration": "PT0.039552707S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 45, "parentId": 44, "tags": [ { "key": "beanName", "value": "tomcatServletWebServerFactory" }, { "key": "beanType", "value": "interface org.springframework.boot.web.servlet.server.ServletWebServerFactory" } ] }, "startTime": "2020-12-10T21:53:44.984760639Z", "endTime": "2020-12-10T21:53:45.199868600Z", "duration": "PT0.215107961S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 72, "parentId": 71, "tags": [ { "key": "beanName", "value": "org.springframework.boot.autoconfigure.session.SessionRepositoryFilterConfiguration" } ] }, "startTime": "2020-12-10T21:53:45.513059790Z", "endTime": "2020-12-10T21:53:45.514515212Z", "duration": "PT0.001455422S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 73, "parentId": 71, "tags": [ { "key": "beanName", "value": "spring.session-org.springframework.boot.autoconfigure.session.SessionProperties" } ] }, "startTime": "2020-12-10T21:53:45.516544507Z", "endTime": "2020-12-10T21:53:45.520674212Z", "duration": "PT0.004129705S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 77, "parentId": 76, "tags": [ { "key": "beanName", "value": "org.springframework.boot.autoconfigure.session.SessionAutoConfiguration$ServletSessionConfiguration" } ] }, "startTime": "2020-12-10T21:53:45.552983786Z", "endTime": "2020-12-10T21:53:45.553408362Z", "duration": "PT0.000424576S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 79, "parentId": 78, "tags": [ { "key": "beanName", "value": "org.springframework.boot.autoconfigure.session.SessionAutoConfiguration$ServletSessionConfiguration$RememberMeServicesConfiguration" } ] }, "startTime": "2020-12-10T21:53:45.556685340Z", "endTime": "2020-12-10T21:53:45.557165607Z", "duration": "PT0.000480267S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 78, "parentId": 76, "tags": [ { "key": "beanName", "value": "rememberMeServicesCookieSerializerCustomizer" } ] }, "startTime": "2020-12-10T21:53:45.556619909Z", "endTime": "2020-12-10T21:53:45.558497017Z", "duration": "PT0.001877108S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 76, "parentId": 75, "tags": [ { "key": "beanName", "value": "cookieSerializer" } ] }, "startTime": "2020-12-10T21:53:45.552927565Z", "endTime": "2020-12-10T21:53:45.561209946Z", "duration": "PT0.008282381S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 81, "parentId": 80, "tags": [ { "key": "beanName", "value": "springBootAdminServletApplication" } ] }, "startTime": "2020-12-10T21:53:45.569859823Z", "endTime": "2020-12-10T21:53:45.570262474Z", "duration": "PT0.000402651S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 83, "parentId": 82, "tags": [ { "key": "beanName", "value": "spring.datasource-org.springframework.boot.autoconfigure.jdbc.DataSourceProperties" } ] }, "startTime": "2020-12-10T21:53:46.011639089Z", "endTime": "2020-12-10T21:53:46.016211767Z", "duration": "PT0.004572678S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 82, "parentId": 80, "tags": [ { "key": "beanName", "value": "org.springframework.boot.autoconfigure.jdbc.DataSourceInitializerInvoker" }, { "key": "beanType", "value": "class org.springframework.boot.autoconfigure.jdbc.DataSourceInitializerInvoker" } ] }, "startTime": "2020-12-10T21:53:46.010279072Z", "endTime": "2020-12-10T21:53:46.019919289Z", "duration": "PT0.009640217S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 80, "parentId": 75, "tags": [ { "key": "beanName", "value": "dataSource" } ] }, "startTime": "2020-12-10T21:53:45.569801791Z", "endTime": "2020-12-10T21:53:46.020439096Z", "duration": "PT0.450637305S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 85, "parentId": 84, "tags": [ { "key": "beanName", "value": "org.springframework.boot.autoconfigure.jdbc.DataSourceTransactionManagerAutoConfiguration$JdbcTransactionManagerConfiguration" } ] }, "startTime": "2020-12-10T21:53:46.021092119Z", "endTime": "2020-12-10T21:53:46.021459004Z", "duration": "PT0.000366885S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 87, "parentId": 86, "tags": [ { "key": "beanName", "value": "org.springframework.boot.autoconfigure.transaction.TransactionAutoConfiguration" } ] }, "startTime": "2020-12-10T21:53:46.025611341Z", "endTime": "2020-12-10T21:53:46.026756109Z", "duration": "PT0.001144768S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 88, "parentId": 86, "tags": [ { "key": "beanName", "value": "spring.transaction-org.springframework.boot.autoconfigure.transaction.TransactionProperties" } ] }, "startTime": "2020-12-10T21:53:46.029897812Z", "endTime": "2020-12-10T21:53:46.031981328Z", "duration": "PT0.002083516S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 86, "parentId": 84, "tags": [ { "key": "beanName", "value": "platformTransactionManagerCustomizers" } ] }, "startTime": "2020-12-10T21:53:46.025555998Z", "endTime": "2020-12-10T21:53:46.032309168Z", "duration": "PT0.00675317S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 84, "parentId": 75, "tags": [ { "key": "beanName", "value": "transactionManager" } ] }, "startTime": "2020-12-10T21:53:46.021041292Z", "endTime": "2020-12-10T21:53:46.035375846Z", "duration": "PT0.014334554S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 89, "parentId": 75, "tags": [ { "key": "beanName", "value": "spring.session.jdbc-org.springframework.boot.autoconfigure.session.JdbcSessionProperties" } ] }, "startTime": "2020-12-10T21:53:46.036412704Z", "endTime": "2020-12-10T21:53:46.039009584Z", "duration": "PT0.00259688S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 75, "parentId": 74, "tags": [ { "key": "beanName", "value": "org.springframework.boot.autoconfigure.session.JdbcSessionConfiguration$SpringBootJdbcHttpSessionConfiguration" } ] }, "startTime": "2020-12-10T21:53:45.524246315Z", "endTime": "2020-12-10T21:53:46.041705815Z", "duration": "PT0.5174595S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 90, "parentId": 74, "tags": [ { "key": "beanName", "value": "sessionRepository" } ] }, "startTime": "2020-12-10T21:53:46.043456920Z", "endTime": "2020-12-10T21:53:46.060635130Z", "duration": "PT0.01717821S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 74, "parentId": 71, "tags": [ { "key": "beanName", "value": "springSessionRepositoryFilter" } ] }, "startTime": "2020-12-10T21:53:45.523988365Z", "endTime": "2020-12-10T21:53:46.061618512Z", "duration": "PT0.537630147S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 71, "parentId": 44, "tags": [ { "key": "beanName", "value": "sessionRepositoryFilterRegistration" }, { "key": "beanType", "value": "interface org.springframework.boot.web.servlet.ServletContextInitializer" } ] }, "startTime": "2020-12-10T21:53:45.512868483Z", "endTime": "2020-12-10T21:53:46.063374312Z", "duration": "PT0.550505829S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 93, "parentId": 92, "tags": [ { "key": "beanName", "value": "management.metrics-org.springframework.boot.actuate.autoconfigure.metrics.MetricsProperties" } ] }, "startTime": "2020-12-10T21:53:46.064045346Z", "endTime": "2020-12-10T21:53:46.068510549Z", "duration": "PT0.004465203S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 92, "parentId": 91, "tags": [ { "key": "beanName", "value": "org.springframework.boot.actuate.autoconfigure.metrics.web.servlet.WebMvcMetricsAutoConfiguration" } ] }, "startTime": "2020-12-10T21:53:46.063447789Z", "endTime": "2020-12-10T21:53:46.068927169Z", "duration": "PT0.00547938S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 95, "parentId": 94, "tags": [ { "key": "beanName", "value": "org.springframework.boot.actuate.autoconfigure.metrics.export.simple.SimpleMetricsExportAutoConfiguration" } ] }, "startTime": "2020-12-10T21:53:46.069363717Z", "endTime": "2020-12-10T21:53:46.070169951Z", "duration": "PT0.000806234S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 97, "parentId": 96, "tags": [ { "key": "beanName", "value": "management.metrics.export.simple-org.springframework.boot.actuate.autoconfigure.metrics.export.simple.SimpleProperties" } ] }, "startTime": "2020-12-10T21:53:46.070891205Z", "endTime": "2020-12-10T21:53:46.071758035Z", "duration": "PT0.00086683S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 96, "parentId": 94, "tags": [ { "key": "beanName", "value": "simpleConfig" } ] }, "startTime": "2020-12-10T21:53:46.070526597Z", "endTime": "2020-12-10T21:53:46.075665626Z", "duration": "PT0.005139029S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 99, "parentId": 98, "tags": [ { "key": "beanName", "value": "org.springframework.boot.actuate.autoconfigure.metrics.MetricsAutoConfiguration" } ] }, "startTime": "2020-12-10T21:53:46.076261330Z", "endTime": "2020-12-10T21:53:46.076595350Z", "duration": "PT0.00033402S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 98, "parentId": 94, "tags": [ { "key": "beanName", "value": "micrometerClock" } ] }, "startTime": "2020-12-10T21:53:46.076202020Z", "endTime": "2020-12-10T21:53:46.077088339Z", "duration": "PT0.000886319S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 100, "parentId": 94, "tags": [ { "key": "beanName", "value": "propertiesMeterFilter" } ] }, "startTime": "2020-12-10T21:53:46.092269013Z", "endTime": "2020-12-10T21:53:46.095111682Z", "duration": "PT0.002842669S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 102, "parentId": 101, "tags": [ { "key": "beanName", "value": "org.springframework.boot.actuate.autoconfigure.metrics.web.client.HttpClientMetricsAutoConfiguration" } ] }, "startTime": "2020-12-10T21:53:46.095176558Z", "endTime": "2020-12-10T21:53:46.095535187Z", "duration": "PT0.000358629S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 101, "parentId": 94, "tags": [ { "key": "beanName", "value": "metricsHttpClientUriTagFilter" } ] }, "startTime": "2020-12-10T21:53:46.095139681Z", "endTime": "2020-12-10T21:53:46.097800734Z", "duration": "PT0.002661053S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 103, "parentId": 94, "tags": [ { "key": "beanName", "value": "metricsHttpServerUriTagFilter" } ] }, "startTime": "2020-12-10T21:53:46.097824090Z", "endTime": "2020-12-10T21:53:46.098401874Z", "duration": "PT0.000577784S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 105, "parentId": 104, "tags": [ { "key": "beanName", "value": "org.springframework.boot.actuate.autoconfigure.metrics.JvmMetricsAutoConfiguration" } ] }, "startTime": "2020-12-10T21:53:46.099352951Z", "endTime": "2020-12-10T21:53:46.099670586Z", "duration": "PT0.000317635S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 104, "parentId": 94, "tags": [ { "key": "beanName", "value": "jvmGcMetrics" } ] }, "startTime": "2020-12-10T21:53:46.099319820Z", "endTime": "2020-12-10T21:53:46.104900455Z", "duration": "PT0.005580635S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 106, "parentId": 94, "tags": [ { "key": "beanName", "value": "jvmMemoryMetrics" } ] }, "startTime": "2020-12-10T21:53:46.104928381Z", "endTime": "2020-12-10T21:53:46.105238153Z", "duration": "PT0.000309772S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 107, "parentId": 94, "tags": [ { "key": "beanName", "value": "jvmThreadMetrics" } ] }, "startTime": "2020-12-10T21:53:46.105259575Z", "endTime": "2020-12-10T21:53:46.105589129Z", "duration": "PT0.000329554S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 108, "parentId": 94, "tags": [ { "key": "beanName", "value": "classLoaderMetrics" } ] }, "startTime": "2020-12-10T21:53:46.105608441Z", "endTime": "2020-12-10T21:53:46.105870038Z", "duration": "PT0.000261597S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 110, "parentId": 109, "tags": [ { "key": "beanName", "value": "org.springframework.boot.actuate.autoconfigure.metrics.LogbackMetricsAutoConfiguration" } ] }, "startTime": "2020-12-10T21:53:46.105916860Z", "endTime": "2020-12-10T21:53:46.106203811Z", "duration": "PT0.000286951S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 109, "parentId": 94, "tags": [ { "key": "beanName", "value": "logbackMetrics" } ] }, "startTime": "2020-12-10T21:53:46.105889073Z", "endTime": "2020-12-10T21:53:46.106749314Z", "duration": "PT0.000860241S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 112, "parentId": 111, "tags": [ { "key": "beanName", "value": "org.springframework.boot.actuate.autoconfigure.metrics.SystemMetricsAutoConfiguration" } ] }, "startTime": "2020-12-10T21:53:46.106798012Z", "endTime": "2020-12-10T21:53:46.107130832Z", "duration": "PT0.00033282S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 111, "parentId": 94, "tags": [ { "key": "beanName", "value": "uptimeMetrics" } ] }, "startTime": "2020-12-10T21:53:46.106768753Z", "endTime": "2020-12-10T21:53:46.107408250Z", "duration": "PT0.000639497S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 113, "parentId": 94, "tags": [ { "key": "beanName", "value": "processorMetrics" } ] }, "startTime": "2020-12-10T21:53:46.107428064Z", "endTime": "2020-12-10T21:53:46.108212222Z", "duration": "PT0.000784158S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 114, "parentId": 94, "tags": [ { "key": "beanName", "value": "fileDescriptorMetrics" } ] }, "startTime": "2020-12-10T21:53:46.108232195Z", "endTime": "2020-12-10T21:53:46.108681150Z", "duration": "PT0.000448955S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 94, "parentId": 91, "tags": [ { "key": "beanName", "value": "simpleMeterRegistry" } ] }, "startTime": "2020-12-10T21:53:46.069316305Z", "endTime": "2020-12-10T21:53:46.130165728Z", "duration": "PT0.060849423S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 115, "parentId": 91, "tags": [ { "key": "beanName", "value": "webMvcTagsProvider" } ] }, "startTime": "2020-12-10T21:53:46.130714312Z", "endTime": "2020-12-10T21:53:46.131749702Z", "duration": "PT0.00103539S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 91, "parentId": 44, "tags": [ { "key": "beanName", "value": "webMvcMetricsFilter" }, { "key": "beanType", "value": "interface org.springframework.boot.web.servlet.ServletContextInitializer" } ] }, "startTime": "2020-12-10T21:53:46.063387603Z", "endTime": "2020-12-10T21:53:46.132148141Z", "duration": "PT0.068760538S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 117, "parentId": 116, "tags": [ { "key": "beanName", "value": "org.springframework.boot.autoconfigure.security.servlet.SecurityFilterAutoConfiguration" } ] }, "startTime": "2020-12-10T21:53:46.132196889Z", "endTime": "2020-12-10T21:53:46.132541784Z", "duration": "PT0.000344895S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 118, "parentId": 116, "tags": [ { "key": "beanName", "value": "spring.security-org.springframework.boot.autoconfigure.security.SecurityProperties" } ] }, "startTime": "2020-12-10T21:53:46.132911980Z", "endTime": "2020-12-10T21:53:46.134132373Z", "duration": "PT0.001220393S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 116, "parentId": 44, "tags": [ { "key": "beanName", "value": "securityFilterChainRegistration" }, { "key": "beanType", "value": "interface org.springframework.boot.web.servlet.ServletContextInitializer" } ] }, "startTime": "2020-12-10T21:53:46.132159123Z", "endTime": "2020-12-10T21:53:46.136038191Z", "duration": "PT0.003879068S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 120, "parentId": 119, "tags": [ { "key": "beanName", "value": "org.springframework.boot.actuate.autoconfigure.endpoint.web.ServletEndpointManagementContextConfiguration$WebMvcServletEndpointManagementContextConfiguration" } ] }, "startTime": "2020-12-10T21:53:46.136089138Z", "endTime": "2020-12-10T21:53:46.136338626Z", "duration": "PT0.000249488S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 121, "parentId": 119, "tags": [ { "key": "beanName", "value": "management.endpoints.web-org.springframework.boot.actuate.autoconfigure.endpoint.web.WebEndpointProperties" } ] }, "startTime": "2020-12-10T21:53:46.136718295Z", "endTime": "2020-12-10T21:53:46.139729775Z", "duration": "PT0.00301148S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 123, "parentId": 122, "tags": [ { "key": "beanName", "value": "org.springframework.boot.actuate.autoconfigure.endpoint.web.WebEndpointAutoConfiguration$WebEndpointServletConfiguration" } ] }, "startTime": "2020-12-10T21:53:46.140298721Z", "endTime": "2020-12-10T21:53:46.140612166Z", "duration": "PT0.000313445S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 125, "parentId": 124, "tags": [ { "key": "beanName", "value": "org.springframework.boot.actuate.autoconfigure.endpoint.web.WebEndpointAutoConfiguration" } ] }, "startTime": "2020-12-10T21:53:46.141857394Z", "endTime": "2020-12-10T21:53:46.143277160Z", "duration": "PT0.001419766S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 124, "parentId": 122, "tags": [ { "key": "beanName", "value": "webEndpointPathMapper" } ] }, "startTime": "2020-12-10T21:53:46.141802028Z", "endTime": "2020-12-10T21:53:46.144641842Z", "duration": "PT0.002839814S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 127, "parentId": 126, "tags": [ { "key": "beanName", "value": "org.springframework.boot.actuate.autoconfigure.endpoint.web.ServletEndpointManagementContextConfiguration" } ] }, "startTime": "2020-12-10T21:53:46.147071119Z", "endTime": "2020-12-10T21:53:46.148824685Z", "duration": "PT0.001753566S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 126, "parentId": 122, "tags": [ { "key": "beanName", "value": "servletExposeExcludePropertyEndpointFilter" } ] }, "startTime": "2020-12-10T21:53:46.146852081Z", "endTime": "2020-12-10T21:53:46.151329239Z", "duration": "PT0.004477158S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 122, "parentId": 119, "tags": [ { "key": "beanName", "value": "servletEndpointDiscoverer" } ] }, "startTime": "2020-12-10T21:53:46.140240187Z", "endTime": "2020-12-10T21:53:46.156874672Z", "duration": "PT0.016634485S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 119, "parentId": 44, "tags": [ { "key": "beanName", "value": "servletEndpointRegistrar" }, { "key": "beanType", "value": "interface org.springframework.boot.web.servlet.ServletContextInitializer" } ] }, "startTime": "2020-12-10T21:53:46.136049717Z", "endTime": "2020-12-10T21:53:46.185605082Z", "duration": "PT0.049555365S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 128, "parentId": 44, "tags": [ { "key": "beanName", "value": "requestContextFilter" }, { "key": "beanType", "value": "interface javax.servlet.Filter" } ] }, "startTime": "2020-12-10T21:53:46.188260303Z", "endTime": "2020-12-10T21:53:46.189964152Z", "duration": "PT0.001703849S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 130, "parentId": 129, "tags": [ { "key": "beanName", "value": "org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration" } ] }, "startTime": "2020-12-10T21:53:46.190051041Z", "endTime": "2020-12-10T21:53:46.190416837Z", "duration": "PT0.000365796S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 129, "parentId": 44, "tags": [ { "key": "beanName", "value": "formContentFilter" }, { "key": "beanType", "value": "interface javax.servlet.Filter" } ] }, "startTime": "2020-12-10T21:53:46.189996928Z", "endTime": "2020-12-10T21:53:46.192485138Z", "duration": "PT0.00248821S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 133, "parentId": 132, "tags": [ { "key": "beanName", "value": "spring.boot.admin.ui-de.codecentric.boot.admin.server.ui.config.AdminServerUiProperties" } ] }, "startTime": "2020-12-10T21:53:46.193178440Z", "endTime": "2020-12-10T21:53:46.203997200Z", "duration": "PT0.01081876S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 134, "parentId": 132, "tags": [ { "key": "beanName", "value": "spring.boot.admin-de.codecentric.boot.admin.server.config.AdminServerProperties" } ] }, "startTime": "2020-12-10T21:53:46.204517539Z", "endTime": "2020-12-10T21:53:46.206180972Z", "duration": "PT0.001663433S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 132, "parentId": 131, "tags": [ { "key": "beanName", "value": "de.codecentric.boot.admin.server.ui.config.AdminServerUiAutoConfiguration$ServletUiConfiguration$AdminUiWebMvcConfig" } ] }, "startTime": "2020-12-10T21:53:46.192560455Z", "endTime": "2020-12-10T21:53:46.206961060Z", "duration": "PT0.014400605S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 131, "parentId": 44, "tags": [ { "key": "beanName", "value": "homepageForwardFilter" }, { "key": "beanType", "value": "interface javax.servlet.Filter" } ] }, "startTime": "2020-12-10T21:53:46.192511158Z", "endTime": "2020-12-10T21:53:46.219367974Z", "duration": "PT0.026856816S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 136, "parentId": 135, "tags": [ { "key": "beanName", "value": "org.springframework.boot.actuate.autoconfigure.trace.http.HttpTraceAutoConfiguration$ServletTraceFilterConfiguration" } ] }, "startTime": "2020-12-10T21:53:46.219432681Z", "endTime": "2020-12-10T21:53:46.219600650Z", "duration": "PT0.000167969S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 137, "parentId": 135, "tags": [ { "key": "beanName", "value": "httpTraceRepository" } ] }, "startTime": "2020-12-10T21:53:46.220188847Z", "endTime": "2020-12-10T21:53:46.221022140Z", "duration": "PT0.000833293S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 139, "parentId": 138, "tags": [ { "key": "beanName", "value": "org.springframework.boot.actuate.autoconfigure.trace.http.HttpTraceAutoConfiguration" } ] }, "startTime": "2020-12-10T21:53:46.221481439Z", "endTime": "2020-12-10T21:53:46.221691883Z", "duration": "PT0.000210444S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 140, "parentId": 138, "tags": [ { "key": "beanName", "value": "management.trace.http-org.springframework.boot.actuate.autoconfigure.trace.http.HttpTraceProperties" } ] }, "startTime": "2020-12-10T21:53:46.222004290Z", "endTime": "2020-12-10T21:53:46.223087226Z", "duration": "PT0.001082936S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 138, "parentId": 135, "tags": [ { "key": "beanName", "value": "httpExchangeTracer" } ] }, "startTime": "2020-12-10T21:53:46.221448477Z", "endTime": "2020-12-10T21:53:46.223638116Z", "duration": "PT0.002189639S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 135, "parentId": 44, "tags": [ { "key": "beanName", "value": "httpTraceFilter" }, { "key": "beanType", "value": "interface javax.servlet.Filter" } ] }, "startTime": "2020-12-10T21:53:46.219383482Z", "endTime": "2020-12-10T21:53:46.224356231Z", "duration": "PT0.004972749S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 141, "parentId": 44, "tags": [ { "key": "beanName", "value": "characterEncodingFilter" }, { "key": "beanType", "value": "interface javax.servlet.Filter" } ] }, "startTime": "2020-12-10T21:53:46.224365251Z", "endTime": "2020-12-10T21:53:46.225772575Z", "duration": "PT0.001407324S" }, { "startupStep": { "name": "spring.boot.webserver.create", "id": 44, "parentId": 5, "tags": [ { "key": "factory", "value": "class org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory" } ] }, "startTime": "2020-12-10T21:53:44.983103652Z", "endTime": "2020-12-10T21:53:46.254260217Z", "duration": "PT1.271156565S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 142, "parentId": 5, "tags": [ { "key": "beanName", "value": "auditLog" } ] }, "startTime": "2020-12-10T21:53:46.260184056Z", "endTime": "2020-12-10T21:53:46.261964834Z", "duration": "PT0.001780778S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 145, "parentId": 144, "tags": [ { "key": "beanName", "value": "de.codecentric.boot.admin.server.config.AdminServerAutoConfiguration" } ] }, "startTime": "2020-12-10T21:53:46.262695873Z", "endTime": "2020-12-10T21:53:46.263535789Z", "duration": "PT0.000839916S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 146, "parentId": 144, "tags": [ { "key": "beanName", "value": "eventStore" } ] }, "startTime": "2020-12-10T21:53:46.264942674Z", "endTime": "2020-12-10T21:53:46.309001968Z", "duration": "PT0.044059294S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 144, "parentId": 143, "tags": [ { "key": "beanName", "value": "instanceRepository" } ] }, "startTime": "2020-12-10T21:53:46.262658948Z", "endTime": "2020-12-10T21:53:46.341252598Z", "duration": "PT0.07859365S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 143, "parentId": 5, "tags": [ { "key": "beanName", "value": "customNotifier" } ] }, "startTime": "2020-12-10T21:53:46.261979794Z", "endTime": "2020-12-10T21:53:46.342816460Z", "duration": "PT0.080836666S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 147, "parentId": 5, "tags": [ { "key": "beanName", "value": "customEndpoint" } ] }, "startTime": "2020-12-10T21:53:46.342831884Z", "endTime": "2020-12-10T21:53:46.343715756Z", "duration": "PT0.000883872S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 148, "parentId": 5, "tags": [ { "key": "beanName", "value": "customHttpHeadersProvider" } ] }, "startTime": "2020-12-10T21:53:46.343726987Z", "endTime": "2020-12-10T21:53:46.344188281Z", "duration": "PT0.000461294S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 149, "parentId": 5, "tags": [ { "key": "beanName", "value": "auditEventRepository" } ] }, "startTime": "2020-12-10T21:53:46.344200362Z", "endTime": "2020-12-10T21:53:46.345116490Z", "duration": "PT0.000916128S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 151, "parentId": 150, "tags": [ { "key": "beanName", "value": "de.codecentric.boot.admin.client.config.SpringBootAdminClientAutoConfiguration$ServletConfiguration" } ] }, "startTime": "2020-12-10T21:53:46.345229616Z", "endTime": "2020-12-10T21:53:46.345456241Z", "duration": "PT0.000226625S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 152, "parentId": 150, "tags": [ { "key": "beanName", "value": "spring.boot.admin.client.instance-de.codecentric.boot.admin.client.config.InstanceProperties" } ] }, "startTime": "2020-12-10T21:53:46.346054405Z", "endTime": "2020-12-10T21:53:46.352459822Z", "duration": "PT0.006405417S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 153, "parentId": 150, "tags": [ { "key": "beanName", "value": "management.server-org.springframework.boot.actuate.autoconfigure.web.server.ManagementServerProperties" } ] }, "startTime": "2020-12-10T21:53:46.352971413Z", "endTime": "2020-12-10T21:53:46.354474518Z", "duration": "PT0.001503105S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 157, "parentId": 156, "tags": [ { "key": "beanName", "value": "org.springframework.boot.actuate.autoconfigure.endpoint.EndpointAutoConfiguration" } ] }, "startTime": "2020-12-10T21:53:46.357056748Z", "endTime": "2020-12-10T21:53:46.357291261Z", "duration": "PT0.000234513S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 156, "parentId": 155, "tags": [ { "key": "beanName", "value": "endpointOperationParameterMapper" } ] }, "startTime": "2020-12-10T21:53:46.357021050Z", "endTime": "2020-12-10T21:53:46.359841644Z", "duration": "PT0.002820594S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 158, "parentId": 155, "tags": [ { "key": "beanName", "value": "endpointMediaTypes" } ] }, "startTime": "2020-12-10T21:53:46.360206450Z", "endTime": "2020-12-10T21:53:46.360513913Z", "duration": "PT0.000307463S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 159, "parentId": 155, "tags": [ { "key": "beanName", "value": "endpointCachingOperationInvokerAdvisor" } ] }, "startTime": "2020-12-10T21:53:46.361394824Z", "endTime": "2020-12-10T21:53:46.362544246Z", "duration": "PT0.001149422S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 160, "parentId": 155, "tags": [ { "key": "beanName", "value": "webExposeExcludePropertyEndpointFilter" } ] }, "startTime": "2020-12-10T21:53:46.363003247Z", "endTime": "2020-12-10T21:53:46.363217562Z", "duration": "PT0.000214315S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 155, "parentId": 154, "tags": [ { "key": "beanName", "value": "webEndpointDiscoverer" } ] }, "startTime": "2020-12-10T21:53:46.356564762Z", "endTime": "2020-12-10T21:53:46.364225029Z", "duration": "PT0.007660267S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 162, "parentId": 161, "tags": [ { "key": "beanName", "value": "controllerExposeExcludePropertyEndpointFilter" } ] }, "startTime": "2020-12-10T21:53:46.364889160Z", "endTime": "2020-12-10T21:53:46.365073444Z", "duration": "PT0.000184284S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 161, "parentId": 154, "tags": [ { "key": "beanName", "value": "controllerEndpointDiscoverer" } ] }, "startTime": "2020-12-10T21:53:46.364340445Z", "endTime": "2020-12-10T21:53:46.365662473Z", "duration": "PT0.001322028S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 165, "parentId": 164, "tags": [ { "key": "beanName", "value": "management.endpoints.jmx-org.springframework.boot.actuate.autoconfigure.endpoint.jmx.JmxEndpointProperties" } ] }, "startTime": "2020-12-10T21:53:46.367257491Z", "endTime": "2020-12-10T21:53:46.368269820Z", "duration": "PT0.001012329S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 164, "parentId": 163, "tags": [ { "key": "beanName", "value": "org.springframework.boot.actuate.autoconfigure.endpoint.jmx.JmxEndpointAutoConfiguration" } ] }, "startTime": "2020-12-10T21:53:46.366361893Z", "endTime": "2020-12-10T21:53:46.368498750Z", "duration": "PT0.002136857S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 166, "parentId": 163, "tags": [ { "key": "beanName", "value": "jmxIncludeExcludePropertyEndpointFilter" } ] }, "startTime": "2020-12-10T21:53:46.368958597Z", "endTime": "2020-12-10T21:53:46.369205653Z", "duration": "PT0.000247056S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 163, "parentId": 154, "tags": [ { "key": "beanName", "value": "jmxAnnotationEndpointDiscoverer" } ] }, "startTime": "2020-12-10T21:53:46.366303853Z", "endTime": "2020-12-10T21:53:46.369800475Z", "duration": "PT0.003496622S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 168, "parentId": 167, "tags": [ { "key": "beanName", "value": "org.springframework.boot.actuate.autoconfigure.cache.CachesEndpointAutoConfiguration" } ] }, "startTime": "2020-12-10T21:53:46.377300778Z", "endTime": "2020-12-10T21:53:46.378055785Z", "duration": "PT0.000755007S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 167, "parentId": 154, "tags": [ { "key": "beanName", "value": "cachesEndpoint" } ] }, "startTime": "2020-12-10T21:53:46.377138298Z", "endTime": "2020-12-10T21:53:46.385562251Z", "duration": "PT0.008423953S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 170, "parentId": 169, "tags": [ { "key": "beanName", "value": "org.springframework.boot.actuate.autoconfigure.health.HealthEndpointConfiguration" } ] }, "startTime": "2020-12-10T21:53:46.389424738Z", "endTime": "2020-12-10T21:53:46.389744658Z", "duration": "PT0.00031992S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 173, "parentId": 172, "tags": [ { "key": "beanName", "value": "management.endpoint.health-org.springframework.boot.actuate.autoconfigure.health.HealthEndpointProperties" } ] }, "startTime": "2020-12-10T21:53:46.391374887Z", "endTime": "2020-12-10T21:53:46.393759473Z", "duration": "PT0.002384586S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 174, "parentId": 172, "tags": [ { "key": "beanName", "value": "healthStatusAggregator" }, { "key": "beanType", "value": "interface org.springframework.boot.actuate.health.StatusAggregator" } ] }, "startTime": "2020-12-10T21:53:46.396613480Z", "endTime": "2020-12-10T21:53:46.399665859Z", "duration": "PT0.003052379S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 175, "parentId": 172, "tags": [ { "key": "beanName", "value": "healthHttpCodeStatusMapper" }, { "key": "beanType", "value": "interface org.springframework.boot.actuate.health.HttpCodeStatusMapper" } ] }, "startTime": "2020-12-10T21:53:46.400613706Z", "endTime": "2020-12-10T21:53:46.401583491Z", "duration": "PT0.000969785S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 172, "parentId": 171, "tags": [ { "key": "beanName", "value": "healthEndpointGroups" } ] }, "startTime": "2020-12-10T21:53:46.390941649Z", "endTime": "2020-12-10T21:53:46.409498838Z", "duration": "PT0.018557189S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 177, "parentId": 176, "tags": [ { "key": "beanName", "value": "org.springframework.boot.actuate.autoconfigure.system.DiskSpaceHealthContributorAutoConfiguration" } ] }, "startTime": "2020-12-10T21:53:46.410208934Z", "endTime": "2020-12-10T21:53:46.410648960Z", "duration": "PT0.000440026S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 178, "parentId": 176, "tags": [ { "key": "beanName", "value": "management.health.diskspace-org.springframework.boot.actuate.autoconfigure.system.DiskSpaceHealthIndicatorProperties" } ] }, "startTime": "2020-12-10T21:53:46.412372572Z", "endTime": "2020-12-10T21:53:46.415201955Z", "duration": "PT0.002829383S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 176, "parentId": 171, "tags": [ { "key": "beanName", "value": "diskSpaceHealthIndicator" } ] }, "startTime": "2020-12-10T21:53:46.410142099Z", "endTime": "2020-12-10T21:53:46.417913383Z", "duration": "PT0.007771284S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 180, "parentId": 179, "tags": [ { "key": "beanName", "value": "org.springframework.boot.actuate.autoconfigure.health.HealthContributorAutoConfiguration" } ] }, "startTime": "2020-12-10T21:53:46.418010774Z", "endTime": "2020-12-10T21:53:46.418287491Z", "duration": "PT0.000276717S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 179, "parentId": 171, "tags": [ { "key": "beanName", "value": "pingHealthContributor" } ] }, "startTime": "2020-12-10T21:53:46.417932644Z", "endTime": "2020-12-10T21:53:46.418587224Z", "duration": "PT0.00065458S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 182, "parentId": 181, "tags": [ { "key": "beanName", "value": "org.springframework.boot.actuate.autoconfigure.jdbc.DataSourceHealthContributorAutoConfiguration" } ] }, "startTime": "2020-12-10T21:53:46.418636720Z", "endTime": "2020-12-10T21:53:46.420897684Z", "duration": "PT0.002260964S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 183, "parentId": 181, "tags": [ { "key": "beanName", "value": "management.health.db-org.springframework.boot.actuate.autoconfigure.jdbc.DataSourceHealthIndicatorProperties" } ] }, "startTime": "2020-12-10T21:53:46.421432227Z", "endTime": "2020-12-10T21:53:46.422156535Z", "duration": "PT0.000724308S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 181, "parentId": 171, "tags": [ { "key": "beanName", "value": "dbHealthContributor" } ] }, "startTime": "2020-12-10T21:53:46.418597281Z", "endTime": "2020-12-10T21:53:46.423040858Z", "duration": "PT0.004443577S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 171, "parentId": 169, "tags": [ { "key": "beanName", "value": "healthContributorRegistry" } ] }, "startTime": "2020-12-10T21:53:46.390336822Z", "endTime": "2020-12-10T21:53:46.426737975Z", "duration": "PT0.036401153S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 169, "parentId": 154, "tags": [ { "key": "beanName", "value": "healthEndpoint" } ] }, "startTime": "2020-12-10T21:53:46.389352629Z", "endTime": "2020-12-10T21:53:46.428264490Z", "duration": "PT0.038911861S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 185, "parentId": 184, "tags": [ { "key": "beanName", "value": "org.springframework.boot.actuate.autoconfigure.env.EnvironmentEndpointAutoConfiguration" } ] }, "startTime": "2020-12-10T21:53:46.428531191Z", "endTime": "2020-12-10T21:53:46.428735911Z", "duration": "PT0.00020472S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 186, "parentId": 184, "tags": [ { "key": "beanName", "value": "management.endpoint.env-org.springframework.boot.actuate.autoconfigure.env.EnvironmentEndpointProperties" } ] }, "startTime": "2020-12-10T21:53:46.429316237Z", "endTime": "2020-12-10T21:53:46.429951643Z", "duration": "PT0.000635406S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 184, "parentId": 154, "tags": [ { "key": "beanName", "value": "environmentEndpoint" } ] }, "startTime": "2020-12-10T21:53:46.428471233Z", "endTime": "2020-12-10T21:53:46.431979243Z", "duration": "PT0.00350801S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 188, "parentId": 187, "tags": [ { "key": "beanName", "value": "org.springframework.boot.actuate.autoconfigure.audit.AuditEventsEndpointAutoConfiguration" } ] }, "startTime": "2020-12-10T21:53:46.437549072Z", "endTime": "2020-12-10T21:53:46.437857408Z", "duration": "PT0.000308336S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 187, "parentId": 154, "tags": [ { "key": "beanName", "value": "auditEventsEndpoint" } ] }, "startTime": "2020-12-10T21:53:46.437486934Z", "endTime": "2020-12-10T21:53:46.439373375Z", "duration": "PT0.001886441S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 190, "parentId": 189, "tags": [ { "key": "beanName", "value": "org.springframework.boot.actuate.autoconfigure.beans.BeansEndpointAutoConfiguration" } ] }, "startTime": "2020-12-10T21:53:46.441035252Z", "endTime": "2020-12-10T21:53:46.441232190Z", "duration": "PT0.000196938S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 189, "parentId": 154, "tags": [ { "key": "beanName", "value": "beansEndpoint" } ] }, "startTime": "2020-12-10T21:53:46.440987269Z", "endTime": "2020-12-10T21:53:46.442236859Z", "duration": "PT0.00124959S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 191, "parentId": 154, "tags": [ { "key": "beanName", "value": "cachesEndpointWebExtension" } ] }, "startTime": "2020-12-10T21:53:46.444272910Z", "endTime": "2020-12-10T21:53:46.445189903Z", "duration": "PT0.000916993S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 193, "parentId": 192, "tags": [ { "key": "beanName", "value": "org.springframework.boot.actuate.autoconfigure.health.HealthEndpointWebExtensionConfiguration" } ] }, "startTime": "2020-12-10T21:53:46.446589507Z", "endTime": "2020-12-10T21:53:46.446801342Z", "duration": "PT0.000211835S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 192, "parentId": 154, "tags": [ { "key": "beanName", "value": "healthEndpointWebExtension" } ] }, "startTime": "2020-12-10T21:53:46.446539629Z", "endTime": "2020-12-10T21:53:46.447490370Z", "duration": "PT0.000950741S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 195, "parentId": 194, "tags": [ { "key": "beanName", "value": "org.springframework.boot.actuate.autoconfigure.info.InfoEndpointAutoConfiguration" } ] }, "startTime": "2020-12-10T21:53:46.448539830Z", "endTime": "2020-12-10T21:53:46.448727149Z", "duration": "PT0.000187319S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 197, "parentId": 196, "tags": [ { "key": "beanName", "value": "org.springframework.boot.actuate.autoconfigure.info.InfoContributorAutoConfiguration" } ] }, "startTime": "2020-12-10T21:53:46.449362123Z", "endTime": "2020-12-10T21:53:46.449719543Z", "duration": "PT0.00035742S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 196, "parentId": 194, "tags": [ { "key": "beanName", "value": "envInfoContributor" } ] }, "startTime": "2020-12-10T21:53:46.449327785Z", "endTime": "2020-12-10T21:53:46.450691781Z", "duration": "PT0.001363996S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 201, "parentId": 200, "tags": [ { "key": "beanName", "value": "spring.info-org.springframework.boot.autoconfigure.info.ProjectInfoProperties" } ] }, "startTime": "2020-12-10T21:53:46.451567286Z", "endTime": "2020-12-10T21:53:46.452275723Z", "duration": "PT0.000708437S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 200, "parentId": 199, "tags": [ { "key": "beanName", "value": "org.springframework.boot.autoconfigure.info.ProjectInfoAutoConfiguration" } ] }, "startTime": "2020-12-10T21:53:46.451159876Z", "endTime": "2020-12-10T21:53:46.452485436Z", "duration": "PT0.00132556S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 199, "parentId": 198, "tags": [ { "key": "beanName", "value": "buildProperties" } ] }, "startTime": "2020-12-10T21:53:46.451128572Z", "endTime": "2020-12-10T21:53:46.455883677Z", "duration": "PT0.004755105S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 198, "parentId": 194, "tags": [ { "key": "beanName", "value": "buildInfoContributor" } ] }, "startTime": "2020-12-10T21:53:46.450714236Z", "endTime": "2020-12-10T21:53:46.456919541Z", "duration": "PT0.006205305S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 194, "parentId": 154, "tags": [ { "key": "beanName", "value": "infoEndpoint" } ] }, "startTime": "2020-12-10T21:53:46.448495438Z", "endTime": "2020-12-10T21:53:46.457247946Z", "duration": "PT0.008752508S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 203, "parentId": 202, "tags": [ { "key": "beanName", "value": "org.springframework.boot.actuate.autoconfigure.condition.ConditionsReportEndpointAutoConfiguration" } ] }, "startTime": "2020-12-10T21:53:46.457760391Z", "endTime": "2020-12-10T21:53:46.457960170Z", "duration": "PT0.000199779S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 202, "parentId": 154, "tags": [ { "key": "beanName", "value": "conditionsReportEndpoint" } ] }, "startTime": "2020-12-10T21:53:46.457711773Z", "endTime": "2020-12-10T21:53:46.458484305Z", "duration": "PT0.000772532S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 205, "parentId": 204, "tags": [ { "key": "beanName", "value": "org.springframework.boot.actuate.autoconfigure.context.properties.ConfigurationPropertiesReportEndpointAutoConfiguration" } ] }, "startTime": "2020-12-10T21:53:46.458942731Z", "endTime": "2020-12-10T21:53:46.459117626Z", "duration": "PT0.000174895S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 206, "parentId": 204, "tags": [ { "key": "beanName", "value": "management.endpoint.configprops-org.springframework.boot.actuate.autoconfigure.context.properties.ConfigurationPropertiesReportEndpointProperties" } ] }, "startTime": "2020-12-10T21:53:46.459653063Z", "endTime": "2020-12-10T21:53:46.460265854Z", "duration": "PT0.000612791S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 204, "parentId": 154, "tags": [ { "key": "beanName", "value": "configurationPropertiesReportEndpoint" } ] }, "startTime": "2020-12-10T21:53:46.458903108Z", "endTime": "2020-12-10T21:53:46.461382121Z", "duration": "PT0.002479013S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 207, "parentId": 154, "tags": [ { "key": "beanName", "value": "environmentEndpointWebExtension" } ] }, "startTime": "2020-12-10T21:53:46.462685348Z", "endTime": "2020-12-10T21:53:46.463738284Z", "duration": "PT0.001052936S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 209, "parentId": 208, "tags": [ { "key": "beanName", "value": "org.springframework.boot.actuate.autoconfigure.logging.LogFileWebEndpointAutoConfiguration" } ] }, "startTime": "2020-12-10T21:53:46.464289055Z", "endTime": "2020-12-10T21:53:46.464481835Z", "duration": "PT0.00019278S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 210, "parentId": 208, "tags": [ { "key": "beanName", "value": "management.endpoint.logfile-org.springframework.boot.actuate.autoconfigure.logging.LogFileWebEndpointProperties" } ] }, "startTime": "2020-12-10T21:53:46.465015370Z", "endTime": "2020-12-10T21:53:46.465669737Z", "duration": "PT0.000654367S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 208, "parentId": 154, "tags": [ { "key": "beanName", "value": "logFileWebEndpoint" } ] }, "startTime": "2020-12-10T21:53:46.464242754Z", "endTime": "2020-12-10T21:53:46.466738329Z", "duration": "PT0.002495575S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 212, "parentId": 211, "tags": [ { "key": "beanName", "value": "org.springframework.boot.actuate.autoconfigure.logging.LoggersEndpointAutoConfiguration" } ] }, "startTime": "2020-12-10T21:53:46.467627551Z", "endTime": "2020-12-10T21:53:46.468331822Z", "duration": "PT0.000704271S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 211, "parentId": 154, "tags": [ { "key": "beanName", "value": "loggersEndpoint" } ] }, "startTime": "2020-12-10T21:53:46.467498723Z", "endTime": "2020-12-10T21:53:46.471583394Z", "duration": "PT0.004084671S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 214, "parentId": 213, "tags": [ { "key": "beanName", "value": "org.springframework.boot.actuate.autoconfigure.management.HeapDumpWebEndpointAutoConfiguration" } ] }, "startTime": "2020-12-10T21:53:46.473282704Z", "endTime": "2020-12-10T21:53:46.473515393Z", "duration": "PT0.000232689S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 213, "parentId": 154, "tags": [ { "key": "beanName", "value": "heapDumpWebEndpoint" } ] }, "startTime": "2020-12-10T21:53:46.473223716Z", "endTime": "2020-12-10T21:53:46.474243487Z", "duration": "PT0.001019771S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 216, "parentId": 215, "tags": [ { "key": "beanName", "value": "org.springframework.boot.actuate.autoconfigure.management.ThreadDumpEndpointAutoConfiguration" } ] }, "startTime": "2020-12-10T21:53:46.475345170Z", "endTime": "2020-12-10T21:53:46.475703859Z", "duration": "PT0.000358689S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 215, "parentId": 154, "tags": [ { "key": "beanName", "value": "dumpEndpoint" } ] }, "startTime": "2020-12-10T21:53:46.475266247Z", "endTime": "2020-12-10T21:53:46.477842581Z", "duration": "PT0.002576334S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 218, "parentId": 217, "tags": [ { "key": "beanName", "value": "org.springframework.boot.actuate.autoconfigure.metrics.MetricsEndpointAutoConfiguration" } ] }, "startTime": "2020-12-10T21:53:46.478705153Z", "endTime": "2020-12-10T21:53:46.478911502Z", "duration": "PT0.000206349S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 217, "parentId": 154, "tags": [ { "key": "beanName", "value": "metricsEndpoint" } ] }, "startTime": "2020-12-10T21:53:46.478646458Z", "endTime": "2020-12-10T21:53:46.481161798Z", "duration": "PT0.00251534S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 220, "parentId": 219, "tags": [ { "key": "beanName", "value": "org.springframework.boot.actuate.autoconfigure.scheduling.ScheduledTasksEndpointAutoConfiguration" } ] }, "startTime": "2020-12-10T21:53:46.483609914Z", "endTime": "2020-12-10T21:53:46.483892551Z", "duration": "PT0.000282637S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 219, "parentId": 154, "tags": [ { "key": "beanName", "value": "scheduledTasksEndpoint" } ] }, "startTime": "2020-12-10T21:53:46.483437516Z", "endTime": "2020-12-10T21:53:46.488259244Z", "duration": "PT0.004821728S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 222, "parentId": 221, "tags": [ { "key": "beanName", "value": "org.springframework.boot.actuate.autoconfigure.session.SessionsEndpointAutoConfiguration" } ] }, "startTime": "2020-12-10T21:53:46.488812054Z", "endTime": "2020-12-10T21:53:46.489040934Z", "duration": "PT0.00022888S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 221, "parentId": 154, "tags": [ { "key": "beanName", "value": "sessionEndpoint" } ] }, "startTime": "2020-12-10T21:53:46.488759626Z", "endTime": "2020-12-10T21:53:46.490654177Z", "duration": "PT0.001894551S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 224, "parentId": 223, "tags": [ { "key": "beanName", "value": "org.springframework.boot.actuate.autoconfigure.startup.StartupEndpointAutoConfiguration" } ] }, "startTime": "2020-12-10T21:53:46.491288436Z", "endTime": "2020-12-10T21:53:46.491612061Z", "duration": "PT0.000323625S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 223, "parentId": 154, "tags": [ { "key": "beanName", "value": "startupEndpoint" } ] }, "startTime": "2020-12-10T21:53:46.491232751Z", "endTime": "2020-12-10T21:53:46.494185789Z", "duration": "PT0.002953038S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 226, "parentId": 225, "tags": [ { "key": "beanName", "value": "org.springframework.boot.actuate.autoconfigure.trace.http.HttpTraceEndpointAutoConfiguration" } ] }, "startTime": "2020-12-10T21:53:46.494542307Z", "endTime": "2020-12-10T21:53:46.494782247Z", "duration": "PT0.00023994S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 225, "parentId": 154, "tags": [ { "key": "beanName", "value": "httpTraceEndpoint" } ] }, "startTime": "2020-12-10T21:53:46.494481745Z", "endTime": "2020-12-10T21:53:46.495995647Z", "duration": "PT0.001513902S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 228, "parentId": 227, "tags": [ { "key": "beanName", "value": "org.springframework.boot.actuate.autoconfigure.web.mappings.MappingsEndpointAutoConfiguration" } ] }, "startTime": "2020-12-10T21:53:46.496602334Z", "endTime": "2020-12-10T21:53:46.496797332Z", "duration": "PT0.000194998S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 230, "parentId": 229, "tags": [ { "key": "beanName", "value": "org.springframework.boot.actuate.autoconfigure.web.mappings.MappingsEndpointAutoConfiguration$ServletWebConfiguration$SpringMvcConfiguration" } ] }, "startTime": "2020-12-10T21:53:46.497511342Z", "endTime": "2020-12-10T21:53:46.497890436Z", "duration": "PT0.000379094S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 229, "parentId": 227, "tags": [ { "key": "beanName", "value": "dispatcherServletMappingDescriptionProvider" } ] }, "startTime": "2020-12-10T21:53:46.497470647Z", "endTime": "2020-12-10T21:53:46.501371507Z", "duration": "PT0.00390086S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 232, "parentId": 231, "tags": [ { "key": "beanName", "value": "org.springframework.boot.actuate.autoconfigure.web.mappings.MappingsEndpointAutoConfiguration$ServletWebConfiguration" } ] }, "startTime": "2020-12-10T21:53:46.501558290Z", "endTime": "2020-12-10T21:53:46.501825886Z", "duration": "PT0.000267596S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 231, "parentId": 227, "tags": [ { "key": "beanName", "value": "servletMappingDescriptionProvider" } ] }, "startTime": "2020-12-10T21:53:46.501429506Z", "endTime": "2020-12-10T21:53:46.502095666Z", "duration": "PT0.00066616S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 233, "parentId": 227, "tags": [ { "key": "beanName", "value": "filterMappingDescriptionProvider" } ] }, "startTime": "2020-12-10T21:53:46.502118244Z", "endTime": "2020-12-10T21:53:46.502439504Z", "duration": "PT0.00032126S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 227, "parentId": 154, "tags": [ { "key": "beanName", "value": "mappingsEndpoint" } ] }, "startTime": "2020-12-10T21:53:46.496553162Z", "endTime": "2020-12-10T21:53:46.504523568Z", "duration": "PT0.007970406S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 154, "parentId": 150, "tags": [ { "key": "beanName", "value": "pathMappedEndpoints" } ] }, "startTime": "2020-12-10T21:53:46.355544900Z", "endTime": "2020-12-10T21:53:46.547665949Z", "duration": "PT0.192121049S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 235, "parentId": 234, "tags": [ { "key": "beanName", "value": "de.codecentric.boot.admin.client.config.SpringBootAdminClientAutoConfiguration" } ] }, "startTime": "2020-12-10T21:53:46.555325889Z", "endTime": "2020-12-10T21:53:46.555650309Z", "duration": "PT0.00032442S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 234, "parentId": 150, "tags": [ { "key": "beanName", "value": "startupDateMetadataContributor" } ] }, "startTime": "2020-12-10T21:53:46.555260946Z", "endTime": "2020-12-10T21:53:46.562847818Z", "duration": "PT0.007586872S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 150, "parentId": 5, "tags": [ { "key": "beanName", "value": "applicationFactory" } ] }, "startTime": "2020-12-10T21:53:46.345191173Z", "endTime": "2020-12-10T21:53:46.566462630Z", "duration": "PT0.221271457S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 238, "parentId": 237, "tags": [ { "key": "beanName", "value": "org.springframework.boot.autoconfigure.web.reactive.function.client.WebClientAutoConfiguration" } ] }, "startTime": "2020-12-10T21:53:46.568224888Z", "endTime": "2020-12-10T21:53:46.569006129Z", "duration": "PT0.000781241S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 240, "parentId": 239, "tags": [ { "key": "beanName", "value": "org.springframework.boot.autoconfigure.web.reactive.function.client.ClientHttpConnectorAutoConfiguration" } ] }, "startTime": "2020-12-10T21:53:46.572258387Z", "endTime": "2020-12-10T21:53:46.572523189Z", "duration": "PT0.000264802S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 242, "parentId": 241, "tags": [ { "key": "beanName", "value": "org.springframework.boot.autoconfigure.web.reactive.function.client.ClientHttpConnectorConfiguration$ReactorNetty" } ] }, "startTime": "2020-12-10T21:53:46.573065055Z", "endTime": "2020-12-10T21:53:46.573272694Z", "duration": "PT0.000207639S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 243, "parentId": 241, "tags": [ { "key": "beanName", "value": "reactorClientResourceFactory" } ] }, "startTime": "2020-12-10T21:53:46.573779319Z", "endTime": "2020-12-10T21:53:46.602076950Z", "duration": "PT0.028297631S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 241, "parentId": 239, "tags": [ { "key": "beanName", "value": "reactorClientHttpConnector" } ] }, "startTime": "2020-12-10T21:53:46.573035243Z", "endTime": "2020-12-10T21:53:46.673499906Z", "duration": "PT0.100464663S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 239, "parentId": 237, "tags": [ { "key": "beanName", "value": "clientConnectorCustomizer" } ] }, "startTime": "2020-12-10T21:53:46.572206200Z", "endTime": "2020-12-10T21:53:46.674605747Z", "duration": "PT0.102399547S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 245, "parentId": 244, "tags": [ { "key": "beanName", "value": "org.springframework.boot.autoconfigure.web.reactive.function.client.WebClientAutoConfiguration$WebClientCodecsConfiguration" } ] }, "startTime": "2020-12-10T21:53:46.674947156Z", "endTime": "2020-12-10T21:53:46.675754536Z", "duration": "PT0.00080738S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 247, "parentId": 246, "tags": [ { "key": "beanName", "value": "org.springframework.boot.autoconfigure.http.codec.CodecsAutoConfiguration$DefaultCodecsConfiguration" } ] }, "startTime": "2020-12-10T21:53:46.676664571Z", "endTime": "2020-12-10T21:53:46.676913383Z", "duration": "PT0.000248812S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 248, "parentId": 246, "tags": [ { "key": "beanName", "value": "spring.codec-org.springframework.boot.autoconfigure.codec.CodecProperties" } ] }, "startTime": "2020-12-10T21:53:46.677357585Z", "endTime": "2020-12-10T21:53:46.678283721Z", "duration": "PT0.000926136S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 246, "parentId": 244, "tags": [ { "key": "beanName", "value": "defaultCodecCustomizer" } ] }, "startTime": "2020-12-10T21:53:46.676611919Z", "endTime": "2020-12-10T21:53:46.678868862Z", "duration": "PT0.002256943S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 250, "parentId": 249, "tags": [ { "key": "beanName", "value": "org.springframework.boot.autoconfigure.http.codec.CodecsAutoConfiguration$JacksonCodecConfiguration" } ] }, "startTime": "2020-12-10T21:53:46.678940289Z", "endTime": "2020-12-10T21:53:46.679133894Z", "duration": "PT0.000193605S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 252, "parentId": 251, "tags": [ { "key": "beanName", "value": "org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration$JacksonObjectMapperConfiguration" } ] }, "startTime": "2020-12-10T21:53:46.679626760Z", "endTime": "2020-12-10T21:53:46.679821661Z", "duration": "PT0.000194901S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 254, "parentId": 253, "tags": [ { "key": "beanName", "value": "org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration$JacksonObjectMapperBuilderConfiguration" } ] }, "startTime": "2020-12-10T21:53:46.680306435Z", "endTime": "2020-12-10T21:53:46.680514183Z", "duration": "PT0.000207748S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 256, "parentId": 255, "tags": [ { "key": "beanName", "value": "org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration$Jackson2ObjectMapperBuilderCustomizerConfiguration" } ] }, "startTime": "2020-12-10T21:53:46.681133453Z", "endTime": "2020-12-10T21:53:46.681408811Z", "duration": "PT0.000275358S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 257, "parentId": 255, "tags": [ { "key": "beanName", "value": "spring.jackson-org.springframework.boot.autoconfigure.jackson.JacksonProperties" } ] }, "startTime": "2020-12-10T21:53:46.682039385Z", "endTime": "2020-12-10T21:53:46.685763710Z", "duration": "PT0.003724325S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 255, "parentId": 253, "tags": [ { "key": "beanName", "value": "standardJacksonObjectMapperBuilderCustomizer" } ] }, "startTime": "2020-12-10T21:53:46.681077427Z", "endTime": "2020-12-10T21:53:46.686259287Z", "duration": "PT0.00518186S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 259, "parentId": 258, "tags": [ { "key": "beanName", "value": "org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration$ParameterNamesModuleConfiguration" } ] }, "startTime": "2020-12-10T21:53:46.687793887Z", "endTime": "2020-12-10T21:53:46.688123511Z", "duration": "PT0.000329624S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 258, "parentId": 253, "tags": [ { "key": "beanName", "value": "parameterNamesModule" } ] }, "startTime": "2020-12-10T21:53:46.687718592Z", "endTime": "2020-12-10T21:53:46.693183422Z", "duration": "PT0.00546483S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 261, "parentId": 260, "tags": [ { "key": "beanName", "value": "org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration" } ] }, "startTime": "2020-12-10T21:53:46.693246392Z", "endTime": "2020-12-10T21:53:46.693494596Z", "duration": "PT0.000248204S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 260, "parentId": 253, "tags": [ { "key": "beanName", "value": "jsonComponentModule" } ] }, "startTime": "2020-12-10T21:53:46.693197129Z", "endTime": "2020-12-10T21:53:46.700240206Z", "duration": "PT0.007043077S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 263, "parentId": 262, "tags": [ { "key": "beanName", "value": "de.codecentric.boot.admin.server.config.AdminServerWebConfiguration" } ] }, "startTime": "2020-12-10T21:53:46.700484376Z", "endTime": "2020-12-10T21:53:46.701829179Z", "duration": "PT0.001344803S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 262, "parentId": 253, "tags": [ { "key": "beanName", "value": "adminJacksonModule" } ] }, "startTime": "2020-12-10T21:53:46.700322196Z", "endTime": "2020-12-10T21:53:46.723724977Z", "duration": "PT0.023402781S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 253, "parentId": 251, "tags": [ { "key": "beanName", "value": "jacksonObjectMapperBuilder" } ] }, "startTime": "2020-12-10T21:53:46.680242195Z", "endTime": "2020-12-10T21:53:46.726172576Z", "duration": "PT0.045930381S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 251, "parentId": 249, "tags": [ { "key": "beanName", "value": "jacksonObjectMapper" } ] }, "startTime": "2020-12-10T21:53:46.679594474Z", "endTime": "2020-12-10T21:53:46.749677053Z", "duration": "PT0.070082579S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 249, "parentId": 244, "tags": [ { "key": "beanName", "value": "jacksonCodecCustomizer" } ] }, "startTime": "2020-12-10T21:53:46.678905388Z", "endTime": "2020-12-10T21:53:46.751160892Z", "duration": "PT0.072255504S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 244, "parentId": 237, "tags": [ { "key": "beanName", "value": "exchangeStrategiesCustomizer" } ] }, "startTime": "2020-12-10T21:53:46.674727404Z", "endTime": "2020-12-10T21:53:46.752108598Z", "duration": "PT0.077381194S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 265, "parentId": 264, "tags": [ { "key": "beanName", "value": "org.springframework.boot.actuate.autoconfigure.metrics.web.client.WebClientMetricsConfiguration" } ] }, "startTime": "2020-12-10T21:53:46.752231635Z", "endTime": "2020-12-10T21:53:46.752605151Z", "duration": "PT0.000373516S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 266, "parentId": 264, "tags": [ { "key": "beanName", "value": "defaultWebClientExchangeTagsProvider" } ] }, "startTime": "2020-12-10T21:53:46.754204774Z", "endTime": "2020-12-10T21:53:46.755271556Z", "duration": "PT0.001066782S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 264, "parentId": 237, "tags": [ { "key": "beanName", "value": "metricsWebClientCustomizer" } ] }, "startTime": "2020-12-10T21:53:46.752159453Z", "endTime": "2020-12-10T21:53:46.756498060Z", "duration": "PT0.004338607S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 237, "parentId": 236, "tags": [ { "key": "beanName", "value": "webClientBuilder" } ] }, "startTime": "2020-12-10T21:53:46.568034754Z", "endTime": "2020-12-10T21:53:46.759015262Z", "duration": "PT0.190980508S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 268, "parentId": 267, "tags": [ { "key": "beanName", "value": "de.codecentric.boot.admin.server.config.AdminServerInstanceWebClientConfiguration$InstanceExchangeFiltersConfiguration" } ] }, "startTime": "2020-12-10T21:53:46.761570412Z", "endTime": "2020-12-10T21:53:46.762444031Z", "duration": "PT0.000873619S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 270, "parentId": 269, "tags": [ { "key": "beanName", "value": "de.codecentric.boot.admin.server.config.AdminServerInstanceWebClientConfiguration$InstanceExchangeFiltersConfiguration$DefaultInstanceExchangeFiltersConfiguration" } ] }, "startTime": "2020-12-10T21:53:46.763769912Z", "endTime": "2020-12-10T21:53:46.764181559Z", "duration": "PT0.000411647S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 272, "parentId": 271, "tags": [ { "key": "beanName", "value": "de.codecentric.boot.admin.server.config.AdminServerInstanceWebClientConfiguration$HttpHeadersProviderConfiguration" } ] }, "startTime": "2020-12-10T21:53:46.765261186Z", "endTime": "2020-12-10T21:53:46.765905305Z", "duration": "PT0.000644119S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 271, "parentId": 269, "tags": [ { "key": "beanName", "value": "basicAuthHttpHeadersProvider" } ] }, "startTime": "2020-12-10T21:53:46.765190212Z", "endTime": "2020-12-10T21:53:46.768165295Z", "duration": "PT0.002975083S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 269, "parentId": 267, "tags": [ { "key": "beanName", "value": "addHeadersInstanceExchangeFilter" } ] }, "startTime": "2020-12-10T21:53:46.763687545Z", "endTime": "2020-12-10T21:53:46.775160129Z", "duration": "PT0.011472584S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 273, "parentId": 267, "tags": [ { "key": "beanName", "value": "rewriteEndpointUrlInstanceExchangeFilter" } ] }, "startTime": "2020-12-10T21:53:46.775213619Z", "endTime": "2020-12-10T21:53:46.775633409Z", "duration": "PT0.00041979S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 274, "parentId": 267, "tags": [ { "key": "beanName", "value": "setDefaultAcceptHeaderInstanceExchangeFilter" } ] }, "startTime": "2020-12-10T21:53:46.775682571Z", "endTime": "2020-12-10T21:53:46.776193154Z", "duration": "PT0.000510583S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 277, "parentId": 276, "tags": [ { "key": "beanName", "value": "de.codecentric.boot.admin.server.config.AdminServerInstanceWebClientConfiguration$LegacyEndpointConvertersConfiguration" } ] }, "startTime": "2020-12-10T21:53:46.778305167Z", "endTime": "2020-12-10T21:53:46.778863670Z", "duration": "PT0.000558503S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 276, "parentId": 275, "tags": [ { "key": "beanName", "value": "healthLegacyEndpointConverter" } ] }, "startTime": "2020-12-10T21:53:46.778137862Z", "endTime": "2020-12-10T21:53:46.791459792Z", "duration": "PT0.01332193S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 278, "parentId": 275, "tags": [ { "key": "beanName", "value": "infoLegacyEndpointConverter" } ] }, "startTime": "2020-12-10T21:53:46.791486149Z", "endTime": "2020-12-10T21:53:46.791981952Z", "duration": "PT0.000495803S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 279, "parentId": 275, "tags": [ { "key": "beanName", "value": "envLegacyEndpointConverter" } ] }, "startTime": "2020-12-10T21:53:46.791998948Z", "endTime": "2020-12-10T21:53:46.792310010Z", "duration": "PT0.000311062S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 280, "parentId": 275, "tags": [ { "key": "beanName", "value": "httptraceLegacyEndpointConverter" } ] }, "startTime": "2020-12-10T21:53:46.792326105Z", "endTime": "2020-12-10T21:53:46.792639117Z", "duration": "PT0.000313012S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 281, "parentId": 275, "tags": [ { "key": "beanName", "value": "threaddumpLegacyEndpointConverter" } ] }, "startTime": "2020-12-10T21:53:46.792654346Z", "endTime": "2020-12-10T21:53:46.792922212Z", "duration": "PT0.000267866S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 282, "parentId": 275, "tags": [ { "key": "beanName", "value": "liquibaseLegacyEndpointConverter" } ] }, "startTime": "2020-12-10T21:53:46.792937267Z", "endTime": "2020-12-10T21:53:46.793232144Z", "duration": "PT0.000294877S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 283, "parentId": 275, "tags": [ { "key": "beanName", "value": "flywayLegacyEndpointConverter" } ] }, "startTime": "2020-12-10T21:53:46.793246558Z", "endTime": "2020-12-10T21:53:46.793484084Z", "duration": "PT0.000237526S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 284, "parentId": 275, "tags": [ { "key": "beanName", "value": "beansLegacyEndpointConverter" } ] }, "startTime": "2020-12-10T21:53:46.793498194Z", "endTime": "2020-12-10T21:53:46.793741443Z", "duration": "PT0.000243249S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 285, "parentId": 275, "tags": [ { "key": "beanName", "value": "configpropsLegacyEndpointConverter" } ] }, "startTime": "2020-12-10T21:53:46.793755993Z", "endTime": "2020-12-10T21:53:46.794276221Z", "duration": "PT0.000520228S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 286, "parentId": 275, "tags": [ { "key": "beanName", "value": "mappingsLegacyEndpointConverter" } ] }, "startTime": "2020-12-10T21:53:46.794297469Z", "endTime": "2020-12-10T21:53:46.794643430Z", "duration": "PT0.000345961S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 287, "parentId": 275, "tags": [ { "key": "beanName", "value": "startupLegacyEndpointConverter" } ] }, "startTime": "2020-12-10T21:53:46.794660743Z", "endTime": "2020-12-10T21:53:46.794943571Z", "duration": "PT0.000282828S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 275, "parentId": 267, "tags": [ { "key": "beanName", "value": "legacyEndpointConverterInstanceExchangeFilter" } ] }, "startTime": "2020-12-10T21:53:46.776253845Z", "endTime": "2020-12-10T21:53:46.795639034Z", "duration": "PT0.019385189S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 288, "parentId": 267, "tags": [ { "key": "beanName", "value": "logfileAcceptWorkaround" } ] }, "startTime": "2020-12-10T21:53:46.795657382Z", "endTime": "2020-12-10T21:53:46.796069149Z", "duration": "PT0.000411767S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 289, "parentId": 267, "tags": [ { "key": "beanName", "value": "retryInstanceExchangeFilter" } ] }, "startTime": "2020-12-10T21:53:46.796087721Z", "endTime": "2020-12-10T21:53:46.797023531Z", "duration": "PT0.00093581S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 290, "parentId": 267, "tags": [ { "key": "beanName", "value": "timeoutInstanceExchangeFilter" } ] }, "startTime": "2020-12-10T21:53:46.797051338Z", "endTime": "2020-12-10T21:53:46.797825583Z", "duration": "PT0.000774245S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 267, "parentId": 236, "tags": [ { "key": "beanName", "value": "filterInstanceWebClientCustomizer" } ] }, "startTime": "2020-12-10T21:53:46.761417471Z", "endTime": "2020-12-10T21:53:46.800454081Z", "duration": "PT0.03903661S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 236, "parentId": 5, "tags": [ { "key": "beanName", "value": "de.codecentric.boot.admin.server.config.AdminServerInstanceWebClientConfiguration" } ] }, "startTime": "2020-12-10T21:53:46.566537007Z", "endTime": "2020-12-10T21:53:46.801318253Z", "duration": "PT0.234781246S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 292, "parentId": 291, "tags": [ { "key": "beanName", "value": "instanceIdGenerator" } ] }, "startTime": "2020-12-10T21:53:46.802802701Z", "endTime": "2020-12-10T21:53:46.804184331Z", "duration": "PT0.00138163S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 291, "parentId": 5, "tags": [ { "key": "beanName", "value": "instanceRegistry" } ] }, "startTime": "2020-12-10T21:53:46.801332149Z", "endTime": "2020-12-10T21:53:46.804443121Z", "duration": "PT0.003110972S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 293, "parentId": 5, "tags": [ { "key": "beanName", "value": "applicationRegistry" } ] }, "startTime": "2020-12-10T21:53:46.804447981Z", "endTime": "2020-12-10T21:53:46.806633054Z", "duration": "PT0.002185073S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 295, "parentId": 294, "tags": [ { "key": "beanName", "value": "instanceWebClientBuilder" } ] }, "startTime": "2020-12-10T21:53:46.807094641Z", "endTime": "2020-12-10T21:53:46.807382671Z", "duration": "PT0.00028803S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 294, "parentId": 5, "tags": [ { "key": "beanName", "value": "statusUpdater" } ] }, "startTime": "2020-12-10T21:53:46.806638708Z", "endTime": "2020-12-10T21:53:46.842959075Z", "duration": "PT0.036320367S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 296, "parentId": 5, "tags": [ { "key": "beanName", "value": "statusUpdateTrigger" } ] }, "startTime": "2020-12-10T21:53:46.842965895Z", "endTime": "2020-12-10T21:53:46.873200243Z", "duration": "PT0.030234348S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 298, "parentId": 297, "tags": [ { "key": "beanName", "value": "instanceWebClientBuilder" } ] }, "startTime": "2020-12-10T21:53:46.873384297Z", "endTime": "2020-12-10T21:53:46.873448450Z", "duration": "PT0.000064153S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 297, "parentId": 5, "tags": [ { "key": "beanName", "value": "endpointDetector" } ] }, "startTime": "2020-12-10T21:53:46.873206258Z", "endTime": "2020-12-10T21:53:46.878419068Z", "duration": "PT0.00521281S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 299, "parentId": 5, "tags": [ { "key": "beanName", "value": "endpointDetectionTrigger" } ] }, "startTime": "2020-12-10T21:53:46.878426754Z", "endTime": "2020-12-10T21:53:46.880483220Z", "duration": "PT0.002056466S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 301, "parentId": 300, "tags": [ { "key": "beanName", "value": "instanceWebClientBuilder" } ] }, "startTime": "2020-12-10T21:53:46.880626520Z", "endTime": "2020-12-10T21:53:46.880672359Z", "duration": "PT0.000045839S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 300, "parentId": 5, "tags": [ { "key": "beanName", "value": "infoUpdater" } ] }, "startTime": "2020-12-10T21:53:46.880488665Z", "endTime": "2020-12-10T21:53:46.881923220Z", "duration": "PT0.001434555S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 302, "parentId": 5, "tags": [ { "key": "beanName", "value": "infoUpdateTrigger" } ] }, "startTime": "2020-12-10T21:53:46.881928216Z", "endTime": "2020-12-10T21:53:46.883933358Z", "duration": "PT0.002005142S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 305, "parentId": 304, "tags": [ { "key": "beanName", "value": "de.codecentric.boot.admin.NotifierConfig" } ] }, "startTime": "2020-12-10T21:53:46.886166982Z", "endTime": "2020-12-10T21:53:46.886525386Z", "duration": "PT0.000358404S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 304, "parentId": 303, "tags": [ { "key": "beanName", "value": "filteringNotifier" } ] }, "startTime": "2020-12-10T21:53:46.886151301Z", "endTime": "2020-12-10T21:53:46.888548365Z", "duration": "PT0.002397064S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 303, "parentId": 5, "tags": [ { "key": "beanName", "value": "de.codecentric.boot.admin.server.config.AdminServerNotifierAutoConfiguration$FilteringNotifierWebConfiguration" } ] }, "startTime": "2020-12-10T21:53:46.883940213Z", "endTime": "2020-12-10T21:53:46.888676795Z", "duration": "PT0.004736582S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 306, "parentId": 5, "tags": [ { "key": "beanName", "value": "notificationFilterController" } ] }, "startTime": "2020-12-10T21:53:46.888680437Z", "endTime": "2020-12-10T21:53:46.893255831Z", "duration": "PT0.004575394S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 307, "parentId": 5, "tags": [ { "key": "beanName", "value": "de.codecentric.boot.admin.server.config.AdminServerNotifierAutoConfiguration$CompositeNotifierConfiguration" } ] }, "startTime": "2020-12-10T21:53:46.893260064Z", "endTime": "2020-12-10T21:53:46.893482101Z", "duration": "PT0.000222037S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 308, "parentId": 5, "tags": [ { "key": "beanName", "value": "de.codecentric.boot.admin.server.config.AdminServerNotifierAutoConfiguration$NotifierTriggerConfiguration" } ] }, "startTime": "2020-12-10T21:53:46.893485250Z", "endTime": "2020-12-10T21:53:46.893614758Z", "duration": "PT0.000129508S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 310, "parentId": 309, "tags": [ { "key": "beanName", "value": "remindingNotifier" } ] }, "startTime": "2020-12-10T21:53:46.893802680Z", "endTime": "2020-12-10T21:53:46.896112013Z", "duration": "PT0.002309333S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 309, "parentId": 5, "tags": [ { "key": "beanName", "value": "notificationTrigger" } ] }, "startTime": "2020-12-10T21:53:46.893617561Z", "endTime": "2020-12-10T21:53:46.897285691Z", "duration": "PT0.00366813S" }, { "startupStep": { "name": "spring.beans.smart-initialize", "id": 311, "parentId": 5, "tags": [ { "key": "beanName", "value": "org.springframework.context.event.internalEventListenerProcessor" } ] }, "startTime": "2020-12-10T21:53:46.897497373Z", "endTime": "2020-12-10T21:53:46.930290439Z", "duration": "PT0.032793066S" }, { "startupStep": { "name": "spring.beans.smart-initialize", "id": 312, "parentId": 5, "tags": [ { "key": "beanName", "value": "org.springframework.context.annotation.internalScheduledAnnotationProcessor" } ] }, "startTime": "2020-12-10T21:53:46.930329936Z", "endTime": "2020-12-10T21:53:46.930447526Z", "duration": "PT0.00011759S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 314, "parentId": 313, "tags": [ { "key": "beanName", "value": "org.springframework.boot.autoconfigure.context.LifecycleAutoConfiguration" } ] }, "startTime": "2020-12-10T21:53:46.930666618Z", "endTime": "2020-12-10T21:53:46.930859878Z", "duration": "PT0.00019326S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 315, "parentId": 313, "tags": [ { "key": "beanName", "value": "spring.lifecycle-org.springframework.boot.autoconfigure.context.LifecycleProperties" } ] }, "startTime": "2020-12-10T21:53:46.931378343Z", "endTime": "2020-12-10T21:53:46.932055916Z", "duration": "PT0.000677573S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 313, "parentId": 5, "tags": [ { "key": "beanName", "value": "lifecycleProcessor" }, { "key": "beanType", "value": "interface org.springframework.context.LifecycleProcessor" } ] }, "startTime": "2020-12-10T21:53:46.930618623Z", "endTime": "2020-12-10T21:53:46.933079457Z", "duration": "PT0.002460834S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 317, "parentId": 316, "tags": [ { "key": "beanName", "value": "org.springframework.boot.autoconfigure.admin.SpringApplicationAdminJmxAutoConfiguration" } ] }, "startTime": "2020-12-10T21:53:47.015401422Z", "endTime": "2020-12-10T21:53:47.015681787Z", "duration": "PT0.000280365S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 319, "parentId": 318, "tags": [ { "key": "beanName", "value": "org.springframework.boot.autoconfigure.jmx.JmxAutoConfiguration" } ] }, "startTime": "2020-12-10T21:53:47.018289498Z", "endTime": "2020-12-10T21:53:47.019078107Z", "duration": "PT0.000788609S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 320, "parentId": 318, "tags": [ { "key": "beanName", "value": "objectNamingStrategy" } ] }, "startTime": "2020-12-10T21:53:47.019791002Z", "endTime": "2020-12-10T21:53:47.021169421Z", "duration": "PT0.001378419S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 321, "parentId": 318, "tags": [ { "key": "beanName", "value": "mbeanServer" }, { "key": "beanType", "value": "interface javax.management.MBeanServer" } ] }, "startTime": "2020-12-10T21:53:47.030156313Z", "endTime": "2020-12-10T21:53:47.041177381Z", "duration": "PT0.011021068S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 318, "parentId": 316, "tags": [ { "key": "beanName", "value": "mbeanExporter" } ] }, "startTime": "2020-12-10T21:53:47.018242119Z", "endTime": "2020-12-10T21:53:47.045939548Z", "duration": "PT0.027697429S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 316, "parentId": 5, "tags": [ { "key": "beanName", "value": "springApplicationAdminRegistrar" }, { "key": "beanType", "value": "interface org.springframework.context.ApplicationListener" } ] }, "startTime": "2020-12-10T21:53:47.015349395Z", "endTime": "2020-12-10T21:53:47.048358116Z", "duration": "PT0.033008721S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 322, "parentId": 5, "tags": [ { "key": "beanName", "value": "delegatingApplicationListener" }, { "key": "beanType", "value": "interface org.springframework.context.ApplicationListener" } ] }, "startTime": "2020-12-10T21:53:47.048482567Z", "endTime": "2020-12-10T21:53:47.048993047Z", "duration": "PT0.00051048S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 323, "parentId": 5, "tags": [ { "key": "event", "value": "org.springframework.boot.web.servlet.context.ServletWebServerInitializedEvent[source=org.springframework.boot.web.embedded.tomcat.TomcatWebServer@1a4083f6]" }, { "key": "listener", "value": "org.springframework.boot.admin.SpringApplicationAdminMXBeanRegistrar@77e9dca8" } ] }, "startTime": "2020-12-10T21:53:47.049108484Z", "endTime": "2020-12-10T21:53:47.050019757Z", "duration": "PT0.000911273S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 324, "parentId": 5, "tags": [ { "key": "event", "value": "org.springframework.boot.web.servlet.context.ServletWebServerInitializedEvent[source=org.springframework.boot.web.embedded.tomcat.TomcatWebServer@1a4083f6]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T21:53:47.050029878Z", "endTime": "2020-12-10T21:53:47.050045152Z", "duration": "PT0.000015274S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 325, "parentId": 5, "tags": [ { "key": "event", "value": "org.springframework.boot.web.servlet.context.ServletWebServerInitializedEvent[source=org.springframework.boot.web.embedded.tomcat.TomcatWebServer@1a4083f6]" }, { "key": "listener", "value": "org.springframework.boot.web.context.ServerPortInfoApplicationContextInitializer@7c22d4f" } ] }, "startTime": "2020-12-10T21:53:47.050047329Z", "endTime": "2020-12-10T21:53:47.050489300Z", "duration": "PT0.000441971S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 326, "parentId": 5, "tags": [ { "key": "event", "value": "org.springframework.boot.web.servlet.context.ServletWebServerInitializedEvent[source=org.springframework.boot.web.embedded.tomcat.TomcatWebServer@1a4083f6]" }, { "key": "listener", "value": "public void de.codecentric.boot.admin.client.registration.DefaultApplicationFactory.onWebServerInitialized(org.springframework.boot.web.context.WebServerInitializedEvent)" } ] }, "startTime": "2020-12-10T21:53:47.050495848Z", "endTime": "2020-12-10T21:53:47.051239107Z", "duration": "PT0.000743259S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 327, "parentId": 5, "tags": [ { "key": "event", "value": "org.springframework.boot.web.servlet.context.ServletWebServerInitializedEvent[source=org.springframework.boot.web.embedded.tomcat.TomcatWebServer@1a4083f6]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T21:53:47.051262258Z", "endTime": "2020-12-10T21:53:47.051370260Z", "duration": "PT0.000108002S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 330, "parentId": 329, "tags": [ { "key": "beanName", "value": "spring.resources-org.springframework.boot.autoconfigure.web.ResourceProperties" } ] }, "startTime": "2020-12-10T21:53:47.059735036Z", "endTime": "2020-12-10T21:53:47.069104748Z", "duration": "PT0.009369712S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 331, "parentId": 329, "tags": [ { "key": "beanName", "value": "spring.web-org.springframework.boot.autoconfigure.web.WebProperties" } ] }, "startTime": "2020-12-10T21:53:47.070665520Z", "endTime": "2020-12-10T21:53:47.072631542Z", "duration": "PT0.001966022S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 332, "parentId": 329, "tags": [ { "key": "beanName", "value": "org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration$WebMvcAutoConfigurationAdapter" } ] }, "startTime": "2020-12-10T21:53:47.088060026Z", "endTime": "2020-12-10T21:53:47.089895037Z", "duration": "PT0.001835011S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 333, "parentId": 329, "tags": [ { "key": "beanName", "value": "metricsWebMvcConfigurer" } ] }, "startTime": "2020-12-10T21:53:47.090040829Z", "endTime": "2020-12-10T21:53:47.091215546Z", "duration": "PT0.001174717S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 334, "parentId": 329, "tags": [ { "key": "beanName", "value": "org.springframework.security.config.annotation.web.configuration.WebMvcSecurityConfiguration" } ] }, "startTime": "2020-12-10T21:53:47.091262332Z", "endTime": "2020-12-10T21:53:47.091952263Z", "duration": "PT0.000689931S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 329, "parentId": 328, "tags": [ { "key": "beanName", "value": "org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration$EnableWebMvcConfiguration" } ] }, "startTime": "2020-12-10T21:53:47.052636222Z", "endTime": "2020-12-10T21:53:47.093363172Z", "duration": "PT0.04072695S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 328, "parentId": 5, "tags": [ { "key": "beanName", "value": "mvcResourceUrlProvider" }, { "key": "beanType", "value": "interface org.springframework.context.ApplicationListener" } ] }, "startTime": "2020-12-10T21:53:47.052471743Z", "endTime": "2020-12-10T21:53:47.097683297Z", "duration": "PT0.045211554S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 335, "parentId": 5, "tags": [ { "key": "event", "value": "org.springframework.context.event.ContextRefreshedEvent[source=org.springframework.boot.web.servlet.context.AnnotationConfigServletWebServerApplicationContext@18518ccf, started on Thu Dec 10 22:53:42 CET 2020]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T21:53:47.097927129Z", "endTime": "2020-12-10T21:53:47.098164107Z", "duration": "PT0.000236978S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 336, "parentId": 5, "tags": [ { "key": "event", "value": "org.springframework.context.event.ContextRefreshedEvent[source=org.springframework.boot.web.servlet.context.AnnotationConfigServletWebServerApplicationContext@18518ccf, started on Thu Dec 10 22:53:42 CET 2020]" }, { "key": "listener", "value": "org.springframework.boot.autoconfigure.logging.ConditionEvaluationReportLoggingListener$ConditionEvaluationReportListener@10c8f62" } ] }, "startTime": "2020-12-10T21:53:47.098176437Z", "endTime": "2020-12-10T21:53:47.113772099Z", "duration": "PT0.015595662S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 337, "parentId": 5, "tags": [ { "key": "event", "value": "org.springframework.context.event.ContextRefreshedEvent[source=org.springframework.boot.web.servlet.context.AnnotationConfigServletWebServerApplicationContext@18518ccf, started on Thu Dec 10 22:53:42 CET 2020]" }, { "key": "listener", "value": "org.springframework.boot.ClearCachesApplicationListener@957e06" } ] }, "startTime": "2020-12-10T21:53:47.113793948Z", "endTime": "2020-12-10T21:53:47.114213999Z", "duration": "PT0.000420051S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 338, "parentId": 5, "tags": [ { "key": "event", "value": "org.springframework.context.event.ContextRefreshedEvent[source=org.springframework.boot.web.servlet.context.AnnotationConfigServletWebServerApplicationContext@18518ccf, started on Thu Dec 10 22:53:42 CET 2020]" }, { "key": "listener", "value": "org.springframework.boot.autoconfigure.SharedMetadataReaderFactoryContextInitializer$SharedMetadataReaderFactoryBean@344344fa" } ] }, "startTime": "2020-12-10T21:53:47.114218184Z", "endTime": "2020-12-10T21:53:47.114278004Z", "duration": "PT0.00005982S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 340, "parentId": 339, "tags": [ { "key": "beanName", "value": "org.springframework.session.jdbc.config.annotation.web.http.JdbcHttpSessionConfiguration$SessionCleanupConfiguration" } ] }, "startTime": "2020-12-10T21:53:47.114765617Z", "endTime": "2020-12-10T21:53:47.116860109Z", "duration": "PT0.002094492S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 339, "parentId": 5, "tags": [ { "key": "event", "value": "org.springframework.context.event.ContextRefreshedEvent[source=org.springframework.boot.web.servlet.context.AnnotationConfigServletWebServerApplicationContext@18518ccf, started on Thu Dec 10 22:53:42 CET 2020]" }, { "key": "listener", "value": "org.springframework.scheduling.annotation.ScheduledAnnotationBeanPostProcessor@4b039c6d" } ] }, "startTime": "2020-12-10T21:53:47.114280422Z", "endTime": "2020-12-10T21:53:47.134151741Z", "duration": "PT0.019871319S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 341, "parentId": 5, "tags": [ { "key": "event", "value": "org.springframework.context.event.ContextRefreshedEvent[source=org.springframework.boot.web.servlet.context.AnnotationConfigServletWebServerApplicationContext@18518ccf, started on Thu Dec 10 22:53:42 CET 2020]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T21:53:47.134157071Z", "endTime": "2020-12-10T21:53:47.134186008Z", "duration": "PT0.000028937S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 342, "parentId": 5, "tags": [ { "key": "event", "value": "org.springframework.context.event.ContextRefreshedEvent[source=org.springframework.boot.web.servlet.context.AnnotationConfigServletWebServerApplicationContext@18518ccf, started on Thu Dec 10 22:53:42 CET 2020]" }, { "key": "listener", "value": "org.springframework.web.servlet.resource.ResourceUrlProvider@697a92af" } ] }, "startTime": "2020-12-10T21:53:47.134187947Z", "endTime": "2020-12-10T21:53:47.135285408Z", "duration": "PT0.001097461S" }, { "startupStep": { "name": "spring.context.refresh", "id": 5, "parentId": 0, "tags": [] }, "startTime": "2020-12-10T21:53:42.906468743Z", "endTime": "2020-12-10T21:53:47.142210094Z", "duration": "PT4.235741351S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 345, "parentId": 344, "tags": [ { "key": "beanName", "value": "org.springframework.boot.actuate.autoconfigure.metrics.web.tomcat.TomcatMetricsAutoConfiguration" } ] }, "startTime": "2020-12-10T21:53:47.144623798Z", "endTime": "2020-12-10T21:53:47.145388101Z", "duration": "PT0.000764303S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 344, "parentId": 343, "tags": [ { "key": "beanName", "value": "tomcatMetricsBinder" }, { "key": "beanType", "value": "interface org.springframework.context.ApplicationListener" } ] }, "startTime": "2020-12-10T21:53:47.144577414Z", "endTime": "2020-12-10T21:53:47.145972989Z", "duration": "PT0.001395575S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 346, "parentId": 343, "tags": [ { "key": "event", "value": "org.springframework.boot.context.event.ApplicationStartedEvent[source=org.springframework.boot.SpringApplication@563172d3]" }, { "key": "listener", "value": "org.springframework.boot.autoconfigure.BackgroundPreinitializer@1de5f259" } ] }, "startTime": "2020-12-10T21:53:47.146057094Z", "endTime": "2020-12-10T21:53:47.146071184Z", "duration": "PT0.00001409S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 347, "parentId": 343, "tags": [ { "key": "event", "value": "org.springframework.boot.context.event.ApplicationStartedEvent[source=org.springframework.boot.SpringApplication@563172d3]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T21:53:47.146073246Z", "endTime": "2020-12-10T21:53:47.146081790Z", "duration": "PT0.000008544S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 348, "parentId": 343, "tags": [ { "key": "event", "value": "org.springframework.boot.context.event.ApplicationStartedEvent[source=org.springframework.boot.SpringApplication@563172d3]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T21:53:47.146082729Z", "endTime": "2020-12-10T21:53:47.146087692Z", "duration": "PT0.000004963S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 349, "parentId": 343, "tags": [ { "key": "event", "value": "org.springframework.boot.context.event.ApplicationStartedEvent[source=org.springframework.boot.SpringApplication@563172d3]" }, { "key": "listener", "value": "org.springframework.boot.actuate.metrics.web.tomcat.TomcatMetricsBinder@42cc420b" } ] }, "startTime": "2020-12-10T21:53:47.146088274Z", "endTime": "2020-12-10T21:53:47.150557442Z", "duration": "PT0.004469168S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 351, "parentId": 350, "tags": [ { "key": "beanName", "value": "org.springframework.boot.autoconfigure.availability.ApplicationAvailabilityAutoConfiguration" } ] }, "startTime": "2020-12-10T21:53:47.152995878Z", "endTime": "2020-12-10T21:53:47.153287652Z", "duration": "PT0.000291774S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 350, "parentId": 343, "tags": [ { "key": "beanName", "value": "applicationAvailability" }, { "key": "beanType", "value": "interface org.springframework.context.ApplicationListener" } ] }, "startTime": "2020-12-10T21:53:47.152950127Z", "endTime": "2020-12-10T21:53:47.154290092Z", "duration": "PT0.001339965S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 352, "parentId": 343, "tags": [ { "key": "event", "value": "org.springframework.boot.availability.AvailabilityChangeEvent[source=org.springframework.boot.web.servlet.context.AnnotationConfigServletWebServerApplicationContext@18518ccf, started on Thu Dec 10 22:53:42 CET 2020]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T21:53:47.154371988Z", "endTime": "2020-12-10T21:53:47.154435726Z", "duration": "PT0.000063738S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 353, "parentId": 343, "tags": [ { "key": "event", "value": "org.springframework.boot.availability.AvailabilityChangeEvent[source=org.springframework.boot.web.servlet.context.AnnotationConfigServletWebServerApplicationContext@18518ccf, started on Thu Dec 10 22:53:42 CET 2020]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T21:53:47.154437982Z", "endTime": "2020-12-10T21:53:47.154463508Z", "duration": "PT0.000025526S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 354, "parentId": 343, "tags": [ { "key": "event", "value": "org.springframework.boot.availability.AvailabilityChangeEvent[source=org.springframework.boot.web.servlet.context.AnnotationConfigServletWebServerApplicationContext@18518ccf, started on Thu Dec 10 22:53:42 CET 2020]" }, { "key": "listener", "value": "org.springframework.boot.availability.ApplicationAvailabilityBean@642c72cf" } ] }, "startTime": "2020-12-10T21:53:47.154465373Z", "endTime": "2020-12-10T21:53:47.154513987Z", "duration": "PT0.000048614S" }, { "startupStep": { "name": "spring.boot.application.started", "id": 343, "parentId": 0, "tags": [] }, "startTime": "2020-12-10T21:53:47.144342226Z", "endTime": "2020-12-10T21:53:47.154517114Z", "duration": "PT0.010174888S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 356, "parentId": 355, "tags": [ { "key": "event", "value": "org.springframework.boot.context.event.ApplicationReadyEvent[source=org.springframework.boot.SpringApplication@563172d3]" }, { "key": "listener", "value": "org.springframework.boot.admin.SpringApplicationAdminMXBeanRegistrar@77e9dca8" } ] }, "startTime": "2020-12-10T21:53:47.157352462Z", "endTime": "2020-12-10T21:53:47.157376821Z", "duration": "PT0.000024359S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 357, "parentId": 355, "tags": [ { "key": "event", "value": "org.springframework.boot.context.event.ApplicationReadyEvent[source=org.springframework.boot.SpringApplication@563172d3]" }, { "key": "listener", "value": "org.springframework.boot.autoconfigure.BackgroundPreinitializer@1de5f259" } ] }, "startTime": "2020-12-10T21:53:47.157382605Z", "endTime": "2020-12-10T21:53:47.157404236Z", "duration": "PT0.000021631S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 358, "parentId": 355, "tags": [ { "key": "event", "value": "org.springframework.boot.context.event.ApplicationReadyEvent[source=org.springframework.boot.SpringApplication@563172d3]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T21:53:47.157405958Z", "endTime": "2020-12-10T21:53:47.157410838Z", "duration": "PT0.00000488S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 361, "parentId": 360, "tags": [ { "key": "beanName", "value": "spring.boot.admin.client-de.codecentric.boot.admin.client.config.ClientProperties" } ] }, "startTime": "2020-12-10T21:53:47.158042048Z", "endTime": "2020-12-10T21:53:47.162614214Z", "duration": "PT0.004572166S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 364, "parentId": 363, "tags": [ { "key": "beanName", "value": "de.codecentric.boot.admin.client.config.SpringBootAdminClientAutoConfiguration$BlockingRegistrationClientConfig" } ] }, "startTime": "2020-12-10T21:53:47.163609397Z", "endTime": "2020-12-10T21:53:47.163984182Z", "duration": "PT0.000374785S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 363, "parentId": 362, "tags": [ { "key": "beanName", "value": "registrationClient" } ] }, "startTime": "2020-12-10T21:53:47.163593435Z", "endTime": "2020-12-10T21:53:47.479438884Z", "duration": "PT0.315845449S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 362, "parentId": 360, "tags": [ { "key": "beanName", "value": "registrator" } ] }, "startTime": "2020-12-10T21:53:47.163195345Z", "endTime": "2020-12-10T21:53:47.481017056Z", "duration": "PT0.317821711S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 360, "parentId": 359, "tags": [ { "key": "beanName", "value": "registrationListener" } ] }, "startTime": "2020-12-10T21:53:47.157422252Z", "endTime": "2020-12-10T21:53:47.482542175Z", "duration": "PT0.325119923S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 359, "parentId": 355, "tags": [ { "key": "event", "value": "org.springframework.boot.context.event.ApplicationReadyEvent[source=org.springframework.boot.SpringApplication@563172d3]" }, { "key": "listener", "value": "public void de.codecentric.boot.admin.client.registration.RegistrationApplicationListener.onApplicationReady(org.springframework.boot.context.event.ApplicationReadyEvent)" } ] }, "startTime": "2020-12-10T21:53:47.157411655Z", "endTime": "2020-12-10T21:53:47.482965434Z", "duration": "PT0.325553779S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 365, "parentId": 355, "tags": [ { "key": "event", "value": "org.springframework.boot.context.event.ApplicationReadyEvent[source=org.springframework.boot.SpringApplication@563172d3]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T21:53:47.482969219Z", "endTime": "2020-12-10T21:53:47.482974850Z", "duration": "PT0.000005631S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 366, "parentId": 355, "tags": [ { "key": "event", "value": "org.springframework.boot.availability.AvailabilityChangeEvent[source=org.springframework.boot.web.servlet.context.AnnotationConfigServletWebServerApplicationContext@18518ccf, started on Thu Dec 10 22:53:42 CET 2020]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T21:53:47.483280744Z", "endTime": "2020-12-10T21:53:47.483331904Z", "duration": "PT0.00005116S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 367, "parentId": 355, "tags": [ { "key": "event", "value": "org.springframework.boot.availability.AvailabilityChangeEvent[source=org.springframework.boot.web.servlet.context.AnnotationConfigServletWebServerApplicationContext@18518ccf, started on Thu Dec 10 22:53:42 CET 2020]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T21:53:47.483334154Z", "endTime": "2020-12-10T21:53:47.483350211Z", "duration": "PT0.000016057S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 368, "parentId": 355, "tags": [ { "key": "event", "value": "org.springframework.boot.availability.AvailabilityChangeEvent[source=org.springframework.boot.web.servlet.context.AnnotationConfigServletWebServerApplicationContext@18518ccf, started on Thu Dec 10 22:53:42 CET 2020]" }, { "key": "listener", "value": "org.springframework.boot.availability.ApplicationAvailabilityBean@642c72cf" } ] }, "startTime": "2020-12-10T21:53:47.483351506Z", "endTime": "2020-12-10T21:53:47.483369802Z", "duration": "PT0.000018296S" }, { "startupStep": { "name": "spring.boot.application.running", "id": 355, "parentId": 0, "tags": [] }, "startTime": "2020-12-10T21:53:47.156946692Z", "endTime": "2020-12-10T21:53:47.483372127Z", "duration": "PT0.326425435S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 369, "parentId": 0, "tags": [ { "key": "beanName", "value": "multipartResolver" }, { "key": "beanType", "value": "interface org.springframework.web.multipart.MultipartResolver" } ] }, "startTime": "2020-12-10T21:53:47.658887403Z", "endTime": "2020-12-10T21:53:47.660893951Z", "duration": "PT0.002006548S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 370, "parentId": 0, "tags": [ { "key": "beanName", "value": "localeResolver" }, { "key": "beanType", "value": "interface org.springframework.web.servlet.LocaleResolver" } ] }, "startTime": "2020-12-10T21:53:47.660956866Z", "endTime": "2020-12-10T21:53:47.662749901Z", "duration": "PT0.001793035S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 371, "parentId": 0, "tags": [ { "key": "beanName", "value": "themeResolver" }, { "key": "beanType", "value": "interface org.springframework.web.servlet.ThemeResolver" } ] }, "startTime": "2020-12-10T21:53:47.662770772Z", "endTime": "2020-12-10T21:53:47.663610323Z", "duration": "PT0.000839551S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 373, "parentId": 372, "tags": [ { "key": "beanName", "value": "mvcContentNegotiationManager" } ] }, "startTime": "2020-12-10T21:53:47.664992763Z", "endTime": "2020-12-10T21:53:47.669374716Z", "duration": "PT0.004381953S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 374, "parentId": 372, "tags": [ { "key": "beanName", "value": "mvcConversionService" } ] }, "startTime": "2020-12-10T21:53:47.670379598Z", "endTime": "2020-12-10T21:53:47.674221814Z", "duration": "PT0.003842216S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 372, "parentId": 0, "tags": [ { "key": "beanName", "value": "requestMappingHandlerMapping" } ] }, "startTime": "2020-12-10T21:53:47.664252029Z", "endTime": "2020-12-10T21:53:47.721223497Z", "duration": "PT0.056971468S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 375, "parentId": 0, "tags": [ { "key": "beanName", "value": "welcomePageHandlerMapping" } ] }, "startTime": "2020-12-10T21:53:47.721236688Z", "endTime": "2020-12-10T21:53:47.739118198Z", "duration": "PT0.01788151S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 376, "parentId": 0, "tags": [ { "key": "beanName", "value": "viewControllerHandlerMapping" } ] }, "startTime": "2020-12-10T21:53:47.739143272Z", "endTime": "2020-12-10T21:53:47.741002110Z", "duration": "PT0.001858838S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 377, "parentId": 0, "tags": [ { "key": "beanName", "value": "beanNameHandlerMapping" } ] }, "startTime": "2020-12-10T21:53:47.741008644Z", "endTime": "2020-12-10T21:53:47.744985260Z", "duration": "PT0.003976616S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 380, "parentId": 379, "tags": [ { "key": "beanName", "value": "org.springframework.boot.autoconfigure.http.HttpMessageConvertersAutoConfiguration" } ] }, "startTime": "2020-12-10T21:53:47.747164800Z", "endTime": "2020-12-10T21:53:47.747780705Z", "duration": "PT0.000615905S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 382, "parentId": 381, "tags": [ { "key": "beanName", "value": "org.springframework.boot.autoconfigure.http.HttpMessageConvertersAutoConfiguration$StringHttpMessageConverterConfiguration" } ] }, "startTime": "2020-12-10T21:53:47.749025990Z", "endTime": "2020-12-10T21:53:47.749232417Z", "duration": "PT0.000206427S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 381, "parentId": 379, "tags": [ { "key": "beanName", "value": "stringHttpMessageConverter" } ] }, "startTime": "2020-12-10T21:53:47.748999349Z", "endTime": "2020-12-10T21:53:47.754251557Z", "duration": "PT0.005252208S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 384, "parentId": 383, "tags": [ { "key": "beanName", "value": "org.springframework.boot.autoconfigure.http.JacksonHttpMessageConvertersConfiguration$MappingJackson2HttpMessageConverterConfiguration" } ] }, "startTime": "2020-12-10T21:53:47.754449369Z", "endTime": "2020-12-10T21:53:47.754632440Z", "duration": "PT0.000183071S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 383, "parentId": 379, "tags": [ { "key": "beanName", "value": "mappingJackson2HttpMessageConverter" } ] }, "startTime": "2020-12-10T21:53:47.754413341Z", "endTime": "2020-12-10T21:53:47.756710408Z", "duration": "PT0.002297067S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 379, "parentId": 378, "tags": [ { "key": "beanName", "value": "messageConverters" } ] }, "startTime": "2020-12-10T21:53:47.747119100Z", "endTime": "2020-12-10T21:53:47.761438170Z", "duration": "PT0.01431907S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 378, "parentId": 0, "tags": [ { "key": "beanName", "value": "routerFunctionMapping" } ] }, "startTime": "2020-12-10T21:53:47.744997982Z", "endTime": "2020-12-10T21:53:47.765919788Z", "duration": "PT0.020921806S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 385, "parentId": 0, "tags": [ { "key": "beanName", "value": "resourceHandlerMapping" } ] }, "startTime": "2020-12-10T21:53:47.765944701Z", "endTime": "2020-12-10T21:53:47.781276035Z", "duration": "PT0.015331334S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 386, "parentId": 0, "tags": [ { "key": "beanName", "value": "defaultServletHandlerMapping" } ] }, "startTime": "2020-12-10T21:53:47.781285944Z", "endTime": "2020-12-10T21:53:47.781489015Z", "duration": "PT0.000203071S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 388, "parentId": 387, "tags": [ { "key": "beanName", "value": "de.codecentric.boot.admin.server.config.AdminServerWebConfiguration$ServletRestApiConfirguation" } ] }, "startTime": "2020-12-10T21:53:47.781506240Z", "endTime": "2020-12-10T21:53:47.781889735Z", "duration": "PT0.000383495S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 387, "parentId": 0, "tags": [ { "key": "beanName", "value": "adminHandlerMapping" } ] }, "startTime": "2020-12-10T21:53:47.781492647Z", "endTime": "2020-12-10T21:53:47.806886505Z", "duration": "PT0.025393858S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 390, "parentId": 389, "tags": [ { "key": "beanName", "value": "org.springframework.boot.actuate.autoconfigure.endpoint.web.servlet.WebMvcEndpointManagementContextConfiguration" } ] }, "startTime": "2020-12-10T21:53:47.807001481Z", "endTime": "2020-12-10T21:53:47.808386394Z", "duration": "PT0.001384913S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 391, "parentId": 389, "tags": [ { "key": "beanName", "value": "management.endpoints.web.cors-org.springframework.boot.actuate.autoconfigure.endpoint.web.CorsEndpointProperties" } ] }, "startTime": "2020-12-10T21:53:47.814121585Z", "endTime": "2020-12-10T21:53:47.818240787Z", "duration": "PT0.004119202S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 389, "parentId": 0, "tags": [ { "key": "beanName", "value": "webEndpointServletHandlerMapping" } ] }, "startTime": "2020-12-10T21:53:47.806921532Z", "endTime": "2020-12-10T21:53:47.856959203Z", "duration": "PT0.050037671S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 392, "parentId": 0, "tags": [ { "key": "beanName", "value": "controllerEndpointHandlerMapping" } ] }, "startTime": "2020-12-10T21:53:47.856978133Z", "endTime": "2020-12-10T21:53:47.872082260Z", "duration": "PT0.015104127S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 394, "parentId": 393, "tags": [ { "key": "beanName", "value": "mvcValidator" } ] }, "startTime": "2020-12-10T21:53:47.875064486Z", "endTime": "2020-12-10T21:53:47.881711050Z", "duration": "PT0.006646564S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 396, "parentId": 395, "tags": [ { "key": "beanName", "value": "org.springframework.boot.autoconfigure.task.TaskExecutionAutoConfiguration" } ] }, "startTime": "2020-12-10T21:53:47.902143183Z", "endTime": "2020-12-10T21:53:47.902581088Z", "duration": "PT0.000437905S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 398, "parentId": 397, "tags": [ { "key": "beanName", "value": "spring.task.execution-org.springframework.boot.autoconfigure.task.TaskExecutionProperties" } ] }, "startTime": "2020-12-10T21:53:47.904533612Z", "endTime": "2020-12-10T21:53:47.906055846Z", "duration": "PT0.001522234S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 397, "parentId": 395, "tags": [ { "key": "beanName", "value": "taskExecutorBuilder" } ] }, "startTime": "2020-12-10T21:53:47.903178276Z", "endTime": "2020-12-10T21:53:47.908637176Z", "duration": "PT0.0054589S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 395, "parentId": 393, "tags": [ { "key": "beanName", "value": "applicationTaskExecutor" } ] }, "startTime": "2020-12-10T21:53:47.902092953Z", "endTime": "2020-12-10T21:53:47.914377651Z", "duration": "PT0.012284698S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 393, "parentId": 0, "tags": [ { "key": "beanName", "value": "requestMappingHandlerAdapter" } ] }, "startTime": "2020-12-10T21:53:47.872968090Z", "endTime": "2020-12-10T21:53:47.945916101Z", "duration": "PT0.072948011S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 399, "parentId": 0, "tags": [ { "key": "beanName", "value": "handlerFunctionAdapter" } ] }, "startTime": "2020-12-10T21:53:47.945924911Z", "endTime": "2020-12-10T21:53:47.947072810Z", "duration": "PT0.001147899S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 400, "parentId": 0, "tags": [ { "key": "beanName", "value": "httpRequestHandlerAdapter" } ] }, "startTime": "2020-12-10T21:53:47.947078047Z", "endTime": "2020-12-10T21:53:47.947311974Z", "duration": "PT0.000233927S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 401, "parentId": 0, "tags": [ { "key": "beanName", "value": "simpleControllerHandlerAdapter" } ] }, "startTime": "2020-12-10T21:53:47.947316613Z", "endTime": "2020-12-10T21:53:47.947539290Z", "duration": "PT0.000222677S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 402, "parentId": 0, "tags": [ { "key": "beanName", "value": "errorAttributes" } ] }, "startTime": "2020-12-10T21:53:47.948040067Z", "endTime": "2020-12-10T21:53:47.948995002Z", "duration": "PT0.000954935S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 403, "parentId": 0, "tags": [ { "key": "beanName", "value": "handlerExceptionResolver" } ] }, "startTime": "2020-12-10T21:53:47.948999819Z", "endTime": "2020-12-10T21:53:47.955223255Z", "duration": "PT0.006223436S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 404, "parentId": 0, "tags": [ { "key": "beanName", "value": "viewNameTranslator" }, { "key": "beanType", "value": "interface org.springframework.web.servlet.RequestToViewNameTranslator" } ] }, "startTime": "2020-12-10T21:53:47.955256193Z", "endTime": "2020-12-10T21:53:47.956040772Z", "duration": "PT0.000784579S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 406, "parentId": 405, "tags": [ { "key": "beanName", "value": "org.springframework.boot.autoconfigure.web.servlet.error.ErrorMvcAutoConfiguration$WhitelabelErrorViewConfiguration" } ] }, "startTime": "2020-12-10T21:53:47.956514208Z", "endTime": "2020-12-10T21:53:47.957276417Z", "duration": "PT0.000762209S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 405, "parentId": 0, "tags": [ { "key": "beanName", "value": "beanNameViewResolver" } ] }, "startTime": "2020-12-10T21:53:47.956497944Z", "endTime": "2020-12-10T21:53:47.957710083Z", "duration": "PT0.001212139S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 407, "parentId": 0, "tags": [ { "key": "beanName", "value": "mvcViewResolver" } ] }, "startTime": "2020-12-10T21:53:47.957714977Z", "endTime": "2020-12-10T21:53:47.959451143Z", "duration": "PT0.001736166S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 408, "parentId": 0, "tags": [ { "key": "beanName", "value": "defaultViewResolver" } ] }, "startTime": "2020-12-10T21:53:47.959458276Z", "endTime": "2020-12-10T21:53:47.963628481Z", "duration": "PT0.004170205S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 411, "parentId": 410, "tags": [ { "key": "beanName", "value": "org.springframework.boot.autoconfigure.thymeleaf.ThymeleafAutoConfiguration$ThymeleafWebMvcConfiguration$ThymeleafViewResolverConfiguration" } ] }, "startTime": "2020-12-10T21:53:47.965207147Z", "endTime": "2020-12-10T21:53:47.965359752Z", "duration": "PT0.000152605S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 412, "parentId": 410, "tags": [ { "key": "beanName", "value": "spring.thymeleaf-org.springframework.boot.autoconfigure.thymeleaf.ThymeleafProperties" } ] }, "startTime": "2020-12-10T21:53:47.965758917Z", "endTime": "2020-12-10T21:53:47.968126268Z", "duration": "PT0.002367351S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 414, "parentId": 413, "tags": [ { "key": "beanName", "value": "org.springframework.boot.autoconfigure.thymeleaf.ThymeleafAutoConfiguration$ThymeleafDefaultConfiguration" } ] }, "startTime": "2020-12-10T21:53:47.968615493Z", "endTime": "2020-12-10T21:53:47.968771868Z", "duration": "PT0.000156375S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 416, "parentId": 415, "tags": [ { "key": "beanName", "value": "de.codecentric.boot.admin.server.ui.config.AdminServerUiAutoConfiguration" } ] }, "startTime": "2020-12-10T21:53:47.975524593Z", "endTime": "2020-12-10T21:53:47.976524566Z", "duration": "PT0.000999973S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 415, "parentId": 413, "tags": [ { "key": "beanName", "value": "adminTemplateResolver" } ] }, "startTime": "2020-12-10T21:53:47.975494301Z", "endTime": "2020-12-10T21:53:47.978498606Z", "duration": "PT0.003004305S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 418, "parentId": 417, "tags": [ { "key": "beanName", "value": "org.springframework.boot.autoconfigure.thymeleaf.ThymeleafAutoConfiguration$DefaultTemplateResolverConfiguration" } ] }, "startTime": "2020-12-10T21:53:47.978627292Z", "endTime": "2020-12-10T21:53:47.981867614Z", "duration": "PT0.003240322S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 417, "parentId": 413, "tags": [ { "key": "beanName", "value": "defaultTemplateResolver" } ] }, "startTime": "2020-12-10T21:53:47.978551973Z", "endTime": "2020-12-10T21:53:47.982242871Z", "duration": "PT0.003690898S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 420, "parentId": 419, "tags": [ { "key": "beanName", "value": "org.springframework.boot.autoconfigure.thymeleaf.ThymeleafAutoConfiguration$ThymeleafJava8TimeDialect" } ] }, "startTime": "2020-12-10T21:53:47.983080846Z", "endTime": "2020-12-10T21:53:47.983215817Z", "duration": "PT0.000134971S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 419, "parentId": 413, "tags": [ { "key": "beanName", "value": "java8TimeDialect" } ] }, "startTime": "2020-12-10T21:53:47.983056380Z", "endTime": "2020-12-10T21:53:47.984112153Z", "duration": "PT0.001055773S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 413, "parentId": 410, "tags": [ { "key": "beanName", "value": "templateEngine" } ] }, "startTime": "2020-12-10T21:53:47.968594071Z", "endTime": "2020-12-10T21:53:47.985490760Z", "duration": "PT0.016896689S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 410, "parentId": 409, "tags": [ { "key": "beanName", "value": "thymeleafViewResolver" } ] }, "startTime": "2020-12-10T21:53:47.965188095Z", "endTime": "2020-12-10T21:53:47.987396255Z", "duration": "PT0.02220816S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 409, "parentId": 0, "tags": [ { "key": "beanName", "value": "viewResolver" } ] }, "startTime": "2020-12-10T21:53:47.963636460Z", "endTime": "2020-12-10T21:53:47.987696295Z", "duration": "PT0.024059835S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 421, "parentId": 0, "tags": [ { "key": "beanName", "value": "flashMapManager" }, { "key": "beanType", "value": "interface org.springframework.web.servlet.FlashMapManager" } ] }, "startTime": "2020-12-10T21:53:47.987729234Z", "endTime": "2020-12-10T21:53:47.989492211Z", "duration": "PT0.001762977S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 425, "parentId": 424, "tags": [ { "key": "beanName", "value": "org.springframework.security.config.annotation.configuration.ObjectPostProcessorConfiguration" } ] }, "startTime": "2020-12-10T21:53:48.006455232Z", "endTime": "2020-12-10T21:53:48.006647218Z", "duration": "PT0.000191986S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 424, "parentId": 423, "tags": [ { "key": "beanName", "value": "objectPostProcessor" } ] }, "startTime": "2020-12-10T21:53:48.006431903Z", "endTime": "2020-12-10T21:53:48.008281180Z", "duration": "PT0.001849277S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 426, "parentId": 423, "tags": [ { "key": "beanName", "value": "autowiredWebSecurityConfigurersIgnoreParents" } ] }, "startTime": "2020-12-10T21:53:48.010906681Z", "endTime": "2020-12-10T21:53:48.012279189Z", "duration": "PT0.001372508S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 429, "parentId": 428, "tags": [ { "key": "beanName", "value": "enableGlobalAuthenticationAutowiredConfigurer" } ] }, "startTime": "2020-12-10T21:53:48.027518433Z", "endTime": "2020-12-10T21:53:48.029254618Z", "duration": "PT0.001736185S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 430, "parentId": 428, "tags": [ { "key": "beanName", "value": "initializeUserDetailsBeanManagerConfigurer" } ] }, "startTime": "2020-12-10T21:53:48.029322891Z", "endTime": "2020-12-10T21:53:48.029959899Z", "duration": "PT0.000637008S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 431, "parentId": 428, "tags": [ { "key": "beanName", "value": "initializeAuthenticationProviderBeanManagerConfigurer" } ] }, "startTime": "2020-12-10T21:53:48.029982709Z", "endTime": "2020-12-10T21:53:48.030595239Z", "duration": "PT0.00061253S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 428, "parentId": 427, "tags": [ { "key": "beanName", "value": "org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration" } ] }, "startTime": "2020-12-10T21:53:48.024557848Z", "endTime": "2020-12-10T21:53:48.030999670Z", "duration": "PT0.006441822S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 427, "parentId": 423, "tags": [ { "key": "beanName", "value": "de.codecentric.boot.admin.SecurityPermitAllConfig" } ] }, "startTime": "2020-12-10T21:53:48.015987116Z", "endTime": "2020-12-10T21:53:48.032529714Z", "duration": "PT0.016542598S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 423, "parentId": 422, "tags": [ { "key": "beanName", "value": "org.springframework.security.config.annotation.web.configuration.WebSecurityConfiguration" } ] }, "startTime": "2020-12-10T21:53:48.001287385Z", "endTime": "2020-12-10T21:53:48.045923531Z", "duration": "PT0.044636146S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 433, "parentId": 432, "tags": [ { "key": "beanName", "value": "org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration" } ] }, "startTime": "2020-12-10T21:53:48.046295568Z", "endTime": "2020-12-10T21:53:48.046457882Z", "duration": "PT0.000162314S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 432, "parentId": 422, "tags": [ { "key": "beanName", "value": "authenticationEventPublisher" }, { "key": "beanType", "value": "interface org.springframework.security.authentication.AuthenticationEventPublisher" } ] }, "startTime": "2020-12-10T21:53:48.046262755Z", "endTime": "2020-12-10T21:53:48.052581707Z", "duration": "PT0.006318952S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 434, "parentId": 422, "tags": [ { "key": "beanName", "value": "authenticationManagerBuilder" }, { "key": "beanType", "value": "class org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder" } ] }, "startTime": "2020-12-10T21:53:48.053048132Z", "endTime": "2020-12-10T21:53:48.055707789Z", "duration": "PT0.002659657S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 435, "parentId": 422, "tags": [ { "key": "beanName", "value": "org.springframework.boot.autoconfigure.security.servlet.WebSecurityEnablerConfiguration" } ] }, "startTime": "2020-12-10T21:53:48.058808080Z", "endTime": "2020-12-10T21:53:48.058945859Z", "duration": "PT0.000137779S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 437, "parentId": 436, "tags": [ { "key": "beanName", "value": "org.springframework.boot.autoconfigure.security.servlet.UserDetailsServiceAutoConfiguration" } ] }, "startTime": "2020-12-10T21:53:48.060237337Z", "endTime": "2020-12-10T21:53:48.060473508Z", "duration": "PT0.000236171S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 436, "parentId": 422, "tags": [ { "key": "beanName", "value": "inMemoryUserDetailsManager" }, { "key": "beanType", "value": "interface org.springframework.security.core.userdetails.UserDetailsService" } ] }, "startTime": "2020-12-10T21:53:48.060216664Z", "endTime": "2020-12-10T21:53:48.064268462Z", "duration": "PT0.004051798S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 422, "parentId": 0, "tags": [ { "key": "beanName", "value": "springSecurityFilterChain" }, { "key": "beanType", "value": "interface javax.servlet.Filter" } ] }, "startTime": "2020-12-10T21:53:48.001246217Z", "endTime": "2020-12-10T21:53:48.154521573Z", "duration": "PT0.153275356S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 438, "parentId": 0, "tags": [ { "key": "beanName", "value": "instancesController" } ] }, "startTime": "2020-12-10T21:53:48.186163045Z", "endTime": "2020-12-10T21:53:48.188637342Z", "duration": "PT0.002474297S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 439, "parentId": 0, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[274ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T21:53:48.453132955Z", "endTime": "2020-12-10T21:53:48.453194743Z", "duration": "PT0.000061788S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 440, "parentId": 0, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[274ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T21:53:48.453199721Z", "endTime": "2020-12-10T21:53:48.453207311Z", "duration": "PT0.00000759S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 441, "parentId": 0, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[17ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T21:53:48.474352219Z", "endTime": "2020-12-10T21:53:48.474370051Z", "duration": "PT0.000017832S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 442, "parentId": 0, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[17ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T21:53:48.474383004Z", "endTime": "2020-12-10T21:53:48.474391168Z", "duration": "PT0.000008164S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 443, "parentId": 0, "tags": [ { "key": "beanName", "value": "applicationsController" } ] }, "startTime": "2020-12-10T21:53:50.416268478Z", "endTime": "2020-12-10T21:53:50.421868315Z", "duration": "PT0.005599837S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 444, "parentId": 0, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/applications]; client=[0:0:0:0:0:0:0:1]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[34ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T21:53:50.447323048Z", "endTime": "2020-12-10T21:53:50.447375543Z", "duration": "PT0.000052495S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 445, "parentId": 0, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/applications]; client=[0:0:0:0:0:0:0:1]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[34ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T21:53:50.447405530Z", "endTime": "2020-12-10T21:53:50.447426836Z", "duration": "PT0.000021306S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 446, "parentId": 0, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/applications]; client=[0:0:0:0:0:0:0:1]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[93ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T21:53:50.541949463Z", "endTime": "2020-12-10T21:53:50.543540835Z", "duration": "PT0.001591372S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 447, "parentId": 0, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/applications]; client=[0:0:0:0:0:0:0:1]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[93ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T21:53:50.543547002Z", "endTime": "2020-12-10T21:53:50.543569297Z", "duration": "PT0.000022295S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 448, "parentId": 0, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/applications]; client=[0:0:0:0:0:0:0:1]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[13ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T21:53:50.569306193Z", "endTime": "2020-12-10T21:53:50.569334750Z", "duration": "PT0.000028557S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 449, "parentId": 0, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/applications]; client=[0:0:0:0:0:0:0:1]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[13ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T21:53:50.569337837Z", "endTime": "2020-12-10T21:53:50.569349819Z", "duration": "PT0.000011982S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 450, "parentId": 0, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/assets/img/favicon-danger.png]; client=[0:0:0:0:0:0:0:1]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[32ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T21:53:51.465488296Z", "endTime": "2020-12-10T21:53:51.465574009Z", "duration": "PT0.000085713S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 451, "parentId": 0, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/assets/img/favicon-danger.png]; client=[0:0:0:0:0:0:0:1]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[32ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T21:53:51.465580246Z", "endTime": "2020-12-10T21:53:51.465606057Z", "duration": "PT0.000025811S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 452, "parentId": 0, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[3ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T21:53:57.498555303Z", "endTime": "2020-12-10T21:53:57.498571809Z", "duration": "PT0.000016506S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 453, "parentId": 0, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[3ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T21:53:57.498574334Z", "endTime": "2020-12-10T21:53:57.498586333Z", "duration": "PT0.000011999S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 454, "parentId": 0, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[2ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T21:53:57.501397327Z", "endTime": "2020-12-10T21:53:57.501419490Z", "duration": "PT0.000022163S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 455, "parentId": 0, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[2ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T21:53:57.501422870Z", "endTime": "2020-12-10T21:53:57.501436886Z", "duration": "PT0.000014016S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 456, "parentId": 0, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/actuator/health]; client=[192.168.1.12]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[37ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T21:54:06.986813891Z", "endTime": "2020-12-10T21:54:06.986833361Z", "duration": "PT0.00001947S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 457, "parentId": 0, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/actuator/health]; client=[192.168.1.12]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[37ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T21:54:06.986835773Z", "endTime": "2020-12-10T21:54:06.986844292Z", "duration": "PT0.000008519S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 458, "parentId": 0, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/actuator]; client=[192.168.1.12]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[5ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T21:54:07.040020441Z", "endTime": "2020-12-10T21:54:07.040043664Z", "duration": "PT0.000023223S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 459, "parentId": 0, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/actuator]; client=[192.168.1.12]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[5ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T21:54:07.040045859Z", "endTime": "2020-12-10T21:54:07.040063414Z", "duration": "PT0.000017555S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 460, "parentId": 0, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/actuator/info]; client=[192.168.1.12]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[4ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T21:54:07.076530187Z", "endTime": "2020-12-10T21:54:07.076550347Z", "duration": "PT0.00002016S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 461, "parentId": 0, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/actuator/info]; client=[192.168.1.12]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[4ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T21:54:07.076551947Z", "endTime": "2020-12-10T21:54:07.076559476Z", "duration": "PT0.000007529S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 463, "parentId": 462, "tags": [ { "key": "beanName", "value": "instanceWebClientBuilder" } ] }, "startTime": "2020-12-10T21:54:07.402049443Z", "endTime": "2020-12-10T21:54:07.402167585Z", "duration": "PT0.000118142S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 462, "parentId": 0, "tags": [ { "key": "beanName", "value": "instancesProxyController" } ] }, "startTime": "2020-12-10T21:54:07.401856549Z", "endTime": "2020-12-10T21:54:07.405663107Z", "duration": "PT0.003806558S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 464, "parentId": 0, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/actuator/info]; client=[192.168.1.12]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[2ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T21:54:07.422719678Z", "endTime": "2020-12-10T21:54:07.422737171Z", "duration": "PT0.000017493S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 465, "parentId": 0, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/actuator/info]; client=[192.168.1.12]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[2ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T21:54:07.422738916Z", "endTime": "2020-12-10T21:54:07.422785974Z", "duration": "PT0.000047058S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 466, "parentId": 0, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances/572038febb74/actuator/info]; client=[0:0:0:0:0:0:0:1]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[35ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T21:54:07.435889726Z", "endTime": "2020-12-10T21:54:07.435911853Z", "duration": "PT0.000022127S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 467, "parentId": 0, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances/572038febb74/actuator/info]; client=[0:0:0:0:0:0:0:1]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[35ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T21:54:07.435914052Z", "endTime": "2020-12-10T21:54:07.435922084Z", "duration": "PT0.000008032S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 468, "parentId": 0, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[2ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T21:54:07.498004482Z", "endTime": "2020-12-10T21:54:07.498019447Z", "duration": "PT0.000014965S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 469, "parentId": 0, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[2ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T21:54:07.498021410Z", "endTime": "2020-12-10T21:54:07.498028110Z", "duration": "PT0.0000067S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 470, "parentId": 0, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T21:54:07.499642620Z", "endTime": "2020-12-10T21:54:07.499658918Z", "duration": "PT0.000016298S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 471, "parentId": 0, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T21:54:07.499660455Z", "endTime": "2020-12-10T21:54:07.499667079Z", "duration": "PT0.000006624S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 472, "parentId": 0, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T21:54:17.494677411Z", "endTime": "2020-12-10T21:54:17.494696886Z", "duration": "PT0.000019475S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 473, "parentId": 0, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T21:54:17.494700122Z", "endTime": "2020-12-10T21:54:17.494710443Z", "duration": "PT0.000010321S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 474, "parentId": 0, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T21:54:17.496566928Z", "endTime": "2020-12-10T21:54:17.496583273Z", "duration": "PT0.000016345S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 475, "parentId": 0, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T21:54:17.496585021Z", "endTime": "2020-12-10T21:54:17.496592609Z", "duration": "PT0.000007588S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 477, "parentId": 476, "tags": [ { "key": "beanName", "value": "uiExtensions" } ] }, "startTime": "2020-12-10T21:54:18.155574704Z", "endTime": "2020-12-10T21:54:18.159466245Z", "duration": "PT0.003891541S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 476, "parentId": 0, "tags": [ { "key": "beanName", "value": "homeUiController" } ] }, "startTime": "2020-12-10T21:54:18.154960929Z", "endTime": "2020-12-10T21:54:18.161661162Z", "duration": "PT0.006700233S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 478, "parentId": 0, "tags": [ { "key": "beanName", "value": "requestDataValueProcessor" }, { "key": "beanType", "value": "interface org.springframework.web.servlet.support.RequestDataValueProcessor" } ] }, "startTime": "2020-12-10T21:54:18.169379874Z", "endTime": "2020-12-10T21:54:18.170248855Z", "duration": "PT0.000868981S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 479, "parentId": 0, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/]; client=[0:0:0:0:0:0:0:1]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[260ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T21:54:18.413851668Z", "endTime": "2020-12-10T21:54:18.413869277Z", "duration": "PT0.000017609S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 480, "parentId": 0, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/]; client=[0:0:0:0:0:0:0:1]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[260ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T21:54:18.413871196Z", "endTime": "2020-12-10T21:54:18.413878554Z", "duration": "PT0.000007358S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 481, "parentId": 0, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/extensions/css/custom.41bd3967.css]; client=[0:0:0:0:0:0:0:1]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[4ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T21:54:18.456829655Z", "endTime": "2020-12-10T21:54:18.456863425Z", "duration": "PT0.00003377S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 482, "parentId": 0, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/extensions/css/custom.41bd3967.css]; client=[0:0:0:0:0:0:0:1]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[4ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T21:54:18.456867336Z", "endTime": "2020-12-10T21:54:18.456879525Z", "duration": "PT0.000012189S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 483, "parentId": 0, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/extensions/js/custom.7501e234.js]; client=[0:0:0:0:0:0:0:1]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[3ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T21:54:18.497947108Z", "endTime": "2020-12-10T21:54:18.497968757Z", "duration": "PT0.000021649S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 484, "parentId": 0, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/extensions/js/custom.7501e234.js]; client=[0:0:0:0:0:0:0:1]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[3ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T21:54:18.497971090Z", "endTime": "2020-12-10T21:54:18.497982319Z", "duration": "PT0.000011229S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 485, "parentId": 0, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/assets/js/chunk-common.js]; client=[0:0:0:0:0:0:0:1]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[9ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T21:54:18.503778634Z", "endTime": "2020-12-10T21:54:18.503804770Z", "duration": "PT0.000026136S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 486, "parentId": 0, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/assets/js/chunk-common.js]; client=[0:0:0:0:0:0:0:1]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[9ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T21:54:18.503807302Z", "endTime": "2020-12-10T21:54:18.503818013Z", "duration": "PT0.000010711S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 487, "parentId": 0, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/assets/js/sba-core.js]; client=[0:0:0:0:0:0:0:1]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[31ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T21:54:18.526077876Z", "endTime": "2020-12-10T21:54:18.526105515Z", "duration": "PT0.000027639S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 488, "parentId": 0, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/assets/js/sba-core.js]; client=[0:0:0:0:0:0:0:1]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[31ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T21:54:18.526108239Z", "endTime": "2020-12-10T21:54:18.526121023Z", "duration": "PT0.000012784S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 489, "parentId": 0, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/assets/js/chunk-vendors.js]; client=[0:0:0:0:0:0:0:1]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[33ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T21:54:18.527764939Z", "endTime": "2020-12-10T21:54:18.527808087Z", "duration": "PT0.000043148S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 490, "parentId": 0, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/assets/js/chunk-vendors.js]; client=[0:0:0:0:0:0:0:1]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[33ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T21:54:18.527811239Z", "endTime": "2020-12-10T21:54:18.527824443Z", "duration": "PT0.000013204S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 491, "parentId": 0, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/sba-settings.js]; client=[0:0:0:0:0:0:0:1]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[39ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T21:54:18.534409722Z", "endTime": "2020-12-10T21:54:18.534466761Z", "duration": "PT0.000057039S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 492, "parentId": 0, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/sba-settings.js]; client=[0:0:0:0:0:0:0:1]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[39ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T21:54:18.534471490Z", "endTime": "2020-12-10T21:54:18.534486303Z", "duration": "PT0.000014813S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 493, "parentId": 0, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/assets/img/icon-spring-boot-admin.svg]; client=[0:0:0:0:0:0:0:1]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[2ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T21:54:18.800797682Z", "endTime": "2020-12-10T21:54:18.800814955Z", "duration": "PT0.000017273S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 494, "parentId": 0, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/assets/img/icon-spring-boot-admin.svg]; client=[0:0:0:0:0:0:0:1]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[2ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T21:54:18.800816627Z", "endTime": "2020-12-10T21:54:18.800823289Z", "duration": "PT0.000006662S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 495, "parentId": 0, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/applications]; client=[0:0:0:0:0:0:0:1]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T21:54:18.810323191Z", "endTime": "2020-12-10T21:54:18.810340211Z", "duration": "PT0.00001702S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 496, "parentId": 0, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/applications]; client=[0:0:0:0:0:0:0:1]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T21:54:18.810342664Z", "endTime": "2020-12-10T21:54:18.810350370Z", "duration": "PT0.000007706S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 497, "parentId": 0, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/applications]; client=[0:0:0:0:0:0:0:1]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T21:54:18.812196965Z", "endTime": "2020-12-10T21:54:18.812215071Z", "duration": "PT0.000018106S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 498, "parentId": 0, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/applications]; client=[0:0:0:0:0:0:0:1]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T21:54:18.812217019Z", "endTime": "2020-12-10T21:54:18.812224820Z", "duration": "PT0.000007801S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 499, "parentId": 0, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/applications]; client=[0:0:0:0:0:0:0:1]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T21:54:18.832227092Z", "endTime": "2020-12-10T21:54:18.832242936Z", "duration": "PT0.000015844S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 500, "parentId": 0, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/applications]; client=[0:0:0:0:0:0:0:1]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T21:54:18.832244821Z", "endTime": "2020-12-10T21:54:18.832252464Z", "duration": "PT0.000007643S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 501, "parentId": 0, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/assets/img/favicon.png]; client=[0:0:0:0:0:0:0:1]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[2ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T21:54:18.870069292Z", "endTime": "2020-12-10T21:54:18.870088943Z", "duration": "PT0.000019651S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 502, "parentId": 0, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/assets/img/favicon.png]; client=[0:0:0:0:0:0:0:1]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[2ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T21:54:18.870090533Z", "endTime": "2020-12-10T21:54:18.870098593Z", "duration": "PT0.00000806S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 503, "parentId": 0, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/actuator/metrics]; client=[192.168.1.12]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[4ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T21:54:19.125905755Z", "endTime": "2020-12-10T21:54:19.125932982Z", "duration": "PT0.000027227S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 505, "parentId": 0, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/actuator/metrics]; client=[192.168.1.12]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[4ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T21:54:19.125935188Z", "endTime": "2020-12-10T21:54:19.125949323Z", "duration": "PT0.000014135S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 504, "parentId": 0, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/actuator/health]; client=[192.168.1.12]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[3ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T21:54:19.125905958Z", "endTime": "2020-12-10T21:54:19.125932982Z", "duration": "PT0.000027024S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 506, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/actuator/health]; client=[192.168.1.12]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[3ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T21:54:19.125970786Z", "endTime": "2020-12-10T21:54:19.125984117Z", "duration": "PT0.000013331S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 507, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances/572038febb74/actuator/metrics]; client=[0:0:0:0:0:0:0:1]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[18ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T21:54:19.130579688Z", "endTime": "2020-12-10T21:54:19.130612381Z", "duration": "PT0.000032693S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 508, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances/572038febb74/actuator/metrics]; client=[0:0:0:0:0:0:0:1]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[18ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T21:54:19.130614904Z", "endTime": "2020-12-10T21:54:19.130627460Z", "duration": "PT0.000012556S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 509, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances/572038febb74/actuator/health]; client=[0:0:0:0:0:0:0:1]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[19ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T21:54:19.131013245Z", "endTime": "2020-12-10T21:54:19.131047093Z", "duration": "PT0.000033848S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 510, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances/572038febb74/actuator/health]; client=[0:0:0:0:0:0:0:1]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[19ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T21:54:19.131049177Z", "endTime": "2020-12-10T21:54:19.131059889Z", "duration": "PT0.000010712S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 511, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/actuator/info]; client=[192.168.1.12]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[2ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T21:54:19.141141538Z", "endTime": "2020-12-10T21:54:19.141158410Z", "duration": "PT0.000016872S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 512, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/actuator/info]; client=[192.168.1.12]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[2ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T21:54:19.141160739Z", "endTime": "2020-12-10T21:54:19.141168161Z", "duration": "PT0.000007422S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 513, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances/572038febb74/actuator/info]; client=[0:0:0:0:0:0:0:1]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[31ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T21:54:19.143324231Z", "endTime": "2020-12-10T21:54:19.143347214Z", "duration": "PT0.000022983S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 514, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances/572038febb74/actuator/info]; client=[0:0:0:0:0:0:0:1]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[31ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T21:54:19.143349830Z", "endTime": "2020-12-10T21:54:19.143359049Z", "duration": "PT0.000009219S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 515, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/actuator/env/PID]; client=[192.168.1.12]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[18ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T21:54:19.207231479Z", "endTime": "2020-12-10T21:54:19.207250641Z", "duration": "PT0.000019162S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 516, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/actuator/env/PID]; client=[192.168.1.12]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[18ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T21:54:19.207252969Z", "endTime": "2020-12-10T21:54:19.207261123Z", "duration": "PT0.000008154S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 517, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances/572038febb74/actuator/env/PID]; client=[0:0:0:0:0:0:0:1]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[31ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T21:54:19.209634707Z", "endTime": "2020-12-10T21:54:19.209656977Z", "duration": "PT0.00002227S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 518, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances/572038febb74/actuator/env/PID]; client=[0:0:0:0:0:0:0:1]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[31ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T21:54:19.209659425Z", "endTime": "2020-12-10T21:54:19.209670086Z", "duration": "PT0.000010661S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 520, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/actuator/metrics/process.uptime]; client=[192.168.1.12]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[36ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T21:54:19.221861229Z", "endTime": "2020-12-10T21:54:19.221882545Z", "duration": "PT0.000021316S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 519, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/actuator/metrics/system.cpu.count]; client=[192.168.1.12]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[36ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T21:54:19.221860903Z", "endTime": "2020-12-10T21:54:19.221882560Z", "duration": "PT0.000021657S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 521, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/actuator/metrics/process.uptime]; client=[192.168.1.12]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[36ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T21:54:19.221884590Z", "endTime": "2020-12-10T21:54:19.221899332Z", "duration": "PT0.000014742S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 522, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/actuator/metrics/system.cpu.count]; client=[192.168.1.12]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[36ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T21:54:19.221891114Z", "endTime": "2020-12-10T21:54:19.221901886Z", "duration": "PT0.000010772S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 523, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/actuator/metrics/jvm.gc.pause]; client=[192.168.1.12]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[5ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T21:54:19.223330282Z", "endTime": "2020-12-10T21:54:19.223399992Z", "duration": "PT0.00006971S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 524, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/actuator/metrics/jvm.gc.pause]; client=[192.168.1.12]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[5ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T21:54:19.223403126Z", "endTime": "2020-12-10T21:54:19.223415777Z", "duration": "PT0.000012651S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 525, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances/572038febb74/actuator/metrics/process.uptime]; client=[0:0:0:0:0:0:0:1]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[47ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T21:54:19.225524305Z", "endTime": "2020-12-10T21:54:19.225547339Z", "duration": "PT0.000023034S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 526, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances/572038febb74/actuator/metrics/process.uptime]; client=[0:0:0:0:0:0:0:1]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[47ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T21:54:19.225550164Z", "endTime": "2020-12-10T21:54:19.225564532Z", "duration": "PT0.000014368S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 527, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/actuator/metrics/system.cpu.usage]; client=[192.168.1.12]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[3ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T21:54:19.225869099Z", "endTime": "2020-12-10T21:54:19.225901765Z", "duration": "PT0.000032666S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 528, "parentId": 527, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/actuator/metrics/process.cpu.usage]; client=[192.168.1.12]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[4ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T21:54:19.225893168Z", "endTime": "2020-12-10T21:54:19.225909499Z", "duration": "PT0.000016331S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 529, "parentId": 527, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/actuator/metrics/system.cpu.usage]; client=[192.168.1.12]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[3ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T21:54:19.225904775Z", "endTime": "2020-12-10T21:54:19.225921885Z", "duration": "PT0.00001711S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 530, "parentId": 527, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/actuator/metrics/process.cpu.usage]; client=[192.168.1.12]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[4ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T21:54:19.225912274Z", "endTime": "2020-12-10T21:54:19.225925395Z", "duration": "PT0.000013121S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 531, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances/572038febb74/actuator/metrics/jvm.gc.pause]; client=[0:0:0:0:0:0:0:1]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[17ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T21:54:19.229399336Z", "endTime": "2020-12-10T21:54:19.229430448Z", "duration": "PT0.000031112S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 532, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances/572038febb74/actuator/metrics/jvm.gc.pause]; client=[0:0:0:0:0:0:0:1]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[17ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T21:54:19.229435293Z", "endTime": "2020-12-10T21:54:19.229447508Z", "duration": "PT0.000012215S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 533, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances/572038febb74/actuator/metrics/system.cpu.usage]; client=[0:0:0:0:0:0:0:1]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[41ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T21:54:19.229711319Z", "endTime": "2020-12-10T21:54:19.229729298Z", "duration": "PT0.000017979S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 534, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances/572038febb74/actuator/metrics/system.cpu.usage]; client=[0:0:0:0:0:0:0:1]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[41ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T21:54:19.229731390Z", "endTime": "2020-12-10T21:54:19.229741687Z", "duration": "PT0.000010297S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 535, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances/572038febb74/actuator/metrics/process.cpu.usage]; client=[0:0:0:0:0:0:0:1]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[44ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T21:54:19.229899897Z", "endTime": "2020-12-10T21:54:19.229916070Z", "duration": "PT0.000016173S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 536, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances/572038febb74/actuator/metrics/process.cpu.usage]; client=[0:0:0:0:0:0:0:1]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[44ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T21:54:19.229918005Z", "endTime": "2020-12-10T21:54:19.229928089Z", "duration": "PT0.000010084S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 539, "parentId": 538, "tags": [ { "key": "beanName", "value": "org.springframework.boot.autoconfigure.web.servlet.error.ErrorMvcAutoConfiguration$DefaultErrorViewResolverConfiguration" } ] }, "startTime": "2020-12-10T21:54:19.236955869Z", "endTime": "2020-12-10T21:54:19.237570187Z", "duration": "PT0.000614318S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 538, "parentId": 537, "tags": [ { "key": "beanName", "value": "conventionErrorViewResolver" } ] }, "startTime": "2020-12-10T21:54:19.236897620Z", "endTime": "2020-12-10T21:54:19.238488559Z", "duration": "PT0.001590939S" }, { "startupStep": { "name": "spring.beans.instantiate", "id": 537, "parentId": null, "tags": [ { "key": "beanName", "value": "basicErrorController" } ] }, "startTime": "2020-12-10T21:54:19.235126639Z", "endTime": "2020-12-10T21:54:19.239225322Z", "duration": "PT0.004098683S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 540, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/actuator/metrics/jvm.threads.live]; client=[192.168.1.12]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[4ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T21:54:19.242069138Z", "endTime": "2020-12-10T21:54:19.242120903Z", "duration": "PT0.000051765S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 541, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/actuator/metrics/jvm.threads.live]; client=[192.168.1.12]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[4ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T21:54:19.242123760Z", "endTime": "2020-12-10T21:54:19.242137931Z", "duration": "PT0.000014171S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 542, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/actuator/metrics/jvm.threads.peak]; client=[192.168.1.12]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[4ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T21:54:19.244065024Z", "endTime": "2020-12-10T21:54:19.244089368Z", "duration": "PT0.000024344S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 543, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/actuator/metrics/jvm.threads.peak]; client=[192.168.1.12]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[4ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T21:54:19.244091Z", "endTime": "2020-12-10T21:54:19.244098496Z", "duration": "PT0.000007496S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 544, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances/572038febb74/actuator/metrics/jvm.threads.live]; client=[0:0:0:0:0:0:0:1]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[15ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T21:54:19.245325498Z", "endTime": "2020-12-10T21:54:19.245386481Z", "duration": "PT0.000060983S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 545, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances/572038febb74/actuator/metrics/jvm.threads.live]; client=[0:0:0:0:0:0:0:1]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[15ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T21:54:19.245389921Z", "endTime": "2020-12-10T21:54:19.245403964Z", "duration": "PT0.000014043S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 546, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances/572038febb74/actuator/metrics/jvm.threads.peak]; client=[0:0:0:0:0:0:0:1]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[14ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T21:54:19.246978855Z", "endTime": "2020-12-10T21:54:19.246999227Z", "duration": "PT0.000020372S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 547, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances/572038febb74/actuator/metrics/jvm.threads.peak]; client=[0:0:0:0:0:0:0:1]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[14ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T21:54:19.247001488Z", "endTime": "2020-12-10T21:54:19.247011184Z", "duration": "PT0.000009696S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 548, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/actuator/metrics/system.cpu.count]; client=[192.168.1.12]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[15ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T21:54:19.247215396Z", "endTime": "2020-12-10T21:54:19.247237309Z", "duration": "PT0.000021913S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 549, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/actuator/metrics/system.cpu.count]; client=[192.168.1.12]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[15ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T21:54:19.247239051Z", "endTime": "2020-12-10T21:54:19.247251917Z", "duration": "PT0.000012866S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 550, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/actuator/metrics/jvm.threads.daemon]; client=[192.168.1.12]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[3ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T21:54:19.247283177Z", "endTime": "2020-12-10T21:54:19.247297912Z", "duration": "PT0.000014735S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 551, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/actuator/metrics/jvm.threads.daemon]; client=[192.168.1.12]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[3ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T21:54:19.247299458Z", "endTime": "2020-12-10T21:54:19.247308915Z", "duration": "PT0.000009457S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 552, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/actuator/metrics/jvm.memory.max]; client=[192.168.1.12]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[4ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T21:54:19.247840093Z", "endTime": "2020-12-10T21:54:19.247859248Z", "duration": "PT0.000019155S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 553, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/actuator/metrics/jvm.memory.max]; client=[192.168.1.12]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[4ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T21:54:19.247860929Z", "endTime": "2020-12-10T21:54:19.247870101Z", "duration": "PT0.000009172S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 554, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances/572038febb74/actuator/metrics/system.cpu.count]; client=[0:0:0:0:0:0:0:1]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[73ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T21:54:19.251377578Z", "endTime": "2020-12-10T21:54:19.251400914Z", "duration": "PT0.000023336S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 555, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances/572038febb74/actuator/metrics/system.cpu.count]; client=[0:0:0:0:0:0:0:1]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[73ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T21:54:19.251403639Z", "endTime": "2020-12-10T21:54:19.251415056Z", "duration": "PT0.000011417S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 556, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances/572038febb74/actuator/metrics/jvm.memory.max]; client=[0:0:0:0:0:0:0:1]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[16ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T21:54:19.252149230Z", "endTime": "2020-12-10T21:54:19.252171499Z", "duration": "PT0.000022269S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 557, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances/572038febb74/actuator/metrics/jvm.memory.max]; client=[0:0:0:0:0:0:0:1]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[16ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T21:54:19.252173859Z", "endTime": "2020-12-10T21:54:19.252183896Z", "duration": "PT0.000010037S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 558, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances/572038febb74/actuator/metrics/jvm.threads.daemon]; client=[0:0:0:0:0:0:0:1]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[15ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T21:54:19.252486658Z", "endTime": "2020-12-10T21:54:19.252505073Z", "duration": "PT0.000018415S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 559, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances/572038febb74/actuator/metrics/jvm.threads.daemon]; client=[0:0:0:0:0:0:0:1]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[15ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T21:54:19.252506943Z", "endTime": "2020-12-10T21:54:19.252516385Z", "duration": "PT0.000009442S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 560, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/actuator/metrics/jvm.memory.max]; client=[192.168.1.12]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T21:54:19.260627370Z", "endTime": "2020-12-10T21:54:19.260643336Z", "duration": "PT0.000015966S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 560, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/actuator/metrics/jvm.memory.used]; client=[192.168.1.12]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T21:54:19.260627329Z", "endTime": "2020-12-10T21:54:19.260645089Z", "duration": "PT0.00001776S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 561, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/actuator/metrics/jvm.memory.max]; client=[192.168.1.12]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T21:54:19.260644927Z", "endTime": "2020-12-10T21:54:19.260652980Z", "duration": "PT0.000008053S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 562, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/actuator/metrics/jvm.memory.used]; client=[192.168.1.12]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T21:54:19.260646284Z", "endTime": "2020-12-10T21:54:19.260653335Z", "duration": "PT0.000007051S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 563, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances/572038febb74/actuator/metrics/jvm.memory.max]; client=[0:0:0:0:0:0:0:1]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[12ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T21:54:19.262775351Z", "endTime": "2020-12-10T21:54:19.262797934Z", "duration": "PT0.000022583S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 564, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances/572038febb74/actuator/metrics/jvm.memory.max]; client=[0:0:0:0:0:0:0:1]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[12ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T21:54:19.262800562Z", "endTime": "2020-12-10T21:54:19.262810709Z", "duration": "PT0.000010147S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 565, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/actuator/metrics/jvm.memory.used]; client=[192.168.1.12]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[2ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T21:54:19.264309235Z", "endTime": "2020-12-10T21:54:19.264332278Z", "duration": "PT0.000023043S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 566, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/actuator/metrics/jvm.memory.used]; client=[192.168.1.12]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[2ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T21:54:19.264334654Z", "endTime": "2020-12-10T21:54:19.264345667Z", "duration": "PT0.000011013S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 567, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/actuator/metrics/jvm.memory.used]; client=[192.168.1.12]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T21:54:19.264622571Z", "endTime": "2020-12-10T21:54:19.264635127Z", "duration": "PT0.000012556S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 568, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/actuator/metrics/jvm.memory.used]; client=[192.168.1.12]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T21:54:19.264636398Z", "endTime": "2020-12-10T21:54:19.264642897Z", "duration": "PT0.000006499S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 569, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances/572038febb74/actuator/metrics/jvm.memory.used]; client=[0:0:0:0:0:0:0:1]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[10ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T21:54:19.266097373Z", "endTime": "2020-12-10T21:54:19.266119621Z", "duration": "PT0.000022248S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 570, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances/572038febb74/actuator/metrics/jvm.memory.used]; client=[0:0:0:0:0:0:0:1]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[10ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T21:54:19.266122285Z", "endTime": "2020-12-10T21:54:19.266134298Z", "duration": "PT0.000012013S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 571, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances/572038febb74/actuator/metrics/jvm.memory.used]; client=[0:0:0:0:0:0:0:1]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[16ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T21:54:19.266884376Z", "endTime": "2020-12-10T21:54:19.266901044Z", "duration": "PT0.000016668S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 572, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances/572038febb74/actuator/metrics/jvm.memory.used]; client=[0:0:0:0:0:0:0:1]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[16ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T21:54:19.266902478Z", "endTime": "2020-12-10T21:54:19.266909210Z", "duration": "PT0.000006732S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 573, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/actuator/metrics/jvm.memory.used]; client=[192.168.1.12]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T21:54:19.285454500Z", "endTime": "2020-12-10T21:54:19.285470352Z", "duration": "PT0.000015852S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 574, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/actuator/metrics/jvm.memory.used]; client=[192.168.1.12]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T21:54:19.285471726Z", "endTime": "2020-12-10T21:54:19.285490812Z", "duration": "PT0.000019086S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 575, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/actuator/metrics/jvm.memory.committed]; client=[192.168.1.12]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[2ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T21:54:19.285855578Z", "endTime": "2020-12-10T21:54:19.285872917Z", "duration": "PT0.000017339S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 576, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/actuator/metrics/jvm.memory.committed]; client=[192.168.1.12]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[2ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T21:54:19.285874034Z", "endTime": "2020-12-10T21:54:19.285880930Z", "duration": "PT0.000006896S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 577, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances/572038febb74/actuator/metrics/jvm.memory.used]; client=[0:0:0:0:0:0:0:1]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[7ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T21:54:19.286968545Z", "endTime": "2020-12-10T21:54:19.287008461Z", "duration": "PT0.000039916S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 578, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances/572038febb74/actuator/metrics/jvm.memory.used]; client=[0:0:0:0:0:0:0:1]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[7ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T21:54:19.287011399Z", "endTime": "2020-12-10T21:54:19.287027874Z", "duration": "PT0.000016475S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 579, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances/572038febb74/actuator/metrics/jvm.memory.committed]; client=[0:0:0:0:0:0:0:1]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[7ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T21:54:19.287256432Z", "endTime": "2020-12-10T21:54:19.287272330Z", "duration": "PT0.000015898S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 580, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances/572038febb74/actuator/metrics/jvm.memory.committed]; client=[0:0:0:0:0:0:0:1]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[7ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T21:54:19.287274240Z", "endTime": "2020-12-10T21:54:19.287284245Z", "duration": "PT0.000010005S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 581, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/actuator/metrics/process.cpu.usage]; client=[192.168.1.12]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T21:54:21.693293342Z", "endTime": "2020-12-10T21:54:21.693311813Z", "duration": "PT0.000018471S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 582, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/actuator/metrics/process.cpu.usage]; client=[192.168.1.12]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T21:54:21.693313679Z", "endTime": "2020-12-10T21:54:21.693320605Z", "duration": "PT0.000006926S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 583, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/actuator/metrics/system.cpu.usage]; client=[192.168.1.12]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[3ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T21:54:21.694195866Z", "endTime": "2020-12-10T21:54:21.694218879Z", "duration": "PT0.000023013S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 584, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/actuator/metrics/system.cpu.usage]; client=[192.168.1.12]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[3ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T21:54:21.694221030Z", "endTime": "2020-12-10T21:54:21.694232159Z", "duration": "PT0.000011129S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 585, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances/572038febb74/actuator/metrics/process.cpu.usage]; client=[0:0:0:0:0:0:0:1]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[10ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T21:54:21.695890629Z", "endTime": "2020-12-10T21:54:21.695915345Z", "duration": "PT0.000024716S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 586, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances/572038febb74/actuator/metrics/process.cpu.usage]; client=[0:0:0:0:0:0:0:1]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[10ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T21:54:21.695917819Z", "endTime": "2020-12-10T21:54:21.695929077Z", "duration": "PT0.000011258S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 587, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances/572038febb74/actuator/metrics/system.cpu.usage]; client=[0:0:0:0:0:0:0:1]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[9ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T21:54:21.695938986Z", "endTime": "2020-12-10T21:54:21.695954904Z", "duration": "PT0.000015918S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 588, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances/572038febb74/actuator/metrics/system.cpu.usage]; client=[0:0:0:0:0:0:0:1]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[9ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T21:54:21.695956742Z", "endTime": "2020-12-10T21:54:21.695965677Z", "duration": "PT0.000008935S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 589, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/actuator/metrics/jvm.gc.pause]; client=[192.168.1.12]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[3ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T21:54:21.698153674Z", "endTime": "2020-12-10T21:54:21.698176483Z", "duration": "PT0.000022809S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 590, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/actuator/metrics/jvm.gc.pause]; client=[192.168.1.12]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[3ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T21:54:21.698178592Z", "endTime": "2020-12-10T21:54:21.698189848Z", "duration": "PT0.000011256S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 591, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances/572038febb74/actuator/metrics/jvm.gc.pause]; client=[0:0:0:0:0:0:0:1]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[11ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T21:54:21.699950728Z", "endTime": "2020-12-10T21:54:21.699968308Z", "duration": "PT0.00001758S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 592, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances/572038febb74/actuator/metrics/jvm.gc.pause]; client=[0:0:0:0:0:0:0:1]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[11ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T21:54:21.699970132Z", "endTime": "2020-12-10T21:54:21.699977825Z", "duration": "PT0.000007693S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 593, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/actuator/metrics/jvm.threads.live]; client=[192.168.1.12]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[2ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T21:54:21.719161716Z", "endTime": "2020-12-10T21:54:21.719186889Z", "duration": "PT0.000025173S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 594, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/actuator/metrics/jvm.threads.live]; client=[192.168.1.12]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[2ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T21:54:21.719189156Z", "endTime": "2020-12-10T21:54:21.719200966Z", "duration": "PT0.00001181S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 595, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances/572038febb74/actuator/metrics/jvm.threads.live]; client=[0:0:0:0:0:0:0:1]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[11ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T21:54:21.721742308Z", "endTime": "2020-12-10T21:54:21.721777501Z", "duration": "PT0.000035193S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 596, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances/572038febb74/actuator/metrics/jvm.threads.live]; client=[0:0:0:0:0:0:0:1]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[11ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T21:54:21.721780663Z", "endTime": "2020-12-10T21:54:21.721789948Z", "duration": "PT0.000009285S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 597, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/actuator/metrics/jvm.threads.peak]; client=[192.168.1.12]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[2ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T21:54:21.722211897Z", "endTime": "2020-12-10T21:54:21.722226712Z", "duration": "PT0.000014815S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 598, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/actuator/metrics/jvm.threads.peak]; client=[192.168.1.12]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[2ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T21:54:21.722227886Z", "endTime": "2020-12-10T21:54:21.722240059Z", "duration": "PT0.000012173S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 599, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances/572038febb74/actuator/metrics/jvm.threads.peak]; client=[0:0:0:0:0:0:0:1]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[9ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T21:54:21.724249922Z", "endTime": "2020-12-10T21:54:21.724277024Z", "duration": "PT0.000027102S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 600, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances/572038febb74/actuator/metrics/jvm.threads.peak]; client=[0:0:0:0:0:0:0:1]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[9ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T21:54:21.724280079Z", "endTime": "2020-12-10T21:54:21.724289791Z", "duration": "PT0.000009712S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 601, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/actuator/metrics/jvm.threads.daemon]; client=[192.168.1.12]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T21:54:21.754347930Z", "endTime": "2020-12-10T21:54:21.754364144Z", "duration": "PT0.000016214S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 602, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/actuator/metrics/jvm.threads.daemon]; client=[192.168.1.12]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T21:54:21.754365816Z", "endTime": "2020-12-10T21:54:21.754372925Z", "duration": "PT0.000007109S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 603, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances/572038febb74/actuator/metrics/jvm.threads.daemon]; client=[0:0:0:0:0:0:0:1]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[6ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T21:54:21.755681594Z", "endTime": "2020-12-10T21:54:21.755699298Z", "duration": "PT0.000017704S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 604, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances/572038febb74/actuator/metrics/jvm.threads.daemon]; client=[0:0:0:0:0:0:0:1]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[6ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T21:54:21.755701423Z", "endTime": "2020-12-10T21:54:21.755708716Z", "duration": "PT0.000007293S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 605, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/actuator/health]; client=[192.168.1.12]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[2ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T21:54:26.882438923Z", "endTime": "2020-12-10T21:54:26.882456648Z", "duration": "PT0.000017725S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 606, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/actuator/health]; client=[192.168.1.12]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[2ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T21:54:26.882458504Z", "endTime": "2020-12-10T21:54:26.882473042Z", "duration": "PT0.000014538S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 607, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[2ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T21:54:27.500808160Z", "endTime": "2020-12-10T21:54:27.500921637Z", "duration": "PT0.000113477S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 608, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[2ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T21:54:27.500925612Z", "endTime": "2020-12-10T21:54:27.500937736Z", "duration": "PT0.000012124S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 609, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[2ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T21:54:27.503817514Z", "endTime": "2020-12-10T21:54:27.503871671Z", "duration": "PT0.000054157S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 610, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[2ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T21:54:27.503873667Z", "endTime": "2020-12-10T21:54:27.503882525Z", "duration": "PT0.000008858S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 611, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[2ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T21:54:37.494905321Z", "endTime": "2020-12-10T21:54:37.494921464Z", "duration": "PT0.000016143S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 612, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[2ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T21:54:37.494923836Z", "endTime": "2020-12-10T21:54:37.494932114Z", "duration": "PT0.000008278S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 613, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T21:54:37.496266916Z", "endTime": "2020-12-10T21:54:37.496279430Z", "duration": "PT0.000012514S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 614, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T21:54:37.496280683Z", "endTime": "2020-12-10T21:54:37.496287425Z", "duration": "PT0.000006742S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 615, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/actuator/health]; client=[192.168.1.12]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[9ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T21:54:46.896425787Z", "endTime": "2020-12-10T21:54:46.896470793Z", "duration": "PT0.000045006S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 616, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/actuator/health]; client=[192.168.1.12]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[9ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T21:54:46.896473972Z", "endTime": "2020-12-10T21:54:46.896491747Z", "duration": "PT0.000017775S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 617, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[2ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T21:54:47.500784501Z", "endTime": "2020-12-10T21:54:47.500799266Z", "duration": "PT0.000014765S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 618, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[2ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T21:54:47.500801679Z", "endTime": "2020-12-10T21:54:47.500808304Z", "duration": "PT0.000006625S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 619, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T21:54:47.502129894Z", "endTime": "2020-12-10T21:54:47.502146408Z", "duration": "PT0.000016514S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 620, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T21:54:47.502148192Z", "endTime": "2020-12-10T21:54:47.502154661Z", "duration": "PT0.000006469S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 621, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/]; client=[0:0:0:0:0:0:0:1]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[4ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T21:54:47.668386658Z", "endTime": "2020-12-10T21:54:47.668400781Z", "duration": "PT0.000014123S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 622, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/]; client=[0:0:0:0:0:0:0:1]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[4ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T21:54:47.668402068Z", "endTime": "2020-12-10T21:54:47.668407996Z", "duration": "PT0.000005928S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 623, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/extensions/css/custom.41bd3967.css]; client=[0:0:0:0:0:0:0:1]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[2ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T21:54:47.715156853Z", "endTime": "2020-12-10T21:54:47.715192332Z", "duration": "PT0.000035479S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 624, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/extensions/css/custom.41bd3967.css]; client=[0:0:0:0:0:0:0:1]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[2ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T21:54:47.715195067Z", "endTime": "2020-12-10T21:54:47.715204900Z", "duration": "PT0.000009833S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 625, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/extensions/js/custom.7501e234.js]; client=[0:0:0:0:0:0:0:1]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[2ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T21:54:47.722258991Z", "endTime": "2020-12-10T21:54:47.722316768Z", "duration": "PT0.000057777S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 626, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/extensions/js/custom.7501e234.js]; client=[0:0:0:0:0:0:0:1]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[2ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T21:54:47.722319432Z", "endTime": "2020-12-10T21:54:47.722337654Z", "duration": "PT0.000018222S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 627, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/assets/js/chunk-common.js]; client=[0:0:0:0:0:0:0:1]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[3ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T21:54:47.728469276Z", "endTime": "2020-12-10T21:54:47.728490359Z", "duration": "PT0.000021083S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 628, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/assets/js/chunk-common.js]; client=[0:0:0:0:0:0:0:1]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[3ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T21:54:47.728492442Z", "endTime": "2020-12-10T21:54:47.728501355Z", "duration": "PT0.000008913S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 629, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/sba-settings.js]; client=[0:0:0:0:0:0:0:1]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[5ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T21:54:47.729628538Z", "endTime": "2020-12-10T21:54:47.729648756Z", "duration": "PT0.000020218S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 630, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/sba-settings.js]; client=[0:0:0:0:0:0:0:1]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[5ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T21:54:47.729650744Z", "endTime": "2020-12-10T21:54:47.729659814Z", "duration": "PT0.00000907S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 631, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/assets/js/sba-core.js]; client=[0:0:0:0:0:0:0:1]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[34ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T21:54:47.759525248Z", "endTime": "2020-12-10T21:54:47.759551200Z", "duration": "PT0.000025952S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 632, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/assets/js/sba-core.js]; client=[0:0:0:0:0:0:0:1]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[34ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T21:54:47.759554281Z", "endTime": "2020-12-10T21:54:47.759564496Z", "duration": "PT0.000010215S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 633, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/assets/js/chunk-vendors.js]; client=[0:0:0:0:0:0:0:1]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[39ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T21:54:47.765466831Z", "endTime": "2020-12-10T21:54:47.765493361Z", "duration": "PT0.00002653S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 634, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/assets/js/chunk-vendors.js]; client=[0:0:0:0:0:0:0:1]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[39ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T21:54:47.765495850Z", "endTime": "2020-12-10T21:54:47.765504614Z", "duration": "PT0.000008764S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 635, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/assets/img/icon-spring-boot-admin.svg]; client=[0:0:0:0:0:0:0:1]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[2ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T21:54:48.036915621Z", "endTime": "2020-12-10T21:54:48.036934388Z", "duration": "PT0.000018767S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 636, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/assets/img/icon-spring-boot-admin.svg]; client=[0:0:0:0:0:0:0:1]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[2ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T21:54:48.036936160Z", "endTime": "2020-12-10T21:54:48.036943714Z", "duration": "PT0.000007554S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 637, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/applications]; client=[0:0:0:0:0:0:0:1]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T21:54:48.045918303Z", "endTime": "2020-12-10T21:54:48.045938175Z", "duration": "PT0.000019872S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 638, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/applications]; client=[0:0:0:0:0:0:0:1]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T21:54:48.045941350Z", "endTime": "2020-12-10T21:54:48.045949658Z", "duration": "PT0.000008308S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 639, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/applications]; client=[0:0:0:0:0:0:0:1]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[2ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T21:54:48.047550018Z", "endTime": "2020-12-10T21:54:48.047564162Z", "duration": "PT0.000014144S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 640, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/applications]; client=[0:0:0:0:0:0:0:1]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[2ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T21:54:48.047565468Z", "endTime": "2020-12-10T21:54:48.047571193Z", "duration": "PT0.000005725S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 641, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/applications]; client=[0:0:0:0:0:0:0:1]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T21:54:48.088792448Z", "endTime": "2020-12-10T21:54:48.088808160Z", "duration": "PT0.000015712S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 642, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/applications]; client=[0:0:0:0:0:0:0:1]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T21:54:48.088809667Z", "endTime": "2020-12-10T21:54:48.088815843Z", "duration": "PT0.000006176S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 643, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/assets/img/favicon.png]; client=[0:0:0:0:0:0:0:1]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T21:54:48.098445887Z", "endTime": "2020-12-10T21:54:48.098461403Z", "duration": "PT0.000015516S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 644, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/assets/img/favicon.png]; client=[0:0:0:0:0:0:0:1]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T21:54:48.098463485Z", "endTime": "2020-12-10T21:54:48.098470898Z", "duration": "PT0.000007413S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 645, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[2ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T21:54:57.493675604Z", "endTime": "2020-12-10T21:54:57.493690649Z", "duration": "PT0.000015045S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 646, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[2ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T21:54:57.493693483Z", "endTime": "2020-12-10T21:54:57.493700344Z", "duration": "PT0.000006861S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 647, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T21:54:57.494942514Z", "endTime": "2020-12-10T21:54:57.494955728Z", "duration": "PT0.000013214S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 648, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T21:54:57.494956905Z", "endTime": "2020-12-10T21:54:57.494963035Z", "duration": "PT0.00000613S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 649, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/actuator/health]; client=[192.168.1.12]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[2ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T21:55:06.882038848Z", "endTime": "2020-12-10T21:55:06.882057112Z", "duration": "PT0.000018264S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 650, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/actuator/health]; client=[192.168.1.12]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[2ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T21:55:06.882058999Z", "endTime": "2020-12-10T21:55:06.882066324Z", "duration": "PT0.000007325S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 651, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[2ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T21:55:07.497825107Z", "endTime": "2020-12-10T21:55:07.497849338Z", "duration": "PT0.000024231S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 652, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[2ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T21:55:07.497852066Z", "endTime": "2020-12-10T21:55:07.497860478Z", "duration": "PT0.000008412S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 653, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T21:55:07.499318393Z", "endTime": "2020-12-10T21:55:07.499344739Z", "duration": "PT0.000026346S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 654, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T21:55:07.499346518Z", "endTime": "2020-12-10T21:55:07.499353923Z", "duration": "PT0.000007405S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 655, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T21:55:17.494212274Z", "endTime": "2020-12-10T21:55:17.494229920Z", "duration": "PT0.000017646S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 656, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T21:55:17.494232458Z", "endTime": "2020-12-10T21:55:17.494240053Z", "duration": "PT0.000007595S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 657, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T21:55:17.495745322Z", "endTime": "2020-12-10T21:55:17.495762407Z", "duration": "PT0.000017085S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 658, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T21:55:17.495764078Z", "endTime": "2020-12-10T21:55:17.495771824Z", "duration": "PT0.000007746S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 659, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/actuator/health]; client=[192.168.1.12]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[2ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T21:55:26.886506503Z", "endTime": "2020-12-10T21:55:26.886528483Z", "duration": "PT0.00002198S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 660, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/actuator/health]; client=[192.168.1.12]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[2ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T21:55:26.886530026Z", "endTime": "2020-12-10T21:55:26.886537945Z", "duration": "PT0.000007919S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 661, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[2ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T21:55:27.504243835Z", "endTime": "2020-12-10T21:55:27.504283989Z", "duration": "PT0.000040154S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 662, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[2ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T21:55:27.504286674Z", "endTime": "2020-12-10T21:55:27.504303239Z", "duration": "PT0.000016565S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 663, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T21:55:27.505467549Z", "endTime": "2020-12-10T21:55:27.505478383Z", "duration": "PT0.000010834S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 664, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T21:55:27.505479613Z", "endTime": "2020-12-10T21:55:27.505485657Z", "duration": "PT0.000006044S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 665, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T21:55:37.492753704Z", "endTime": "2020-12-10T21:55:37.492767580Z", "duration": "PT0.000013876S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 666, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T21:55:37.492769626Z", "endTime": "2020-12-10T21:55:37.492777015Z", "duration": "PT0.000007389S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 667, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T21:55:37.493969883Z", "endTime": "2020-12-10T21:55:37.493980563Z", "duration": "PT0.00001068S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 668, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T21:55:37.493981680Z", "endTime": "2020-12-10T21:55:37.493987638Z", "duration": "PT0.000005958S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 669, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/actuator/health]; client=[192.168.1.12]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[2ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T21:55:46.885118614Z", "endTime": "2020-12-10T21:55:46.885135608Z", "duration": "PT0.000016994S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 670, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/actuator/health]; client=[192.168.1.12]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[2ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T21:55:46.885137637Z", "endTime": "2020-12-10T21:55:46.885145604Z", "duration": "PT0.000007967S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 671, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/actuator/info]; client=[192.168.1.12]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[2ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T21:55:46.890841879Z", "endTime": "2020-12-10T21:55:46.890855185Z", "duration": "PT0.000013306S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 672, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/actuator/info]; client=[192.168.1.12]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[2ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T21:55:46.890856546Z", "endTime": "2020-12-10T21:55:46.890862374Z", "duration": "PT0.000005828S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 673, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T21:55:47.502003260Z", "endTime": "2020-12-10T21:55:47.502018117Z", "duration": "PT0.000014857S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 674, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T21:55:47.502020823Z", "endTime": "2020-12-10T21:55:47.502028223Z", "duration": "PT0.0000074S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 675, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T21:55:47.503439671Z", "endTime": "2020-12-10T21:55:47.503457818Z", "duration": "PT0.000018147S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 676, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T21:55:47.503459105Z", "endTime": "2020-12-10T21:55:47.503466031Z", "duration": "PT0.000006926S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 677, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T21:55:57.494691467Z", "endTime": "2020-12-10T21:55:57.494730539Z", "duration": "PT0.000039072S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 678, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T21:55:57.494733062Z", "endTime": "2020-12-10T21:55:57.494747026Z", "duration": "PT0.000013964S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 679, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[0ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T21:55:57.495895450Z", "endTime": "2020-12-10T21:55:57.495904568Z", "duration": "PT0.000009118S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 680, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[0ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T21:55:57.495905516Z", "endTime": "2020-12-10T21:55:57.495910171Z", "duration": "PT0.000004655S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 681, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/actuator/health]; client=[192.168.1.12]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[4ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T21:56:06.885993268Z", "endTime": "2020-12-10T21:56:06.886039858Z", "duration": "PT0.00004659S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 682, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/actuator/health]; client=[192.168.1.12]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[4ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T21:56:06.886043395Z", "endTime": "2020-12-10T21:56:06.886054065Z", "duration": "PT0.00001067S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 683, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T21:56:07.496408552Z", "endTime": "2020-12-10T21:56:07.496423081Z", "duration": "PT0.000014529S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 684, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T21:56:07.496425061Z", "endTime": "2020-12-10T21:56:07.496431848Z", "duration": "PT0.000006787S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 685, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T21:56:07.497537167Z", "endTime": "2020-12-10T21:56:07.497548443Z", "duration": "PT0.000011276S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 686, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T21:56:07.497549626Z", "endTime": "2020-12-10T21:56:07.497556704Z", "duration": "PT0.000007078S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 687, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[2ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T21:56:17.497794404Z", "endTime": "2020-12-10T21:56:17.497809433Z", "duration": "PT0.000015029S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 688, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[2ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T21:56:17.497812115Z", "endTime": "2020-12-10T21:56:17.497819066Z", "duration": "PT0.000006951S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 689, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T21:56:17.499395505Z", "endTime": "2020-12-10T21:56:17.499413826Z", "duration": "PT0.000018321S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 690, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T21:56:17.499415936Z", "endTime": "2020-12-10T21:56:17.499428159Z", "duration": "PT0.000012223S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 691, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/applications]; client=[0:0:0:0:0:0:0:1]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[0ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T21:56:21.705334741Z", "endTime": "2020-12-10T21:56:21.705347641Z", "duration": "PT0.0000129S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 692, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/applications]; client=[0:0:0:0:0:0:0:1]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[0ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T21:56:21.705349658Z", "endTime": "2020-12-10T21:56:21.705356097Z", "duration": "PT0.000006439S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 693, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/applications]; client=[0:0:0:0:0:0:0:1]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T21:56:21.706739782Z", "endTime": "2020-12-10T21:56:21.706750176Z", "duration": "PT0.000010394S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 694, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/applications]; client=[0:0:0:0:0:0:0:1]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T21:56:21.706751185Z", "endTime": "2020-12-10T21:56:21.706756199Z", "duration": "PT0.000005014S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 695, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/applications]; client=[0:0:0:0:0:0:0:1]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[0ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T21:56:21.722153492Z", "endTime": "2020-12-10T21:56:21.722165923Z", "duration": "PT0.000012431S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 696, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/applications]; client=[0:0:0:0:0:0:0:1]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[0ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T21:56:21.722167668Z", "endTime": "2020-12-10T21:56:21.722173165Z", "duration": "PT0.000005497S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 697, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/actuator/health]; client=[192.168.1.12]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[2ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T21:56:26.886451644Z", "endTime": "2020-12-10T21:56:26.886466218Z", "duration": "PT0.000014574S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 698, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/actuator/health]; client=[192.168.1.12]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[2ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T21:56:26.886467803Z", "endTime": "2020-12-10T21:56:26.886474908Z", "duration": "PT0.000007105S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 699, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[2ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T21:56:27.516255502Z", "endTime": "2020-12-10T21:56:27.516270074Z", "duration": "PT0.000014572S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 700, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[2ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T21:56:27.516272124Z", "endTime": "2020-12-10T21:56:27.516279103Z", "duration": "PT0.000006979S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 701, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T21:56:27.517514973Z", "endTime": "2020-12-10T21:56:27.517527429Z", "duration": "PT0.000012456S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 702, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T21:56:27.517528960Z", "endTime": "2020-12-10T21:56:27.517540886Z", "duration": "PT0.000011926S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 703, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T21:56:37.496673688Z", "endTime": "2020-12-10T21:56:37.496785126Z", "duration": "PT0.000111438S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 704, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T21:56:37.496787615Z", "endTime": "2020-12-10T21:56:37.496794945Z", "duration": "PT0.00000733S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 705, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[0ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T21:56:37.497992249Z", "endTime": "2020-12-10T21:56:37.498004431Z", "duration": "PT0.000012182S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 706, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[0ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T21:56:37.498005671Z", "endTime": "2020-12-10T21:56:37.498065395Z", "duration": "PT0.000059724S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 707, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/actuator/health]; client=[192.168.1.12]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[2ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T21:56:46.881176730Z", "endTime": "2020-12-10T21:56:46.881188986Z", "duration": "PT0.000012256S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 708, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/actuator/health]; client=[192.168.1.12]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[2ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T21:56:46.881190699Z", "endTime": "2020-12-10T21:56:46.881196186Z", "duration": "PT0.000005487S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 709, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/actuator/info]; client=[192.168.1.12]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[2ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T21:56:46.888446097Z", "endTime": "2020-12-10T21:56:46.888454219Z", "duration": "PT0.000008122S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 710, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/actuator/info]; client=[192.168.1.12]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[2ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T21:56:46.888455576Z", "endTime": "2020-12-10T21:56:46.888459039Z", "duration": "PT0.000003463S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 711, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T21:56:47.502777686Z", "endTime": "2020-12-10T21:56:47.502788814Z", "duration": "PT0.000011128S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 712, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T21:56:47.502790856Z", "endTime": "2020-12-10T21:56:47.502795839Z", "duration": "PT0.000004983S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 713, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[0ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T21:56:47.503897855Z", "endTime": "2020-12-10T21:56:47.503906553Z", "duration": "PT0.000008698S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 714, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[0ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T21:56:47.503907964Z", "endTime": "2020-12-10T21:56:47.503912756Z", "duration": "PT0.000004792S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 715, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/actuator/health]; client=[192.168.1.12]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[2ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T21:56:56.887939332Z", "endTime": "2020-12-10T21:56:56.887950171Z", "duration": "PT0.000010839S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 716, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/actuator/health]; client=[192.168.1.12]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[2ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T21:56:56.887951880Z", "endTime": "2020-12-10T21:56:56.887957569Z", "duration": "PT0.000005689S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 717, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T21:56:57.502631004Z", "endTime": "2020-12-10T21:56:57.502641139Z", "duration": "PT0.000010135S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 718, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T21:56:57.502643011Z", "endTime": "2020-12-10T21:56:57.502648474Z", "duration": "PT0.000005463S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 719, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[0ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T21:56:57.503721912Z", "endTime": "2020-12-10T21:56:57.503730504Z", "duration": "PT0.000008592S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 720, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[0ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T21:56:57.503731487Z", "endTime": "2020-12-10T21:56:57.503735526Z", "duration": "PT0.000004039S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 721, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[4ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T21:57:07.504540072Z", "endTime": "2020-12-10T21:57:07.504566898Z", "duration": "PT0.000026826S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 722, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[4ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T21:57:07.504571620Z", "endTime": "2020-12-10T21:57:07.504583832Z", "duration": "PT0.000012212S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 723, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[3ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T21:57:07.507594802Z", "endTime": "2020-12-10T21:57:07.507616947Z", "duration": "PT0.000022145S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 724, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[3ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T21:57:07.507620415Z", "endTime": "2020-12-10T21:57:07.507632745Z", "duration": "PT0.00001233S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 725, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/actuator/health]; client=[192.168.1.12]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[4ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T21:57:16.890175069Z", "endTime": "2020-12-10T21:57:16.890201976Z", "duration": "PT0.000026907S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 726, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/actuator/health]; client=[192.168.1.12]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[4ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T21:57:16.890206377Z", "endTime": "2020-12-10T21:57:16.890219502Z", "duration": "PT0.000013125S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 727, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[3ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T21:57:17.507762695Z", "endTime": "2020-12-10T21:57:17.507788456Z", "duration": "PT0.000025761S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 728, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[3ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T21:57:17.507794485Z", "endTime": "2020-12-10T21:57:17.507808007Z", "duration": "PT0.000013522S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 729, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[55ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T21:57:17.563527525Z", "endTime": "2020-12-10T21:57:17.563556463Z", "duration": "PT0.000028938S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 730, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[55ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T21:57:17.563561110Z", "endTime": "2020-12-10T21:57:17.563575524Z", "duration": "PT0.000014414S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 731, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[3ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T21:57:27.500863976Z", "endTime": "2020-12-10T21:57:27.500888929Z", "duration": "PT0.000024953S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 732, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[3ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T21:57:27.500893893Z", "endTime": "2020-12-10T21:57:27.500906594Z", "duration": "PT0.000012701S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 733, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[2ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T21:57:27.503886966Z", "endTime": "2020-12-10T21:57:27.503909848Z", "duration": "PT0.000022882S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 734, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[2ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T21:57:27.503912918Z", "endTime": "2020-12-10T21:57:27.503924936Z", "duration": "PT0.000012018S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 735, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/actuator/health]; client=[192.168.1.12]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[4ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T21:57:36.890368232Z", "endTime": "2020-12-10T21:57:36.890394663Z", "duration": "PT0.000026431S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 736, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/actuator/health]; client=[192.168.1.12]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[4ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T21:57:36.890398300Z", "endTime": "2020-12-10T21:57:36.890425011Z", "duration": "PT0.000026711S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 737, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[4ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T21:57:37.514939560Z", "endTime": "2020-12-10T21:57:37.514976131Z", "duration": "PT0.000036571S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 738, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[4ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T21:57:37.514982449Z", "endTime": "2020-12-10T21:57:37.514995769Z", "duration": "PT0.00001332S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 739, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[2ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T21:57:37.518099610Z", "endTime": "2020-12-10T21:57:37.518124128Z", "duration": "PT0.000024518S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 740, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[2ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T21:57:37.518128087Z", "endTime": "2020-12-10T21:57:37.518149202Z", "duration": "PT0.000021115S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 741, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/actuator/health]; client=[192.168.1.12]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T21:57:46.905986584Z", "endTime": "2020-12-10T21:57:46.905996199Z", "duration": "PT0.000009615S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 742, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/actuator/health]; client=[192.168.1.12]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T21:57:46.905997696Z", "endTime": "2020-12-10T21:57:46.906001796Z", "duration": "PT0.0000041S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 743, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/actuator/info]; client=[192.168.1.12]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[2ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T21:57:47.052268487Z", "endTime": "2020-12-10T21:57:47.052278064Z", "duration": "PT0.000009577S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 744, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/actuator/info]; client=[192.168.1.12]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[2ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T21:57:47.052279259Z", "endTime": "2020-12-10T21:57:47.052283439Z", "duration": "PT0.00000418S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 745, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T21:57:47.557167021Z", "endTime": "2020-12-10T21:57:47.557177993Z", "duration": "PT0.000010972S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 746, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T21:57:47.557179784Z", "endTime": "2020-12-10T21:57:47.557185010Z", "duration": "PT0.000005226S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 747, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[0ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T21:57:47.558115415Z", "endTime": "2020-12-10T21:57:47.558121542Z", "duration": "PT0.000006127S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 748, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[0ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T21:57:47.558122435Z", "endTime": "2020-12-10T21:57:47.558125973Z", "duration": "PT0.000003538S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 749, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[3ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T21:57:57.505192840Z", "endTime": "2020-12-10T21:57:57.505218326Z", "duration": "PT0.000025486S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 750, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[3ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T21:57:57.505223353Z", "endTime": "2020-12-10T21:57:57.505236182Z", "duration": "PT0.000012829S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 751, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[2ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T21:57:57.508076110Z", "endTime": "2020-12-10T21:57:57.508097432Z", "duration": "PT0.000021322S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 752, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[2ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T21:57:57.508102211Z", "endTime": "2020-12-10T21:57:57.508114148Z", "duration": "PT0.000011937S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 753, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/actuator/health]; client=[192.168.1.12]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[4ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T21:58:06.896607428Z", "endTime": "2020-12-10T21:58:06.896634345Z", "duration": "PT0.000026917S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 754, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/actuator/health]; client=[192.168.1.12]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[4ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T21:58:06.896638297Z", "endTime": "2020-12-10T21:58:06.896665401Z", "duration": "PT0.000027104S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 755, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[4ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T21:58:07.560510387Z", "endTime": "2020-12-10T21:58:07.560537118Z", "duration": "PT0.000026731S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 756, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[4ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T21:58:07.560542019Z", "endTime": "2020-12-10T21:58:07.560565195Z", "duration": "PT0.000023176S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 757, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[2ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T21:58:07.563334873Z", "endTime": "2020-12-10T21:58:07.563357769Z", "duration": "PT0.000022896S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 758, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[2ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T21:58:07.563361110Z", "endTime": "2020-12-10T21:58:07.563373266Z", "duration": "PT0.000012156S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 759, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[3ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T21:58:17.504008917Z", "endTime": "2020-12-10T21:58:17.504044594Z", "duration": "PT0.000035677S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 760, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[3ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T21:58:17.504049483Z", "endTime": "2020-12-10T21:58:17.504062637Z", "duration": "PT0.000013154S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 761, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[2ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T21:58:17.506968910Z", "endTime": "2020-12-10T21:58:17.506990091Z", "duration": "PT0.000021181S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 762, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[2ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T21:58:17.506993193Z", "endTime": "2020-12-10T21:58:17.507017688Z", "duration": "PT0.000024495S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 763, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/actuator/health]; client=[192.168.1.12]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[7ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T21:58:26.898961704Z", "endTime": "2020-12-10T21:58:26.898990313Z", "duration": "PT0.000028609S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 764, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/actuator/health]; client=[192.168.1.12]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[7ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T21:58:26.898995473Z", "endTime": "2020-12-10T21:58:26.899009465Z", "duration": "PT0.000013992S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 765, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[4ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T21:58:27.513525879Z", "endTime": "2020-12-10T21:58:27.513550783Z", "duration": "PT0.000024904S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 766, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[4ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T21:58:27.513555619Z", "endTime": "2020-12-10T21:58:27.513569170Z", "duration": "PT0.000013551S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 767, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[2ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T21:58:27.516300102Z", "endTime": "2020-12-10T21:58:27.516323187Z", "duration": "PT0.000023085S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 768, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[2ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T21:58:27.516326595Z", "endTime": "2020-12-10T21:58:27.516338090Z", "duration": "PT0.000011495S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 769, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/actuator/health]; client=[192.168.1.12]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T21:58:36.971143763Z", "endTime": "2020-12-10T21:58:36.971153763Z", "duration": "PT0.00001S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 770, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/actuator/health]; client=[192.168.1.12]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T21:58:36.971155135Z", "endTime": "2020-12-10T21:58:36.971160143Z", "duration": "PT0.000005008S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 771, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T21:58:37.818808480Z", "endTime": "2020-12-10T21:58:37.818816284Z", "duration": "PT0.000007804S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 772, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T21:58:37.818818068Z", "endTime": "2020-12-10T21:58:37.818821330Z", "duration": "PT0.000003262S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 773, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T21:58:37.819589951Z", "endTime": "2020-12-10T21:58:37.819595949Z", "duration": "PT0.000005998S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 774, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T21:58:37.819596711Z", "endTime": "2020-12-10T21:58:37.819599741Z", "duration": "PT0.00000303S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 775, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/actuator/info]; client=[192.168.1.12]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[5ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T21:58:46.897675942Z", "endTime": "2020-12-10T21:58:46.897704176Z", "duration": "PT0.000028234S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 776, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/actuator/info]; client=[192.168.1.12]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[5ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T21:58:46.897707900Z", "endTime": "2020-12-10T21:58:46.897721141Z", "duration": "PT0.000013241S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 777, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[3ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T21:58:47.507999968Z", "endTime": "2020-12-10T21:58:47.508026711Z", "duration": "PT0.000026743S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 778, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[3ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T21:58:47.508031727Z", "endTime": "2020-12-10T21:58:47.508045332Z", "duration": "PT0.000013605S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 779, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[2ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T21:58:47.510862094Z", "endTime": "2020-12-10T21:58:47.510883528Z", "duration": "PT0.000021434S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 780, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[2ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T21:58:47.510886673Z", "endTime": "2020-12-10T21:58:47.510898952Z", "duration": "PT0.000012279S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 781, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/actuator/health]; client=[192.168.1.12]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[4ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T21:58:56.891416873Z", "endTime": "2020-12-10T21:58:56.891457729Z", "duration": "PT0.000040856S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 782, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/actuator/health]; client=[192.168.1.12]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[4ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T21:58:56.891462419Z", "endTime": "2020-12-10T21:58:56.891476532Z", "duration": "PT0.000014113S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 783, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[3ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T21:58:57.511872021Z", "endTime": "2020-12-10T21:58:57.511896467Z", "duration": "PT0.000024446S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 784, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[3ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T21:58:57.511901136Z", "endTime": "2020-12-10T21:58:57.511913808Z", "duration": "PT0.000012672S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 785, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[2ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T21:58:57.514517067Z", "endTime": "2020-12-10T21:58:57.514538436Z", "duration": "PT0.000021369S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 786, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[2ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T21:58:57.514541305Z", "endTime": "2020-12-10T21:58:57.514552700Z", "duration": "PT0.000011395S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 787, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[3ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T21:59:07.507658188Z", "endTime": "2020-12-10T21:59:07.507683933Z", "duration": "PT0.000025745S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 788, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[3ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T21:59:07.507688724Z", "endTime": "2020-12-10T21:59:07.507701142Z", "duration": "PT0.000012418S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 789, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[2ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T21:59:07.510407254Z", "endTime": "2020-12-10T21:59:07.510428335Z", "duration": "PT0.000021081S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 790, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[2ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T21:59:07.510431433Z", "endTime": "2020-12-10T21:59:07.510444085Z", "duration": "PT0.000012652S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 791, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/actuator/health]; client=[192.168.1.12]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[4ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T21:59:16.890473383Z", "endTime": "2020-12-10T21:59:16.890500339Z", "duration": "PT0.000026956S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 792, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/actuator/health]; client=[192.168.1.12]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[4ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T21:59:16.890503643Z", "endTime": "2020-12-10T21:59:16.890516467Z", "duration": "PT0.000012824S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 793, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[3ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T21:59:17.514437479Z", "endTime": "2020-12-10T21:59:17.514462629Z", "duration": "PT0.00002515S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 794, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[3ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T21:59:17.514467308Z", "endTime": "2020-12-10T21:59:17.514479258Z", "duration": "PT0.00001195S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 795, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[2ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T21:59:17.517176555Z", "endTime": "2020-12-10T21:59:17.517197686Z", "duration": "PT0.000021131S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 796, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[2ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T21:59:17.517200738Z", "endTime": "2020-12-10T21:59:17.517213558Z", "duration": "PT0.00001282S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 797, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[3ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T21:59:27.506403526Z", "endTime": "2020-12-10T21:59:27.506429625Z", "duration": "PT0.000026099S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 798, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[3ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T21:59:27.506434262Z", "endTime": "2020-12-10T21:59:27.506446972Z", "duration": "PT0.00001271S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 799, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[3ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T21:59:27.509354595Z", "endTime": "2020-12-10T21:59:27.509375892Z", "duration": "PT0.000021297S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 800, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[3ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T21:59:27.509379262Z", "endTime": "2020-12-10T21:59:27.509400105Z", "duration": "PT0.000020843S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 801, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/actuator/health]; client=[192.168.1.12]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[3ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T21:59:36.890912964Z", "endTime": "2020-12-10T21:59:36.890940218Z", "duration": "PT0.000027254S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 802, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/actuator/health]; client=[192.168.1.12]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[3ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T21:59:36.890943922Z", "endTime": "2020-12-10T21:59:36.890957533Z", "duration": "PT0.000013611S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 803, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[3ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T21:59:37.502097787Z", "endTime": "2020-12-10T21:59:37.502123054Z", "duration": "PT0.000025267S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 804, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[3ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T21:59:37.502127568Z", "endTime": "2020-12-10T21:59:37.502140707Z", "duration": "PT0.000013139S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 805, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[2ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T21:59:37.504773003Z", "endTime": "2020-12-10T21:59:37.504795803Z", "duration": "PT0.0000228S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 806, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[2ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T21:59:37.504799232Z", "endTime": "2020-12-10T21:59:37.504810468Z", "duration": "PT0.000011236S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 807, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[3ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T21:59:47.500575851Z", "endTime": "2020-12-10T21:59:47.500610046Z", "duration": "PT0.000034195S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 808, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[3ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T21:59:47.500615060Z", "endTime": "2020-12-10T21:59:47.500628709Z", "duration": "PT0.000013649S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 809, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[3ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T21:59:47.503595166Z", "endTime": "2020-12-10T21:59:47.503617033Z", "duration": "PT0.000021867S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 810, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[3ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T21:59:47.503620388Z", "endTime": "2020-12-10T21:59:47.503644543Z", "duration": "PT0.000024155S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 811, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/actuator/health]; client=[192.168.1.12]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[3ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T21:59:56.889416987Z", "endTime": "2020-12-10T21:59:56.889441145Z", "duration": "PT0.000024158S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 812, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/actuator/health]; client=[192.168.1.12]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[3ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T21:59:56.889444514Z", "endTime": "2020-12-10T21:59:56.889456144Z", "duration": "PT0.00001163S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 813, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[2ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T21:59:57.518395627Z", "endTime": "2020-12-10T21:59:57.518419750Z", "duration": "PT0.000024123S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 814, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[2ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T21:59:57.518423852Z", "endTime": "2020-12-10T21:59:57.518434889Z", "duration": "PT0.000011037S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 815, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[2ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T21:59:57.520697857Z", "endTime": "2020-12-10T21:59:57.520718365Z", "duration": "PT0.000020508S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 816, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[2ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T21:59:57.520721379Z", "endTime": "2020-12-10T21:59:57.520731226Z", "duration": "PT0.000009847S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 817, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[2ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T22:00:07.504465789Z", "endTime": "2020-12-10T22:00:07.504487886Z", "duration": "PT0.000022097S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 818, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[2ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T22:00:07.504501455Z", "endTime": "2020-12-10T22:00:07.504513330Z", "duration": "PT0.000011875S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 819, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[2ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T22:00:07.507206626Z", "endTime": "2020-12-10T22:00:07.507226499Z", "duration": "PT0.000019873S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 820, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[2ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T22:00:07.507229699Z", "endTime": "2020-12-10T22:00:07.507245865Z", "duration": "PT0.000016166S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 821, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/actuator/health]; client=[192.168.1.12]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[2ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T22:00:16.891389224Z", "endTime": "2020-12-10T22:00:16.891410836Z", "duration": "PT0.000021612S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 822, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/actuator/health]; client=[192.168.1.12]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[2ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T22:00:16.891413626Z", "endTime": "2020-12-10T22:00:16.891423656Z", "duration": "PT0.00001003S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 823, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[2ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T22:00:17.509618729Z", "endTime": "2020-12-10T22:00:17.509638781Z", "duration": "PT0.000020052S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 824, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[2ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T22:00:17.509642282Z", "endTime": "2020-12-10T22:00:17.509652096Z", "duration": "PT0.000009814S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 825, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T22:00:17.511544709Z", "endTime": "2020-12-10T22:00:17.511560109Z", "duration": "PT0.0000154S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 826, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T22:00:17.511562366Z", "endTime": "2020-12-10T22:00:17.511572034Z", "duration": "PT0.000009668S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 827, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[2ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T22:00:27.493875347Z", "endTime": "2020-12-10T22:00:27.493891753Z", "duration": "PT0.000016406S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 828, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[2ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T22:00:27.493894714Z", "endTime": "2020-12-10T22:00:27.493902958Z", "duration": "PT0.000008244S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 829, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T22:00:27.495583489Z", "endTime": "2020-12-10T22:00:27.495597233Z", "duration": "PT0.000013744S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 830, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T22:00:27.495598876Z", "endTime": "2020-12-10T22:00:27.495606271Z", "duration": "PT0.000007395S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 831, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/actuator/health]; client=[192.168.1.12]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T22:00:36.881747377Z", "endTime": "2020-12-10T22:00:36.881767735Z", "duration": "PT0.000020358S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 832, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/actuator/health]; client=[192.168.1.12]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T22:00:36.881769895Z", "endTime": "2020-12-10T22:00:36.881776711Z", "duration": "PT0.000006816S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 833, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[2ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T22:00:37.514853556Z", "endTime": "2020-12-10T22:00:37.514867959Z", "duration": "PT0.000014403S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 834, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[2ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T22:00:37.514871498Z", "endTime": "2020-12-10T22:00:37.514878528Z", "duration": "PT0.00000703S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 835, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T22:00:37.516244113Z", "endTime": "2020-12-10T22:00:37.516256601Z", "duration": "PT0.000012488S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 836, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T22:00:37.516258215Z", "endTime": "2020-12-10T22:00:37.516264690Z", "duration": "PT0.000006475S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 837, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/actuator/info]; client=[192.168.1.12]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[3ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T22:00:46.891713555Z", "endTime": "2020-12-10T22:00:46.891726985Z", "duration": "PT0.00001343S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 838, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/actuator/info]; client=[192.168.1.12]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[3ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T22:00:46.891729394Z", "endTime": "2020-12-10T22:00:46.891735311Z", "duration": "PT0.000005917S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 839, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T22:00:47.490518894Z", "endTime": "2020-12-10T22:00:47.490539548Z", "duration": "PT0.000020654S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 840, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T22:00:47.490541990Z", "endTime": "2020-12-10T22:00:47.490548284Z", "duration": "PT0.000006294S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 841, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[0ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T22:00:47.491790824Z", "endTime": "2020-12-10T22:00:47.491812236Z", "duration": "PT0.000021412S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 842, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[0ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T22:00:47.491813647Z", "endTime": "2020-12-10T22:00:47.491818325Z", "duration": "PT0.000004678S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 843, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/actuator/health]; client=[192.168.1.12]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[2ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T22:00:56.880110424Z", "endTime": "2020-12-10T22:00:56.880123248Z", "duration": "PT0.000012824S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 844, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/actuator/health]; client=[192.168.1.12]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[2ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T22:00:56.880125241Z", "endTime": "2020-12-10T22:00:56.880130776Z", "duration": "PT0.000005535S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 845, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T22:00:57.498720590Z", "endTime": "2020-12-10T22:00:57.498731598Z", "duration": "PT0.000011008S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 846, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T22:00:57.498733703Z", "endTime": "2020-12-10T22:00:57.498739100Z", "duration": "PT0.000005397S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 847, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[0ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T22:00:57.499795159Z", "endTime": "2020-12-10T22:00:57.499804395Z", "duration": "PT0.000009236S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 848, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[0ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T22:00:57.499805641Z", "endTime": "2020-12-10T22:00:57.499810502Z", "duration": "PT0.000004861S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 849, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T22:01:07.489270395Z", "endTime": "2020-12-10T22:01:07.489281033Z", "duration": "PT0.000010638S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 850, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T22:01:07.489282776Z", "endTime": "2020-12-10T22:01:07.489291243Z", "duration": "PT0.000008467S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 851, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T22:01:07.490339322Z", "endTime": "2020-12-10T22:01:07.490351568Z", "duration": "PT0.000012246S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 852, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T22:01:07.490352700Z", "endTime": "2020-12-10T22:01:07.490360399Z", "duration": "PT0.000007699S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 853, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/actuator/health]; client=[192.168.1.12]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[2ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T22:01:16.879338957Z", "endTime": "2020-12-10T22:01:16.879349894Z", "duration": "PT0.000010937S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 854, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/actuator/health]; client=[192.168.1.12]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[2ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T22:01:16.879351769Z", "endTime": "2020-12-10T22:01:16.879357055Z", "duration": "PT0.000005286S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 855, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T22:01:17.503617711Z", "endTime": "2020-12-10T22:01:17.503628442Z", "duration": "PT0.000010731S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 856, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T22:01:17.503630347Z", "endTime": "2020-12-10T22:01:17.503634773Z", "duration": "PT0.000004426S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 857, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T22:01:17.504625211Z", "endTime": "2020-12-10T22:01:17.504634644Z", "duration": "PT0.000009433S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 858, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T22:01:17.504636047Z", "endTime": "2020-12-10T22:01:17.504640600Z", "duration": "PT0.000004553S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 859, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/actuator/health]; client=[192.168.1.12]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T22:01:26.885014257Z", "endTime": "2020-12-10T22:01:26.885025189Z", "duration": "PT0.000010932S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 860, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/actuator/health]; client=[192.168.1.12]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T22:01:26.885026800Z", "endTime": "2020-12-10T22:01:26.885031191Z", "duration": "PT0.000004391S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 861, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T22:01:27.489335647Z", "endTime": "2020-12-10T22:01:27.489345181Z", "duration": "PT0.000009534S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 862, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T22:01:27.489347088Z", "endTime": "2020-12-10T22:01:27.489353651Z", "duration": "PT0.000006563S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 863, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T22:01:27.490233916Z", "endTime": "2020-12-10T22:01:27.490243881Z", "duration": "PT0.000009965S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 864, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T22:01:27.490245308Z", "endTime": "2020-12-10T22:01:27.490255404Z", "duration": "PT0.000010096S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 865, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T22:01:37.499626225Z", "endTime": "2020-12-10T22:01:37.499635493Z", "duration": "PT0.000009268S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 866, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T22:01:37.499637043Z", "endTime": "2020-12-10T22:01:37.499644805Z", "duration": "PT0.000007762S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 867, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T22:01:37.500511059Z", "endTime": "2020-12-10T22:01:37.500521677Z", "duration": "PT0.000010618S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 868, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T22:01:37.500522728Z", "endTime": "2020-12-10T22:01:37.500525732Z", "duration": "PT0.000003004S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 869, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/actuator/health]; client=[192.168.1.12]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[2ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T22:01:46.887654454Z", "endTime": "2020-12-10T22:01:46.887665945Z", "duration": "PT0.000011491S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 870, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/actuator/health]; client=[192.168.1.12]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[2ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T22:01:46.887667837Z", "endTime": "2020-12-10T22:01:46.887673607Z", "duration": "PT0.00000577S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 871, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T22:01:47.500122366Z", "endTime": "2020-12-10T22:01:47.500132389Z", "duration": "PT0.000010023S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 872, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T22:01:47.500133941Z", "endTime": "2020-12-10T22:01:47.500138466Z", "duration": "PT0.000004525S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 873, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[0ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T22:01:47.500974015Z", "endTime": "2020-12-10T22:01:47.500980702Z", "duration": "PT0.000006687S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 874, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[0ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T22:01:47.500981674Z", "endTime": "2020-12-10T22:01:47.500985250Z", "duration": "PT0.000003576S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 875, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T22:01:57.502297589Z", "endTime": "2020-12-10T22:01:57.502308200Z", "duration": "PT0.000010611S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 876, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T22:01:57.502310187Z", "endTime": "2020-12-10T22:01:57.502315016Z", "duration": "PT0.000004829S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 877, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[0ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T22:01:57.503322406Z", "endTime": "2020-12-10T22:01:57.503330258Z", "duration": "PT0.000007852S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 878, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[0ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T22:01:57.503331284Z", "endTime": "2020-12-10T22:01:57.503335343Z", "duration": "PT0.000004059S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 879, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/actuator/health]; client=[192.168.1.12]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T22:02:06.883235351Z", "endTime": "2020-12-10T22:02:06.883247521Z", "duration": "PT0.00001217S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 880, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/actuator/health]; client=[192.168.1.12]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T22:02:06.883249050Z", "endTime": "2020-12-10T22:02:06.883254422Z", "duration": "PT0.000005372S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 881, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T22:02:07.496545129Z", "endTime": "2020-12-10T22:02:07.496559085Z", "duration": "PT0.000013956S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 882, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T22:02:07.496560778Z", "endTime": "2020-12-10T22:02:07.496564427Z", "duration": "PT0.000003649S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 883, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[0ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T22:02:07.497326525Z", "endTime": "2020-12-10T22:02:07.497332970Z", "duration": "PT0.000006445S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 884, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[0ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T22:02:07.497333676Z", "endTime": "2020-12-10T22:02:07.497337164Z", "duration": "PT0.000003488S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 885, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T22:02:17.507322166Z", "endTime": "2020-12-10T22:02:17.507330775Z", "duration": "PT0.000008609S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 886, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T22:02:17.507332254Z", "endTime": "2020-12-10T22:02:17.507335889Z", "duration": "PT0.000003635S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 887, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T22:02:17.508012342Z", "endTime": "2020-12-10T22:02:17.508017709Z", "duration": "PT0.000005367S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 888, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T22:02:17.508018351Z", "endTime": "2020-12-10T22:02:17.508020747Z", "duration": "PT0.000002396S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 889, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/actuator/health]; client=[192.168.1.12]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T22:02:26.877475667Z", "endTime": "2020-12-10T22:02:26.877484092Z", "duration": "PT0.000008425S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 890, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/actuator/health]; client=[192.168.1.12]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T22:02:26.877485134Z", "endTime": "2020-12-10T22:02:26.877488653Z", "duration": "PT0.000003519S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 891, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T22:02:27.487834230Z", "endTime": "2020-12-10T22:02:27.487841949Z", "duration": "PT0.000007719S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 892, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T22:02:27.487843176Z", "endTime": "2020-12-10T22:02:27.487846492Z", "duration": "PT0.000003316S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 893, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[0ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T22:02:27.488490824Z", "endTime": "2020-12-10T22:02:27.488495391Z", "duration": "PT0.000004567S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 894, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[0ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T22:02:27.488495996Z", "endTime": "2020-12-10T22:02:27.488498563Z", "duration": "PT0.000002567S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 895, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T22:02:37.493861847Z", "endTime": "2020-12-10T22:02:37.493870223Z", "duration": "PT0.000008376S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 896, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T22:02:37.493871455Z", "endTime": "2020-12-10T22:02:37.493878287Z", "duration": "PT0.000006832S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 897, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[0ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T22:02:37.494659599Z", "endTime": "2020-12-10T22:02:37.494669833Z", "duration": "PT0.000010234S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 898, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[0ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T22:02:37.494670791Z", "endTime": "2020-12-10T22:02:37.494674168Z", "duration": "PT0.000003377S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 899, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/actuator/health]; client=[192.168.1.12]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T22:02:46.879764764Z", "endTime": "2020-12-10T22:02:46.879776372Z", "duration": "PT0.000011608S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 900, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/actuator/health]; client=[192.168.1.12]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T22:02:46.879778364Z", "endTime": "2020-12-10T22:02:46.879788439Z", "duration": "PT0.000010075S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 901, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/actuator/info]; client=[192.168.1.12]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[2ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T22:02:46.899432706Z", "endTime": "2020-12-10T22:02:46.899443552Z", "duration": "PT0.000010846S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 902, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/actuator/info]; client=[192.168.1.12]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[2ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T22:02:46.899445087Z", "endTime": "2020-12-10T22:02:46.899449950Z", "duration": "PT0.000004863S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 903, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T22:02:47.491355917Z", "endTime": "2020-12-10T22:02:47.491366885Z", "duration": "PT0.000010968S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 904, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T22:02:47.491368636Z", "endTime": "2020-12-10T22:02:47.491374112Z", "duration": "PT0.000005476S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 905, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T22:02:47.492345976Z", "endTime": "2020-12-10T22:02:47.492353306Z", "duration": "PT0.00000733S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 906, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T22:02:47.492354466Z", "endTime": "2020-12-10T22:02:47.492358979Z", "duration": "PT0.000004513S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 907, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[2ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T22:02:57.503356895Z", "endTime": "2020-12-10T22:02:57.503368789Z", "duration": "PT0.000011894S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 908, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[2ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T22:02:57.503370821Z", "endTime": "2020-12-10T22:02:57.503376026Z", "duration": "PT0.000005205S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 909, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T22:02:57.504413367Z", "endTime": "2020-12-10T22:02:57.504422665Z", "duration": "PT0.000009298S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 910, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T22:02:57.504423692Z", "endTime": "2020-12-10T22:02:57.504428678Z", "duration": "PT0.000004986S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 911, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/actuator/health]; client=[192.168.1.12]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[2ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T22:03:06.885039019Z", "endTime": "2020-12-10T22:03:06.885050927Z", "duration": "PT0.000011908S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 912, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/actuator/health]; client=[192.168.1.12]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[2ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T22:03:06.885052822Z", "endTime": "2020-12-10T22:03:06.885058034Z", "duration": "PT0.000005212S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 913, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T22:03:07.498578715Z", "endTime": "2020-12-10T22:03:07.498590360Z", "duration": "PT0.000011645S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 914, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T22:03:07.498591969Z", "endTime": "2020-12-10T22:03:07.498597262Z", "duration": "PT0.000005293S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 915, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T22:03:07.499519844Z", "endTime": "2020-12-10T22:03:07.499527498Z", "duration": "PT0.000007654S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 916, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T22:03:07.499528594Z", "endTime": "2020-12-10T22:03:07.499532579Z", "duration": "PT0.000003985S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 917, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[2ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T22:03:17.500385073Z", "endTime": "2020-12-10T22:03:17.500396900Z", "duration": "PT0.000011827S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 918, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[2ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T22:03:17.500398763Z", "endTime": "2020-12-10T22:03:17.500403615Z", "duration": "PT0.000004852S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 919, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T22:03:17.501477528Z", "endTime": "2020-12-10T22:03:17.501485643Z", "duration": "PT0.000008115S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 920, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T22:03:17.501486802Z", "endTime": "2020-12-10T22:03:17.501490849Z", "duration": "PT0.000004047S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 921, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/actuator/health]; client=[192.168.1.12]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T22:03:26.884474639Z", "endTime": "2020-12-10T22:03:26.884484810Z", "duration": "PT0.000010171S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 922, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/actuator/health]; client=[192.168.1.12]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T22:03:26.884486085Z", "endTime": "2020-12-10T22:03:26.884490834Z", "duration": "PT0.000004749S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 923, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T22:03:27.489805605Z", "endTime": "2020-12-10T22:03:27.489816862Z", "duration": "PT0.000011257S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 924, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T22:03:27.489818720Z", "endTime": "2020-12-10T22:03:27.489824037Z", "duration": "PT0.000005317S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 925, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T22:03:27.490766124Z", "endTime": "2020-12-10T22:03:27.490774689Z", "duration": "PT0.000008565S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 926, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T22:03:27.490775914Z", "endTime": "2020-12-10T22:03:27.490780836Z", "duration": "PT0.000004922S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 927, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T22:03:37.504175143Z", "endTime": "2020-12-10T22:03:37.504186443Z", "duration": "PT0.0000113S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 928, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T22:03:37.504188238Z", "endTime": "2020-12-10T22:03:37.504196429Z", "duration": "PT0.000008191S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 929, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T22:03:37.505284115Z", "endTime": "2020-12-10T22:03:37.505292982Z", "duration": "PT0.000008867S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 930, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T22:03:37.505294237Z", "endTime": "2020-12-10T22:03:37.505307251Z", "duration": "PT0.000013014S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 931, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/actuator/health]; client=[192.168.1.12]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[2ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T22:03:46.887539845Z", "endTime": "2020-12-10T22:03:46.887550987Z", "duration": "PT0.000011142S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 932, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/actuator/health]; client=[192.168.1.12]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[2ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T22:03:46.887552471Z", "endTime": "2020-12-10T22:03:46.887561346Z", "duration": "PT0.000008875S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 933, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T22:03:47.491081975Z", "endTime": "2020-12-10T22:03:47.491092803Z", "duration": "PT0.000010828S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 934, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T22:03:47.491094387Z", "endTime": "2020-12-10T22:03:47.491099304Z", "duration": "PT0.000004917S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 935, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[0ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T22:03:47.492011791Z", "endTime": "2020-12-10T22:03:47.492019922Z", "duration": "PT0.000008131S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 936, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[0ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T22:03:47.492020993Z", "endTime": "2020-12-10T22:03:47.492025260Z", "duration": "PT0.000004267S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 937, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T22:03:57.499838040Z", "endTime": "2020-12-10T22:03:57.499897042Z", "duration": "PT0.000059002S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 938, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T22:03:57.499898879Z", "endTime": "2020-12-10T22:03:57.499912828Z", "duration": "PT0.000013949S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 939, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T22:03:57.500910995Z", "endTime": "2020-12-10T22:03:57.500917027Z", "duration": "PT0.000006032S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 940, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T22:03:57.500918068Z", "endTime": "2020-12-10T22:03:57.500921533Z", "duration": "PT0.000003465S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 941, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/actuator/health]; client=[192.168.1.12]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[2ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T22:04:06.881506419Z", "endTime": "2020-12-10T22:04:06.881514325Z", "duration": "PT0.000007906S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 942, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/actuator/health]; client=[192.168.1.12]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[2ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T22:04:06.881515721Z", "endTime": "2020-12-10T22:04:06.881519831Z", "duration": "PT0.00000411S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 943, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T22:04:07.491740249Z", "endTime": "2020-12-10T22:04:07.491746022Z", "duration": "PT0.000005773S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 944, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T22:04:07.491747607Z", "endTime": "2020-12-10T22:04:07.491750939Z", "duration": "PT0.000003332S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 945, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T22:04:07.492911927Z", "endTime": "2020-12-10T22:04:07.492915751Z", "duration": "PT0.000003824S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 946, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T22:04:07.492916338Z", "endTime": "2020-12-10T22:04:07.492919763Z", "duration": "PT0.000003425S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 947, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T22:04:17.506355039Z", "endTime": "2020-12-10T22:04:17.506362405Z", "duration": "PT0.000007366S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 948, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T22:04:17.506363870Z", "endTime": "2020-12-10T22:04:17.506386171Z", "duration": "PT0.000022301S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 949, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[0ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T22:04:17.507364510Z", "endTime": "2020-12-10T22:04:17.507370367Z", "duration": "PT0.000005857S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 950, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[0ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T22:04:17.507371114Z", "endTime": "2020-12-10T22:04:17.507373722Z", "duration": "PT0.000002608S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 951, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/actuator/health]; client=[192.168.1.12]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T22:04:26.879014570Z", "endTime": "2020-12-10T22:04:26.879019724Z", "duration": "PT0.000005154S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 952, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/actuator/health]; client=[192.168.1.12]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T22:04:26.879020541Z", "endTime": "2020-12-10T22:04:26.879027172Z", "duration": "PT0.000006631S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 953, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[2ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T22:04:27.498072940Z", "endTime": "2020-12-10T22:04:27.498080721Z", "duration": "PT0.000007781S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 954, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[2ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T22:04:27.498082405Z", "endTime": "2020-12-10T22:04:27.498086015Z", "duration": "PT0.00000361S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 955, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T22:04:27.499000204Z", "endTime": "2020-12-10T22:04:27.499004798Z", "duration": "PT0.000004594S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 956, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T22:04:27.499005500Z", "endTime": "2020-12-10T22:04:27.499007538Z", "duration": "PT0.000002038S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 957, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[0ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T22:04:37.498615183Z", "endTime": "2020-12-10T22:04:37.498620519Z", "duration": "PT0.000005336S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 958, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[0ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T22:04:37.498621836Z", "endTime": "2020-12-10T22:04:37.498624346Z", "duration": "PT0.00000251S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 959, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[0ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T22:04:37.499512389Z", "endTime": "2020-12-10T22:04:37.499533974Z", "duration": "PT0.000021585S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 960, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[0ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T22:04:37.499534616Z", "endTime": "2020-12-10T22:04:37.499536687Z", "duration": "PT0.000002071S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 961, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/actuator/health]; client=[192.168.1.12]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T22:04:46.880364523Z", "endTime": "2020-12-10T22:04:46.880376010Z", "duration": "PT0.000011487S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 962, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/actuator/health]; client=[192.168.1.12]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T22:04:46.880376893Z", "endTime": "2020-12-10T22:04:46.880379318Z", "duration": "PT0.000002425S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 963, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/actuator/info]; client=[192.168.1.12]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T22:04:46.891748133Z", "endTime": "2020-12-10T22:04:46.891753572Z", "duration": "PT0.000005439S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 964, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/actuator/info]; client=[192.168.1.12]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T22:04:46.891754393Z", "endTime": "2020-12-10T22:04:46.891756629Z", "duration": "PT0.000002236S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 965, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T22:04:47.488019169Z", "endTime": "2020-12-10T22:04:47.488024542Z", "duration": "PT0.000005373S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 966, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T22:04:47.488025557Z", "endTime": "2020-12-10T22:04:47.488027660Z", "duration": "PT0.000002103S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 967, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T22:04:47.488920687Z", "endTime": "2020-12-10T22:04:47.488941315Z", "duration": "PT0.000020628S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 968, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T22:04:47.488942089Z", "endTime": "2020-12-10T22:04:47.488944004Z", "duration": "PT0.000001915S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 969, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T22:04:57.502386096Z", "endTime": "2020-12-10T22:04:57.502392634Z", "duration": "PT0.000006538S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 970, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T22:04:57.502394195Z", "endTime": "2020-12-10T22:04:57.502396708Z", "duration": "PT0.000002513S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 971, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T22:04:57.503164457Z", "endTime": "2020-12-10T22:04:57.503168718Z", "duration": "PT0.000004261S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 972, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T22:04:57.503169340Z", "endTime": "2020-12-10T22:04:57.503171491Z", "duration": "PT0.000002151S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 973, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/actuator/health]; client=[192.168.1.12]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T22:05:06.877654085Z", "endTime": "2020-12-10T22:05:06.877659530Z", "duration": "PT0.000005445S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 974, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/actuator/health]; client=[192.168.1.12]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T22:05:06.877660353Z", "endTime": "2020-12-10T22:05:06.877663202Z", "duration": "PT0.000002849S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 975, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T22:05:07.491654258Z", "endTime": "2020-12-10T22:05:07.491659629Z", "duration": "PT0.000005371S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 976, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T22:05:07.491660839Z", "endTime": "2020-12-10T22:05:07.491663601Z", "duration": "PT0.000002762S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 977, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T22:05:07.492240271Z", "endTime": "2020-12-10T22:05:07.492244189Z", "duration": "PT0.000003918S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 978, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T22:05:07.492244666Z", "endTime": "2020-12-10T22:05:07.492246275Z", "duration": "PT0.000001609S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 979, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[2ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T22:05:17.497418517Z", "endTime": "2020-12-10T22:05:17.497424486Z", "duration": "PT0.000005969S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 980, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[2ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T22:05:17.497425556Z", "endTime": "2020-12-10T22:05:17.497427971Z", "duration": "PT0.000002415S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 981, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T22:05:17.498159601Z", "endTime": "2020-12-10T22:05:17.498170047Z", "duration": "PT0.000010446S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 982, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T22:05:17.498170609Z", "endTime": "2020-12-10T22:05:17.498172449Z", "duration": "PT0.00000184S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 983, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/actuator/health]; client=[192.168.1.12]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T22:05:26.877290680Z", "endTime": "2020-12-10T22:05:26.877314186Z", "duration": "PT0.000023506S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 984, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/actuator/health]; client=[192.168.1.12]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T22:05:26.877315441Z", "endTime": "2020-12-10T22:05:26.877317835Z", "duration": "PT0.000002394S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 985, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T22:05:27.489784064Z", "endTime": "2020-12-10T22:05:27.489789129Z", "duration": "PT0.000005065S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 986, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T22:05:27.489790006Z", "endTime": "2020-12-10T22:05:27.489795689Z", "duration": "PT0.000005683S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 987, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T22:05:27.490408782Z", "endTime": "2020-12-10T22:05:27.490411891Z", "duration": "PT0.000003109S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 988, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T22:05:27.490412383Z", "endTime": "2020-12-10T22:05:27.490414168Z", "duration": "PT0.000001785S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 989, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T22:05:37.507817401Z", "endTime": "2020-12-10T22:05:37.507825330Z", "duration": "PT0.000007929S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 990, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T22:05:37.507827323Z", "endTime": "2020-12-10T22:05:37.507830928Z", "duration": "PT0.000003605S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 991, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T22:05:37.508885758Z", "endTime": "2020-12-10T22:05:37.508893042Z", "duration": "PT0.000007284S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 992, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T22:05:37.508894415Z", "endTime": "2020-12-10T22:05:37.508897530Z", "duration": "PT0.000003115S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 993, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/actuator/health]; client=[192.168.1.12]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T22:05:46.877488658Z", "endTime": "2020-12-10T22:05:46.877511044Z", "duration": "PT0.000022386S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 994, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/actuator/health]; client=[192.168.1.12]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T22:05:46.877511886Z", "endTime": "2020-12-10T22:05:46.877514484Z", "duration": "PT0.000002598S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 995, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T22:05:47.494622373Z", "endTime": "2020-12-10T22:05:47.494627998Z", "duration": "PT0.000005625S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 996, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T22:05:47.494629228Z", "endTime": "2020-12-10T22:05:47.494637117Z", "duration": "PT0.000007889S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 997, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T22:05:47.495334956Z", "endTime": "2020-12-10T22:05:47.495338448Z", "duration": "PT0.000003492S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 998, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T22:05:47.495339012Z", "endTime": "2020-12-10T22:05:47.495378140Z", "duration": "PT0.000039128S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 999, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T22:05:57.507058863Z", "endTime": "2020-12-10T22:05:57.507073424Z", "duration": "PT0.000014561S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 1000, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T22:05:57.507074809Z", "endTime": "2020-12-10T22:05:57.507077998Z", "duration": "PT0.000003189S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 1001, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T22:05:57.508070800Z", "endTime": "2020-12-10T22:05:57.508075867Z", "duration": "PT0.000005067S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 1002, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T22:05:57.508076552Z", "endTime": "2020-12-10T22:05:57.508079120Z", "duration": "PT0.000002568S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 1003, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/actuator/health]; client=[192.168.1.12]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[2ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T22:06:06.883636946Z", "endTime": "2020-12-10T22:06:06.883655632Z", "duration": "PT0.000018686S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 1004, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/actuator/health]; client=[192.168.1.12]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[2ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T22:06:06.883656830Z", "endTime": "2020-12-10T22:06:06.883660346Z", "duration": "PT0.000003516S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 1005, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T22:06:07.494998389Z", "endTime": "2020-12-10T22:06:07.495006322Z", "duration": "PT0.000007933S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 1006, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T22:06:07.495007700Z", "endTime": "2020-12-10T22:06:07.495010957Z", "duration": "PT0.000003257S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 1007, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T22:06:07.495817924Z", "endTime": "2020-12-10T22:06:07.495823045Z", "duration": "PT0.000005121S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 1008, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T22:06:07.495823786Z", "endTime": "2020-12-10T22:06:07.495826440Z", "duration": "PT0.000002654S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 1009, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T22:06:17.502741320Z", "endTime": "2020-12-10T22:06:17.502748979Z", "duration": "PT0.000007659S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 1010, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T22:06:17.502750364Z", "endTime": "2020-12-10T22:06:17.502753504Z", "duration": "PT0.00000314S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 1011, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T22:06:17.503642678Z", "endTime": "2020-12-10T22:06:17.503647699Z", "duration": "PT0.000005021S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 1012, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T22:06:17.503648368Z", "endTime": "2020-12-10T22:06:17.503651188Z", "duration": "PT0.00000282S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 1013, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/actuator/health]; client=[192.168.1.12]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T22:06:26.877630800Z", "endTime": "2020-12-10T22:06:26.877636451Z", "duration": "PT0.000005651S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 1014, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/actuator/health]; client=[192.168.1.12]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T22:06:26.877637157Z", "endTime": "2020-12-10T22:06:26.877639338Z", "duration": "PT0.000002181S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 1015, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T22:06:27.494317188Z", "endTime": "2020-12-10T22:06:27.494324417Z", "duration": "PT0.000007229S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 1016, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T22:06:27.494325733Z", "endTime": "2020-12-10T22:06:27.494335105Z", "duration": "PT0.000009372S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 1017, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T22:06:27.495312186Z", "endTime": "2020-12-10T22:06:27.495317792Z", "duration": "PT0.000005606S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 1018, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T22:06:27.495318458Z", "endTime": "2020-12-10T22:06:27.495331803Z", "duration": "PT0.000013345S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 1019, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T22:06:37.500572018Z", "endTime": "2020-12-10T22:06:37.500591568Z", "duration": "PT0.00001955S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 1020, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T22:06:37.500593569Z", "endTime": "2020-12-10T22:06:37.500598089Z", "duration": "PT0.00000452S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 1021, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[0ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T22:06:37.501609531Z", "endTime": "2020-12-10T22:06:37.501614550Z", "duration": "PT0.000005019S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 1022, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[0ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T22:06:37.501615318Z", "endTime": "2020-12-10T22:06:37.501624749Z", "duration": "PT0.000009431S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 1023, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/actuator/health]; client=[192.168.1.12]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[0ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T22:06:46.880776301Z", "endTime": "2020-12-10T22:06:46.880783080Z", "duration": "PT0.000006779S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 1024, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/actuator/health]; client=[192.168.1.12]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[0ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T22:06:46.880783946Z", "endTime": "2020-12-10T22:06:46.880786406Z", "duration": "PT0.00000246S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 1025, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/actuator/info]; client=[192.168.1.12]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T22:06:46.897676657Z", "endTime": "2020-12-10T22:06:46.897682762Z", "duration": "PT0.000006105S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 1026, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/actuator/info]; client=[192.168.1.12]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T22:06:46.897684082Z", "endTime": "2020-12-10T22:06:46.897687137Z", "duration": "PT0.000003055S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 1027, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T22:06:47.491464696Z", "endTime": "2020-12-10T22:06:47.491482689Z", "duration": "PT0.000017993S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 1028, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T22:06:47.491484506Z", "endTime": "2020-12-10T22:06:47.491487842Z", "duration": "PT0.000003336S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 1029, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T22:06:47.492505587Z", "endTime": "2020-12-10T22:06:47.492519746Z", "duration": "PT0.000014159S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 1030, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T22:06:47.492520612Z", "endTime": "2020-12-10T22:06:47.492523230Z", "duration": "PT0.000002618S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 1031, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T22:06:57.504802479Z", "endTime": "2020-12-10T22:06:57.504809544Z", "duration": "PT0.000007065S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 1032, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T22:06:57.504810936Z", "endTime": "2020-12-10T22:06:57.504822327Z", "duration": "PT0.000011391S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 1033, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T22:06:57.505888896Z", "endTime": "2020-12-10T22:06:57.505902914Z", "duration": "PT0.000014018S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 1034, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T22:06:57.505903682Z", "endTime": "2020-12-10T22:06:57.505906646Z", "duration": "PT0.000002964S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 1035, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/actuator/health]; client=[192.168.1.12]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T22:07:06.877625401Z", "endTime": "2020-12-10T22:07:06.877630882Z", "duration": "PT0.000005481S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 1036, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/actuator/health]; client=[192.168.1.12]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T22:07:06.877631796Z", "endTime": "2020-12-10T22:07:06.877639466Z", "duration": "PT0.00000767S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 1037, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T22:07:07.490104249Z", "endTime": "2020-12-10T22:07:07.490110371Z", "duration": "PT0.000006122S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 1038, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T22:07:07.490111653Z", "endTime": "2020-12-10T22:07:07.490114506Z", "duration": "PT0.000002853S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 1039, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T22:07:07.491074421Z", "endTime": "2020-12-10T22:07:07.491079766Z", "duration": "PT0.000005345S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 1040, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T22:07:07.491080377Z", "endTime": "2020-12-10T22:07:07.491083370Z", "duration": "PT0.000002993S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 1041, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/actuator/health]; client=[192.168.1.12]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T22:07:16.883216847Z", "endTime": "2020-12-10T22:07:16.883223426Z", "duration": "PT0.000006579S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 1042, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/actuator/health]; client=[192.168.1.12]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T22:07:16.883224381Z", "endTime": "2020-12-10T22:07:16.883227026Z", "duration": "PT0.000002645S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 1043, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[2ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T22:07:17.495058618Z", "endTime": "2020-12-10T22:07:17.495065897Z", "duration": "PT0.000007279S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 1044, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[2ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T22:07:17.495067282Z", "endTime": "2020-12-10T22:07:17.495070800Z", "duration": "PT0.000003518S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 1045, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[0ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T22:07:17.495981227Z", "endTime": "2020-12-10T22:07:17.495986745Z", "duration": "PT0.000005518S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 1046, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[0ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T22:07:17.495987924Z", "endTime": "2020-12-10T22:07:17.495990627Z", "duration": "PT0.000002703S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 1047, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T22:07:27.488106927Z", "endTime": "2020-12-10T22:07:27.488117500Z", "duration": "PT0.000010573S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 1048, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T22:07:27.488118643Z", "endTime": "2020-12-10T22:07:27.488121238Z", "duration": "PT0.000002595S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 1049, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[0ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T22:07:27.488855637Z", "endTime": "2020-12-10T22:07:27.488859168Z", "duration": "PT0.000003531S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 1050, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[0ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T22:07:27.488859741Z", "endTime": "2020-12-10T22:07:27.488868797Z", "duration": "PT0.000009056S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 1051, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/actuator/health]; client=[192.168.1.12]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T22:07:36.879699707Z", "endTime": "2020-12-10T22:07:36.879708068Z", "duration": "PT0.000008361S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 1052, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/actuator/health]; client=[192.168.1.12]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T22:07:36.879709207Z", "endTime": "2020-12-10T22:07:36.879713126Z", "duration": "PT0.000003919S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 1053, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T22:07:37.507528855Z", "endTime": "2020-12-10T22:07:37.507535359Z", "duration": "PT0.000006504S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 1054, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T22:07:37.507536522Z", "endTime": "2020-12-10T22:07:37.507539538Z", "duration": "PT0.000003016S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 1055, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T22:07:37.508259078Z", "endTime": "2020-12-10T22:07:37.508263947Z", "duration": "PT0.000004869S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 1056, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T22:07:37.508264599Z", "endTime": "2020-12-10T22:07:37.508266874Z", "duration": "PT0.000002275S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 1057, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T22:07:47.489352906Z", "endTime": "2020-12-10T22:07:47.489358872Z", "duration": "PT0.000005966S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 1058, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T22:07:47.489359850Z", "endTime": "2020-12-10T22:07:47.489362077Z", "duration": "PT0.000002227S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 1059, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[0ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T22:07:47.490075163Z", "endTime": "2020-12-10T22:07:47.490079166Z", "duration": "PT0.000004003S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 1060, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[0ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T22:07:47.490079651Z", "endTime": "2020-12-10T22:07:47.490081986Z", "duration": "PT0.000002335S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 1061, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/actuator/health]; client=[192.168.1.12]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[2ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T22:07:56.887522092Z", "endTime": "2020-12-10T22:07:56.887530368Z", "duration": "PT0.000008276S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 1062, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/actuator/health]; client=[192.168.1.12]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[2ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T22:07:56.887531893Z", "endTime": "2020-12-10T22:07:56.887535743Z", "duration": "PT0.00000385S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 1063, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T22:07:57.505754829Z", "endTime": "2020-12-10T22:07:57.505762155Z", "duration": "PT0.000007326S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 1064, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T22:07:57.505763588Z", "endTime": "2020-12-10T22:07:57.505774871Z", "duration": "PT0.000011283S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 1065, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T22:07:57.506782405Z", "endTime": "2020-12-10T22:07:57.506787376Z", "duration": "PT0.000004971S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 1066, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T22:07:57.506787931Z", "endTime": "2020-12-10T22:07:57.506790490Z", "duration": "PT0.000002559S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 1067, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T22:08:07.490828528Z", "endTime": "2020-12-10T22:08:07.490835591Z", "duration": "PT0.000007063S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 1068, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T22:08:07.490837347Z", "endTime": "2020-12-10T22:08:07.490840385Z", "duration": "PT0.000003038S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 1069, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T22:08:07.491615088Z", "endTime": "2020-12-10T22:08:07.491619413Z", "duration": "PT0.000004325S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 1070, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T22:08:07.491619991Z", "endTime": "2020-12-10T22:08:07.491622229Z", "duration": "PT0.000002238S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 1071, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/actuator/health]; client=[192.168.1.12]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T22:08:16.878198088Z", "endTime": "2020-12-10T22:08:16.878204994Z", "duration": "PT0.000006906S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 1072, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/actuator/health]; client=[192.168.1.12]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T22:08:16.878205879Z", "endTime": "2020-12-10T22:08:16.878208829Z", "duration": "PT0.00000295S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 1073, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T22:08:17.504795236Z", "endTime": "2020-12-10T22:08:17.504802062Z", "duration": "PT0.000006826S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 1074, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T22:08:17.504803408Z", "endTime": "2020-12-10T22:08:17.504806246Z", "duration": "PT0.000002838S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 1075, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T22:08:17.505638516Z", "endTime": "2020-12-10T22:08:17.505642852Z", "duration": "PT0.000004336S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 1076, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T22:08:17.505643392Z", "endTime": "2020-12-10T22:08:17.505645638Z", "duration": "PT0.000002246S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 1077, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/actuator/health]; client=[192.168.1.12]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T22:08:26.916236190Z", "endTime": "2020-12-10T22:08:26.916249718Z", "duration": "PT0.000013528S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 1078, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/actuator/health]; client=[192.168.1.12]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T22:08:26.916250606Z", "endTime": "2020-12-10T22:08:26.916252987Z", "duration": "PT0.000002381S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 1079, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[2ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T22:08:27.487949428Z", "endTime": "2020-12-10T22:08:27.487954729Z", "duration": "PT0.000005301S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 1080, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[2ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T22:08:27.487955747Z", "endTime": "2020-12-10T22:08:27.487962942Z", "duration": "PT0.000007195S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 1081, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T22:08:27.489135248Z", "endTime": "2020-12-10T22:08:27.489139595Z", "duration": "PT0.000004347S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 1082, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T22:08:27.489140241Z", "endTime": "2020-12-10T22:08:27.489164621Z", "duration": "PT0.00002438S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 1083, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[5ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T22:08:37.509795708Z", "endTime": "2020-12-10T22:08:37.509812645Z", "duration": "PT0.000016937S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 1084, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[5ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T22:08:37.509816824Z", "endTime": "2020-12-10T22:08:37.509845202Z", "duration": "PT0.000028378S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 1085, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[2ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T22:08:37.512671648Z", "endTime": "2020-12-10T22:08:37.512688222Z", "duration": "PT0.000016574S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 1086, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[2ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T22:08:37.512690731Z", "endTime": "2020-12-10T22:08:37.512698598Z", "duration": "PT0.000007867S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 1087, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/actuator/info]; client=[192.168.1.12]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[9ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T22:08:46.900565840Z", "endTime": "2020-12-10T22:08:46.900583721Z", "duration": "PT0.000017881S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 1088, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/actuator/info]; client=[192.168.1.12]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[9ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T22:08:46.900586621Z", "endTime": "2020-12-10T22:08:46.900596479Z", "duration": "PT0.000009858S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 1089, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/actuator/health]; client=[192.168.1.12]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[3ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T22:08:46.946426169Z", "endTime": "2020-12-10T22:08:46.946444746Z", "duration": "PT0.000018577S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 1090, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/actuator/health]; client=[192.168.1.12]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[3ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T22:08:46.946447439Z", "endTime": "2020-12-10T22:08:46.946457863Z", "duration": "PT0.000010424S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 1091, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[2ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T22:08:47.517006698Z", "endTime": "2020-12-10T22:08:47.517024621Z", "duration": "PT0.000017923S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 1092, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[2ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T22:08:47.517028153Z", "endTime": "2020-12-10T22:08:47.517055256Z", "duration": "PT0.000027103S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 1093, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T22:08:47.519304849Z", "endTime": "2020-12-10T22:08:47.519319096Z", "duration": "PT0.000014247S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 1094, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T22:08:47.519321097Z", "endTime": "2020-12-10T22:08:47.519354724Z", "duration": "PT0.000033627S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 1095, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[3ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T22:08:57.498338235Z", "endTime": "2020-12-10T22:08:57.498355181Z", "duration": "PT0.000016946S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 1096, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[3ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T22:08:57.498359354Z", "endTime": "2020-12-10T22:08:57.498376644Z", "duration": "PT0.00001729S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 1097, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[2ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T22:08:57.500725563Z", "endTime": "2020-12-10T22:08:57.500748950Z", "duration": "PT0.000023387S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 1098, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[2ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T22:08:57.500751536Z", "endTime": "2020-12-10T22:08:57.500759558Z", "duration": "PT0.000008022S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 1099, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/actuator/health]; client=[192.168.1.12]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[3ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T22:09:06.890378996Z", "endTime": "2020-12-10T22:09:06.890399834Z", "duration": "PT0.000020838S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 1100, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/actuator/health]; client=[192.168.1.12]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[3ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T22:09:06.890402672Z", "endTime": "2020-12-10T22:09:06.890411212Z", "duration": "PT0.00000854S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 1101, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[3ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T22:09:07.511491420Z", "endTime": "2020-12-10T22:09:07.511554921Z", "duration": "PT0.000063501S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 1102, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[3ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T22:09:07.511558648Z", "endTime": "2020-12-10T22:09:07.511568273Z", "duration": "PT0.000009625S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 1103, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[2ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T22:09:07.514026107Z", "endTime": "2020-12-10T22:09:07.514064432Z", "duration": "PT0.000038325S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 1104, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[2ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T22:09:07.514066821Z", "endTime": "2020-12-10T22:09:07.514074597Z", "duration": "PT0.000007776S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 1105, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[3ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T22:09:17.498341609Z", "endTime": "2020-12-10T22:09:17.498359609Z", "duration": "PT0.000018S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 1106, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[3ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T22:09:17.498363300Z", "endTime": "2020-12-10T22:09:17.498372616Z", "duration": "PT0.000009316S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 1107, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T22:09:17.500650941Z", "endTime": "2020-12-10T22:09:17.500666717Z", "duration": "PT0.000015776S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 1108, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T22:09:17.500668933Z", "endTime": "2020-12-10T22:09:17.500676539Z", "duration": "PT0.000007606S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 1109, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/actuator/health]; client=[192.168.1.12]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T22:09:26.906479151Z", "endTime": "2020-12-10T22:09:26.906486093Z", "duration": "PT0.000006942S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 1110, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/actuator/health]; client=[192.168.1.12]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T22:09:26.906487350Z", "endTime": "2020-12-10T22:09:26.906490676Z", "duration": "PT0.000003326S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 1111, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T22:09:27.498489563Z", "endTime": "2020-12-10T22:09:27.498496491Z", "duration": "PT0.000006928S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 1112, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T22:09:27.498497787Z", "endTime": "2020-12-10T22:09:27.498500762Z", "duration": "PT0.000002975S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 1113, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T22:09:27.499353245Z", "endTime": "2020-12-10T22:09:27.499358599Z", "duration": "PT0.000005354S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 1114, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T22:09:27.499359370Z", "endTime": "2020-12-10T22:09:27.499369458Z", "duration": "PT0.000010088S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 1115, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[3ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T22:09:37.508636877Z", "endTime": "2020-12-10T22:09:37.508681680Z", "duration": "PT0.000044803S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 1116, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[3ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T22:09:37.508685531Z", "endTime": "2020-12-10T22:09:37.508695007Z", "duration": "PT0.000009476S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 1117, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[2ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T22:09:37.511172766Z", "endTime": "2020-12-10T22:09:37.511187144Z", "duration": "PT0.000014378S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 1118, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[2ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T22:09:37.511189093Z", "endTime": "2020-12-10T22:09:37.511196269Z", "duration": "PT0.000007176S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 1119, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/actuator/info]; client=[192.168.1.12]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[6ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T22:09:46.899800461Z", "endTime": "2020-12-10T22:09:46.899845846Z", "duration": "PT0.000045385S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 1120, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/actuator/info]; client=[192.168.1.12]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[6ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T22:09:46.899848957Z", "endTime": "2020-12-10T22:09:46.899858714Z", "duration": "PT0.000009757S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 1121, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/actuator/health]; client=[192.168.1.12]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[3ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T22:09:46.909673114Z", "endTime": "2020-12-10T22:09:46.909692398Z", "duration": "PT0.000019284S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 1122, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/actuator/health]; client=[192.168.1.12]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[3ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T22:09:46.909695714Z", "endTime": "2020-12-10T22:09:46.909736202Z", "duration": "PT0.000040488S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 1123, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[3ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T22:09:47.499994793Z", "endTime": "2020-12-10T22:09:47.500012457Z", "duration": "PT0.000017664S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 1124, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[3ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T22:09:47.500017115Z", "endTime": "2020-12-10T22:09:47.500044001Z", "duration": "PT0.000026886S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 1125, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[2ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T22:09:47.502668352Z", "endTime": "2020-12-10T22:09:47.502684126Z", "duration": "PT0.000015774S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 1126, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[2ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T22:09:47.502686356Z", "endTime": "2020-12-10T22:09:47.502727844Z", "duration": "PT0.000041488S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 1127, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[2ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T22:09:57.515643784Z", "endTime": "2020-12-10T22:09:57.515660977Z", "duration": "PT0.000017193S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 1128, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[2ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T22:09:57.515664005Z", "endTime": "2020-12-10T22:09:57.515671803Z", "duration": "PT0.000007798S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 1129, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T22:09:57.517849800Z", "endTime": "2020-12-10T22:09:57.517864002Z", "duration": "PT0.000014202S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 1130, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T22:09:57.517866002Z", "endTime": "2020-12-10T22:09:57.517872710Z", "duration": "PT0.000006708S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 1131, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/actuator/health]; client=[192.168.1.12]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[3ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T22:10:06.891155205Z", "endTime": "2020-12-10T22:10:06.891173489Z", "duration": "PT0.000018284S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 1132, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/actuator/health]; client=[192.168.1.12]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[3ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T22:10:06.891176187Z", "endTime": "2020-12-10T22:10:06.891186157Z", "duration": "PT0.00000997S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 1133, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[3ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T22:10:07.498275970Z", "endTime": "2020-12-10T22:10:07.498314047Z", "duration": "PT0.000038077S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 1134, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[3ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T22:10:07.498317280Z", "endTime": "2020-12-10T22:10:07.498326307Z", "duration": "PT0.000009027S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 1135, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[2ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T22:10:07.500776981Z", "endTime": "2020-12-10T22:10:07.500813790Z", "duration": "PT0.000036809S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 1136, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[2ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T22:10:07.500815990Z", "endTime": "2020-12-10T22:10:07.500823353Z", "duration": "PT0.000007363S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 1137, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/actuator/health]; client=[192.168.1.12]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T22:10:16.895527975Z", "endTime": "2020-12-10T22:10:16.895534720Z", "duration": "PT0.000006745S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 1138, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/actuator/health]; client=[192.168.1.12]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T22:10:16.895535717Z", "endTime": "2020-12-10T22:10:16.895545716Z", "duration": "PT0.000009999S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 1139, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T22:10:17.489418657Z", "endTime": "2020-12-10T22:10:17.489424979Z", "duration": "PT0.000006322S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 1140, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T22:10:17.489426096Z", "endTime": "2020-12-10T22:10:17.489428927Z", "duration": "PT0.000002831S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 1141, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[0ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T22:10:17.490261043Z", "endTime": "2020-12-10T22:10:17.490265395Z", "duration": "PT0.000004352S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 1142, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[0ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T22:10:17.490265954Z", "endTime": "2020-12-10T22:10:17.490268057Z", "duration": "PT0.000002103S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 1143, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[2ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T22:10:27.498968058Z", "endTime": "2020-12-10T22:10:27.498985563Z", "duration": "PT0.000017505S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 1144, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[2ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T22:10:27.498989080Z", "endTime": "2020-12-10T22:10:27.498997041Z", "duration": "PT0.000007961S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 1145, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[2ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T22:10:27.501631450Z", "endTime": "2020-12-10T22:10:27.501645612Z", "duration": "PT0.000014162S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 1146, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[2ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T22:10:27.501647791Z", "endTime": "2020-12-10T22:10:27.501655118Z", "duration": "PT0.000007327S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 1147, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/actuator/health]; client=[192.168.1.12]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[4ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T22:10:36.891515760Z", "endTime": "2020-12-10T22:10:36.891553849Z", "duration": "PT0.000038089S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 1148, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/actuator/health]; client=[192.168.1.12]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[4ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T22:10:36.891557023Z", "endTime": "2020-12-10T22:10:36.891565794Z", "duration": "PT0.000008771S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 1149, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[3ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T22:10:37.536341150Z", "endTime": "2020-12-10T22:10:37.536358259Z", "duration": "PT0.000017109S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 1150, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[3ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T22:10:37.536362460Z", "endTime": "2020-12-10T22:10:37.536381881Z", "duration": "PT0.000019421S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 1151, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[2ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T22:10:37.538807852Z", "endTime": "2020-12-10T22:10:37.538827614Z", "duration": "PT0.000019762S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 1152, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[2ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T22:10:37.538829918Z", "endTime": "2020-12-10T22:10:37.538837597Z", "duration": "PT0.000007679S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 1153, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/actuator/info]; client=[192.168.1.12]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[5ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T22:10:46.903149771Z", "endTime": "2020-12-10T22:10:46.903168364Z", "duration": "PT0.000018593S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 1154, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/actuator/info]; client=[192.168.1.12]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[5ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T22:10:46.903205378Z", "endTime": "2020-12-10T22:10:46.903215506Z", "duration": "PT0.000010128S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 1155, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[4ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T22:10:47.502454258Z", "endTime": "2020-12-10T22:10:47.502473305Z", "duration": "PT0.000019047S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 1156, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[4ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T22:10:47.502476939Z", "endTime": "2020-12-10T22:10:47.502503252Z", "duration": "PT0.000026313S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 1157, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[2ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T22:10:47.505013336Z", "endTime": "2020-12-10T22:10:47.505028033Z", "duration": "PT0.000014697S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 1158, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[2ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T22:10:47.505030194Z", "endTime": "2020-12-10T22:10:47.505063914Z", "duration": "PT0.00003372S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 1159, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/actuator/health]; client=[192.168.1.12]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[3ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T22:10:56.890274421Z", "endTime": "2020-12-10T22:10:56.890295026Z", "duration": "PT0.000020605S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 1160, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/actuator/health]; client=[192.168.1.12]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[3ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T22:10:56.890298060Z", "endTime": "2020-12-10T22:10:56.890327090Z", "duration": "PT0.00002903S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 1161, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[3ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T22:10:57.517731844Z", "endTime": "2020-12-10T22:10:57.517766738Z", "duration": "PT0.000034894S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 1162, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[3ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T22:10:57.517770577Z", "endTime": "2020-12-10T22:10:57.517778727Z", "duration": "PT0.00000815S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 1163, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[2ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T22:10:57.520413126Z", "endTime": "2020-12-10T22:10:57.520431020Z", "duration": "PT0.000017894S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 1164, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[2ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T22:10:57.520434322Z", "endTime": "2020-12-10T22:10:57.520442872Z", "duration": "PT0.00000855S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 1165, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/actuator/health]; client=[192.168.1.12]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T22:11:06.879634827Z", "endTime": "2020-12-10T22:11:06.879651006Z", "duration": "PT0.000016179S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 1166, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/actuator/health]; client=[192.168.1.12]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T22:11:06.879652325Z", "endTime": "2020-12-10T22:11:06.879655282Z", "duration": "PT0.000002957S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 1167, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T22:11:07.489006098Z", "endTime": "2020-12-10T22:11:07.489011219Z", "duration": "PT0.000005121S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 1168, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T22:11:07.489012678Z", "endTime": "2020-12-10T22:11:07.489014860Z", "duration": "PT0.000002182S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 1169, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T22:11:07.489640693Z", "endTime": "2020-12-10T22:11:07.489649014Z", "duration": "PT0.000008321S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 1170, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T22:11:07.489649708Z", "endTime": "2020-12-10T22:11:07.489651852Z", "duration": "PT0.000002144S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 1171, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[3ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T22:11:17.506262926Z", "endTime": "2020-12-10T22:11:17.506279898Z", "duration": "PT0.000016972S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 1172, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[3ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T22:11:17.506283844Z", "endTime": "2020-12-10T22:11:17.506292195Z", "duration": "PT0.000008351S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 1173, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[4ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T22:11:17.511309753Z", "endTime": "2020-12-10T22:11:17.511368753Z", "duration": "PT0.000059S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 1174, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[4ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T22:11:17.511372844Z", "endTime": "2020-12-10T22:11:17.511385690Z", "duration": "PT0.000012846S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 1175, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/actuator/health]; client=[192.168.1.12]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[4ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T22:11:26.895205137Z", "endTime": "2020-12-10T22:11:26.895229338Z", "duration": "PT0.000024201S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 1176, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/actuator/health]; client=[192.168.1.12]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[4ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T22:11:26.895233969Z", "endTime": "2020-12-10T22:11:26.895245765Z", "duration": "PT0.000011796S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 1177, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[5ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T22:11:27.633616668Z", "endTime": "2020-12-10T22:11:27.633633482Z", "duration": "PT0.000016814S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 1178, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[5ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T22:11:27.633637612Z", "endTime": "2020-12-10T22:11:27.633668129Z", "duration": "PT0.000030517S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 1179, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[2ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T22:11:27.636240547Z", "endTime": "2020-12-10T22:11:27.636283384Z", "duration": "PT0.000042837S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 1180, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[2ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T22:11:27.636285958Z", "endTime": "2020-12-10T22:11:27.636293935Z", "duration": "PT0.000007977S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 1181, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[3ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T22:11:37.501252799Z", "endTime": "2020-12-10T22:11:37.501275079Z", "duration": "PT0.00002228S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 1182, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[3ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T22:11:37.501279674Z", "endTime": "2020-12-10T22:11:37.501291307Z", "duration": "PT0.000011633S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 1183, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[2ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T22:11:37.504279340Z", "endTime": "2020-12-10T22:11:37.504320445Z", "duration": "PT0.000041105S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 1184, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[2ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T22:11:37.504323079Z", "endTime": "2020-12-10T22:11:37.504331242Z", "duration": "PT0.000008163S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 1185, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/actuator/health]; client=[192.168.1.12]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[5ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T22:11:46.896205473Z", "endTime": "2020-12-10T22:11:46.896229600Z", "duration": "PT0.000024127S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 1186, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/actuator/health]; client=[192.168.1.12]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[5ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T22:11:46.896233878Z", "endTime": "2020-12-10T22:11:46.896275677Z", "duration": "PT0.000041799S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 1187, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/actuator/info]; client=[192.168.1.12]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[7ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T22:11:46.913410683Z", "endTime": "2020-12-10T22:11:46.913434941Z", "duration": "PT0.000024258S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 1188, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/actuator/info]; client=[192.168.1.12]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[7ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T22:11:46.913438303Z", "endTime": "2020-12-10T22:11:46.913450928Z", "duration": "PT0.000012625S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 1189, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[3ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T22:11:47.515135421Z", "endTime": "2020-12-10T22:11:47.515153112Z", "duration": "PT0.000017691S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 1190, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[3ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T22:11:47.515157502Z", "endTime": "2020-12-10T22:11:47.515165911Z", "duration": "PT0.000008409S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 1191, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[2ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T22:11:47.518409979Z", "endTime": "2020-12-10T22:11:47.518428488Z", "duration": "PT0.000018509S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 1192, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[2ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T22:11:47.518431512Z", "endTime": "2020-12-10T22:11:47.518440079Z", "duration": "PT0.000008567S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 1193, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T22:11:59.760222952Z", "endTime": "2020-12-10T22:11:59.760228197Z", "duration": "PT0.000005245S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 1194, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T22:11:59.760229671Z", "endTime": "2020-12-10T22:11:59.760231820Z", "duration": "PT0.000002149S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 1195, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T22:11:59.761415021Z", "endTime": "2020-12-10T22:11:59.761419447Z", "duration": "PT0.000004426S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 1196, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T22:11:59.761424097Z", "endTime": "2020-12-10T22:11:59.761426142Z", "duration": "PT0.000002045S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 1197, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/actuator/health]; client=[192.168.1.12]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T22:12:06.880494527Z", "endTime": "2020-12-10T22:12:06.880507005Z", "duration": "PT0.000012478S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 1198, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/actuator/health]; client=[192.168.1.12]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T22:12:06.880508169Z", "endTime": "2020-12-10T22:12:06.880510682Z", "duration": "PT0.000002513S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 1199, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T22:12:07.491074789Z", "endTime": "2020-12-10T22:12:07.491080293Z", "duration": "PT0.000005504S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 1200, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T22:12:07.491081171Z", "endTime": "2020-12-10T22:12:07.491086523Z", "duration": "PT0.000005352S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 1201, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T22:12:07.491682127Z", "endTime": "2020-12-10T22:12:07.491690077Z", "duration": "PT0.00000795S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 1202, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T22:12:07.491690590Z", "endTime": "2020-12-10T22:12:07.491692112Z", "duration": "PT0.000001522S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 1203, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/actuator/health]; client=[192.168.1.12]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T22:12:16.889218378Z", "endTime": "2020-12-10T22:12:16.889230464Z", "duration": "PT0.000012086S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 1204, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/actuator/health]; client=[192.168.1.12]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T22:12:16.889231499Z", "endTime": "2020-12-10T22:12:16.889234139Z", "duration": "PT0.00000264S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 1205, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[2ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T22:12:17.504897943Z", "endTime": "2020-12-10T22:12:17.504904039Z", "duration": "PT0.000006096S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 1206, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[2ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T22:12:17.504905924Z", "endTime": "2020-12-10T22:12:17.504908415Z", "duration": "PT0.000002491S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 1207, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T22:12:17.506217772Z", "endTime": "2020-12-10T22:12:17.506224163Z", "duration": "PT0.000006391S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 1208, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T22:12:17.506225197Z", "endTime": "2020-12-10T22:12:17.506227562Z", "duration": "PT0.000002365S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 1209, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T22:12:27.496070692Z", "endTime": "2020-12-10T22:12:27.496076481Z", "duration": "PT0.000005789S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 1210, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T22:12:27.496077964Z", "endTime": "2020-12-10T22:12:27.496086518Z", "duration": "PT0.000008554S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 1211, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T22:12:27.496815490Z", "endTime": "2020-12-10T22:12:27.496827189Z", "duration": "PT0.000011699S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 1212, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T22:12:27.496827813Z", "endTime": "2020-12-10T22:12:27.496829920Z", "duration": "PT0.000002107S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 1213, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/actuator/health]; client=[192.168.1.12]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T22:12:36.879147913Z", "endTime": "2020-12-10T22:12:36.879154290Z", "duration": "PT0.000006377S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 1214, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/actuator/health]; client=[192.168.1.12]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T22:12:36.879155293Z", "endTime": "2020-12-10T22:12:36.879157601Z", "duration": "PT0.000002308S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 1215, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T22:12:37.528395926Z", "endTime": "2020-12-10T22:12:37.528401371Z", "duration": "PT0.000005445S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 1216, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T22:12:37.528403065Z", "endTime": "2020-12-10T22:12:37.528410441Z", "duration": "PT0.000007376S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 1217, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T22:12:37.529276434Z", "endTime": "2020-12-10T22:12:37.529287126Z", "duration": "PT0.000010692S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 1218, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T22:12:37.529287891Z", "endTime": "2020-12-10T22:12:37.529289944Z", "duration": "PT0.000002053S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 1219, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/actuator/info]; client=[192.168.1.12]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[2ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T22:12:46.895450451Z", "endTime": "2020-12-10T22:12:46.895457343Z", "duration": "PT0.000006892S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 1220, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/actuator/info]; client=[192.168.1.12]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[2ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T22:12:46.895458397Z", "endTime": "2020-12-10T22:12:46.895460758Z", "duration": "PT0.000002361S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 1221, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T22:12:47.490070038Z", "endTime": "2020-12-10T22:12:47.490084660Z", "duration": "PT0.000014622S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 1222, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T22:12:47.490086428Z", "endTime": "2020-12-10T22:12:47.490089095Z", "duration": "PT0.000002667S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 1223, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T22:12:47.491477441Z", "endTime": "2020-12-10T22:12:47.491485581Z", "duration": "PT0.00000814S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 1224, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T22:12:47.491486983Z", "endTime": "2020-12-10T22:12:47.491506076Z", "duration": "PT0.000019093S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 1225, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/actuator/health]; client=[192.168.1.12]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T22:12:56.885596621Z", "endTime": "2020-12-10T22:12:56.885617071Z", "duration": "PT0.00002045S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 1226, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/actuator/health]; client=[192.168.1.12]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T22:12:56.885618407Z", "endTime": "2020-12-10T22:12:56.885622070Z", "duration": "PT0.000003663S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 1227, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T22:12:57.498961595Z", "endTime": "2020-12-10T22:12:57.498967653Z", "duration": "PT0.000006058S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 1228, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T22:12:57.498968701Z", "endTime": "2020-12-10T22:12:57.498971233Z", "duration": "PT0.000002532S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 1229, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[0ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T22:12:57.499701683Z", "endTime": "2020-12-10T22:12:57.499705744Z", "duration": "PT0.000004061S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 1230, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[0ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T22:12:57.499706330Z", "endTime": "2020-12-10T22:12:57.499708387Z", "duration": "PT0.000002057S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 1231, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T22:13:07.494174797Z", "endTime": "2020-12-10T22:13:07.494188700Z", "duration": "PT0.000013903S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 1232, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T22:13:07.494190182Z", "endTime": "2020-12-10T22:13:07.494193562Z", "duration": "PT0.00000338S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 1233, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T22:13:07.495326852Z", "endTime": "2020-12-10T22:13:07.495332003Z", "duration": "PT0.000005151S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 1234, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T22:13:07.495332755Z", "endTime": "2020-12-10T22:13:07.495343481Z", "duration": "PT0.000010726S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 1235, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/actuator/health]; client=[192.168.1.12]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[2ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T22:13:16.884228319Z", "endTime": "2020-12-10T22:13:16.884236016Z", "duration": "PT0.000007697S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 1236, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/actuator/health]; client=[192.168.1.12]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[2ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T22:13:16.884237184Z", "endTime": "2020-12-10T22:13:16.884245920Z", "duration": "PT0.000008736S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 1237, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T22:13:17.496485905Z", "endTime": "2020-12-10T22:13:17.496493377Z", "duration": "PT0.000007472S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 1238, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T22:13:17.496494875Z", "endTime": "2020-12-10T22:13:17.496497484Z", "duration": "PT0.000002609S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 1239, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T22:13:17.497265619Z", "endTime": "2020-12-10T22:13:17.497277307Z", "duration": "PT0.000011688S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 1240, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T22:13:17.497277940Z", "endTime": "2020-12-10T22:13:17.497279915Z", "duration": "PT0.000001975S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 1241, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[2ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T22:13:27.495137644Z", "endTime": "2020-12-10T22:13:27.495146320Z", "duration": "PT0.000008676S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 1242, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[2ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T22:13:27.495149347Z", "endTime": "2020-12-10T22:13:27.495153948Z", "duration": "PT0.000004601S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 1243, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T22:13:27.496374280Z", "endTime": "2020-12-10T22:13:27.496380402Z", "duration": "PT0.000006122S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 1244, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T22:13:27.496381162Z", "endTime": "2020-12-10T22:13:27.496393874Z", "duration": "PT0.000012712S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 1245, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/actuator/health]; client=[192.168.1.12]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[2ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T22:13:36.885142231Z", "endTime": "2020-12-10T22:13:36.885148233Z", "duration": "PT0.000006002S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 1246, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/actuator/health]; client=[192.168.1.12]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[2ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T22:13:36.885149073Z", "endTime": "2020-12-10T22:13:36.885166134Z", "duration": "PT0.000017061S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 1247, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T22:13:37.500247814Z", "endTime": "2020-12-10T22:13:37.500260424Z", "duration": "PT0.00001261S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 1248, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T22:13:37.500262332Z", "endTime": "2020-12-10T22:13:37.500264893Z", "duration": "PT0.000002561S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 1249, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T22:13:37.501035303Z", "endTime": "2020-12-10T22:13:37.501040330Z", "duration": "PT0.000005027S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 1250, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T22:13:37.501040987Z", "endTime": "2020-12-10T22:13:37.501043150Z", "duration": "PT0.000002163S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 1251, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T22:13:47.494395844Z", "endTime": "2020-12-10T22:13:47.494401427Z", "duration": "PT0.000005583S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 1252, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[1ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T22:13:47.494402538Z", "endTime": "2020-12-10T22:13:47.494409784Z", "duration": "PT0.000007246S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 1253, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[2ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.boot.context.config.DelegatingApplicationListener@729d991e" } ] }, "startTime": "2020-12-10T22:13:47.495762691Z", "endTime": "2020-12-10T22:13:47.495766898Z", "duration": "PT0.000004207S" }, { "startupStep": { "name": "spring.event.invoke-listener", "id": 1254, "parentId": null, "tags": [ { "key": "event", "value": "ServletRequestHandledEvent: url=[/instances]; client=[127.0.0.1]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[2ms]; status=[OK]" }, { "key": "listener", "value": "org.springframework.security.context.DelegatingApplicationListener@fcd0e8d" } ] }, "startTime": "2020-12-10T22:13:47.495767434Z", "endTime": "2020-12-10T22:13:47.495769441Z", "duration": "PT0.000002007S" } ] } } ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/services/startup-actuator.spec.ts ================================================ /* * Copyright 2014-2020 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 { cloneDeep } from 'lodash-es'; import { beforeEach, describe, expect, it } from 'vitest'; import { StartupActuatorService } from './startup-actuator'; import fixture from './startup-actuator.fixture.spec.json'; describe('StartupActuatorService', () => { let data: any = {}; let events: any = {}; beforeEach(() => { data = cloneDeep(fixture); events = data.timeline.events; }); it('should find element by id', () => { const item8 = StartupActuatorService.getById(events, 8); expect.assertions(1); expect(item8).toEqual({ startupStep: { name: 'spring.beans.instantiate', id: 8, parentId: 7, tags: [ { key: 'beanName', value: 'org.springframework.boot.autoconfigure.internalCachingMetadataReaderFactory', }, ], }, startTime: '2020-12-10T21:53:42.958550077Z', endTime: '2020-12-10T21:53:42.960040652Z', duration: 'PT0.001490575S', }); }); it('should add parents as reference', () => { const tree = StartupActuatorService.parseAsTree(data); const child = tree.getById(8); const parent = tree.getById(7); expect.assertions(2); expect(child.startupStep.parent).toBe(parent); expect(child.startupStep.depth).toBe(3); }); it('should add children as reference', () => { const tree = StartupActuatorService.parseAsTree(data); const parent = tree.getById(7); const child = tree.getById(8); expect.assertions(1); expect(parent.startupStep.children).toContain(child); }); it('should extract map from event tags', () => { const tag = { key: 'event', value: 'ServletRequestHandledEvent: url=[/applications]; client=[0:0:0:0:0:0:0:1]; method=[GET, POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[13ms]; status=[OK]', }; const parsedTag = StartupActuatorService.parseTag(tag); expect.assertions(1); expect(parsedTag).toStrictEqual({ ...tag, parsed: { eventName: 'ServletRequestHandledEvent', url: ['/applications'], client: ['0:0:0:0:0:0:0:1'], method: ['GET', 'POST'], servlet: ['dispatcherServlet'], time: ['13ms'], status: ['OK'], session: ['null'], user: ['null'], }, }); }); it('should parse tags when iterating startupSteps', () => { const tree = StartupActuatorService.parseAsTree(data); const children = tree.getByParentId(6); expect.assertions(2); expect(children.length).toBe(25); expect(children.map((event) => event.startupStep.id)).toStrictEqual([ 7, 9, 11, 12, 13, 15, 16, 17, 18, 19, 20, 21, 24, 25, 26, 27, 28, 31, 32, 34, 35, 36, 37, 38, 43, ]); }); it('should find start and end time', () => { const tree = StartupActuatorService.parseAsTree(data); const startTime = tree.getStartTime(); const endTime = tree.getEndTime(); expect.assertions(2); expect(startTime).toBe(Date.parse('2020-12-10T21:53:41.836728041Z')); expect(endTime).toBe(Date.parse('2020-12-10T22:13:47.495769441Z')); }); it('should return progress of event in context of whole tree', () => { const tree = StartupActuatorService.parseAsTree(data); const period = tree.getPeriod(tree.getById(1)); expect.assertions(2); expect(period.start).toBe(0); expect(period.end).toBe(0.000038153408219073554); }); it('should return the path in tree for a given id (no pun intended)', () => { const tree = StartupActuatorService.parseAsTree(data); const path = tree.getPath(10); expect.assertions(1); expect(path).toEqual([10, 9, 6, 5]); }); it('should parse duration to seconds', () => { const tree = StartupActuatorService.parseAsTree(data); const event = tree.getById(1); expect.assertions(1); expect(event.duration).toBe(45.279861); }); }); ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/services/startup-actuator.ts ================================================ /* * Copyright 2014-2020 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 { StartupActuatorEventTree } from '@/services/startup-activator-tree'; import { parse, toMilliseconds } from '@/utils/iso8601-duration'; const regex = new RegExp('([^=\\s]*)=\\[([^\\]]*)\\]', 'gi'); function mapDuration(duration) { if (typeof duration === 'string') { return toMilliseconds(parse(duration)); } else if (!isNaN(Number.parseFloat(duration))) { return toMilliseconds(duration); } else { return -1; } } export const StartupActuatorService = { parseAsTree(data) { const events = data.timeline.events || []; const eventsForTree = events .sort((a, b) => a.startupStep.id - b.startupStep.id) .map((event) => { event.startupStep.parent = this.getById( events, event.startupStep.parentId, ); event.startupStep.children = this.getByParentId( events, event.startupStep.id, ); event.startupStep.tags = event.startupStep.tags.map(this.parseTag); event.duration = mapDuration(event.duration); event.startupStep.depth = 0; return event; }) .map((event) => { let parent = event.startupStep.parent; while (parent !== null && parent !== undefined) { parent = parent.startupStep.parent; event.startupStep.depth++; } return event; }); return new StartupActuatorEventTree(eventsForTree); }, getById(events, id) { return (events || []).find((event) => event.startupStep.id === id); }, getByParentId(events, id) { return (events || []).filter((event) => event.startupStep.parentId === id); }, parseTag(param) { if (param.key === 'event') { const parsed = {}; parsed['eventName'] = param.value.split(':')[0]; const matcher = param.value.matchAll(regex); for (const match of matcher) { parsed[match[1]] = match[2].split(',').map((s) => s.trim()); } param.parsed = parsed; } return param; }, }; ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/shell/i18n.de.json ================================================ { "navbar": { "signedInAs": "Angemeldet als", "logout": "Abmelden" } } ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/shell/i18n.en.json ================================================ { "navbar": { "signedInAs": "Signed in as", "logout": "Log out" } } ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/shell/i18n.es.json ================================================ { "navbar": { "logout": "Salir" } } ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/shell/i18n.fr.json ================================================ { "navbar": { "logout": "Déconnexion" } } ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/shell/i18n.is.json ================================================ { "navbar": { "logout": "Skrá út" } } ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/shell/i18n.ko.json ================================================ { "navbar": { "logout": "로그아웃" } } ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/shell/i18n.ru.json ================================================ { "navbar": { "logout": "Выход" } } ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/shell/i18n.zh-CN.json ================================================ { "navbar": { "logout": "注销" } } ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/shell/i18n.zh-TW.json ================================================ { "navbar": { "signedInAs": "目前登入身分", "logout": "登出" } } ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/shell/index.vue ================================================ ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/shell/navbar.spec.ts ================================================ import { screen } from '@testing-library/vue'; import { Mocked, afterEach, describe, expect, it, vi } from 'vitest'; import { getAvailableLocales } from '@/i18n'; import { getCurrentUser } from '@/sba-config'; import Navbar from '@/shell/navbar.vue'; import { render } from '@/test-utils'; vi.mock('@/sba-config', async () => { const sbaConfig = await vi.importActual('@/sba-config'); return { ...sbaConfig, getCurrentUser: vi.fn(), getAvailableLanguages: vi.fn(), }; }); vi.mock('@/i18n', async () => { const i18n = await vi.importActual('@/i18n'); return { ...i18n, getAvailableLocales: vi.fn().mockReturnValue([]), }; }); describe('Navbar', function () { afterEach(() => { vi.clearAllMocks(); }); it('User menu is visible, when a user is logged in', async function () { (getCurrentUser as Mocked).mockReturnValue({ name: 'mail@example.org', }); render(Navbar); expect(screen.getByTestId('usermenu')).toBeVisible(); expect(screen.getByText('mail@example.org')).toBeVisible(); }); it.each` user ${null} ${{}} `("User menu is hidden, when user object is '$user'", async function (user) { (getCurrentUser as Mocked).mockReturnValue(user); render(Navbar); expect(screen.queryByTestId('usermenu')).not.toBeInTheDocument(); }); it('Language menu is visible, when more than one language is available', async function () { (getAvailableLocales as Mocked).mockReturnValue(['de', 'en', 'fr']); render(Navbar, { locale: 'de' }); expect(screen.queryByText('Deutsch')).toBeInTheDocument(); }); it('Language menu is hidden, when just one language is available', async function () { (getAvailableLocales as Mocked).mockReturnValue(['de']); render(Navbar, { locale: 'de' }); expect(screen.queryByText('Deutsch')).not.toBeInTheDocument(); }); }); ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/shell/navbar.vue ================================================ ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/shell/sba-dropdown-logout-item.vue ================================================ ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/shell/sba-nav-language-selector.spec.ts ================================================ import userEvent from '@testing-library/user-event'; import { screen } from '@testing-library/vue'; import { describe, expect, it } from 'vitest'; import SbaNavLanguageSelector from '@/shell/sba-nav-language-selector.vue'; import { render } from '@/test-utils'; describe('NavbarItemLanguageSelector', () => { it('should print the locale with the country for selected language/locale', async () => { render(SbaNavLanguageSelector, { locale: 'de', props: { availableLocales: ['de', 'fr'], }, }); const buttons = await screen.findByText('Deutsch'); expect(buttons).toBeDefined(); }); it('should print locale with the country for available language in menu', async () => { render(SbaNavLanguageSelector, { locale: 'de', props: { availableLocales: ['de', 'fr'], }, }); const languageButton = await screen.findByText('Deutsch'); await userEvent.click(languageButton); expect(await screen.findByText('français')).toBeDefined(); }); it('should print the locale as label when it cannot be translated', async () => { render(SbaNavLanguageSelector, { locale: 'zz', props: { availableLocales: ['zz'], }, }); const htmlElement = await screen.findByText('zz'); expect(htmlElement).toBeDefined(); }); it('should emit the selected locale', async () => { const wrapper = render(SbaNavLanguageSelector, { locale: 'de', props: { availableLocales: ['de', 'fr'], }, }); await userEvent.click(await screen.findByText('Deutsch')); await userEvent.click(await screen.findByText('français')); const emitted = wrapper.emitted(); expect(emitted['locale-changed'][0]).toContain('fr'); }); it.each` locale | expected ${'de'} | ${'Deutsch'} ${'is'} | ${'íslenska'} ${'de-DE'} | ${'Deutsch (Deutschland)'} ${'zh-CN'} | ${'简体中文'} ${'zh-TW'} | ${'繁體中文'} `( "should show '$expected' for given '$locale'", async ({ locale, expected }) => { render(SbaNavLanguageSelector, { locale, propsData: { availableLocales: ['de'], }, }); await userEvent.click(await screen.findByText(expected)); }, ); }); ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/shell/sba-nav-language-selector.vue ================================================ ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/shell/sba-nav-usermenu.vue ================================================ ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/store.spec.ts ================================================ import { waitFor } from '@testing-library/vue'; import { HttpResponse, http } from 'msw'; import { ReplaySubject } from 'rxjs'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { registerWithOneInstance } from '@/mocks/fixtures/eventStream/registerWithOneInstance'; import { registerWithTwoInstances } from '@/mocks/fixtures/eventStream/registerWithTwoInstances'; import { server } from '@/mocks/server'; import Application from '@/services/application'; import ApplicationStore from '@/store'; describe('store', () => { let applicationStore; let mockSubject; vi.spyOn(Application, 'getStream').mockImplementation(function () { return mockSubject; }); let changedListener; let addedListener; let updateListener; let removedListener; beforeEach(() => { server.use( http.get('/applications', () => { return HttpResponse.json([]); }), ); changedListener = vi.fn(); addedListener = vi.fn(); updateListener = vi.fn(); removedListener = vi.fn(); mockSubject = new ReplaySubject(); applicationStore = new ApplicationStore(); applicationStore.start(); applicationStore.addEventListener('changed', changedListener); applicationStore.addEventListener('added', addedListener); applicationStore.addEventListener('updated', updateListener); applicationStore.addEventListener('removed', removedListener); applicationStore.addEventListener('error', (error) => console.error(error)); }); afterEach(() => { applicationStore.stop(); }); it('registers a new instance', async () => { mockSubject.next({ data: registerWithOneInstance }); await waitFor(() => { const applications = applicationStore.applications; expect(applications).toHaveLength(1); expect(applications[0].instances).toHaveLength(1); }); expect(changedListener).toHaveBeenCalled(); expect(addedListener).toHaveBeenCalled(); expect(updateListener).not.toHaveBeenCalled(); expect(removedListener).not.toHaveBeenCalled(); }); it('registers one instance and then another one', async () => { mockSubject.next({ data: registerWithOneInstance }); mockSubject.next({ data: registerWithTwoInstances }); await waitFor(() => { const applications = applicationStore.applications; expect(applications).toHaveLength(1); expect(applications[0].instances).toHaveLength(2); }); expect(changedListener).toHaveBeenCalled(); expect(addedListener).toHaveBeenCalled(); expect(updateListener).toHaveBeenCalled(); expect(removedListener).not.toHaveBeenCalled(); }); it('deregisters an instance', async () => { mockSubject.next({ data: registerWithTwoInstances }); mockSubject.next({ data: registerWithOneInstance }); await waitFor(() => { const applications = applicationStore.applications; expect(applications).toHaveLength(1); expect(applications[0].instances).toHaveLength(1); }); expect(changedListener).toHaveBeenCalled(); expect(addedListener).toHaveBeenCalled(); expect(updateListener).toHaveBeenCalled(); expect(removedListener).not.toHaveBeenCalled(); }); it('removes an application', async () => { mockSubject.next({ data: registerWithOneInstance }); await waitFor(() => { expect(applicationStore.applications).toHaveLength(1); }); const data = { ...registerWithOneInstance, instances: [] }; mockSubject.next({ data: data }); await waitFor(() => { expect(applicationStore.applications).toHaveLength(0); }); expect(changedListener).toHaveBeenCalled(); expect(addedListener).toHaveBeenCalled(); expect(updateListener).not.toHaveBeenCalled(); expect(removedListener).toHaveBeenCalled(); }); }); ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/store.ts ================================================ /* * Copyright 2014-2019 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 { Subscription, bufferTime, concat, concatMap, defer, delay, filter, map, retryWhen, tap, } from 'rxjs'; import Application from './services/application'; export const findInstance = ( applications: Application[], instanceId: string, ) => { for (const application of applications) { const instance = application.findInstance(instanceId); if (instance) { return instance; } } return undefined; }; export const findApplicationForInstance = ( applications: Application[], instanceId: string, ) => { return applications.find((application) => Boolean(application.findInstance(instanceId)), ); }; type NoopListener = () => void; type ApplicationAddedListener = (newApplications: Application[]) => void; type ApplicationStoreListener = NoopListener | ApplicationAddedListener; export default class ApplicationStore { private _listeners: { [p: string]: Array } = {}; private _applications: Map = new Map(); private applications: Application[] = []; private subscription: Subscription = null; addEventListener(type: string, listener: ApplicationStoreListener) { if (type in this._listeners) { this._listeners[type].push(listener); } else { this._listeners[type] = [listener]; } } removeEventListener(type: string, listener: ApplicationStoreListener) { if (!(type in this._listeners)) { return; } const idx = this._listeners[type].indexOf(listener); if (idx > 0) { this._listeners[type].splice(idx, 1); } } _dispatchEvent(type: string, ...args: any[]) { if (!(type in this._listeners)) { return; } this._listeners[type].forEach((listener) => listener.call(this, ...args)); } start() { // Do not resubscribe when already started if (this.subscription !== null) { return; } const list = defer(() => Application.list()).pipe( tap(() => this._dispatchEvent('connected')), concatMap((message) => message.data), ); const stream = Application.getStream().pipe(map((message) => message.data)); this.subscription = concat(list, stream) .pipe( retryWhen((errors) => errors.pipe( tap((error) => this._dispatchEvent('error', error)), delay(5000), ), ), bufferTime(250), filter((a) => a.length > 0), ) .subscribe({ next: (applications) => this.updateApplications(applications), }); } updateApplications(applications: Application[]) { applications.forEach((a) => this.updateApplication(a)); this.applications = [...this._applications.values()]; this._dispatchEvent('changed', this.applications); } updateApplication(application: Application) { const oldApplication = this._applications.get(application.name); if (!oldApplication && application.instances.length > 0) { this._applications.set(application.name, application); this._dispatchEvent('added', application); } else if (oldApplication && application.instances.length > 0) { this._applications.set(application.name, application); this._dispatchEvent('updated', application, oldApplication); } else if (oldApplication && application.instances.length <= 0) { this._applications.delete(application.name); this._dispatchEvent('removed', oldApplication); } } stop() { if (this.subscription && !this.subscription.closed) { try { this.subscription.unsubscribe(); } finally { this.subscription = null; } } } findApplicationByInstanceId(instanceId: string) { return findApplicationForInstance(this.applications, instanceId); } } ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/test-utils.ts ================================================ import NotificationcenterPlugin from '@stekoe/vue-toast-notificationcenter'; import { RenderResult, render as tlRender } from '@testing-library/vue'; import { RouterLinkStub } from '@vue/test-utils'; import { merge } from 'lodash-es'; import PrimeVue from 'primevue/config'; import Tooltip from 'primevue/tooltip'; import { createI18n } from 'vue-i18n'; import { createRouter, createWebHashHistory } from 'vue-router'; import components from './components/index'; import SbaModalPlugin from './plugins/modal'; import { createViewRegistry } from '@/composables/ViewRegistry'; import ViewRegistry from '@/viewRegistry'; let terms = {}; const modules: Record = import.meta.glob('@/**/i18n.en.json', { eager: true, }); for (const modulesKey in modules) { terms = { ...terms, ...modules[modulesKey] }; } export let router; createViewRegistry(); export const render = (testComponent, options?): RenderResult => { const routes = [{ path: '/', component: testComponent }]; if (testComponent.install) { const viewRegistry = new ViewRegistry(); testComponent.install({ viewRegistry }); const routeForComponent = viewRegistry._toRoutes(() => true)[0]; routes.push({ ...routeForComponent, path: '/' + routeForComponent.path, }); } router = createRouter({ history: createWebHashHistory(), routes: routes, }); const renderOptions = merge( { global: { plugins: [ router, createI18n({ locale: options?.locale || 'en', messages: { en: terms, }, legacy: false, fallbackWarn: false, missingWarn: false, }), NotificationcenterPlugin, SbaModalPlugin, [ PrimeVue, { theme: { options: { darkModeSelector: false, }, }, }, ], components, ], directives: { tooltip: Tooltip, }, stubs: { RouterLink: RouterLinkStub, 'sba-exchanges-chart': true, }, }, }, options, ); return tlRender(testComponent, renderOptions); }; ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/tests/setup.ts ================================================ import '@testing-library/jest-dom'; import '@testing-library/jest-dom/vitest'; import { cleanup } from '@testing-library/vue'; import { afterAll, afterEach, beforeAll, vi } from 'vitest'; import { server } from '@/mocks/server'; import sbaConfig from '@/sba-config'; global.IntersectionObserver = vi.fn().mockImplementation(function () { return { observe: vi.fn(), unobserve: vi.fn(), disconnect: vi.fn(), }; }); global.ResizeObserver = vi.fn().mockImplementation(function () { return { observe: vi.fn(), unobserve: vi.fn(), disconnect: vi.fn(), }; }); global.matchMedia = vi.fn().mockReturnValue({ addEventListener: vi.fn(), removeEventListener: vi.fn(), }); global.EventSource = class { constructor() {} close() {} }; global.SBA = sbaConfig; beforeAll(() => server.listen({ onUnhandledRequest: 'error' })); afterAll(() => server.close()); afterEach(() => server.resetHandlers()); // runs a cleanup after each test case (e.g. clearing jsdom) afterEach(() => { vi.clearAllMocks(); cleanup(); }); ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/toast-theme.css ================================================ @import '@stekoe/vue-toast-notificationcenter/dist/style.css'; .v-toast-container { z-index: 100; padding: 4em 1em; } .v-toast__text { overflow: hidden; } ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/utils/array.ts ================================================ /* * 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. */ /** * Adds values to an array and recursively flattens nested arrays. * Ignores null and undefined values. * * @example * const arr: number[] = []; * pushFlattened(arr, 1, [2, [3, null]], undefined, 4); * arr = [1, 2, 3, 4] * * @template T Type of array elements * @param {T[]} target Target array to which values are added * @param {...(T | T[] | null | undefined)} values Values or arrays of values to add and flatten * @returns {T[]} The target array with added and flattened values */ export const pushFlattened = ( target: T[], ...values: (T | T[] | null | undefined)[] ): T[] => { for (const value of values) { if (Array.isArray(value)) { // recursively flatten pushFlattened(target, ...value); } else if (value !== null && value !== undefined) { target.push(value as T); } } return target; }; ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/utils/autolink.spec.ts ================================================ /* * 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 { describe, expect, it } from 'vitest'; import autolink, { createAutolink } from './autolink'; describe('autolink should', () => { it('return the input string for normal text', () => { const str = 'This is just a normal text containing no hyperlinks'; expect(autolink(str)).toBe(str); }); it('return string with anchor tag for the hyperlink', () => { const str = 'Please visit http://example.com.'; expect(autolink(str)).toBe( 'Please visit http://example.com.', ); }); it('return string with anchor tag with shortened text for the hyperlink', () => { const str = 'Please visit http://extraordinary.com/very/very/log/hyperlink.'; const customAutolink = createAutolink({ truncate: { length: 30, location: 'smart', }, }); expect(customAutolink(str)).toBe( 'Please visit extraordinary.com/very…rlink.', ); }); it('return string with anchor for hyperlinks in dense json', () => { const str = '{"name":"John Smith","links":[{"rel":"random-link1","href":"https://localhost:8000/api/123/query?action=do_something&age=21","hreflang":null,"media":null,"title":null,"type":null,"deprecation":null}]}'; expect(autolink(str)).toBe( '{"name":"John Smith","links":[{"rel":"random-link1","href":"https://localhost:8000/api/123/query?action=do_something&age=21","hreflang":null,"media":null,"title":null,"type":null,"deprecation":null}]}', ); }); it('does not take version numbers as as link', () => { const str = '1.2.3.4'; expect(autolink(str)).toBe('1.2.3.4'); }); }); ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/utils/autolink.ts ================================================ /* * 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 _Autolinker, { AutolinkerConfig } from 'autolinker'; export const defaults: AutolinkerConfig = { urls: { schemeMatches: true, tldMatches: false, ipV4Matches: false, }, email: false, phone: false, mention: false, hashtag: false, stripPrefix: false, stripTrailingSlash: false, newWindow: true, className: '', }; const autolinker = new _Autolinker(defaults); export default (s: any) => { if (typeof s !== 'string') return s; try { return autolinker.link(s); } catch { return s; } }; export function createAutolink(cfg) { const autolinker = new _Autolinker({ ...defaults, ...cfg }); return (s) => { try { return autolinker.link(s); } catch { return s; } }; } ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/utils/axios.spec.ts ================================================ /* * 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 { beforeEach, describe, expect, it, vi } from 'vitest'; import { redirectOn401 } from './axios'; describe('redirectOn401', () => { beforeEach(() => { Object.defineProperty(window, 'location', { writable: true, value: { assign: vi.fn(), href: 'http://example.com/', }, }); }); it('should not redirect on 500', async () => { const error = { response: { status: 500, }, }; try { await redirectOn401()(error); } catch (e) { expect(e).toBe(error); } expect(window.location.assign).not.toBeCalled(); }); it('should redirect on 401', async () => { const error = { response: { status: 401, }, }; try { await redirectOn401()(error); } catch (e) { expect(e).toBe(error); } expect(window.location.assign).toBeCalledWith( 'login?redirectTo=http%3A%2F%2Fexample.com%2F&error=401', ); }); it('should not redirect on 401 for predicate yields false', async () => { const error = { response: { status: 401, }, }; try { await redirectOn401(() => false)(error); } catch (e) { expect(e).toBe(error); } expect(window.location.assign).not.toBeCalled(); }); }); ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/utils/axios.ts ================================================ /* * Copyright 2014-2019 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 { useNotificationCenter } from '@stekoe/vue-toast-notificationcenter'; import axios, { type AxiosError, type AxiosInstance } from 'axios'; import sbaConfig from '../sba-config'; const nc = useNotificationCenter(); axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest'; axios.defaults.xsrfHeaderName = sbaConfig.csrf.headerName; export const redirectOn401 = (predicate: (error: AxiosError) => boolean = () => true) => (error: AxiosError) => { if (error.response && error.response.status === 401 && predicate(error)) { window.location.assign( `login?redirectTo=${encodeURIComponent( window.location.href, )}&error=401`, ); } return Promise.reject(error); }; axios.defaults.withCredentials = true; axios.defaults.headers.common['Accept'] = 'application/json'; axios.interceptors.response.use((response) => response, redirectOn401()); export default axios; export const registerErrorToastInterceptor = (axios: AxiosInstance): void => { if (sbaConfig.uiSettings.enableToasts) { axios.interceptors.response.use( (response) => response, (error: AxiosError) => { const data = error.request; const message = ` Request failed: ${data.statusText}
${data.responseURL} `; nc.error(message, { context: data.status ?? 'axios', title: `Error ${data.status}`, duration: 10000, }); }, ); } }; ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/utils/collections.spec.ts ================================================ /* * Copyright 2014-2019 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 { describe, expect, it, vi } from 'vitest'; import { anyValueMatches } from './collections'; describe('anyValueMatches', () => { it('should return predicate value', () => { const predicate = vi.fn(() => true); expect(anyValueMatches('test', predicate)).toBe(true); }); it('should call predicate for string', () => { const predicate = vi.fn(); anyValueMatches('test', predicate); expect(predicate).toHaveBeenCalledWith('test'); }); it('should call predicate for number', () => { const predicate = vi.fn(); anyValueMatches(1, predicate); expect(predicate).toHaveBeenCalledWith(1); }); it('should call predicate for boolean', () => { const predicate = vi.fn(); anyValueMatches(true, predicate); expect(predicate).toHaveBeenCalledWith(true); }); it('should call predicate for null', () => { const predicate = vi.fn(); anyValueMatches(null, predicate); expect(predicate).toHaveBeenCalledWith(null); }); it('should call predicate for undefined', () => { const predicate = vi.fn(); anyValueMatches(undefined, predicate); expect(predicate).toHaveBeenCalledWith(undefined); }); it('should not call predicate for empty object', () => { const predicate = vi.fn(); anyValueMatches({}, predicate); expect(predicate).not.toHaveBeenCalled(); }); it('should not call predicate for empty array', () => { const predicate = vi.fn(); anyValueMatches([], predicate); expect(predicate).not.toHaveBeenCalled(); }); it('should not call predicate for elements in array', () => { const predicate = vi.fn(); anyValueMatches( ['test', 1, true, { value: 'nested-obj' }, ['nested-array'], [], {}], predicate, ); expect(predicate).toHaveBeenNthCalledWith(1, 'test'); expect(predicate).toHaveBeenNthCalledWith(2, 1); expect(predicate).toHaveBeenNthCalledWith(3, true); expect(predicate).toHaveBeenNthCalledWith(4, 'nested-obj'); expect(predicate).toHaveBeenNthCalledWith(5, 'nested-array'); }); }); ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/utils/collections.ts ================================================ /* * Copyright 2014-2019 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. */ export const compareBy = (mapper) => (a, b) => { const valA = mapper(a); const valB = mapper(b); return valA > valB ? 1 : valA < valB ? -1 : 0; }; export const anyValueMatches = (obj, predicate) => { if (Array.isArray(obj)) { return obj.some((e) => anyValueMatches(e, predicate)); } else if (obj !== null && typeof obj === 'object') { return anyValueMatches(Object.values(obj), predicate); } return predicate(obj); }; ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/utils/d3.ts ================================================ /* * 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 * as array from 'd3-array'; import * as axis from 'd3-axis'; import * as brush from 'd3-brush'; import * as scale from 'd3-scale'; import * as selection from 'd3-selection'; import * as shape from 'd3-shape'; import * as time from 'd3-time'; export default { ...array, ...axis, ...brush, ...scale, ...selection, ...shape, ...time, }; ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/utils/eventsource-polyfill.ts ================================================ /* * 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. */ export default async () => { if (typeof window.EventSource === 'undefined') { return import( /* webpackChunkName: "event-source-polyfill" */ 'event-source-polyfill' ); } return Promise.resolve(); }; ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/utils/formatWithDataTypes.spec.ts ================================================ /* * Copyright 2014-2026 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * 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 { describe, expect, it } from 'vitest'; import { formatWithDataTypes } from './formatWithDataTypes'; describe('formatWithDataTypes', () => { it('returns primitive values unchanged', () => { expect(formatWithDataTypes(42)).toBe(42); expect(formatWithDataTypes('foo')).toBe('foo'); expect(formatWithDataTypes(null)).toBe(null); expect(formatWithDataTypes(undefined)).toBe(undefined); }); it('returns object unchanged when no config provided', () => { const input = { a: 1, b: 2 }; const output = formatWithDataTypes(input); expect(output).toEqual({ a: 1, b: 2 }); }); it('applies prettyBytes for bytes datatype', () => { const input = { memory: { heap: { committed: 1024 } } }; const output = formatWithDataTypes(input, { 'memory.heap.committed': 'bytes', }); expect(output.memory.heap.committed).toBe('1.02 kB'); }); it('wraps value with type for non-bytes datatypes', () => { const input = { duration: 5000 }; const output = formatWithDataTypes(input, { duration: 'milliseconds' }); expect(output.duration).toEqual(5000); }); it('handles multiple config entries', () => { const input = { memory: { heap: { committed: 2048 } }, duration: 3000, }; const output = formatWithDataTypes(input, { 'memory.heap.committed': 'bytes', duration: 'milliseconds', }); expect(output.memory.heap.committed).toBe('2.05 kB'); expect(output.duration).toEqual(3000); }); it('ignores missing keys in config', () => { const input = { a: 1 }; const output = formatWithDataTypes(input, { 'b.c.d': 'bytes' }); expect(output).toEqual({ a: 1 }); }); it('formats date from timestamp', () => { const input = { timestamp: 1717243496000 }; const output = formatWithDataTypes(input, { timestamp: 'date' }); expect(output.timestamp).toMatch(/01\.06\.2024, 14:04:56/); }); it('formats date from ISO string', () => { const input = { created: '2024-06-01T12:34:56Z' }; const output = formatWithDataTypes(input, { created: 'date' }); expect(output.created).toMatch(/01\.06\.2024, 14:34:56/); }); it('handles formatting errors gracefully', () => { const input = { invalidDate: 'not-a-date', invalidBytes: 'not-a-number' }; const output = formatWithDataTypes(input, { invalidDate: 'date', invalidBytes: 'bytes', }); expect(output.invalidDate).toBe('not-a-date'); expect(output.invalidBytes).toBe('not-a-number'); }); it('does not format negative bytes', () => { const input = { size: -1024 }; const output = formatWithDataTypes(input, { size: 'bytes' }); expect(output.size).toBe(-1024); }); }); ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/utils/formatWithDataTypes.ts ================================================ /* * Copyright 2014-2026 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * 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 prettyBytes from 'pretty-bytes'; import { formatDateTime } from './prettyTime'; /** * Configuration object mapping property paths to their data types. * Keys are dot-notation paths (e.g., 'memory.heap.committed'), values are data type names. */ type DataTypeConfig = { [key: string]: string; }; /** * Formats object values based on their data types. * * Converts a proxy or regular object to a raw object and applies formatting * to specified properties based on their data types. * * Supported data types: * - 'bytes': Formats numeric byte values to human-readable format (e.g., '1.02 kB'). * Only formats non-negative values. * - 'date': Formats timestamps or ISO strings to localized date-time strings. * * @template T - The type of the input object * @param input - The object to format. Primitive values are returned unchanged. * @param config - Optional configuration mapping property paths to data types. * Uses dot notation for nested properties (e.g., 'memory.heap.committed'). * @returns A new object with formatted values, or the original input if it's not an object. * * @example * const data = { memory: { heap: { committed: 1024 } }, timestamp: 1717243496000 }; * const formatted = formatWithDataTypes(data, { * 'memory.heap.committed': 'bytes', * 'timestamp': 'date' * }); * // Returns: { memory: { heap: { committed: '1.02 kB' } }, timestamp: 'Jun 1, 2024, 12:34:56 PM' } */ export function formatWithDataTypes(input: T, config?: DataTypeConfig): T { if (typeof input !== 'object' || input === null) { return input; } if (!config) { return input; } const result = JSON.parse(JSON.stringify(input)) as any; // Process each configured property path for (const [path, dataType] of Object.entries(config)) { const keys = path.split('.'); let current = result; // Navigate through nested objects to find the parent of the target property for (let i = 0; i < keys.length - 1; i++) { if (current[keys[i]] === undefined) { break; } current = current[keys[i]]; } // Apply formatting to the target property const lastKey = keys[keys.length - 1]; if (current && current[lastKey] !== undefined) { try { if (dataType === 'bytes' && current[lastKey] >= 0) { current[lastKey] = prettyBytes(current[lastKey]); } else if (dataType === 'date') { current[lastKey] = formatDateTime(current[lastKey]); } } catch { // Keep original value on error } } } return result as T; } ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/utils/http-status.ts ================================================ /* * Copyright 2014-2026 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * 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. */ const HTTP_STATUS_TEXTS: Record = { // 1xx Informational 100: 'Continue', 101: 'Switching Protocols', 102: 'Processing', 103: 'Early Hints', // 2xx Success 200: 'OK', 201: 'Created', 202: 'Accepted', 203: 'Non-Authoritative Information', 204: 'No Content', 205: 'Reset Content', 206: 'Partial Content', 207: 'Multi-Status', 208: 'Already Reported', 226: 'IM Used', // 3xx Redirection 300: 'Multiple Choices', 301: 'Moved Permanently', 302: 'Found', 303: 'See Other', 304: 'Not Modified', 305: 'Use Proxy', 307: 'Temporary Redirect', 308: 'Permanent Redirect', // 4xx Client Error 400: 'Bad Request', 401: 'Unauthorized', 402: 'Payment Required', 403: 'Forbidden', 404: 'Not Found', 405: 'Method Not Allowed', 406: 'Not Acceptable', 407: 'Proxy Authentication Required', 408: 'Request Timeout', 409: 'Conflict', 410: 'Gone', 411: 'Length Required', 412: 'Precondition Failed', 413: 'Payload Too Large', 414: 'URI Too Long', 415: 'Unsupported Media Type', 416: 'Range Not Satisfiable', 417: 'Expectation Failed', 418: "I'm a teapot", 421: 'Misdirected Request', 422: 'Unprocessable Entity', 423: 'Locked', 424: 'Failed Dependency', 425: 'Too Early', 426: 'Upgrade Required', 428: 'Precondition Required', 429: 'Too Many Requests', 431: 'Request Header Fields Too Large', 451: 'Unavailable For Legal Reasons', // 5xx Server Error 500: 'Internal Server Error', 501: 'Not Implemented', 502: 'Bad Gateway', 503: 'Service Unavailable', 504: 'Gateway Timeout', 505: 'HTTP Version Not Supported', 506: 'Variant Also Negotiates', 507: 'Insufficient Storage', 508: 'Loop Detected', 510: 'Not Extended', 511: 'Network Authentication Required', }; /** * Returns the HTTP status text for a given status code. * @param statusCode - The HTTP status code * @returns The status text (e.g., "OK" for 200) */ export const getHttpStatusText = (statusCode: number): string => { return HTTP_STATUS_TEXTS[statusCode] || ''; }; /** * Returns the formatted HTTP status with code and text. * @param statusCode - The HTTP status code * @returns Formatted status string (e.g., "200 OK") */ export const getHttpStatus = (statusCode: number): string => { const statusText = HTTP_STATUS_TEXTS[statusCode]; return statusText ? `${statusCode} ${statusText}` : `${statusCode}`; }; ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/utils/iso8601-duration.spec.ts ================================================ /* * Copyright 2014-2020 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 { describe, expect, test } from 'vitest'; import { toMilliseconds } from '@/utils/iso8601-duration'; describe('iso8601-duration', () => { test.each([ [1.023, 0, 0, 0, 0, 1_023], [1.023, 1, 1, 0, 0, 3_661_023], [1.023, 1, 1, 1, 0, 90_061_023], [1.023, 1, 1, 1, 1, 694_861_023], ])( 'should return the miliseconds of a duration object with %n seconds, %n minutes, %n hours, %n days, %n weeks', (seconds, minutes, hours, days, weeks, expected) => { const duration = { seconds, minutes, hours, days, weeks, }; const milliseconds = toMilliseconds(duration); expect(milliseconds).toBeCloseTo(expected); }, ); }); ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/utils/iso8601-duration.ts ================================================ /* * Copyright 2014-2020 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. */ export { parse } from 'iso8601-duration'; /** * Convert ISO8601 duration object to milliseconds * * Hint: Years and months are ignored. * Calculating based on JavaScript date is too imprecise. * * @param {Object} duration - The duration object * @return {Number} */ export const toMilliseconds = (duration) => { let result = duration.seconds; result += duration.minutes * 60; result += duration.hours * 60 * 60; result += duration.days * 60 * 60 * 24; result += duration.weeks * 60 * 60 * 24 * 7; return result * 1000; }; ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/utils/logtail.ts ================================================ /* * Copyright 2014-2019 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 { EMPTY, Observable, catchError, concatMap, of, timer } from './rxjs'; export default (getFn, interval, initialSize = 300 * 1024) => { let range = `bytes=-${initialSize}`; let size = 0; return timer(0, interval).pipe( concatMap(() => { return new Observable((observer) => { getFn({ headers: { range, Accept: 'text/plain' } }) .then((response) => { observer.next(response); observer.complete(); }) .catch((error) => observer.error(error)); }).pipe( catchError((error) => of({ data: '', status: error.response.status })), ); }), concatMap((response) => { let initial = size === 0; const contentLength = response.data.length; if (response.status === 200) { if (!initial) { throw 'Expected 206 - Partial Content on subsequent requests.'; } size = contentLength; range = `bytes=${size - 1}-`; } else if (response.status === 206) { size = parseInt(response.headers['content-range'].split('/')[1]); range = `bytes=${size - 1}-`; } else if (response.status === 416) { size = 0; range = `bytes=-${initialSize}`; initial = true; } else { throw 'Unexpected response status: ' + response.status; } let addendum = null; let skipped = 0; if (initial) { if (contentLength >= size) { addendum = response.data; } else { // In case of a partial response find the first line break. addendum = response.data.substring(response.data.indexOf('\n') + 1); skipped = size - addendum.length; } } else if (response.data.length > 1) { // Remove the first byte which has been part of the previos response. addendum = response.data.substring(1); } return addendum ? of({ totalBytes: size, skipped, addendum, }) : EMPTY; }), ); }; ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/utils/objToYaml.ts ================================================ /* * 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 { dump } from 'js-yaml'; /** * Convert JSON to YAML. * * Example: * objToYaml({foo: "bar"}) * Output: * foo: bar * * @param input A JSON object or a JSON string. * @param options Optional js-yaml dump options. * @returns YAML string. */ export function objToYaml( input: object | string, options: Parameters[1] = {}, ): string { if (typeof input === 'string') { return input; } return dump(input, { noRefs: true, indent: 2, lineWidth: -1, ...options, }); } ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/utils/prettyTime.ts ================================================ import moment from 'moment/moment'; import { useI18n } from 'vue-i18n'; export const enum PrettyTimeUnit { years = 'years', months = 'months', weeks = 'weeks', days = 'days', hours = 'hours', minutes = 'minutes', seconds = 'seconds', milliseconds = 'milliseconds', } /** * Formats a Date or ISO string to a localized date-time string. * @param time - Date object or ISO string to format * @param locale - Optional locale string (defaults to browser default) * @returns Formatted date-time string */ export const formatDateTime = ( time: Date | string | number, locale?: string, ) => { return new Intl.DateTimeFormat(locale, { dateStyle: 'medium', timeStyle: 'medium', }).format(new Date(time)); }; /** * usePrettyTime provides utility functions for formatting time durations and date-times. * * - formatTime: Converts a duration in milliseconds to a human-readable string (e.g., "2 days 3 hours"). * - formatDateTime: Formats a Date or ISO string to a localized date-time string. * * Example: * const { formatTime, formatDateTime } = usePrettyTime(); * formatTime(90061000); // "1 days 1 hours 1 minutes 1 seconds" * formatDateTime("2024-06-01T12:34:56Z"); // "Jun 1, 2024, 12:34:56 PM" (locale-dependent) */ export const usePrettyTime = () => { const { t, locale } = useI18n(); const formatTime = (time: number) => { if (time < 0) { return t('time.unknown'); } const duration = moment.duration(time); const output = { [PrettyTimeUnit.years]: Math.floor(duration.asYears()), [PrettyTimeUnit.months]: Math.floor(duration.asMonths()), [PrettyTimeUnit.weeks]: Math.floor(duration.asWeeks()), [PrettyTimeUnit.days]: Math.floor(duration.asDays()), [PrettyTimeUnit.hours]: duration.hours(), [PrettyTimeUnit.minutes]: duration.minutes(), [PrettyTimeUnit.seconds]: duration.seconds(), [PrettyTimeUnit.milliseconds]: duration.milliseconds(), }; return Object.entries(output).reduce((acc, [key, value]) => { if (value > 0) { return acc + t(`time_short.${key}`, { count: value }) + ' '; } return acc; }, ''); }; return { formatTime, formatDateTime: (time: Date | string | number) => formatDateTime(time, locale.value), }; }; ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/utils/rxjs.spec.ts ================================================ /* * Copyright 2014-2019 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 { describe, expect, it, vi } from 'vitest'; import { delay, doOnSubscribe, listen } from './rxjs'; import { EMPTY, concat, of, throwError } from '@/utils/rxjs'; describe('doOnSubscribe', () => { it('should call callback when subscribing', () => { return new Promise((resolve) => { const cb = vi.fn(); EMPTY.pipe(doOnSubscribe(cb)).subscribe({ complete: () => { expect(cb).toHaveBeenCalledTimes(1); resolve(true); }, }); }); }); }); describe('listen', () => { it('should call callback with complete', () => { return new Promise((resolve) => { const cb = vi.fn(); EMPTY.pipe(listen(cb)).subscribe({ complete: () => { expect(cb).toHaveBeenCalledTimes(1); expect(cb).toHaveBeenCalledWith('completed'); resolve(true); }, }); }); }); it('should call callback with executing and complete', () => { return new Promise((resolve) => { const cb = vi.fn(); of(1) .pipe(delay(10), listen(cb, 1)) .subscribe({ complete: () => { expect(cb).toHaveBeenCalledTimes(2); expect(cb).toHaveBeenCalledWith('executing'); expect(cb).toHaveBeenCalledWith('completed'); resolve(true); }, }); }); }); it('should call callback with failed', () => { return new Promise((resolve) => { const cb = vi.fn(); console.warn = vi.fn(); throwError(new Error('test')) .pipe(listen(cb)) .subscribe({ error: () => { expect(cb).toHaveBeenCalledTimes(1); expect(cb).toHaveBeenCalledWith('failed'); resolve(true); }, }); }); }); it('should call callback with executing and failed', () => { return new Promise((done) => { const cb = vi.fn(); concat(of(1).pipe(delay(10)), throwError(new Error('test'))) .pipe(listen(cb, 1)) .subscribe({ error: () => { expect(cb).toHaveBeenCalledTimes(2); expect(cb).toHaveBeenCalledWith('executing'); expect(cb).toHaveBeenCalledWith('failed'); done(true); }, }); }); }); }); ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/utils/rxjs.ts ================================================ /* * Copyright 2014-2019 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 { defer, tap } from 'rxjs'; export { of, defer, concat, catchError, throwError, EMPTY, from, timer, Observable, Subject, animationFrameScheduler, concatMap, delay, debounceTime, mergeWith, map, retryWhen, tap, filter, concatAll, ignoreElements, bufferTime, finalize, } from 'rxjs'; export const doOnSubscribe = (cb) => (source) => defer(() => { cb(); return source; }); export const listen = (cb, execDelay = 150) => (source) => { let handle = null; return source.pipe( doOnSubscribe( () => (handle = setTimeout(() => cb('executing'), execDelay)), ), tap({ complete: () => { clearTimeout(handle); cb('completed'); }, error: (error) => { console.warn('Operation failed:', error); clearTimeout(handle); cb('failed'); }, }), ); }; ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/utils/sanitizeHtml.spec.ts ================================================ /* * 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 { describe, expect, it } from 'vitest'; import { sanitizeHtml } from './sanitizeHtml'; describe('sanitizeHtml', () => { it('should return plain text unchanged', () => { const input = 'This is plain text'; expect(sanitizeHtml(input)).toBe('This is plain text'); }); it('should preserve safe HTML tags', () => { const input = '

Hello world

'; expect(sanitizeHtml(input)).toBe('

Hello world

'); }); it('should remove script tags', () => { const input = '

Hello

'; expect(sanitizeHtml(input)).toBe('

Hello

'); }); it('should remove potentially dangerous attributes', () => { const input = '

Click me

'; expect(sanitizeHtml(input)).toBe('

Click me

'); }); it('should remove iframe tags', () => { const input = '

Content

'; expect(sanitizeHtml(input)).toBe('

Content

'); }); it('should transform anchor tags to open in new tab', () => { const input = 'Link'; const result = sanitizeHtml(input); expect(result).toContain('target="_blank"'); expect(result).toContain('https://example.com'); }); it('should preserve existing anchor href and add target="_blank"', () => { const input = 'Spring'; const result = sanitizeHtml(input); expect(result).toBe( 'Spring', ); }); it('should handle anchor tags that already have target attribute', () => { const input = 'Link'; const result = sanitizeHtml(input); // The simpleTransform should override the target attribute expect(result).toContain('target="_blank"'); }); it('should remove javascript: protocol from links', () => { const input = 'Click'; const result = sanitizeHtml(input); expect(result).not.toContain('javascript:'); }); it('should handle empty string', () => { expect(sanitizeHtml('')).toBe(''); }); it('should handle multiple paragraphs with mixed content', () => { const input = '

First paragraph

Second paragraph

'; expect(sanitizeHtml(input)).toBe( '

First paragraph

Second paragraph

', ); }); it('should remove style tags', () => { const input = '

Content

'; expect(sanitizeHtml(input)).toBe('

Content

'); }); it('should handle nested HTML elements', () => { const input = '

Nested content

'; const result = sanitizeHtml(input); expect(result).toContain('Nested'); expect(result).toContain('content'); }); it('should remove object and embed tags', () => { const input = '

Safe content

'; expect(sanitizeHtml(input)).toBe('

Safe content

'); }); it('should preserve multiple links with target="_blank" for each', () => { const input = 'First and Second'; const result = sanitizeHtml(input); expect(result).toBe( 'First and Second', ); }); it('should handle HTML entities correctly', () => { const input = '

Price: <$10>

'; expect(sanitizeHtml(input)).toBe('

Price: <$10>

'); }); it('should remove form elements', () => { const input = '

Content

'; expect(sanitizeHtml(input)).toBe('

Content

'); }); it('should handle links with query parameters', () => { const input = 'Link'; const result = sanitizeHtml(input); expect(result).toContain('https://example.com?param=value&other=123'); expect(result).toContain('target="_blank"'); }); it('should preserve list structures', () => { const input = '
  • Item 1
  • Item 2
'; expect(sanitizeHtml(input)).toBe('
  • Item 1
  • Item 2
'); }); it('should preserve download attribute in anchor tag', () => { const input = 'Link'; const result = sanitizeHtml(input); expect(result).toContain('https://example.com'); expect(result).toContain('download'); }); }); ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/utils/sanitizeHtml.ts ================================================ import _sanitizeHtml from 'sanitize-html'; export function sanitizeHtml(dirty: string): string { return _sanitizeHtml(dirty, { allowedEmptyAttributes: _sanitizeHtml.defaults.allowedEmptyAttributes.concat('download'), allowedAttributes: { a: _sanitizeHtml.defaults.allowedAttributes['a'].concat('download'), }, transformTags: { a: function (tagName, attribs) { let newAttribs = attribs ? attribs : {}; // When download attribute is not set, set target attribute if (attribs.download === undefined) { newAttribs = { ...newAttribs, target: '_blank', }; } return { tagName, attribs: { ...newAttribs, }, }; }, }, }); } ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/utils/shortenClassname.spec.ts ================================================ /* * 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 { describe, expect, it } from 'vitest'; import shortenClassname from './shortenClassname'; describe('shortenClassname', () => { it('should shorten when too long', () => { expect( shortenClassname( 'de.codecentric.boot.admin.server.config.AdminServerAutoConfiguration', 40, ), ).toBe('d.c.b.a.s.config.AdminServerAutoConfiguration'); }); it('should not shorten when string is small enough', () => { expect( shortenClassname( 'de.codecentric.boot.admin.server.config.AdminServerAutoConfiguration', 300, ), ).toBe( 'de.codecentric.boot.admin.server.config.AdminServerAutoConfiguration', ); }); it('should not shorten when no package is present', () => { expect(shortenClassname('AdminServerAutoConfiguration', 1)).toBe( 'AdminServerAutoConfiguration', ); }); }); ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/utils/shortenClassname.ts ================================================ /* * 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. */ /** * Shortens a fully qualified class name to fit within a target length. * Example: * shortenClassname('org.springframework.boot.actuate.health.HealthEndpoint', 20) * // returns: "o.s.b.a.h.HealthEndpoint" * * @param {string} fullName - The fully qualified class name. * @param {number} targetLength - The maximum allowed length for the shortened name. * @returns {string} The shortened class name. */ export default (fullName, targetLength) => { if (!fullName || fullName.length < targetLength) { return fullName; } const tokens = fullName.split('.'); let shortened = tokens.pop(); while (tokens.length > 0) { const next = tokens.pop(); if (next.length + 1 + shortened.length < targetLength) { shortened = next + '.' + shortened; } else { shortened = next[0] + '.' + shortened; } } return shortened; }; ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/utils/sortObject.spec.ts ================================================ import { describe, expect, it } from 'vitest'; import { sortObject } from './sortObject'; describe('sortObject', () => { it('sorts a flat object alphabetically by keys', () => { const input = { b: 2, a: 1, c: 3 }; const output = sortObject(input); expect(Object.keys(output)).toEqual(['a', 'b', 'c']); expect(output).toEqual({ a: 1, b: 2, c: 3 }); }); it('sorts nested objects recursively', () => { const input = { z: 1, a: { d: 4, b: 2, c: 3 }, b: 2 }; const output = sortObject(input); expect(Object.keys(output)).toEqual(['a', 'b', 'z']); expect(Object.keys(output.a)).toEqual(['b', 'c', 'd']); expect(output).toEqual({ a: { b: 2, c: 3, d: 4 }, b: 2, z: 1 }); }); it('leaves arrays unchanged', () => { const input = { arr: [3, 1, 2], b: 2, a: 1 }; const output = sortObject(input); expect(output.arr).toEqual([3, 1, 2]); expect(Object.keys(output)).toEqual(['a', 'arr', 'b']); }); it('returns empty objects unchanged', () => { const input = {}; const output = sortObject(input); expect(output).toEqual({}); }); it('returns primitive values unchanged', () => { expect(sortObject(42)).toBe(42); expect(sortObject('foo')).toBe('foo'); expect(sortObject(null)).toBe(null); expect(sortObject(undefined)).toBe(undefined); }); }); ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/utils/sortObject.ts ================================================ export function sortObject(input: any): any { if (Array.isArray(input)) { return input.map(sortObject); } else if (input !== null && typeof input === 'object') { return Object.keys(input) .sort() .reduce( (acc, key) => { acc[key] = sortObject(input[key]); return acc; }, {} as Record, ); } return input; } ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/utils/toast.ts ================================================ ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/utils/transformToJSON.spec.ts ================================================ import { describe, expect, it } from 'vitest'; import { transformToJSON } from '@/utils/transformToJSON'; const input = { 'service-url': 'http://localhost:8080', 'sidebar.links.0.iframe': 'true', 'sidebar.links.0.label': '🏠 Home', 'sidebar.links.0.url': 'https://codecentric.de', 'tags.environment': 'test', }; describe('transformToNestedPojo', () => { it('transforms simple key', () => { const output = transformToJSON(input); expect(output['service-url']).toEqual('http://localhost:8080'); }); it('transforms nested key', () => { const output = transformToJSON(input); expect(output.tags.environment).toEqual('test'); }); it('transforms array', () => { const output = transformToJSON(input); expect(output.sidebar.links).toHaveLength(1); expect(output.sidebar.links[0]).toEqual({ iframe: 'true', label: '🏠 Home', url: 'https://codecentric.de', }); }); it('handles conflict when property is both string and object', () => { const conflictInput = { 'my-service-headless.kubernetes': 'namespace', 'my-service-headless': 'some-value', }; const output = transformToJSON(conflictInput, 'LAX'); expect(output['my-service-headless']['__']).toEqual('some-value'); expect(output['my-service-headless']['kubernetes']).toEqual('namespace'); }); }); ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/utils/transformToJSON.ts ================================================ /** * Converts a flat object with dot notation keys into a nested POJO. * * Supports two modes: * - **STRICT** (default): Later values overwrite earlier values for the same path. * - **LAX**: Resolves conflicts between primitive values and nested objects by using * a sentinel key `__` to preserve both values. * * @param input - The flat object to transform. * @param mode - Transformation mode: 'STRICT' or 'LAX'. Defaults to 'STRICT'. * @returns The nested POJO. * * @example * // Basic transformation with nested objects and arrays * transformToJSON({ * 'user.name': 'Alice', * 'user.address.street': 'Main St', * 'user.address.zip': '12345', * 'items.0.id': 1, * 'items.0.name': 'Item 1', * 'items.1.id': 2, * 'items.1.name': 'Item 2' * }); * // Returns: * // { * // user: { * // name: 'Alice', * // address: { street: 'Main St', zip: '12345' } * // }, * // items: [ * // { id: 1, name: 'Item 1' }, * // { id: 2, name: 'Item 2' } * // ] * // } * * @example * // LAX mode: Conflict resolution with sentinel key '__' * // When a property is both a primitive and has nested properties * transformToJSON({ * 'my-service': 'simple-value', * 'my-service.kubernetes': 'namespace' * }, 'LAX'); * // Returns: * // { * // 'my-service': { * // __: 'simple-value', // Original primitive value preserved * // kubernetes: 'namespace' // Nested property added * // } * // } * * @example * // LAX mode: Reverse order (deep path first, then shallow) * transformToJSON({ * 'config.server.port': '8080', * 'config': 'default-config' * }, 'LAX'); * // Returns: * // { * // config: { * // __: 'default-config', // Later primitive value preserved * // server: { port: '8080' } // Earlier nested structure kept * // } * // } * * @remarks * - The sentinel key `__` is only used in LAX mode when conflicts occur. * - Numeric keys (e.g., '0', '1') create arrays automatically. * - In STRICT mode, the last value for a given path wins. * - Avoid using `__` as a key in your input data when using LAX mode. */ export function transformToJSON( input: Record, mode: 'STRICT' | 'LAX' = 'STRICT', ): any { const result: any = {}; const isPrimitive = (val: any) => val === null || ['string', 'number', 'boolean'].includes(typeof val); for (const [flatKey, value] of Object.entries(input)) { const parts = flatKey.split('.'); let current: any = result; for (let i = 0; i < parts.length; i++) { const part = parts[i]; const nextPart = parts[i + 1]; const isNextArrayIndex = nextPart !== undefined && /^\d+$/.test(nextPart); const isLast = i === parts.length - 1; if (isLast) { // Final segment: assign the value if (mode === 'LAX') { const existing = current[part]; // Handle conflict: existing object receives primitive value at root level if (existing && typeof existing === 'object') { if (!('__' in existing)) { existing.__ = value; } continue; // Preserve existing nested structure } } current[part] = value; } else { // Intermediate segment if (/^\d+$/.test(part)) { const idx = Number(part); if (!Array.isArray(current)) { const arr: any[] = []; Object.assign(arr, current); current = arr; } if (!current[idx]) current[idx] = isNextArrayIndex ? [] : {}; current = current[idx]; } else { // Object key: navigate or create path if (!current[part]) { current[part] = isNextArrayIndex ? [] : {}; } else if (mode === 'LAX' && isPrimitive(current[part])) { current[part] = { __: current[part] }; } current = current[part]; } } } } return result; } ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/utils/uri.spec.ts ================================================ /* * 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 { describe, expect, it } from 'vitest'; import uri from './uri'; describe('uri', () => { it('should escape uris properly', () => { expect(uri`http://app/${'foo/bar'}?q=${'???'}`).toBe( 'http://app/foo%2Fbar?q=%3F%3F%3F', ); expect(uri`http://app/${'foo/bar'}?q=1`).toBe('http://app/foo%2Fbar?q=1'); expect(uri`http://app/${'foo/bar'}`).toBe('http://app/foo%2Fbar'); expect(uri`http://app/foo`).toBe('http://app/foo'); expect(uri``).toBe(''); }); }); ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/utils/uri.ts ================================================ /* * 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. */ /** * URI template tag function that encodes interpolated values using encodeURIComponent. * * Usage example: * const userId = 'john/doe'; * const url = uri`/api/users/${userId}/profile`; * // url === "/api/users/john%2Fdoe/profile" * * @param strings Template string array * @param values Interpolated values to encode * @returns Encoded URI string */ export default (strings, ...values) => { let result = strings[0]; for (let i = 0; i < values.length; ++i) { result += encodeURIComponent(values[i]) + strings[i + 1]; } return result; }; ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/utils/useRouterState.spec.ts ================================================ import { waitFor } from '@testing-library/vue'; import { beforeEach, describe, expect, it, vi } from 'vitest'; import { reactive } from 'vue'; import { useRouterState } from '@/utils/useRouterState'; const routerReplace = vi.fn(); const mockedQuery = vi.fn().mockReturnValue({}); vi.mock('vue-router', () => { return { useRouter: () => ({ replace: routerReplace, }), useRoute: () => reactive({ get query() { return mockedQuery(); }, }), }; }); describe('useRouterState', () => { beforeEach(() => { vi.resetAllMocks(); }); it('should initialize routerState with the initial query parameters', async () => { mockedQuery.mockReturnValue({ string: 'foo', boolean: 'true', number: '0', float: '0.123', }); const routerState = useRouterState(); await waitFor(() => { expect(routerState).toEqual({ string: 'foo', boolean: true, number: 0, float: 0.123, }); }); }); it('should call router with initial state', () => { useRouterState({ foo: 'bar', }); waitFor(() => { expect(routerReplace).toHaveBeenCalledWith({ query: { foo: 'bar', }, }); }); }); it('should extend existing query params', () => { mockedQuery.mockReturnValue({ foo: 'bar', }); const routerState = useRouterState({ bar: 'baz', }); waitFor(() => { expect(routerReplace).toHaveBeenCalledWith({ query: { foo: 'bar', bar: 'baz', }, }); expect(routerState).toEqual({ foo: 'bar', bar: 'baz', }); }); }); it('should override existing query params', () => { mockedQuery.mockReturnValue({ foo: 'bar', }); const routerState = useRouterState({ foo: 'baz', }); waitFor(() => { expect(routerReplace).toHaveBeenCalledWith({ query: { foo: 'baz', }, }); expect(routerState).toEqual({ foo: 'baz', }); }); }); }); ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/utils/useRouterState.ts ================================================ import { isEqual } from 'lodash-es'; import qs from 'qs'; import { UnwrapNestedRefs, reactive, watch } from 'vue'; import { LocationQuery, useRoute, useRouter } from 'vue-router'; /** * Hook to synchronize the query parameters of the current route with a reactive object. * * @param initialState The initial state of the reactive object. * @returns The reactive object. */ export function useRouterState( initialState: T = {} as T, ): UnwrapNestedRefs { const route = useRoute(); const router = useRouter(); let routerState = reactive({ ...initialState, ...correctTypesInRouterQuery(route.query), }); watch(route, (_route) => { const queryParams = JSON.stringify(_route.query); routerState = parseQueryParams(queryParams); }); watch(routerState, (newValue: any) => { const to = { name: route.name, query: { ...route.query, ...newValue, }, }; const routerQueryKeys = Object.keys(route.query); const newRouterQueryKeys = Object.keys({ ...route.query, ...newValue }); if (isEqual(routerQueryKeys, newRouterQueryKeys)) { router.replace(to); } else { router.push(to); } }); return routerState; } function parseQueryParams(queryParams: string) { return qs.parse(queryParams, { decoder: (str, defaultDecoder, charset, type) => { const bools = { true: true, false: false, }; if (type === 'value' && typeof bools[str] === 'boolean') { return bools[str]; } else { return defaultDecoder(str); } }, }); } function correctTypesInRouterQuery(query: LocationQuery) { return ( query !== undefined && JSON.parse(JSON.stringify(query), (_, value) => { if (value === 'false') return false; if (value === 'true') return true; const float = Number.parseFloat(value); if (!isNaN(float)) return float; const number = Number.parseInt(value); if (!isNaN(number)) return number; return value; }) ); } ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/utils/useSubscription.ts ================================================ /* * 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 * * 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 { Subscription } from 'rxjs'; import { onBeforeUnmount } from 'vue'; /** * When a subscription is passed, it will be unsubscribed on unmount. * * @param subscription Subscription */ export const useSubscription = (subscription: Subscription) => { onBeforeUnmount(() => { if (subscription && !subscription.closed) { try { subscription.unsubscribe(); } finally { subscription = null; } } }); }; ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/viewRegistry.spec.ts ================================================ /* * Copyright 2014-2019 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 { describe, expect, it } from 'vitest'; import ViewRegistry from './viewRegistry'; import sbaConfig from '@/sba-config'; describe('viewRegistry', () => { it('should replace the existing one', async () => { const viewRegistry = new ViewRegistry(); viewRegistry.addView( ...[ { name: 'view', group: 'group', path: '' }, { name: 'duplicateView', group: 'group' }, { name: 'duplicateView', group: 'group' }, ], ); expect(viewRegistry.views).toHaveLength(2); }); it('should create a redirect based on path', () => { const viewRegistry = new ViewRegistry(); viewRegistry.addRedirect('/', 'asString'); viewRegistry.addRedirect('/', { name: 'asObject' }); expect(viewRegistry.routes).toContainEqual( expect.objectContaining({ path: '/', redirect: { name: 'asString' }, }), ); expect(viewRegistry.routes).toContainEqual( expect.objectContaining({ path: '/', redirect: { name: 'asObject' }, }), ); }); it('hide or show views depending on their settings', () => { sbaConfig.uiSettings.viewSettings = [ { name: 'disabledView', enabled: false }, { name: 'explicitlyEnabledView', enabled: true }, ]; const viewRegistry = new ViewRegistry(); viewRegistry.addView( ...[ { name: 'disabledView', group: 'group' }, { name: 'explicitlyEnabledView', group: 'group' }, { name: 'implicitlyEnabledView', group: 'group' }, ], ); const disabledView = viewRegistry.getViewByName('disabledView'); expect(disabledView).toBeDefined(); expect(disabledView.isEnabled()).toBeFalsy(); const implicitlyEnabledView = viewRegistry.getViewByName( 'implicitlyEnabledView', ); expect(implicitlyEnabledView).toBeDefined(); expect(implicitlyEnabledView.isEnabled()).toBeTruthy(); const explicitlyEnabledView = viewRegistry.getViewByName( 'explicitlyEnabledView', ); expect(explicitlyEnabledView).toBeDefined(); expect(explicitlyEnabledView.isEnabled()).toBeTruthy(); }); it('should render a translated label', () => { const viewRegistry = new ViewRegistry(); viewRegistry.addView(...[{ path: 'parent', label: 'parent.label' }]); expect(viewRegistry.views[0].handle.render).toBeDefined(); }); it('derives name from parent and path', () => { const viewRegistry = new ViewRegistry(); viewRegistry.addView( ...[{ path: 'parent' }, { parent: 'parent', path: 'path' }], ); expect(viewRegistry.views).toContainEqual( expect.objectContaining({ name: 'parent' }), ); expect(viewRegistry.views).toContainEqual( expect.objectContaining({ name: 'parent/path' }), ); }); it('parent/child routes are generated correctly', () => { const viewRegistry = new ViewRegistry(); viewRegistry.addView( ...[ { path: 'parent', component: {} }, { parent: 'parent', path: 'path', component: {} }, ], ); expect(viewRegistry.routes).toContainEqual( expect.objectContaining({ name: 'parent', }), ); expect(viewRegistry.routes[0].children).toContainEqual( expect.objectContaining({ name: 'parent/path', }), ); }); }); ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/viewRegistry.ts ================================================ /* * Copyright 2014-2019 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 { remove } from 'lodash-es'; import { Text, VNode, h, markRaw, reactive, shallowRef, toRaw } from 'vue'; import { Router, createRouter, createWebHistory } from 'vue-router'; import sbaConfig from './sba-config'; import { VIEW_GROUP, VIEW_GROUP_ICON } from './views/ViewGroup'; let router: Router; const createI18nTextVNode = (label: string) => shallowRef({ render(): VNode { return h(Text, this.$t(label)); }, }); type ViewFilterFunction = (view: SbaView) => boolean; type ViewConfig = { isEnabled?: (obj?) => boolean; [key: string]: any; }; export default class ViewRegistry { private readonly _redirects: any[] = []; private _views: SbaView[] = reactive([]); get views(): SbaView[] { return this._views; } get routes() { const routes = this._toRoutes((view) => view.path && !view.parent); return [...routes, ...this._redirects]; } get router(): Router { return router; } setGroupIcon(name, icon) { VIEW_GROUP_ICON[name] = icon; } createRouter() { const routesKnownToBackend = sbaConfig.uiSettings.routes.map( (r) => new RegExp(`^${r.replace('/**', '(/.*)?')}$`), ); const unknownRoutes = this.routes.filter( (vr) => vr.path !== '/' && !routesKnownToBackend.some((br) => br.test(vr.path)), ); if (unknownRoutes.length > 0) { console.warn( `The routes ${JSON.stringify( unknownRoutes.map((r) => r.path), )} aren't known to the backend and may be not properly routed!`, ); } router = createRouter({ history: createWebHistory(), linkActiveClass: 'is-active', routes: this.routes, }); return router; } getViewByName(name) { return Array.prototype.find.call(this._views, (v) => v.name === name); } addView(...views: View[]): SbaView[] { return views.map((view) => this._addView(view)); } addRedirect(path: string, redirect: string | object) { if (typeof redirect === 'string') { this._redirects.push({ path, redirect: { name: redirect } }); } else { this._redirects.push({ path, redirect }); } } _addView(viewConfig: ViewConfig): SbaView { const view = { ...viewConfig } as SbaView; view.hasChildren = !!viewConfig.children; if (!viewConfig.name) { view.name = [viewConfig.parent, viewConfig.path] .filter((p) => !!p) .join('/'); } if (viewConfig.label && !viewConfig.handle) { view.handle = createI18nTextVNode(viewConfig.label); } if (viewConfig.handle) { view.handle = markRaw(viewConfig.handle); } if (!viewConfig.group) { view.group = VIEW_GROUP.NONE; } if (viewConfig.component) { view.component = markRaw(viewConfig.component); } if (!viewConfig.isEnabled) { view.isEnabled = () => { const viewSettings = sbaConfig.uiSettings.viewSettings.find( (vs) => vs.name === viewConfig.name, ); return !viewSettings || viewSettings.enabled === true; }; } else { view.isEnabled = viewConfig.isEnabled; } this._removeExistingView(view); this._views.push(view); return view; } _removeExistingView(view) { remove(this._views, (v) => { return v.name === view.name && v.group === view.group; }); } _toRoutes(filter: ViewFilterFunction) { return this._views.filter(filter).map((view) => { const children = this._toRoutes( (childView) => childView.parent === view.name, ); return { path: view.path, name: view.name, component: view.component, props: view.props, meta: { view: toRaw(view) }, children, }; }); } } ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/views/ViewGroup.ts ================================================ export const VIEW_GROUP = { WEB: 'web', INSIGHTS: 'insights', DATA: 'data', JVM: 'jvm', LOGGING: 'logging', NONE: 'none', SECURITY: 'security', DEPENDENCIES: 'dependencies', }; export const VIEW_GROUP_ICON = { [VIEW_GROUP.WEB]: '', [VIEW_GROUP.INSIGHTS]: '', [VIEW_GROUP.DATA]: '', [VIEW_GROUP.JVM]: '', [VIEW_GROUP.LOGGING]: '', [VIEW_GROUP.DEPENDENCIES]: '' + ' ' + '', [VIEW_GROUP.NONE]: '
 
', [VIEW_GROUP.SECURITY]: '', }; ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/views/about/handle.vue ================================================ ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/views/about/i18n.de.json ================================================ { "about": { "title": "Über Spring Boot Admin", "label": "Über" } } ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/views/about/i18n.en.json ================================================ { "about": { "title": "About Spring Boot Admin", "label": "About" } } ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/views/about/i18n.es.json ================================================ { "about": { "title": "Acerca de Spring Boot Admin", "label": "Acerca de SBA" } } ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/views/about/i18n.fr.json ================================================ { "about": { "title": "À propos de Spring Boot Admin", "label": "À propos" } } ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/views/about/i18n.is.json ================================================ { "about": { "title": "Um Spring Boot Admin", "label": "Um" } } ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/views/about/i18n.ko.json ================================================ { "about": { "title": "Spring Boot Admin 소개", "label": "소개" } } ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/views/about/i18n.pt-BR.json ================================================ { "about": { "title": "Sobre o Spring Boot Admin", "label": "Sobre" } } ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/views/about/i18n.ru.json ================================================ { "about": { "title": "О Spring Boot Admin", "label": "О проекте" } } ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/views/about/i18n.zh-CN.json ================================================ { "about": { "title": "关于Spring Boot Admin", "label": "关于我们" } } ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/views/about/i18n.zh-TW.json ================================================ { "about": { "title": "關於 Spring Boot Admin", "label": "關於" } } ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/views/about/index.vue ================================================ ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/views/applications/ActionHandler.ts ================================================ import Application from '@/services/application'; import Instance from '@/services/instance'; export interface ActionHandler { restart(item: any): Promise; unregister(item: any): Promise; shutdown(item: any): Promise; } export class InstanceActionHandler implements ActionHandler { constructor( private $sbaModal: any, private t: any, private notificationCenter: any, ) {} async unregister(item: Instance) { const isConfirmed = await this.$sbaModal.confirm( this.t('applications.actions.unregister'), this.t('instances.unregister', { name: item.id }), ); if (!isConfirmed) { return; } try { await item.unregister(); this.notificationCenter.success( this.t('instances.unregister_successful', { name: item.id }), ); } catch (error) { this.notificationCenter.error( this.t('instances.unregister_failed', { name: item.id || item.name, error: error.response.status, }), ); } } async shutdown(item: Instance) { const isConfirmed = await this.$sbaModal.confirm( this.t('applications.actions.shutdown'), this.t('instances.shutdown', { name: item.id }), ); if (!isConfirmed) { return; } try { await item.shutdown(); this.notificationCenter.success( this.t('instances.shutdown_successful', { name: item.id }), ); } catch (error) { this.notificationCenter.error( this.t('instances.shutdown_failed', { name: item.id || item.name, error: error.response.status, }), ); } } async restart(item: Instance) { const isConfirmed = await this.$sbaModal.confirm( this.t('applications.actions.restart'), this.t('instances.restart', { name: item.id }), ); if (!isConfirmed) { return; } try { await item.restart(); this.notificationCenter.success( this.t('instances.restarted', { name: item.id }), ); } catch (error) { this.notificationCenter.error( this.t('instances.restart_failed', { name: item.id || item.name, error: error.response.status, }), ); } } } export class ApplicationActionHandler implements ActionHandler { constructor( private $sbaModal: any, private t: any, private notificationCenter: any, ) {} async restart(application: Application) { const isConfirmed = await this.$sbaModal.confirm( this.t('applications.actions.restart'), this.t('applications.restart', { name: application.name }), ); if (!isConfirmed) { return; } try { await application.restart(); this.notificationCenter.success( this.t('applications.restarted', { name: application.name }), ); } catch (error) { this.notificationCenter.error( this.t('applications.restart_failed', { name: application.name, error: error.response.status, }), ); } } async shutdown(application: Application) { const isConfirmed = await this.$sbaModal.confirm( this.t('applications.actions.shutdown'), this.t('applications.shutdown', { name: application.name }), ); if (!isConfirmed) { return; } try { await application.shutdown(); this.notificationCenter.success( this.t('applications.shutdown_successful', { name: application.name }), ); } catch (error) { this.notificationCenter.error( this.t('applications.shutdown_failed', { name: application.name, error: error.response.status, }), ); } } async unregister(application: Application) { const isConfirmed = await this.$sbaModal.confirm( this.t('applications.actions.unregister'), this.t('applications.unregister', { name: application.name }), ); if (!isConfirmed) { return; } try { await application.unregister(); this.notificationCenter.success( this.t('applications.unregister_successful', { name: application.name, }), ); } catch (error) { this.notificationCenter.error( this.t('applications.unregister_failed', { name: application.name, error: error.response.status, }), ); } } } ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/views/applications/ApplicationListItemAction.spec.ts ================================================ /* * Copyright 2014-2021 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 userEvent from '@testing-library/user-event'; import { screen, waitFor } from '@testing-library/vue'; import { cloneDeep } from 'lodash-es'; import { HttpResponse, http } from 'msw'; import { beforeEach, describe, expect, it, vi } from 'vitest'; import Application from '../../services/application'; import { applications } from '@/mocks/applications/data'; import { server } from '@/mocks/server'; import Instance from '@/services/instance'; import { render } from '@/test-utils'; import ApplicationsListItem from '@/views/applications/ApplicationListItemAction'; async function clickConfirmModal() { await waitFor(() => { expect(screen.getByRole('dialog')).toBeInTheDocument(); }); const buttonOK = screen.queryByRole('button', { name: 'term.ok' }); await userEvent.click(buttonOK); } describe('ApplicationListItemAction', () => { let application: Application; let instance: Instance; beforeEach(() => { server.use( // Instances http.delete('/instances/:instanceId', () => { return HttpResponse.json({}); }), http.post('/instances/:instanceId/actuator/restart', () => { return HttpResponse.json({}); }), http.post('/instances/:instanceId/actuator/shutdown', () => { return HttpResponse.json({}); }), // Applications http.delete('/applications/:name', () => { return HttpResponse.json({}); }), http.post('/applications/:name/actuator/restart', () => { return HttpResponse.json({}); }), http.post('/applications/:name/actuator/shutdown', () => { return HttpResponse.json({}); }), ); }); beforeEach(() => { application = new Application(cloneDeep(applications[0])); instance = application.instances[0]; }); describe('unregister', () => { it('on instance', async () => { const spy = vi.spyOn(instance, 'unregister'); render(ApplicationsListItem, { props: { item: instance }, }); await userEvent.click(screen.getByTitle('Unregister')); await clickConfirmModal(); expect(spy).toHaveBeenCalledOnce(); }); it('on application', async () => { const spy = vi.spyOn(application, 'unregister'); render(ApplicationsListItem, { props: { item: application }, }); await userEvent.click(screen.getByTitle('Unregister')); await clickConfirmModal(); expect(spy).toHaveBeenCalledOnce(); }); }); describe('restart', () => { it('on instance', async () => { const spy = vi.spyOn(instance, 'restart'); render(ApplicationsListItem, { props: { item: instance }, }); await userEvent.click(screen.getByTitle('Restart')); await clickConfirmModal(); expect(spy).toHaveBeenCalledOnce(); }); it('on application', async () => { const spy = vi.spyOn(application, 'restart'); render(ApplicationsListItem, { props: { item: application }, }); await userEvent.click(screen.getByTitle('Restart')); await clickConfirmModal(); expect(spy).toHaveBeenCalledOnce(); }); }); describe('shutdown', () => { it('on application', async () => { const spy = vi.spyOn(application, 'shutdown'); render(ApplicationsListItem, { props: { item: application }, }); await userEvent.click(screen.getByTitle('Shutdown')); await clickConfirmModal(); expect(spy).toHaveBeenCalledOnce(); }); it('on instance', async () => { const spy = vi.spyOn(instance, 'shutdown'); render(ApplicationsListItem, { props: { item: instance }, }); await userEvent.click(await screen.getByTitle('Shutdown')); await clickConfirmModal(); expect(spy).toHaveBeenCalledOnce(); }); }); }); ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/views/applications/ApplicationListItemAction.vue ================================================ ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/views/applications/ApplicationNotificationCenter.vue ================================================ ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/views/applications/ApplicationStats.vue ================================================ ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/views/applications/ApplicationStatusHero.spec.ts ================================================ import { screen } from '@testing-library/vue'; import { beforeEach, describe, expect, it, vi } from 'vitest'; import { Ref, ref } from 'vue'; import { useApplicationStore } from '@/composables/useApplicationStore'; import Application from '@/services/application'; import Instance from '@/services/instance'; import { render } from '@/test-utils'; import ApplicationStatusHero from '@/views/applications/ApplicationStatusHero.vue'; vi.mock('@/composables/useApplicationStore', () => ({ useApplicationStore: vi.fn(), })); describe('ApplicationStatusHero', () => { let applications: Ref; beforeEach(async () => { applications = ref([]); // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore useApplicationStore.mockReturnValue({ applicationsInitialized: ref(true), applications, error: ref(null), }); }); it.each` instance1Status | instance2Status | expectedMessage | expectedIcon ${'UP'} | ${'UP'} | ${'all up'} | ${'check-circle'} ${'OFFLINE'} | ${'OFFLINE'} | ${'all down'} | ${'minus-circle'} ${'UNKNOWN'} | ${'UNKNOWN'} | ${'all in unknown state'} | ${'question-circle'} ${'UP'} | ${'UNKNOWN'} | ${'some instances are in unknown state'} | ${'question-circle'} ${'UP'} | ${'DOWN'} | ${'some instances are down'} | ${'minus-circle'} ${'UP'} | ${'OFFLINE'} | ${'some instances are down'} | ${'minus-circle'} ${'DOWN'} | ${'UNKNOWN'} | ${'some instances are down'} | ${'minus-circle'} ${'DOWN'} | ${'OFFLINE'} | ${'all down'} | ${'minus-circle'} ${'OFFLINE'} | ${'UP'} | ${'some instances are down'} | ${'minus-circle'} `( '`$expectedMessage` is shown when `$instance1Status` and `$instance2Status`', ({ instance1Status, instance2Status, expectedMessage, expectedIcon }) => { applications.value = [ new Application({ name: 'Test Application', statusTimestamp: Date.now(), instances: [ new Instance({ id: '4711', statusInfo: { status: instance1Status }, }), new Instance({ id: '4712', statusInfo: { status: instance2Status }, }), ], }), ]; render(ApplicationStatusHero, { global: { stubs: { 'font-awesome-icon': { template: '', }, }, }, }); expect(screen.getByTestId('icon')).toHaveAttribute('icon', expectedIcon); expect(screen.getByText(expectedMessage)).toBeVisible(); }, ); }); ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/views/applications/ApplicationStatusHero.vue ================================================ ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/views/applications/InstancesList.spec.ts ================================================ import { screen } from '@testing-library/vue'; import { deepMerge } from '@vitest/utils'; import Instance from '@/services/instance'; import { render } from '@/test-utils'; import InstancesList from '@/views/applications/InstancesList.vue'; describe('InstancesList', () => { describe('Metadata: hide-url', () => { it('should show service url when hide-url is set to false', async () => { render(InstancesList, { props: { instances: [ createInstance({ registration: { metadata: { 'hide-url': 'false' }, }, }), ], }, }); expect(await screen.queryByText('Spring Boot Admin')).toBeVisible(); expect(await screen.queryByText('http://localhost:8080')).toBeVisible(); }); it('should show service url when hide-url not set', async () => { render(InstancesList, { props: { instances: [createInstance()], }, }); expect(await screen.queryByText('Spring Boot Admin')).toBeVisible(); expect(await screen.queryByText('http://localhost:8080')).toBeVisible(); }); it('should hide service url when hide-url is set to true', async () => { render(InstancesList, { props: { instances: [ createInstance({ registration: { metadata: { 'hide-url': 'true' }, }, }), ], }, }); expect(await screen.queryByText('Spring Boot Admin')).toBeVisible(); expect( await screen.queryByText('http://localhost:8080'), ).not.toBeInTheDocument(); }); }); describe('Metadata: disable-url', () => { it('should show service url homepage button when diable-url is set to false', async () => { render(InstancesList, { props: { instances: [ createInstance({ registration: { metadata: { 'disable-url': 'false' }, }, }), ], }, }); expect( await screen.queryByRole('link', { name: 'Homepage' }), ).toBeVisible(); }); it('should show service url homepage button when diable-url is not set', async () => { render(InstancesList, { props: { instances: [createInstance()], }, }); expect( await screen.queryByRole('link', { name: 'Homepage' }), ).toBeVisible(); }); it('should show service url homepage button when diable-url is set to true', async () => { render(InstancesList, { props: { instances: [ createInstance({ registration: { metadata: { 'disable-url': 'true' }, }, }), ], }, }); expect( await screen.queryByRole('link', { name: 'Homepage' }), ).not.toBeInTheDocument(); }); }); }); // Utility functions function createInstance(options: InstanceType = {}): Instance { const defaultData = { id: 'Spring Boot Admin', statusInfo: { status: 'UP' }, registration: { serviceUrl: 'http://localhost:8080', }, }; const instance = deepMerge(defaultData, options); return new Instance(instance); } ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/views/applications/InstancesList.vue ================================================ ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/views/applications/NotificationFilterSettings.vue ================================================ ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/views/applications/applications.spec.ts ================================================ import userEvent from '@testing-library/user-event'; import { screen, waitFor } from '@testing-library/vue'; import { beforeEach, describe, expect, it, vi } from 'vitest'; import { Ref, ref } from 'vue'; import { useApplicationStore } from '@/composables/useApplicationStore'; import Application from '@/services/application'; import Instance, { Registration } from '@/services/instance'; import { render } from '@/test-utils'; import Applications from '@/views/applications/index.vue'; vi.mock('@/composables/useApplicationStore', () => ({ useApplicationStore: vi.fn(), })); describe('Applications', () => { let applicationsInitialized: Ref; let applications: Ref; let error: Ref; beforeEach(async () => { applicationsInitialized = ref(false); applications = ref([]); error = ref(null); // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore useApplicationStore.mockReturnValue({ applicationStore: { findApplicationByInstanceId: (id: string) => { return applications.value.find((a) => { return a.instances.some((i) => i.id === id); }); }, }, applicationsInitialized, applications, error, }); render(Applications); }); it('when applications are loading, a hint should be shown', async () => { expect(await screen.findByText('Loading applications...')).toBeVisible(); }); it('when there are no applications, a corresponding text is shown', async () => { applicationsInitialized.value = true; await waitFor(() => { expect(screen.getByText('No applications registered.')).toBeVisible(); }); }); describe('when there are applications', () => { beforeEach(async () => { const instance = new Instance({ id: 'id', statusInfo: { status: 'UP', }, registration: { name: 'spring-boot-admin-sample-servlet', serviceUrl: 'serviceUrl', metadata: {}, } as Registration, }); applicationsInitialized.value = true; applications.value = [ new Application({ id: 'app-id', name: 'spring-boot-admin-sample-servlet', instances: [instance], status: 'UP', }), ]; }); it('name of applications are shown', async () => { await waitFor(() => { expect( screen.getByRole('button', { name: /spring-boot-admin-sample-servlet/i, }), ).toBeVisible(); }); }); it('when the search does not match, a corresponding text is shown', async () => { const filterInput = screen.getByLabelText('Filter'); await userEvent.type(filterInput, 'does-not-match'); expect( await screen.findByText('No results matching your filter.'), ).toBeVisible(); }); it('when the search matches, the application is shown', async () => { const filterInput = screen.getByLabelText('Filter'); await userEvent.type(filterInput, 'sample'); await waitFor(() => { expect( screen.getByRole('button', { name: /spring-boot-admin-sample-servlet/i, }), ).toBeVisible(); }); }); it('clicking on the name opens list of instances', async () => { const applicationElement = await screen.findByRole('button', { name: /spring-boot-admin-sample-servlet/i, }); await userEvent.click(applicationElement); expect(await screen.findByText('serviceUrl')).toBeVisible(); }); describe('application list', () => { beforeEach(async () => { applications.value = [ new Application({ id: 'app-id', name: 'spring-boot-admin-sample-servlet-up', instances: [ { id: 'id', statusInfo: { status: 'UP', }, registration: { name: 'spring-boot-admin-sample-servlet-up', serviceUrl: 'serviceUrl', metadata: {}, } as Registration, }, ], status: 'UP', }), new Application({ id: 'app-id2', name: 'spring-boot-admin-sample-servlet-down', instances: [ { id: 'id2', statusInfo: { status: 'DOWN', }, registration: { name: 'spring-boot-admin-sample-servlet-down', serviceUrl: 'serviceUrl', metadata: {}, } as Registration, }, ], status: 'DOWN', }), new Application({ id: 'app-id3', name: 'spring-boot-admin-sample-servlet-up2', instances: [ { id: 'id5', statusInfo: { status: 'UP', }, registration: { name: 'spring-boot-admin-sample-servlet-up2', serviceUrl: 'serviceUrl', metadata: {}, } as Registration, }, ], status: 'UP', }), new Application({ id: 'app-id4', name: 'spring-boot-admin-sample-servlet-restricted', instances: [ { id: 'id6', statusInfo: { status: 'UP', }, registration: { name: 'spring-boot-admin-sample-servlet-restricted', serviceUrl: 'serviceUrl', metadata: {}, } as Registration, }, { id: 'id7', statusInfo: { status: 'DOWN', }, registration: { name: 'spring-boot-admin-sample-servlet-restricted', serviceUrl: 'serviceUrl', metadata: {}, } as Registration, }, ], status: 'RESTRICTED', }), ]; }); it('should show applications ordered by status DOWN, RESTRICTED and UP', () => { const allByRole = screen.getAllByRole('button'); const getIndex = (status: string) => allByRole.findIndex((element: HTMLElement) => element.textContent.startsWith( `${status}spring-boot-admin-sample-servlet`, ), ); const indexDown = getIndex('DOWN'); const indexRestricted = getIndex('RESTRICTED'); const indexUp = getIndex('UP'); expect(indexDown).toBeGreaterThan(-1); expect(indexDown).toBeLessThan(indexRestricted); expect(indexRestricted).toBeLessThan(indexUp); }); }); }); }); ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/views/applications/handle.vue ================================================ ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/views/applications/i18n.de.json ================================================ { "applications": { "actions": { "journal": "Journal", "notification_filters": "Benachrichtigungsfilter", "refresh_applications": "Anwendungen aktualisieren", "restart": "Neu starten", "shutdown": "Herunterfahren", "switch_to_grouping_by_application": "Aggregiere Instanzen zu Applikationen (gleiche Namen)", "switch_to_grouping_by_group": "Gruppiere Instanzen anhand des Gruppennamens", "unregister": "Deregistrieren" }, "all_up": "Alle verfügbar", "all_down": "Alle Instanzen sind ausgefallen", "all_unknown": "Alle Instanzen haben unbekannten Status", "some_unknown": "Einige Instanzen haben unbekannten Status", "some_down": "Einige Instanzen sind ausgefallen", "applications": "Anwendungen", "fetching_notification_filters_failed": "Der Abruf der Benachrichtigungseinstellungen ist fehlgeschlagen.", "notification_filter": { "none": "Keine Benachrichtigungseinstellungen gesetzt.", "removed": "Benachrichtigungseinstellungen wurden geändert." }, "instances": "Instanzen", "loading_applications": "Anwendungen werden geladen...", "no_applications_registered": "Keine Anwendungen registriert.", "notifications_suppressed_for": "Benachrichtigungen für {name} werden unterdrückt für", "restricted": "eingeschränkt", "server_connection_failed": "Verbindung zum Server fehlgeschlagen.", "suppress_notifications_on": "Benachrichtungen für {name} für", "status": "Status", "label": "Anwendungen", "shutdown": "Möchten Sie die Anwendung {name} herunterfahren?", "refreshed": "Anwendungen wurden aktualisiert.", "restart": "Möchten Sie Anwendung {name} neu starten?", "restarted": "Die Anwendung wurde neu gestartet.", "unregister": "Möchten Sie die Anwendung {name} deregistrieren?", "unregister_successful": "Die Anwendung {name} wurde erfolgreich deregistriert.", "unregister_failed": "Deregistration der Anwendung {name} fehlgeschlagen ({error})." }, "instances": { "shutdown": "Möchten Sie die Instanz {name} herunterfahren?", "restart": "Möchten Sie die Instanz {name} neu starten?", "restarted": "Die Instanz {name} wurde neu gestartet.", "unregister": "Möchten Sie die Instanz {name} deregistrieren?", "unregister_successful": "Die Instanz {name} wurde erfolgreich deregistriert.", "unregister_failed": "Deregistration der Instanz {name} fehlgeschlagen ({error})." }, "notification_filter_center": { "description": "Benachrichtigungen für folgende Applikationen / Instanzen werden unterdrückt." }, "filter": { "no_results": "Keine Ergebnisse für den eingestellten Filter." } } ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/views/applications/i18n.en.json ================================================ { "applications": { "actions": { "journal": "Journal", "notification_filters": "Notification filters", "refresh_applications": "Refresh applications", "restart": "Restart", "shutdown": "Shutdown", "switch_to_grouping_by_application": "Aggregate same instances as application", "switch_to_grouping_by_group": "Group same instance by same group name", "unregister": "Unregister" }, "all_up": "all up", "all_down": "all down", "all_unknown": "all in unknown state", "some_unknown": "some instances are in unknown state", "some_down": "some instances are down", "applications": "Applications", "fetching_notification_filters_failed": "Fetching notification filters failed.", "notification_filter": { "none": "No notification filters set.", "removed": "Notification filter removed." }, "instances": "Instances", "loading_applications": "Loading applications...", "no_applications_registered": "No applications registered.", "notifications_suppressed_for": "Notifications on {name} are suppressed for", "restricted": "restricted", "server_connection_failed": "Server connection failed.", "suppress_notifications_on": "Suppress notifications on {name} for", "status": "Status", "label": "Applications", "up": "up", "down": "down", "offline": "offline", "shutdown": "Shutdown application {name}?", "shutdown_successful": "Successfully shutdown application {name}.", "refreshed": "Applications refreshed.", "restart": "Restart application {name}?", "restarted": "Successfully restarted application {name}.", "unregister": "Deregister application {name}?", "unregister_successful": "Successfully deregistered application {name}.", "unregister_failed": "Deregistration of application {name} failed ({error})." }, "instances": { "shutdown": "Shutdown instance {name}?", "shutdown_successful": "Successfully shutdown instances {name}.", "shutdown_failed": "Failed to shutdown instances {name}.", "restart": "Restart instance {name}?", "restarted": "Successfully restarted instance {name}.", "unregister": "Deregister instance {name}?", "unregister_successful": "Successfully deregistered instance {name}.", "unregister_failed": "Deregistration of instance {name} failed ({error})." }, "notification_filter_center": { "description": "Notifications on following applications or instances will be suppressed." }, "filter": { "no_results": "No results matching your filter." } } ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/views/applications/i18n.es.json ================================================ { "applications": { "all_up": "Todo arriba", "applications": "Aplicaciones", "instances": "Instancias", "some_down": "instancias caídas", "loading_applications": "Cargando aplicaciones...", "no_applications_registered": "No hay aplicaciones registradas.", "notifications_suppressed_for": "Notificaciones para {name} estás suspendidas por", "restricted": "restringido", "server_connection_failed": "Conexión al servidor fallida.", "suppress_notifications_on": "Suspender notificaciones para {name} por", "status": "Estado", "label": "Aplicaciones", "up": "Arriba", "shutdown": "Detener aplicación {name}?", "restart": "Reiniciar aplicación {name}?", "restarted": "Aplicación {name} reiniciada exitosamente" }, "instances": { "shutdown": "Detener instancia {name}?", "restart": "Reinciar instancia {name}?", "restarted": "Instancia reiniciada exitosamente" } } ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/views/applications/i18n.fr.json ================================================ { "applications": { "all_up": "tout est disponible", "applications": "Applications", "instances": "Instances", "some_down": "instances inaccessibles", "loading_applications": "Chargement des applications...", "no_applications_registered": "Aucune application enregistrée.", "notifications_suppressed_for": "Les notifications sur {name} sont supprimées", "restricted": "limité", "server_connection_failed": "Echec de la connexion au serveur.", "suppress_notifications_on": "Supression des notifications sur {name}", "status": "Statut", "label": "Applications", "up": "OK", "shutdown": "Shutdown application {name}?", "restart": "Restart application {name}?", "restarted": "Successfully restarted application {name}" }, "instances": { "shutdown": "Shutdown instance {name}?", "restart": "Restart instance {name}?", "restarted": "Successfully restarted instance" } } ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/views/applications/i18n.is.json ================================================ { "applications": { "all_up": "Öll fáanleg", "applications": "Forrit", "instances": "Eintök", "some_down": "Eintök ekki fáanleg", "loading_applications": "Forrit eru að ræsa…", "no_applications_registered": "Engin forrit skráð.", "notifications_suppressed_for": "Tilkynningar fyrir {name} eru hunsuð fyrir", "restricted": "skert", "server_connection_failed": "Mistókst sambandið við þjón.", "suppress_notifications_on": "Tilkynningar fyrir {name} fyrir", "status": "Staða", "label": "Forrit", "up": "OK", "shutdown": "Shutdown application {name}?", "restart": "Restart application {name}?", "restarted": "Successfully restarted application {name}" }, "instances": { "shutdown": "Shutdown instance {name}?", "restart": "Restart instance {name}?", "restarted": "Successfully restarted instance" } } ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/views/applications/i18n.ko.json ================================================ { "applications": { "actions": { "journal": "일지", "notification_filters": "알림 필터", "unregister": "등록 해제", "shutdown": "종료", "restart": "재시작" }, "all_up": "전체 인스턴스 가동중", "all_down": "전체 인스턴스 중단됨", "all_unknown": "전체 인스턴스 상태 불명", "some_unknown": "일부 인스턴스 상태 불명", "some_down": "일부 인스턴스 중단됨", "applications": "애플리케이션", "fetching_notification_filters_failed": "알림 필터를 가져오지 못하였습니다.", "notification_filter": { "none": "설정된 알림 필터가 없습니다.", "removed": "알림 필터가 제거되었습니다." }, "instances": "인스턴스", "loading_applications": "애플리케이션 불러오는 중...", "no_applications_registered": "등록된 애플리케이션이 없습니다.", "notifications_suppressed_for": "{name}에 대한 알림이 억제됩니다.", "restricted": "제한됨", "server_connection_failed": "서버 연결에 실패하였습니다.", "suppress_notifications_on": "{name}에 대한 알림", "status": "상태", "label": "애플리케이션", "up": "가동", "down": "중단", "offline": "오프라인", "shutdown": "{name} 애플리케이션을 종료할까요?", "shutdown_successful": "{name} 애플리케이션을 종료하였습니다.", "refreshed": "애플리케이션 새로고침됨", "restart": "{name} 애플리케이션을 재시작할까요?", "restarted": "{name} 애플리케이션을 재시작하였습니다.", "unregister": "<{name} 애플리케이션 등록을 해제할까요?", "unregister_successful": "{name} 애플리케이션 등록을 해제하였습니다.", "unregister_failed": "{name} 애플리케이션 등록을 해제하지 못하였습니다. ({error})" }, "instances": { "shutdown": "{name} 인스턴스를 종료할까요?", "shutdown_successful": "{name} 인스턴스를 종료하였습니다.", "shutdown_failed": "{name} 인스턴스를 종료하지 못하였습니다.", "restart": "{name} 인스턴스를 재시작할까요?", "restarted": "{name} 인스턴스를 재시작하였습니다.", "unregister": "{name} 인스턴스 등록을 해제할까요?", "unregister_successful": "{name} 인스턴스 등록을 해제하였습니다.", "unregister_failed": "{name} 인스턴스 등록을 해제하지 못하였습니다. ({error})" }, "notification_filter_center": { "description": "다음 애플리케이션 또는 인스턴스에 대한 알림이 억제됩니다." }, "filter": { "no_results": "필터에 일치하는 결과가 없습니다." } } ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/views/applications/i18n.pt-BR.json ================================================ { "applications": { "all_up": "Todas up", "applications": "Aplicações", "instances": "Instâncias", "some_down": "Instâncias down", "loading_applications": "Carregando aplicações...", "no_applications_registered": "Nenhuma aplicação registrada.", "notifications_suppressed_for": "As notificações de {name} estão suspensas por", "restricted": "restrito", "server_connection_failed": "Falha na conexão do servidor.", "suppress_notifications_on": "Suspenda notificações de {name} por", "status": "Estado", "label": "Aplicações", "up": "up", "shutdown": "Shutdown application {name}?", "restart": "Restart application {name}?", "restarted": "Successfully restarted application {name}" }, "instances": { "shutdown": "Shutdown instance {name}?", "restart": "Restart instance {name}?", "restarted": "Successfully restarted instance" } } ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/views/applications/i18n.ru.json ================================================ { "applications": { "actions": { "journal": "Журнал", "notification_filters": "Фильтры уведомлений", "unregister": "Удалить регистрацию", "shutdown": "Остановить", "restart": "Перезапустить" }, "all_up": "все доступны", "all_down": "все недоступны", "all_unknown": "все в неизвестном состоянии", "some_unknown": "некоторые экземпляры в неизвестном состоянии", "some_down": "экземпляры недоступны", "applications": "Приложения", "fetching_notification_filters_failed": "Ошибка загрузки фильтров уведомлений.", "notification_filter": { "none": "Фильтры уведомлений не установлены.", "removed": "Фильтр уведомлений удалён." }, "instances": "Экземпляры", "loading_applications": "Загрузка приложений...", "no_applications_registered": "Нет зарегистрированных приложений.", "notifications_suppressed_for": "Уведомления для {name} отключены на", "restricted": "ограничен", "server_connection_failed": "Ошибка при подключении к серверу.", "suppress_notifications_on": "Скрыть уведомления для {name} на", "status": "Статус", "label": "Приложения", "up": "ОК", "down": "недоступен", "offline": "оффлайн", "shutdown": "Остановить приложение {name}?", "shutdown_successful": "Приложение {name} успешно остановлено.", "refreshed": "Приложения обновлены.", "restart": "Перезапустить приложение {name}?", "restarted": "Приложение {name} успешно перезапущено.", "unregister": "Удалить регистрацию приложения {name}?", "unregister_successful": "Регистрация приложения {name} успешно удалена.", "unregister_failed": "Ошибка удаления регистрации приложения {name} ({error})." }, "instances": { "shutdown": "Остановить экземпляр {name}?", "shutdown_successful": "Экземпляр {name} успешно остановлен.", "shutdown_failed": "Ошибка остановки экземпляра {name}.", "restart": "Перезапустить экземпляр {name}?", "restarted": "Экземпляр {name} успешно перезапущен.", "unregister": "Удалить регистрацию экземпляра {name}?", "unregister_successful": "Регистрация экземпляра {name} успешно удалена.", "unregister_failed": "Ошибка удаления регистрации экземпляра {name} ({error})." }, "notification_filter_center": { "description": "Уведомления для следующих приложений или экземпляров будут отключены." }, "filter": { "no_results": "Нет результатов, соответствующих вашему фильтру." } } ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/views/applications/i18n.zh-CN.json ================================================ { "applications": { "all_up": "全部在线", "applications": "应用数", "instances": "实例数", "some_down": "离线实例", "loading_applications": "应用加载中...", "no_applications_registered": "暂无应用注册。", "notifications_suppressed_for": "Notifications on {name} are suppressed for", "restricted": "受限的", "server_connection_failed": "服务连接失败。", "suppress_notifications_on": "Suppress notifications on {name} for", "status": "实例状态", "label": "应用", "up": "在线", "shutdown": "Shutdown application {name}?", "restart": "Restart application {name}?", "restarted": "Successfully restarted application {name}" }, "instances": { "shutdown": "Shutdown instance {name}?", "restart": "Restart instance {name}?", "restarted": "Successfully restarted instance" } } ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/views/applications/i18n.zh-TW.json ================================================ { "applications": { "actions": { "journal": "日誌", "notification_filters": "通知篩選器", "unregister": "取消註冊", "shutdown": "關閉", "restart": "重新啟動" }, "all_up": "全部正常", "all_down": "全部停止", "all_unknown": "全部狀態未知", "some_unknown": "部分執行個體狀態未知", "some_down": "部分執行個體已停止", "applications": "應用程式", "fetching_notification_filters_failed": "取得通知篩選器失敗。", "notification_filter": { "none": "未設定通知篩選器。", "removed": "已移除通知篩選器。" }, "instances": "執行個體", "loading_applications": "正在載入應用程式...", "no_applications_registered": "目前沒有已註冊的應用程式。", "notifications_suppressed_for": "{name} 的通知已隱藏,期間為", "restricted": "受限", "server_connection_failed": "伺服器連線失敗。", "suppress_notifications_on": "隱藏 {name} 的通知,期間為", "status": "狀態", "label": "應用程式", "up": "正常", "down": "停止", "offline": "離線", "shutdown": "確定要關閉應用程式 {name}?", "shutdown_successful": "應用程式 {name} 已成功關閉。", "refreshed": "應用程式已重新整理。", "restart": "確定要重新啟動應用程式 {name}?", "restarted": "應用程式 {name} 已成功重新啟動。", "unregister": "確定要取消註冊應用程式 {name}?", "unregister_successful": "應用程式 {name} 已成功取消註冊。", "unregister_failed": "取消註冊應用程式 {name} 失敗 ({error})。" }, "instances": { "shutdown": "確定要關閉執行個體 {name}?", "shutdown_successful": "執行個體 {name} 已成功關閉。", "shutdown_failed": "關閉執行個體 {name} 失敗。", "restart": "確定要重新啟動執行個體 {name}?", "restarted": "執行個體 {name} 已成功重新啟動。", "unregister": "確定要取消註冊執行個體 {name}?", "unregister_successful": "執行個體 {name} 已成功取消註冊。", "unregister_failed": "取消註冊執行個體 {name} 失敗 ({error})。" }, "notification_filter_center": { "description": "以下應用程式或執行個體的通知將被隱藏。" }, "filter": { "no_results": "沒有符合篩選條件的結果。" } } ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/views/applications/index.vue ================================================ ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/views/applications/listItem/ItemInformation.vue ================================================ ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/views/applications/listItem/ItemTags.vue ================================================ ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/views/external/index.spec.ts ================================================ import { beforeEach, describe, expect, it } from 'vitest'; import ViewRegistry from '@/viewRegistry'; import { addExternalLink, addIframeView } from '@/views/external/index'; describe('External View', () => { let viewRegistry: ViewRegistry; beforeEach(() => { viewRegistry = new ViewRegistry(); }); describe('External Link', () => { it('will be added to view registry', () => { addExternalLink(viewRegistry, { label: 'External Link', url: 'https://www.codecentric.de', }); expect(viewRegistry.views).toHaveLength(1); expect(viewRegistry.views.map((v) => v.label)).toContain('External Link'); }); it('will be added to view registry with children', () => { addExternalLink(viewRegistry, { label: 'External Link', url: 'https://www.codecentric.de', children: [ { label: 'External Link Child', url: 'https://www.codecentric.de', }, ], }); expect(viewRegistry.views).toHaveLength(2); expect(viewRegistry.views.map((v) => v.label)).toContain('External Link'); expect(viewRegistry.views.map((v) => v.label)).toContain( 'External Link Child', ); }); it('will add multiple external links', () => { addExternalLink(viewRegistry, { label: 'External Link 1', url: 'https://www.codecentric.de', }); addExternalLink(viewRegistry, { label: 'External Link 2', url: 'https://www.codecentric.de', }); expect(viewRegistry.views).toHaveLength(2); expect(viewRegistry.views.map((v) => v.label)).toContain( 'External Link 1', ); expect(viewRegistry.views.map((v) => v.label)).toContain( 'External Link 2', ); }); }); describe('External Iframe', () => { it('will be added to view registry', () => { addIframeView(viewRegistry, { label: 'External Iframe', url: 'https://www.codecentric.de', iframe: true, }); expect(viewRegistry.views).toHaveLength(1); expect(viewRegistry.views.map((v) => v.label)).toContain( 'External Iframe', ); }); it('will add multiple iframes', () => { addIframeView(viewRegistry, { label: 'External Link 1', url: 'https://www.codecentric.de', }); addIframeView(viewRegistry, { label: 'External Link 2', url: 'https://www.codecentric.de', }); expect(viewRegistry.views).toHaveLength(2); expect(viewRegistry.views.map((v) => v.label)).toContain( 'External Link 1', ); expect(viewRegistry.views.map((v) => v.label)).toContain( 'External Link 2', ); }); }); }); ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/views/external/index.ts ================================================ /* * Copyright 2014-2020 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 { h } from 'vue'; import './style.css'; import sbaConfig from '@/sba-config'; import ViewRegistry from '@/viewRegistry'; function addExternalView( viewRegistry: ViewRegistry, view: ExternalView, parent?: string, ) { if (view.iframe) { addIframeView(viewRegistry, view, parent); } else { addExternalLink(viewRegistry, view, parent); } } function getViewOpts(view: ExternalView, parent?: string) { const safeLabel = view.label.replace(/[^a-zA-Z0-9-_]/g, ''); const name = `/external/${safeLabel}`; return { name, path: name, parent, label: view.label, order: view.order, }; } export const addIframeView = ( viewRegistry: ViewRegistry, view: ExternalView, parent?: string, ) => { const viewOpts = { ...getViewOpts(view, parent), component: { inheritAttrs: false, render() { return h('div', { class: 'external-view' }, [ h('iframe', { src: view.url }), ]); }, }, } as ComponentView; viewRegistry.addView(viewOpts); view.children?.forEach((view) => { addExternalView(viewRegistry, view, viewOpts.name); }); }; export const addExternalLink = ( viewRegistry: ViewRegistry, view: ExternalView, parent?: string, ) => { const viewOpts = { ...getViewOpts(view, parent), href: view.url, } as LinkView; viewRegistry.addView(viewOpts); view.children?.forEach((view) => { addExternalView(viewRegistry, view, viewOpts.name); }); }; export default { install({ viewRegistry }) { const views = sbaConfig.uiSettings.externalViews; views.forEach((view) => { addExternalView(viewRegistry, view); }); }, }; ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/views/external/style.css ================================================ /*! * Copyright 2014-2019 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. */ .external-view { display: flex; width: 100%; height: calc(100vh - 52px); flex-direction: column; } .external-view > * { flex-grow: 1; border: none; margin: 0; padding: 0; } ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/views/index.ts ================================================ /* * 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. */ const isStorybook = Object.prototype.hasOwnProperty.call(window, 'STORIES'); const views = []; if (!isStorybook) { const context: Record = import.meta.glob( './**/index.(js|vue|ts)', { eager: true }, ); Object.keys(context) .filter((key) => { const contextElement = context[key]; return 'default' in contextElement; }) .forEach(function (key) { const defaultExport = context[key].default; if (defaultExport && defaultExport.install) { views.push(defaultExport); } }); } export default views; ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/views/instances/auditevents/auditevents-list.stories.ts ================================================ import { applications } from '../../../mocks/applications/data'; import Instance from '../../../services/instance'; import Index from './index.vue'; export default { component: Index, title: 'SBA View/AuditeventsList', }; const Template = (args, { argTypes }) => ({ components: { Index }, props: Object.keys(argTypes), template: '', }); export const Test = { render: Template, args: { instance: new Instance({ id: 'bba333956ae6', ...applications[0].instances[0], }), }, }; ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/views/instances/auditevents/auditevents-list.vue ================================================ ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/views/instances/auditevents/i18n.de.json ================================================ { "instances": { "auditevents": { "audit_log_not_supported_spring_boot_1": "Spring Boot 1.x Anwendungen unterstützen keine Audit-Logs.", "event": "Ereignis", "loading_audit_events": "Audit-Events werden geladen...", "no_audit_events_found": "Keine Audit-Events vorhanden.", "principal": "Prinzipal", "remote_address": "Remote address", "session_id": "Session Id", "timestamp": "Zeitstempel", "type": "Typ", "label": "Audit-Protokoll" } } } ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/views/instances/auditevents/i18n.en.json ================================================ { "instances": { "auditevents": { "audit_log_not_supported_spring_boot_1": "Audit Log is not supported for Spring Boot 1.x applications.", "event": "Event", "loading_audit_events": "Loading Audit Events...", "no_audit_events_found": "No Audit Events found.", "principal": "Principal", "remote_address": "Remote address", "session_id": "Session Id", "timestamp": "Timestamp", "type": "Type", "label": "Audit Log" } } } ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/views/instances/auditevents/i18n.es.json ================================================ { "instances": { "auditevents": { "audit_log_not_supported_spring_boot_1": "Log de auditoría no soportado para aplicaciones Spring Boot 1.x .", "event": "Evento", "loading_audit_events": "Cargando Eventos de Auditoría...", "no_audit_events_found": "No se encontraron eventos de auditoría.", "principal": "Principal", "remote_address": "Dirección remota", "session_id": "Session Id", "timestamp": "Horario", "type": "Tipo", "label": "Log de Auditoría" } } } ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/views/instances/auditevents/i18n.fr.json ================================================ { "instances": { "auditevents": { "audit_log_not_supported_spring_boot_1": "Les Audit de Log ne sont pas supportés par les applications Spring Boot 1.x.", "event": "Événement", "loading_audit_events": "Chargement des Audit d'évènements...", "no_audit_events_found": "Aucun Audit d'évènements trouvés.", "principal": "Principal", "remote_address": "Adresse distante", "session_id": "Session Id", "timestamp": "Timestamp", "type": "Type", "label": "Audit Log" } } } ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/views/instances/auditevents/i18n.is.json ================================================ { "instances": { "auditevents": { "audit_log_not_supported_spring_boot_1": "Spring Boot 1.x forrit bjóða enga endurskoðunarannála (audit logs).", "event": "Atburður", "loading_audit_events": "Endurskoðunaratburðir eru að hlaða…", "no_audit_events_found": "Engir endurskoðunaratburðir eru í boði.", "principal": "Principal", "remote_address": "Remote address", "session_id": "Seta Id", "timestamp": "Tímapunktur", "type": "Gerð", "label": "Endurskoðunarannáll" } } } ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/views/instances/auditevents/i18n.ko.json ================================================ { "instances": { "auditevents": { "audit_log_not_supported_spring_boot_1": "감사(Audit) 로그는 Spring Boot 1.x 버전의 애플리케이션은 지원하지 않습니다.", "event": "이벤트", "loading_audit_events": "감사(Audit) 이벤트 불러오는 중...", "no_audit_events_found": "감사(Audit) 이벤트를 찾을 수 없습니다.", "principal": "접근 주체", "remote_address": "원격 주소", "session_id": "Session Id", "timestamp": "Timestamp", "type": "Type", "label": "Audit Log" } } } ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/views/instances/auditevents/i18n.pt-BR.json ================================================ { "instances": { "auditevents": { "audit_log_not_supported_spring_boot_1": "O log de auditoria não é suportado em aplicações Spring Boot 1.x.", "event": "Evento", "loading_audit_events": "Carregando Eventos de Auditoria...", "no_audit_events_found": "Nenhum Evento de Auditoria encontrado.", "principal": "Principal", "remote_address": "Endereço Remoto", "session_id": "Id de sessão", "timestamp": "Timestamp", "type": "Tipo", "label": "Log de Auditoria" } } } ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/views/instances/auditevents/i18n.ru.json ================================================ { "instances": { "auditevents": { "audit_log_not_supported_spring_boot_1": "Аудирование не поддерживается для приложений на Spring Boot 1.x.", "event": "Событие", "loading_audit_events": "Загрузка событий аудита...", "no_audit_events_found": "События аудита не найдены.", "principal": "Принципал", "remote_address": "Адрес", "session_id": "Идентификатор сессии", "timestamp": "Время", "type": "Тип", "label": "Журнал аудита" } } } ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/views/instances/auditevents/i18n.zh-CN.json ================================================ { "instances": { "auditevents": { "audit_log_not_supported_spring_boot_1": "Spring Boot 1.x的应用不支持审计日志。", "event": "事件", "loading_audit_events": "加载审计事件中...", "no_audit_events_found": "没有审计事件。", "principal": "当事人", "remote_address": "远程地址", "session_id": "会话ID", "timestamp": "时间戳", "type": "类型", "label": "审计日志" } } } ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/views/instances/auditevents/i18n.zh-TW.json ================================================ { "instances": { "auditevents": { "audit_log_not_supported_spring_boot_1": "Spring Boot 1.x 的應用程式不支援稽核日誌。", "event": "事件", "loading_audit_events": "正在載入稽核事件...", "no_audit_events_found": "找不到稽核事件。", "principal": "主體", "remote_address": "遠端位址", "session_id": "Session ID", "timestamp": "時間戳記", "type": "類型", "label": "稽核日誌" } } } ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/views/instances/auditevents/index.spec.ts ================================================ import userEvent from '@testing-library/user-event'; import { screen, waitFor } from '@testing-library/vue'; import { beforeEach, describe, expect, it, vi } from 'vitest'; import Instance from '@/services/instance'; import { render } from '@/test-utils'; import Auditevents from '@/views/instances/auditevents/index.vue'; describe('Auditevents', () => { const fetchAuditevents = vi.fn().mockResolvedValue({ data: { events: [], }, }); beforeEach(async () => { render(Auditevents, { props: { instance: createInstance(fetchAuditevents), }, }); }); it('fetches data on startup', async () => { await waitFor(() => expect(fetchAuditevents).toHaveBeenCalled()); }); it('fetches data when filter for Principal is changed', async () => { const input = await screen.findByPlaceholderText( 'instances.auditevents.principal', ); await userEvent.type(input, 'Abc'); await waitFor(() => { const calls = fetchAuditevents.mock.calls; expect(calls[calls.length - 1][0].principal).toEqual('Abc'); }); }); it('fetches data when filter for Type is changed', async () => { const input = await screen.findByPlaceholderText( 'instances.auditevents.type', ); await userEvent.type(input, 'AUTHENTICATION_FAILURE'); await waitFor(() => { const calls = fetchAuditevents.mock.calls; expect(calls[calls.length - 1][0].type).toEqual('AUTHENTICATION_FAILURE'); }); }); it('handles error when fetching data', async () => { render(Auditevents, { props: { instance: createInstance( vi.fn().mockRejectedValue({ response: { headers: { 'content-type': 'application/vnd.spring-boot.actuator.v2', }, }, }), ), }, }); await screen.findByText('Fetching of data failed.'); }); function createInstance(fetchAuditevents) { const instance = new Instance({ id: 4711 }); instance.fetchAuditevents = fetchAuditevents; return instance; } }); ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/views/instances/auditevents/index.vue ================================================ ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/views/instances/beans/beans-list-details.vue ================================================ ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/views/instances/beans/beans-list.vue ================================================ ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/views/instances/beans/i18n.de.json ================================================ { "instances": { "beans": { "label": "Beans", "name": "Name", "aliases": "Aliase", "type": "Typ", "resource": "Ressource", "dependencies": "Abhängigkeiten" } } } ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/views/instances/beans/i18n.en.json ================================================ { "instances": { "beans": { "label": "Beans", "name": "Name", "aliases": "Aliases", "type": "Type", "resource": "Resource", "dependencies": "Dependencies" } } } ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/views/instances/beans/i18n.es.json ================================================ { "instances": { "beans": { "label": "Beans", "name": "Nombre", "aliases": "Alias", "type": "Tipo", "resource": "Recurso", "dependencies": "Dependencias" } } } ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/views/instances/beans/i18n.fr.json ================================================ { "instances": { "beans": { "label": "Beans", "name": "Nom", "aliases": "Alias", "type": "Type", "resource": "Ressource", "dependencies": "Dépendances" } } } ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/views/instances/beans/i18n.is.json ================================================ { "instances": { "beans": { "label": "Baunir", "name": "Nafn", "aliases": "Samnefni", "type": "Gerð", "resource": "Resource", "dependencies": "Fylgni" } } } ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/views/instances/beans/i18n.ko.json ================================================ { "instances": { "beans": { "label": "빈(Bean)", "name": "이름", "aliases": "별칭", "type": "타입", "resource": "리소스", "dependencies": "의존성" } } } ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/views/instances/beans/i18n.pt-BR.json ================================================ { "instances": { "beans": { "label": "Beans", "name": "Nome", "aliases": "Aliases", "type": "Tipo", "resource": "Recurso", "dependencies": "Dependências" } } } ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/views/instances/beans/i18n.ru.json ================================================ { "instances": { "beans": { "label": "Бины", "name": "Имя", "aliases": "Псевдонимы", "type": "Тип", "resource": "Ресурс", "dependencies": "Зависимости" } } } ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/views/instances/beans/i18n.zh-CN.json ================================================ { "instances": { "beans": { "label": "类", "name": "名称", "aliases": "别名", "type": "类型", "resource": "资源", "dependencies": "依赖关系" } } } ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/views/instances/beans/i18n.zh-TW.json ================================================ { "instances": { "beans": { "label": "Bean", "name": "名稱", "aliases": "別名", "type": "類型", "resource": "資源", "dependencies": "相依元件" } } } ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/views/instances/beans/index.vue ================================================ ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/views/instances/caches/caches-list.vue ================================================ ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/views/instances/caches/i18n.de.json ================================================ { "instances": { "caches": { "label": "Caches", "cache_manager": "Cache Manager", "loading_caches": "Cache-Informationen werden geladen...", "name": "Name", "no_caches_found": "Keine Cache-Informationen gefunden." } } } ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/views/instances/caches/i18n.en.json ================================================ { "instances": { "caches": { "label": "Caches", "cache_manager": "Cache Manager", "loading": "Loading Caches...", "name": "Name", "no_caches_found": "No caches found." } } } ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/views/instances/caches/i18n.es.json ================================================ { "instances": { "caches": { "label": "Caches", "cache_manager": "Cache Manager", "loading": "Cargando Caches...", "name": "Nombre", "no_caches_found": "No se encontraron caches." } } } ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/views/instances/caches/i18n.fr.json ================================================ { "instances": { "caches": { "label": "Caches", "cache_manager": "Manager de cache", "loading": "Chargement des Caches...", "name": "Nom", "no_caches_found": "Aucun caches trouvés." } } } ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/views/instances/caches/i18n.is.json ================================================ { "instances": { "caches": { "label": "Skyndiminni", "cache_manager": "Skyndiminnastjórnandi", "loading_caches": "Skyndiminnaupplýsingar eru að hlaða…", "name": "Nafn", "no_caches_found": "Engar skyndiminnaupplýsingar eru í boði." } } } ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/views/instances/caches/i18n.ko.json ================================================ { "instances": { "caches": { "label": "캐시", "cache_manager": "캐시 관리자", "loading": "캐시 정보 불러오는 중...", "name": "이름", "no_caches_found": "캐시 정보를 찾을 수 없습니다." } } } ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/views/instances/caches/i18n.pt-BR.json ================================================ { "instances": { "caches": { "label": "Caches", "cache_manager": "Gerenciador de Cache", "loading": "Carregando Caches...", "name": "Name", "no_caches_found": "Nenhum cache encontrado." } } } ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/views/instances/caches/i18n.ru.json ================================================ { "instances": { "caches": { "label": "Кэши", "cache_manager": "Кэш-менеджер", "loading": "Загрузка кэшей...", "name": "Имя", "no_caches_found": "Кэши не найдены." } } } ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/views/instances/caches/i18n.zh-CN.json ================================================ { "instances": { "caches": { "label": "缓存", "cache_manager": "缓存管理", "loading": "加载缓存中...", "name": "名称", "no_caches_found": "未找到缓存。" } } } ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/views/instances/caches/i18n.zh-TW.json ================================================ { "instances": { "caches": { "label": "快取", "cache_manager": "快取管理", "loading": "正在載入快取...", "name": "名稱", "no_caches_found": "找不到快取。" } } } ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/views/instances/caches/index.vue ================================================ ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/views/instances/conditions/conditions-list-details.spec.ts ================================================ import { screen } from '@testing-library/vue'; import { describe, expect, it } from 'vitest'; import { render } from '@/test-utils'; import ConditionsListDetails from '@/views/instances/conditions/conditions-list-details.vue'; describe('ConditionsListDetails', () => { it('should display condition', async () => { render(ConditionsListDetails, { props: { condition: { condition: 'SpringBootAdminServerEnabledCondition', }, }, }); expect( await screen.findByLabelText('instances.conditions.condition'), ).toHaveTextContent('SpringBootAdminServerEnabledCondition'); }); it('should display message', async () => { render(ConditionsListDetails, { props: { condition: { message: 'matched', }, }, }); expect( await screen.findByLabelText('instances.conditions.message'), ).toHaveTextContent('matched'); }); it.each` condition ${undefined} ${null} ${''} `( 'should not display condition if condition is $condition', async ({ condition }) => { render(ConditionsListDetails, { props: { condition: { condition, }, }, }); expect( screen.queryByLabelText('instances.conditions.condition'), ).not.toBeInTheDocument(); }, ); it.each` message ${undefined} ${null} ${''} `( 'should not display message if message is $message', async ({ message }) => { render(ConditionsListDetails, { props: { condition: { message, }, }, }); expect( screen.queryByLabelText('instances.conditions.message'), ).not.toBeInTheDocument(); }, ); }); ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/views/instances/conditions/conditions-list-details.vue ================================================ ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/views/instances/conditions/conditions-list.vue ================================================ ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/views/instances/conditions/i18n.de.json ================================================ { "instances": { "conditions": { "label": "Bedingungen", "positive-matches": "Übereinstimmungen", "negative-matches": "Keine Übereinstimmungen", "condition": "Bedingung", "message": "Hinweis", "not-matched": "Nicht übereinstimmend", "matched": "Übereinstimmend" } } } ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/views/instances/conditions/i18n.en.json ================================================ { "instances": { "conditions": { "label": "Conditions", "positive-matches": "Positive matches", "negative-matches": "Negative matches", "condition": "Condition", "message": "Message", "not-matched": "Not matched", "matched": "Matched" } } } ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/views/instances/conditions/i18n.zh-TW.json ================================================ { "instances": { "conditions": { "label": "條件評估", "positive-matches": "符合條件", "negative-matches": "不符合條件", "condition": "條件", "message": "訊息", "not-matched": "不符合", "matched": "符合" } } } ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/views/instances/conditions/index.vue ================================================ ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/views/instances/configprops/i18n.de.json ================================================ { "instances": { "configprops": { "label": "Configuration Properties", "no_properties_set": "Keine Konfiguration gefunden." } } } ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/views/instances/configprops/i18n.en.json ================================================ { "instances": { "configprops": { "label": "Configuration Properties", "no_properties_set": "No properties set" } } } ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/views/instances/configprops/i18n.es.json ================================================ { "instances": { "configprops": { "label": "Configuración", "no_properties_set": "No hay propiedades configuradas" } } } ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/views/instances/configprops/i18n.fr.json ================================================ { "instances": { "configprops": { "label": "Propriétés de configuration", "no_properties_set": "Aucune propriétés définies" } } } ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/views/instances/configprops/i18n.is.json ================================================ { "instances": { "configprops": { "label": "Stillingareiginleikar", "no_properties_set": "Engin stilling í boði." } } } ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/views/instances/configprops/i18n.ko.json ================================================ { "instances": { "configprops": { "label": "구성 속성(Configuration Properties)", "no_properties_set": "설정된 구성 속성 정보가 없습니다." } } } ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/views/instances/configprops/i18n.pt-BR.json ================================================ { "instances": { "configprops": { "label": "Propriedades de Configuração", "no_properties_set": "Nenhuma propriedade definida" } } } ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/views/instances/configprops/i18n.ru.json ================================================ { "instances": { "configprops": { "label": "Свойства конфигураций", "no_properties_set": "Нет установленных конфигураций" } } } ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/views/instances/configprops/i18n.zh-CN.json ================================================ { "instances": { "configprops": { "label": "配置属性", "no_properties_set": "未设置属性" } } } ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/views/instances/configprops/i18n.zh-TW.json ================================================ { "instances": { "configprops": { "label": "設定屬性", "no_properties_set": "未設定屬性" } } } ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/views/instances/configprops/index.vue ================================================ ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/views/instances/dependencies/SbomList.vue ================================================ ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/views/instances/dependencies/i18n.de.json ================================================ { "instances": { "dependencies": { "label": "Abhängigkeiten", "no_data_provided": "Es wurden keine Daten bereitgestellt. Bitte überprüfen Sie, ob Sie die software bill of materials (SBOM) korrekt konfiguriert haben.", "list": { "header": { "group": "Gruppe", "name": "Name", "version": "Version", "publisher": "Veröffentlicht von", "description": "Beschreibung", "licenses": "Lizenzen" } } } } } ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/views/instances/dependencies/i18n.en.json ================================================ { "instances": { "dependencies": { "label": "Dependencies", "no_data_provided": "No data provided. Please check if you have configured software bill of materials (SBOM) correctly.", "list": { "header": { "group": "Group", "name": "Name", "version": "Version", "publisher": "Publisher", "description": "Description", "licenses": "Licenses" } } } } } ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/views/instances/dependencies/i18n.zh-TW.json ================================================ { "instances": { "dependencies": { "label": "相依元件", "list": { "header": { "group": "群組", "name": "名稱", "version": "版本", "publisher": "發布者", "description": "說明", "licenses": "授權條款" } } } } } ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/views/instances/dependencies/index.spec.ts ================================================ import userEvent from '@testing-library/user-event'; import { screen } from '@testing-library/vue'; import { HttpResponse, http } from 'msw'; import { beforeEach, describe, expect, it } from 'vitest'; import { applications } from '@/mocks/applications/data'; import { server } from '@/mocks/server'; import Application from '@/services/application'; import { render } from '@/test-utils'; import Dependencies from '@/views/instances/dependencies/index.vue'; describe('Dependencies', () => { const GROUP_TABLE_COLUMN_INDEX = 0; const NAME_TABLE_COLUMN_INDEX = 1; const VERSION_TABLE_COLUMN_INDEX = 2; const LICENSES_TABLE_COLUMN_INDEX = 3; const PUBLISHER_TABLE_COLUMN_INDEX = 4; const DESCRIPTION_TABLE_COLUMN_INDEX = 5; const application = new Application(applications[0]); const instance = application.instances[0]; describe('Render correctly', () => { beforeEach(() => { render(Dependencies, { props: { instance, }, }); }); it('both sbom ids', async () => { expect(await screen.findByText(/application \(63\/63\)/)).toBeVisible(); expect(await screen.findByText(/system \(63\/63\)/)).toBeVisible(); }); it('table shows correct information', async () => { // header order const tableHeaders = await screen.findAllByTestId('sbom-table-header'); tableHeaders.forEach((tableHeader) => { // only one header row expect(tableHeader.children.length).toBe(1); // having exactly this order expect( tableHeader.children[0].children[GROUP_TABLE_COLUMN_INDEX] .textContent, ).toContain('instances.dependencies.list.header.group'); expect( tableHeader.children[0].children[NAME_TABLE_COLUMN_INDEX].textContent, ).toContain('instances.dependencies.list.header.name'); expect( tableHeader.children[0].children[VERSION_TABLE_COLUMN_INDEX] .textContent, ).toContain('instances.dependencies.list.header.version'); expect( tableHeader.children[0].children[LICENSES_TABLE_COLUMN_INDEX] .textContent, ).toContain('instances.dependencies.list.header.licenses'); expect( tableHeader.children[0].children[PUBLISHER_TABLE_COLUMN_INDEX] .textContent, ).toContain('instances.dependencies.list.header.publisher'); expect( tableHeader.children[0].children[DESCRIPTION_TABLE_COLUMN_INDEX] .textContent, ).toContain('instances.dependencies.list.header.description'); }); // columns should be filled with values matching headers const log4jRows = await screen.findAllByText('log4j-api'); log4jRows.forEach((row) => { const log4jRow = row.parentElement; expect( log4jRow.children[GROUP_TABLE_COLUMN_INDEX].textContent, ).toContain('org.apache.logging.log4j'); expect( log4jRow.children[NAME_TABLE_COLUMN_INDEX].textContent, ).toContain('log4j-api'); expect( log4jRow.children[VERSION_TABLE_COLUMN_INDEX].textContent, ).toContain('2.23.1'); expect( log4jRow.children[LICENSES_TABLE_COLUMN_INDEX].textContent, ).toContain('Apache-2.0'); expect( log4jRow.children[PUBLISHER_TABLE_COLUMN_INDEX].textContent, ).toContain('The Apache Software Foundation'); expect( log4jRow.children[DESCRIPTION_TABLE_COLUMN_INDEX].textContent, ).toContain('The Apache Log4j API'); }); }); }); describe('filter correctly', () => { beforeEach(() => { render(Dependencies, { props: { instance, }, }); }); it('for dependency name', async () => { const filterInput = screen.getByLabelText('Filter'); await userEvent.type(filterInput, 'log4j-api'); expect(await screen.findByText(/application \(1\/63\)/)).toBeVisible(); expect(await screen.findByText(/system \(1\/63\)/)).toBeVisible(); (await screen.findAllByTestId('sbom-table-body')).forEach((tbody) => { expect(tbody.children.length).toBe(1); }); }); it('for dependency group', async () => { const filterInput = screen.getByLabelText('Filter'); await userEvent.type(filterInput, 'org.apache.logging.log4j'); expect(await screen.findByText(/application \(2\/63\)/)).toBeVisible(); expect(await screen.findByText(/system \(2\/63\)/)).toBeVisible(); (await screen.findAllByTestId('sbom-table-body')).forEach((tbody) => { expect(tbody.children.length).toBe(2); }); }); it('for dependency version', async () => { const filterInput = screen.getByLabelText('Filter'); await userEvent.type(filterInput, '2.17.0'); expect(await screen.findByText(/application \(6\/63\)/)).toBeVisible(); expect(await screen.findByText(/system \(6\/63\)/)).toBeVisible(); (await screen.findAllByTestId('sbom-table-body')).forEach((tbody) => { expect(tbody.children.length).toBe(6); }); }); it('for dependency license', async () => { const filterInput = screen.getByLabelText('Filter'); await userEvent.type(filterInput, 'BSD-3-Clause'); expect(await screen.findByText(/application \(3\/63\)/)).toBeVisible(); expect(await screen.findByText(/system \(3\/63\)/)).toBeVisible(); (await screen.findAllByTestId('sbom-table-body')).forEach((tbody) => { expect(tbody.children.length).toBe(3); }); }); it('for dependency publisher', async () => { const filterInput = screen.getByLabelText('Filter'); await userEvent.type(filterInput, 'Eclipse Foundation'); expect(await screen.findByText(/application \(4\/63\)/)).toBeVisible(); expect(await screen.findByText(/system \(4\/63\)/)).toBeVisible(); (await screen.findAllByTestId('sbom-table-body')).forEach((tbody) => { expect(tbody.children.length).toBe(4); }); }); }); describe('sort correctly', () => { beforeEach(() => { server.use( http.get('/instances/:instanceId/actuator/sbom', () => { return HttpResponse.json({ ids: ['application'], }); }), http.get('/instances/:instanceId/actuator/sbom/application', () => { return HttpResponse.json({ components: [ { publisher: 'CCC', group: 'c.cccc.ccc', name: 'ccc', version: '3.0.0', description: 'C description', licenses: [ { license: { id: 'C', }, }, ], }, { publisher: 'AAA', group: 'a.aaaa.aaa', name: 'aaa', version: '1.0.0', description: 'A description', licenses: [ { license: { id: 'Apache-2.0', }, }, ], }, { publisher: 'BBB', group: 'b.bbbb.bbb', name: 'bbb', version: '2.0.0', description: 'B description', licenses: [ { license: { id: 'BSD', }, }, ], }, ], }); }), ); render(Dependencies, { props: { instance, }, }); }); const resetDefaultSort = async () => { for (const header of await screen.findAllByTestId('sbom-table-header')) { const tableHeaderColumns = header.children[0].children; await userEvent.click(tableHeaderColumns[GROUP_TABLE_COLUMN_INDEX]); await userEvent.click(tableHeaderColumns[GROUP_TABLE_COLUMN_INDEX]); await userEvent.click(tableHeaderColumns[NAME_TABLE_COLUMN_INDEX]); await userEvent.click(tableHeaderColumns[NAME_TABLE_COLUMN_INDEX]); await userEvent.click(tableHeaderColumns[VERSION_TABLE_COLUMN_INDEX]); await userEvent.click(tableHeaderColumns[VERSION_TABLE_COLUMN_INDEX]); } expect( await screen.queryByTestId('sorted-icon-group-ASC'), ).not.toBeInTheDocument(); expect( await screen.queryByTestId('sorted-icon-name-ASC'), ).not.toBeInTheDocument(); expect( await screen.queryByTestId('sorted-icon-version-ASC'), ).not.toBeInTheDocument(); }; it('initial sort by group, name and version', async () => { expect( (await screen.findAllByTestId('sorted-icon-group-ASC'))[0], ).toBeVisible(); expect( (await screen.findAllByTestId('sorted-icon-name-ASC'))[0], ).toBeVisible(); expect( (await screen.findAllByTestId('sorted-icon-version-ASC'))[0], ).toBeVisible(); const dependencyRows = await screen.findAllByTestId( 'sbom-table-body-row', ); expect( dependencyRows[0].children[GROUP_TABLE_COLUMN_INDEX].textContent, ).toContain('a.aaaa.aaa'); expect( dependencyRows[0].children[NAME_TABLE_COLUMN_INDEX].textContent, ).toContain('aaa'); expect( dependencyRows[0].children[VERSION_TABLE_COLUMN_INDEX].textContent, ).toContain('1.0.0'); expect( dependencyRows[1].children[GROUP_TABLE_COLUMN_INDEX].textContent, ).toContain('b.bbbb.bbb'); expect( dependencyRows[1].children[NAME_TABLE_COLUMN_INDEX].textContent, ).toContain('bbb'); expect( dependencyRows[1].children[VERSION_TABLE_COLUMN_INDEX].textContent, ).toContain('2.0.0'); expect( dependencyRows[2].children[GROUP_TABLE_COLUMN_INDEX].textContent, ).toContain('c.cccc.ccc'); expect( dependencyRows[2].children[NAME_TABLE_COLUMN_INDEX].textContent, ).toContain('ccc'); expect( dependencyRows[2].children[VERSION_TABLE_COLUMN_INDEX].textContent, ).toContain('3.0.0'); }); it.each` property | tableColumnIndex | expectedValues ${'group'} | ${GROUP_TABLE_COLUMN_INDEX} | ${['a.aaaa.aaa', 'b.bbbb.bbb', 'c.cccc.ccc']} ${'name'} | ${NAME_TABLE_COLUMN_INDEX} | ${['aaa', 'bbb', 'ccc']} ${'version'} | ${VERSION_TABLE_COLUMN_INDEX} | ${['1.0.0', '2.0.0', '3.0.0']} ${'publisher'} | ${PUBLISHER_TABLE_COLUMN_INDEX} | ${['AAA', 'BBB', 'CCC']} `( 'by $property ASC', async ({ property, tableColumnIndex, expectedValues }) => { await resetDefaultSort(); // activate sort ASC const tableHeaderColumns = ( await screen.findAllByTestId('sbom-table-header') )[0].children[0].children; await userEvent.click(tableHeaderColumns[tableColumnIndex]); expect( (await screen.findAllByTestId(`sorted-icon-${property}-ASC`))[0], ).toBeVisible(); // check sort const dependencyRows = await screen.findAllByTestId( 'sbom-table-body-row', ); expect( dependencyRows[0].children[tableColumnIndex].textContent, ).toContain(expectedValues[0]); expect( dependencyRows[1].children[tableColumnIndex].textContent, ).toContain(expectedValues[1]); expect( dependencyRows[2].children[tableColumnIndex].textContent, ).toContain(expectedValues[2]); }, ); it.each` property | tableColumnIndex | expectedValues ${'group'} | ${GROUP_TABLE_COLUMN_INDEX} | ${['a.aaaa.aaa', 'b.bbbb.bbb', 'c.cccc.ccc'].reverse()} ${'name'} | ${NAME_TABLE_COLUMN_INDEX} | ${['aaa', 'bbb', 'ccc'].reverse()} ${'version'} | ${VERSION_TABLE_COLUMN_INDEX} | ${['1.0.0', '2.0.0', '3.0.0'].reverse()} ${'publisher'} | ${PUBLISHER_TABLE_COLUMN_INDEX} | ${['AAA', 'BBB', 'CCC'].reverse()} `( 'by $property DESC', async ({ property, tableColumnIndex, expectedValues }) => { await resetDefaultSort(); // activate sort DESC const tableHeaderColumns = ( await screen.findAllByTestId('sbom-table-header') )[0].children[0].children; await userEvent.click(tableHeaderColumns[tableColumnIndex]); await userEvent.click(tableHeaderColumns[tableColumnIndex]); expect( (await screen.findAllByTestId(`sorted-icon-${property}-DESC`))[0], ).toBeVisible(); // check sort const dependencyRows = await screen.findAllByTestId( 'sbom-table-body-row', ); expect( dependencyRows[0].children[tableColumnIndex].textContent, ).toContain(expectedValues[0]); expect( dependencyRows[1].children[tableColumnIndex].textContent, ).toContain(expectedValues[1]); expect( dependencyRows[2].children[tableColumnIndex].textContent, ).toContain(expectedValues[2]); }, ); }); }); ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/views/instances/dependencies/index.vue ================================================ ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/views/instances/details/LineChart.vue ================================================ ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/views/instances/details/__snapshots__/health-details.spec.ts.snap ================================================ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html exports[`HealthDetails > Health .details > should format object details correctly 1`] = `
    - status: VALID
  chain:
    - subject: CN=example.com, OU=IT, O=Example Corp, L=San Francisco, ST=CA, C=US
      issuer: CN=R3, O=Let's Encrypt, C=US

  
`; ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/views/instances/details/cache-chart.vue ================================================ ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/views/instances/details/datasource-chart.vue ================================================ ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/views/instances/details/details-cache.spec.ts ================================================ import { screen, waitFor } from '@testing-library/vue'; import { enableAutoUnmount } from '@vue/test-utils'; import { HttpResponse, http } from 'msw'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { applications } from '@/mocks/applications/data'; import { server } from '@/mocks/server'; import Application from '@/services/application'; import { render } from '@/test-utils'; import DetailsCache from '@/views/instances/details/details-cache.vue'; vi.mock('@/sba-config', async () => { const sbaConfig: any = await vi.importActual('@/sba-config'); return { default: { ...sbaConfig.default, uiSettings: { pollTimer: { cache: 100, }, }, }, }; }); describe('DetailsCache', () => { enableAutoUnmount(afterEach); const CACHE_NAME = 'foo-cache'; const HITS = [24.0, 32.0, 48.0]; const MISSES = [8.0, 8.0, 16.0]; const TOTAL = [32, 40, 64]; const HITS_PER_INTERVAL = [0, 8, 16]; const MISSES_PER_INTERVAL = [0, 0, 8]; const TOTAL_PER_INTERVAL = [0, 8, 24]; const stubChart = { props: ['data'], template: `
{{ JSON.stringify($props.data) }}
`, }; const renderComponent = async (stubs = {}) => { const application = new Application(applications[0]); const instance = application.instances[0]; return render(DetailsCache, { global: { stubs, }, props: { instance, cacheName: CACHE_NAME, index: 0, }, }); }; beforeEach(() => { const hitsGenerator = (function* () { yield* HITS; })(); const missesGenerator = (function* () { yield* MISSES; })(); server.use( http.get( '/instances/:instanceId/actuator/metrics/cache.gets', ({ request }) => { const url = new URL(request.url); const tags = url.searchParams.getAll('tag'); if (tags.includes('result:hit')) { return HttpResponse.json({ measurements: [{ value: hitsGenerator.next()?.value }], }); } else if (tags.includes('result:miss')) { return HttpResponse.json({ measurements: [{ value: missesGenerator.next()?.value }], }); } }, ), http.get('/instances/:instanceId/actuator/metrics/cache.size', () => { return HttpResponse.json({ measurements: [{ value: '1337' }], }); }), ); }); it('should render cache name', async () => { await renderComponent(); expect( await screen.findByRole('heading', { name: `Cache: ${CACHE_NAME}` }), ).toBeVisible(); }); it('should render hits and misses', async () => { await renderComponent(); expect( await screen.findByLabelText('instances.details.cache.hits'), ).toHaveTextContent(`${HITS[0]}`); expect( await screen.findByLabelText('instances.details.cache.misses'), ).toHaveTextContent(`${MISSES[0]}`); }); it('should calculate and render hit/miss ratio', async () => { await renderComponent(); expect( await screen.findByLabelText('instances.details.cache.hit_ratio'), ).toHaveTextContent('75.00%'); // 24 hits, 8 misses }); it('should calculate total', async () => { const application = new Application(applications[0]); const instance = application.instances[0]; const { container } = await render(DetailsCache, { global: { stubs: { cacheChart: stubChart } }, props: { instance, cacheName: CACHE_NAME, index: 0, }, }); // wait until chart stub receives at least 3 data points await waitFor( () => { const el = container.querySelector('[data-test="chart"]'); expect(el).toBeTruthy(); const text = (el?.textContent as string) || '[]'; const parsed = JSON.parse(text); expect(parsed).toHaveLength(3); }, { timeout: 2000 }, ); const chartText = (container.querySelector('[data-test="chart"]')?.textContent as string) || '[]'; const chartData = JSON.parse(chartText); for (let index = 0; index < chartData.length; index++) { expect(chartData[index].total).toEqual(TOTAL[index]); } }); it('should calculate hits, misses and total per interval', async () => { const application = new Application(applications[0]); const instance = application.instances[0]; const { container } = await render(DetailsCache, { global: { stubs: { cacheChart: stubChart } }, props: { instance, cacheName: CACHE_NAME, index: 0, }, }); // wait until chart stub receives at least 3 data points await waitFor( () => { const el = container.querySelector('[data-test="chart"]'); expect(el).toBeTruthy(); const text = (el?.textContent as string) || '[]'; const parsed = JSON.parse(text); expect(parsed).toHaveLength(3); }, { timeout: 2000 }, ); const chartText2 = (container.querySelector('[data-test="chart"]')?.textContent as string) || '[]'; const chartData = JSON.parse(chartText2); for (let index = 0; index < chartData.length; index++) { expect(chartData[index].hitsPerInterval).toEqual( HITS_PER_INTERVAL[index], ); expect(chartData[index].missesPerInterval).toEqual( MISSES_PER_INTERVAL[index], ); expect(chartData[index].totalPerInterval).toEqual( TOTAL_PER_INTERVAL[index], ); } }); it('should reinitialize metrics when instance changes', async () => { const application = new Application(applications[0]); const instance = application.instances[0]; const { getByText, rerender, queryByText } = await render(DetailsCache, { props: { instance, cacheName: CACHE_NAME, index: 0, }, }); // wait until initial fetch rendered a numeric value await waitFor(() => { expect(getByText(`${HITS[0]}`)).toBeTruthy(); }); // simulate switching to a different instance const newApp = new Application({ name: 'Other', statusTimestamp: Date.now(), instances: [{ id: 'other-1', statusInfo: { status: 'UP' } }], }); const newInstance = newApp.instances[0]; await rerender({ instance: newInstance, cacheName: CACHE_NAME, index: 0 }); // component should have reset its rendered data await waitFor(() => { expect(queryByText(`${HITS[0]}`)).not.toBeInTheDocument(); }); }); }); ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/views/instances/details/details-cache.vue ================================================ ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/views/instances/details/details-caches.vue ================================================ ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/views/instances/details/details-datasource.spec.ts ================================================ import { screen, waitFor } from '@testing-library/vue'; import { enableAutoUnmount } from '@vue/test-utils'; import { HttpResponse, http } from 'msw'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { applications } from '@/mocks/applications/data'; import { server } from '@/mocks/server'; import Application from '@/services/application'; import { render } from '@/test-utils'; import DetailsDatasource from '@/views/instances/details/details-datasource.vue'; vi.mock('@/sba-config', async () => { const sbaConfig: any = await vi.importActual('@/sba-config'); return { default: { ...sbaConfig.default, uiSettings: { pollTimer: { datasource: 100, }, }, }, }; }); describe('DetailsDatasource', () => { enableAutoUnmount(afterEach); const DATA_SOURCE = 'my-ds'; const ACTIVE = [2.0, 3.0, 4.0]; const MIN = [1.0, 1.0, 1.0]; const MAX = [5.0, -1.0, 10.0]; // include unlimited (-1) case beforeEach(() => { const activeGenerator = (function* () { yield* ACTIVE; })(); const minGenerator = (function* () { yield* MIN; })(); const maxGenerator = (function* () { yield* MAX; })(); server.use( http.get('/instances/:instanceId/actuator/metrics', () => { return HttpResponse.json({ names: [ 'jdbc.connections.active', 'jdbc.connections.min', 'jdbc.connections.max', ], }); }), http.get( '/instances/:instanceId/actuator/metrics/jdbc.connections.active', () => { return HttpResponse.json({ measurements: [{ value: activeGenerator.next()?.value }], }); }, ), http.get( '/instances/:instanceId/actuator/metrics/jdbc.connections.min', () => { return HttpResponse.json({ measurements: [{ value: minGenerator.next()?.value }], }); }, ), http.get( '/instances/:instanceId/actuator/metrics/jdbc.connections.max', () => { return HttpResponse.json({ measurements: [{ value: maxGenerator.next()?.value }], }); }, ), ); }); async function renderComponent(stubs = {}) { const application = new Application(applications[0]); const instance = application.instances[0]; return render(DetailsDatasource, { global: { stubs, }, props: { instance, dataSource: DATA_SOURCE, }, }); } it('renders panel title with datasource name', async () => { await renderComponent(); // title uses i18n keys in the test environment; ensure heading is present const heading = await screen.findByRole('heading'); expect(heading).toBeVisible(); }); it('renders active, min and max values', async () => { await renderComponent(); // labels use i18n keys; assert numeric values are rendered expect(await screen.findByText(`${ACTIVE[0]}`)).toBeVisible(); expect(await screen.findByText(`${MIN[0]}`)).toBeVisible(); expect(await screen.findByText(`${MAX[0]}`)).toBeVisible(); }); it('handles unlimited max (-1) and pushes chartData points', async () => { const application = new Application(applications[0]); const instance = application.instances[0]; await render(DetailsDatasource, { props: { instance, dataSource: DATA_SOURCE, }, }); // wait until initial metric value is visible await waitFor(() => { expect(screen.getByText(`${ACTIVE[0]}`)).toBeVisible(); }); // second measurement MAX[1] === -1 should be displayed as 'unlimited' in rendered markup expect( await screen.findByText('instances.details.datasource.unlimited'), ).toBeVisible(); }); it('should reinitialize metrics when instance changes', async () => { const application = new Application(applications[0]); const instance = application.instances[0]; const { rerender } = await render(DetailsDatasource, { props: { instance, dataSource: DATA_SOURCE, }, }); // Wait for the component to poll and populate chartData (3 samples) await waitFor(() => { // find any of the numeric texts to ensure initial fetch happened expect(screen.getByText(`${ACTIVE[0]}`)).toBeVisible(); }); const newApp = new Application({ name: 'Other', statusTimestamp: Date.now(), instances: [{ id: 'other-1', statusInfo: { status: 'UP' } }], }); const newInstance = newApp.instances[0]; await rerender({ instance: newInstance, dataSource: DATA_SOURCE }); // After rerender with a different instance, the component should reset await waitFor(() => { expect(screen.queryByText(`${ACTIVE[0]}`)).not.toBeInTheDocument(); }); }); }); ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/views/instances/details/details-datasource.vue ================================================ ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/views/instances/details/details-datasources.vue ================================================ ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/views/instances/details/details-gc.vue ================================================ ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/views/instances/details/details-health.spec.ts ================================================ import userEvent from '@testing-library/user-event'; import { screen, waitFor } from '@testing-library/vue'; import { HttpResponse, http } from 'msw'; import { beforeEach, describe, expect, it } from 'vitest'; import { applications } from '@/mocks/applications/data'; import { server } from '@/mocks/server'; import Application from '@/services/application'; import { render } from '@/test-utils'; import DetailsHealth from '@/views/instances/details/details-health.vue'; describe('DetailsHealth', () => { beforeEach(() => { server.use( http.get('/instances/:instanceId/actuator/health', () => { return HttpResponse.json({ instance: 'UP', groups: ['liveness'], }); }), http.get('/instances/:instanceId/actuator/health/liveness', () => { return HttpResponse.json({ status: 'UP', details: { disk: { status: 'UNKNOWN' }, database: { status: 'UNKNOWN' }, }, }); }), ); }); it('should display groups as part of health section', async () => { const application = new Application(applications[0]); const instance = application.instances[0]; render(DetailsHealth, { props: { instance, }, }); await waitFor(() => expect(screen.queryByRole('status')).not.toBeInTheDocument(), ); expect( await screen.findByRole('button', { name: /instances.details.health_group.title: liveness/, }), ).toBeVisible(); }); it('health groups are toggleable, when details are available', async () => { const application = new Application(applications[0]); const instance = application.instances[0]; instance.statusInfo = { status: 'UP', details: {} }; render(DetailsHealth, { props: { instance, }, }); await waitFor(() => expect(screen.queryByRole('status')).not.toBeInTheDocument(), ); const button = screen.queryByRole('button', { name: /instances.details.health_group.title: liveness/, }); expect(button).toBeVisible(); expect(screen.queryByLabelText('disk')).toBeNull(); expect(screen.queryByLabelText('database')).toBeNull(); await userEvent.click(button); expect(screen.queryByLabelText('disk')).toBeDefined(); expect(screen.queryByLabelText('database')).toBeDefined(); }); it('should update health details when instance prop changes (watch)', async () => { const application = new Application(applications[0]); const instance1 = application.instances[0]; const instance2 = { ...instance1, id: 'other-id', statusInfo: { status: 'DOWN', details: {} }, }; const { rerender } = render(DetailsHealth, { props: { instance: instance1, }, }); await waitFor(() => expect(screen.queryByRole('status')).not.toBeInTheDocument(), ); // Simulate prop change await rerender({ instance: instance2 }); // Wait for the component to react to the prop change await waitFor(() => expect( screen.queryByRole('button', { name: /instances.details.health_group.title: liveness/, }), ).toBeVisible(), ); }); it('should not display health group button if no groups are present', async () => { server.use( http.get('/instances/:instanceId/actuator/health', () => { return HttpResponse.json({ instance: 'UP', groups: [], }); }), ); const application = new Application(applications[0]); const instance = application.instances[0]; render(DetailsHealth, { props: { instance, }, }); await waitFor(() => expect(screen.queryByRole('status')).not.toBeInTheDocument(), ); expect( screen.queryByRole('button', { name: /instances.details.health_group.title: liveness/, }), ).toBeNull(); }); }); ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/views/instances/details/details-health.vue ================================================ ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/views/instances/details/details-hero.vue ================================================ ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/views/instances/details/details-info.spec.ts ================================================ import { screen, waitFor } from '@testing-library/vue'; import { AxiosHeaders } from 'axios'; import { HttpResponse, http } from 'msw'; import { beforeEach, describe, expect, it, vi } from 'vitest'; import DetailsInfo from './details-info.vue'; import { applications } from '@/mocks/applications/data'; import { server } from '@/mocks/server'; import Application from '@/services/application'; import { render } from '@/test-utils'; describe('DetailsInfo', () => { beforeEach(() => { server.use( http.get('/instances/:instanceId/actuator/info', () => { return HttpResponse.json({ app: { version: '1.0.0', name: 'TestApp' }, java: { version: '17' }, }); }), ); }); it('should render info table with keys and values', async () => { const application = new Application(applications[0]); const instance = application.instances[0]; instance.hasEndpoint = () => true; instance.fetchInfo = async () => ({ data: { app: { version: '1.0.0', name: 'TestApp' }, java: { version: '17' }, }, status: 200, statusText: 'OK', headers: new AxiosHeaders(), config: { headers: new AxiosHeaders() }, }); render(DetailsInfo, { props: { instance }, }); await waitFor(() => { expect(screen.queryByRole('status')).not.toBeInTheDocument(); }); expect(await screen.findByText('app')).toBeVisible(); expect(await screen.findByText('java')).toBeVisible(); // YAML-formatted output is rendered in a
 block, so match the full string
    expect(
      await screen.findByText(/version: 1.0.0[\s\S]*name: TestApp/),
    ).toBeVisible();
    expect(await screen.findByText(/version: '17'/)).toBeVisible();
  });

  it('should show no info message if info is empty', async () => {
    const application = new Application(applications[0]);
    const instance = application.instances[0];
    instance.hasEndpoint = () => true;
    instance.fetchInfo = async () => ({
      data: {},
      status: 200,
      statusText: 'OK',
      headers: new AxiosHeaders(),
      config: { headers: new AxiosHeaders() },
    });

    render(DetailsInfo, {
      props: { instance },
    });

    await waitFor(() => {
      expect(screen.queryByRole('status')).not.toBeInTheDocument();
    });

    expect(
      await screen.findByText('instances.details.info.no_info_provided'),
    ).toBeVisible();
  });

  it('should show error alert if fetch fails', async () => {
    const application = new Application(applications[0]);
    const instance = application.instances[0];
    instance.hasEndpoint = () => true;
    instance.fetchInfo = async () => {
      throw new Error('fail');
    };

    render(DetailsInfo, {
      props: { instance },
    });

    await waitFor(() => {
      expect(screen.getByRole('alert')).toBeVisible();
    });
    expect(screen.getByRole('alert')).toHaveTextContent(
      'Fetching of data failed.',
    );
  });

  it('should call fetchInfo when instance changes (watcher)', async () => {
    const application = new Application(applications[0]);
    const instance1 = application.instances[0];
    const instance2 = {
      ...instance1,
      id: 'other-id',
      hasEndpoint: () => true,
      fetchInfo: async () => ({ data: { foo: 'bar' } }),
    };
    instance1.hasEndpoint = () => true;
    const fetchInfoSpy = vi
      .fn()
      .mockResolvedValue({ data: { app: { version: '1.0.0' } } });
    instance1.fetchInfo = fetchInfoSpy;

    const { rerender } = render(DetailsInfo, {
      props: { instance: instance1 },
    });

    await waitFor(() => {
      expect(fetchInfoSpy).toHaveBeenCalled();
    });

    // Now rerender with a new instance (different id)
    await rerender({ instance: instance2 });

    // Should show the new info from instance2
    expect(await screen.findByText('foo')).toBeVisible();
    expect(await screen.findByText('bar')).toBeVisible();
  });
});


================================================
FILE: spring-boot-admin-server-ui/src/main/frontend/views/instances/details/details-info.vue
================================================









================================================
FILE: spring-boot-admin-server-ui/src/main/frontend/views/instances/details/details-memory.spec.ts
================================================
import { screen } from '@testing-library/vue';
import rxjs from 'rxjs';
import { beforeEach, describe, expect, it, vi } from 'vitest';

import { render } from '@/test-utils';
import DetailsMemory from '@/views/instances/details/details-memory.vue';

vi.mock('@/sba-config', async () => {
  const sbaConfig: any = await vi.importActual('@/sba-config');
  return {
    default: {
      ...sbaConfig.default,
      uiSettings: {
        pollTimer: {
          memory: 1234,
        },
      },
    },
  };
});

describe('DetailsMemory', () => {
  it('should call timer with configured amount', async () => {
    const timer = vi.spyOn(rxjs, 'timer');

    const Instance = (await import('@/services/instance')).default;
    render(DetailsMemory, {
      stubs: {
        MemChart: true,
      },
      props: {
        instance: new Instance({ id: '1' }),
        type: 'heap',
      },
    });

    expect(timer).toHaveBeenCalledWith(0, 1234);
  });

  describe('when type is heap', async () => {
    const Instance = (await import('@/services/instance')).default;

    beforeEach(() => {
      render(DetailsMemory, {
        stubs: {
          MemChart: true,
        },
        props: {
          instance: new Instance({
            id: '1',
            availableMetrics: [
              'jvm.memory.used',
              'jvm.memory.max',
              'jvm.memory.committed',
            ],
          }),
          type: 'heap',
        },
      });
    });

    it('should render memory used', async () => {
      expect(
        await screen.findByLabelText('instances.details.memory.used'),
      ).toHaveTextContent('115 MB');
    });

    it('should render memory size', async () => {
      expect(
        await screen.findByLabelText('instances.details.memory.size'),
      ).toHaveTextContent('197 MB');
    });

    it('should render memory max', async () => {
      expect(
        await screen.findByLabelText('instances.details.memory.max'),
      ).toHaveTextContent('8.59 GB');
    });

    it('should not render memory metaspace', async () => {
      expect(
        screen.queryByLabelText('instances.details.memory.metaspace'),
      ).not.toBeInTheDocument();
    });
  });

  describe('when type is nonheap', async () => {
    const Instance = (await import('@/services/instance')).default;

    beforeEach(() => {
      render(DetailsMemory, {
        stubs: {
          MemChart: true,
        },
        props: {
          instance: new Instance({
            id: '1',
            availableMetrics: [
              'jvm.memory.used',
              'jvm.memory.max',
              'jvm.memory.committed',
            ],
          }),
          type: 'nonheap',
        },
      });
    });

    it('should render memory used', async () => {
      expect(
        await screen.findByLabelText('instances.details.memory.used'),
      ).toHaveTextContent('115 MB');
    });

    it('should render memory size', async () => {
      expect(
        await screen.findByLabelText('instances.details.memory.size'),
      ).toHaveTextContent('197 MB');
    });

    it('should render memory max', async () => {
      expect(
        await screen.findByLabelText('instances.details.memory.max'),
      ).toHaveTextContent('8.59 GB');
    });

    it('should render memory metaspace', async () => {
      expect(
        await screen.findByLabelText('instances.details.memory.metaspace'),
      ).toHaveTextContent('115 MB');
    });
  });
});


================================================
FILE: spring-boot-admin-server-ui/src/main/frontend/views/instances/details/details-memory.vue
================================================









================================================
FILE: spring-boot-admin-server-ui/src/main/frontend/views/instances/details/details-metadata.spec.ts
================================================
import { screen } from '@testing-library/vue';
import { describe, expect, it } from 'vitest';

import DetailsMetadata from './details-metadata.vue';

import { applications } from '@/mocks/applications/data';
import Application from '@/services/application';
import Instance from '@/services/instance';
import { render } from '@/test-utils';

describe('DetailsMetadata', () => {
  it('should render metadata table with keys and values', () => {
    const application = new Application(applications[0]);
    const instance = application.instances[0];

    render(DetailsMetadata, {
      props: { instance },
    });

    expect(screen.getByText('startup')).toBeVisible();
    expect(screen.getByText('2021-10-29T08:50:07.486289+02:00')).toBeVisible();
    expect(screen.getByText('tags.environment')).toBeVisible();
    expect(screen.getByText('test')).toBeVisible();
  });

  it('should show metadata count in title', () => {
    const application = new Application(applications[0]);
    const instance = application.instances[0];

    render(DetailsMetadata, {
      props: { instance },
    });

    expect(screen.getByText('(2)')).toBeVisible();
  });

  it('should show no metadata message if metadata is empty', () => {
    const application = new Application(applications[0]);
    const instance = new Instance({
      ...application.instances[0],
      registration: {
        ...application.instances[0].registration,
        metadata: {},
      },
    });

    render(DetailsMetadata, {
      props: { instance },
    });

    expect(
      screen.getByText('instances.details.metadata.no_data_provided'),
    ).toBeVisible();
  });

  it('should show count as (0) when metadata is empty', () => {
    const application = new Application(applications[0]);
    const instance = new Instance({
      ...application.instances[0],
      registration: {
        ...application.instances[0].registration,
        metadata: {},
      },
    });

    render(DetailsMetadata, {
      props: { instance },
    });

    expect(screen.getByText('(0)')).toBeVisible();
  });

  it('should sort metadata keys alphabetically', () => {
    const application = new Application(applications[0]);
    const instance = new Instance({
      ...application.instances[0],
      registration: {
        ...application.instances[0].registration,
        metadata: {
          zebra: 'value1',
          apple: 'value2',
          banana: 'value3',
        },
      },
    });

    render(DetailsMetadata, {
      props: { instance },
    });

    const keys = screen.getAllByRole('term');
    expect(keys[0]).toHaveTextContent('apple');
    expect(keys[1]).toHaveTextContent('banana');
    expect(keys[2]).toHaveTextContent('zebra');
  });
});


================================================
FILE: spring-boot-admin-server-ui/src/main/frontend/views/instances/details/details-metadata.vue
================================================







================================================
FILE: spring-boot-admin-server-ui/src/main/frontend/views/instances/details/details-nav.vue
================================================





================================================
FILE: spring-boot-admin-server-ui/src/main/frontend/views/instances/details/details-process.vue
================================================







================================================
FILE: spring-boot-admin-server-ui/src/main/frontend/views/instances/details/details-threads.spec.ts
================================================
import { screen } from '@testing-library/vue';
import { HttpResponse, http } from 'msw';
import { beforeEach, describe, expect, it, vi } from 'vitest';

import { applications } from '@/mocks/applications/data';
import { server } from '@/mocks/server';
import Application from '@/services/application';
import { render } from '@/test-utils';
import DetailsThreads from '@/views/instances/details/details-threads.vue';

vi.mock('@/sba-config', async () => {
  const sbaConfig: any = await vi.importActual('@/sba-config');
  return {
    default: {
      ...sbaConfig.default,
      uiSettings: {
        pollTimer: {
          threads: 100,
        },
      },
    },
  };
});

describe('DetailsThreads', () => {
  const LIVE = [10.0, 12.0, 14.0];
  const PEAK = [15.0, 16.0, 18.0];
  const DAEMON = [4.0, 5.0, 6.0];

  beforeEach(() => {
    const liveGen = (function* () {
      yield* LIVE;
    })();
    const peakGen = (function* () {
      yield* PEAK;
    })();
    const daemonGen = (function* () {
      yield* DAEMON;
    })();

    server.use(
      http.get('/instances/:instanceId/actuator/metrics', () => {
        return HttpResponse.json({
          names: ['jvm.threads.live', 'jvm.threads.peak', 'jvm.threads.daemon'],
        });
      }),
      http.get(
        '/instances/:instanceId/actuator/metrics/jvm.threads.live',
        () => {
          return HttpResponse.json({
            measurements: [{ value: liveGen.next()?.value }],
          });
        },
      ),
      http.get(
        '/instances/:instanceId/actuator/metrics/jvm.threads.peak',
        () => {
          return HttpResponse.json({
            measurements: [{ value: peakGen.next()?.value }],
          });
        },
      ),
      http.get(
        '/instances/:instanceId/actuator/metrics/jvm.threads.daemon',
        () => {
          return HttpResponse.json({
            measurements: [{ value: daemonGen.next()?.value }],
          });
        },
      ),
    );
  });

  const renderComponent = async () => {
    const stubChart = {
      props: ['data'],
      template: `
        
{{ JSON.stringify($props.data) }}
`, }; const application = new Application(applications[0]); const instance = application.instances[0]; return render(DetailsThreads, { global: { stubs: { threadsChart: stubChart } }, props: { instance }, }); }; it('pushes chartData points and exposes them via stub', async () => { await renderComponent(); const text = (await screen.findByTestId('chart')).textContent; const data = JSON.parse(text); // first sample matches generators expect(data[0].live).toEqual(LIVE[0]); expect(data[0].peak).toEqual(PEAK[0]); expect(data[0].daemon).toEqual(DAEMON[0]); }); it('should reinitialize metrics when instance changes', async () => { const { rerender } = await renderComponent(); const newApp = new Application({ name: 'Other', statusTimestamp: Date.now(), instances: [{ id: 'other-1', statusInfo: { status: 'UP' } }], }); const newInstance = newApp.instances[0]; await rerender({ instance: newInstance }); const text = (await screen.findByTestId('chart')).textContent; const data = JSON.parse(text); // first sample matches generators expect(data[0].live).toEqual(LIVE[0]); expect(data[0].peak).toEqual(PEAK[0]); expect(data[0].daemon).toEqual(DAEMON[0]); }); }); ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/views/instances/details/details-threads.vue ================================================ ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/views/instances/details/health-details.spec.ts ================================================ import { screen, within } from '@testing-library/vue'; import { beforeEach, describe, expect, it } from 'vitest'; import { render } from '@/test-utils'; import HealthDetails from '@/views/instances/details/health-details.vue'; describe('HealthDetails', () => { describe('Health .details', () => { beforeEach(() => { const healthMock = { status: 'UP', details: { clientConfigServer: { status: 'UNKNOWN', details: { error: 'no property sources located' }, }, db: { status: 'UP', details: { database: 'HSQL Database Engine', validationQuery: 'isValid()', }, }, discoveryComposite: { description: 'Discovery Client not initialized', status: 'UNKNOWN', details: { discoveryClient: { description: 'Discovery Client not initialized', status: 'DOWN', }, }, }, diskSpace: { status: 'UP', details: { total: 994662584320, free: 300063879168, threshold: 10485760, exists: true, }, }, diskSpace2: { status: 'UP', details: { total: 1024, free: 2048, threshold: 4096, exists: false, }, }, ssl: { status: 'UP', details: { validChains: JSON.parse( '[{"status": "VALID", "chain": [{"subject": "CN=example.com, OU=IT, O=Example Corp, L=San Francisco, ST=CA, C=US", "issuer": "CN=R3, O=Let\'s Encrypt, C=US"}]}]', ), }, }, }, }; render(HealthDetails, { props: { name: 'Name', health: healthMock, }, }); }); it.each` componentId | status ${'clientConfigServer'} | ${'UNKNOWN'} ${'discoveryComposite'} | ${'UNKNOWN'} ${'discoveryClient'} | ${'DOWN'} ${'diskSpace'} | ${'UP'} `( 'should display health components status', async ({ componentId, status }) => { const clientConfigServer = await screen.findByRole('definition', { name: componentId, }); expect( await within(clientConfigServer).findByRole('status'), ).toHaveTextContent(status); }, ); it('should format diskSpace details correctly', async () => { const diskSpaceInfo = await screen.findByRole('definition', { name: 'diskSpace2', }); // Assert pretty-printed numbers via pretty-bytes and other primitive values const dsi = within(diskSpaceInfo); // total: 994662584320 bytes -> 995 GB (rounded) expect( await dsi.findByRole('definition', { name: 'total' }), ).toHaveTextContent('1.02 kB'); // free: 300063879168 bytes -> 300 GB (rounded) expect( await dsi.findByRole('definition', { name: 'free' }), ).toHaveTextContent('2.05 kB'); // threshold: 10485760 bytes -> 10 MB expect( await dsi.findByRole('definition', { name: 'threshold' }), ).toHaveTextContent('4.1 kB'); // exists: boolean unchanged expect( await dsi.findByRole('definition', { name: 'exists' }), ).toHaveTextContent('false'); }); it('should format object details correctly', async () => { const sslInfo = await screen.findByRole('definition', { name: 'validChains', }); expect(sslInfo).toMatchSnapshot(); }); }); describe('Health .components', () => { beforeEach(() => { const healthMock = { status: 'UP', components: { clientConfigServer: { status: 'UNKNOWN', details: { error: 'no property sources located' }, }, discoveryComposite: { description: 'Discovery Client not initialized', status: 'UNKNOWN', components: { discoveryClient: { description: 'Discovery Client not initialized', status: 'DOWN', }, }, }, diskSpace: { status: 'UP', details: { total: 994662584320, free: 116363821056, threshold: 10485760, exists: true, }, }, }, }; render(HealthDetails, { props: { health: healthMock, }, }); }); it('should display health status', async () => { expect( screen.getByRole('definition', { name: 'clientConfigServer' }), ).toBeInTheDocument(); }); it.each` componentId | status ${'clientConfigServer'} | ${'UNKNOWN'} ${'discoveryComposite'} | ${'UNKNOWN'} ${'discoveryClient'} | ${'DOWN'} ${'diskSpace'} | ${'UP'} `( 'should display health components status', async ({ componentId, status }) => { const clientConfigServer = await screen.findByRole('definition', { name: componentId, }); expect( await within(clientConfigServer).findByRole('status'), ).toHaveTextContent(status); }, ); }); }); ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/views/instances/details/health-details.vue ================================================ ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/views/instances/details/i18n.de.json ================================================ { "instances": { "details": { "label": "Details", "cache": { "hit_ratio": "Treffer-Rate", "hits": "Treffer", "misses": "Fehltreffer", "size": "Größe" }, "datasource": { "active_connections": "Aktive Verbindungen", "min_connections": "Min. Verbindungen", "max_connections": "Max. Verbindungen", "title": "Datenquelle: {dataSource}", "unlimited": "unlimitiert" }, "gc": { "count": "Anzahl", "time_spent_max": "Max. Zeitdauer", "time_spent_total": "Gesamtzeit", "title": "GC Pausen" }, "health": { "title": "Zustand" }, "health_group": { "title": "Zustandsgruppe" }, "info": { "no_info_provided": "Keine Informationen verfügbar.", "title": "Info" }, "memory": { "max": "Max", "metaspace": "Metaspace", "size": "Größe", "title": "Speicher", "used": "In Verwendung", "committed": "Zugesichert" }, "metadata": { "no_data_provided": "Keine Metadaten vorhanden", "title": "Metadaten" }, "process": { "cpus": "CPUs", "pid": "PID", "parent_pid": "Eltern-PID", "owner": "Besitzer", "process_cpu_usage": "CPU-Auslastung (%)", "system_cpu_usage": "System-Auslastung (%)", "title": "Prozess", "uptime": "Uptime" }, "threads": { "daemon": "Daemon", "live": "Live", "peak_live": "Peak Live", "title": "Threads" } } } } ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/views/instances/details/i18n.en.json ================================================ { "instances": { "details": { "label": "Details", "cache": { "hit_ratio": "Hit ratio", "hits": "Hits", "misses": "Misses", "size": "Size" }, "datasource": { "active_connections": "Active connections", "min_connections": "Min connections", "max_connections": "Max connections", "title": "Datasource: {dataSource}", "unlimited": "unlimited" }, "gc": { "count": "Count", "count_short": "#", "time_spent_max": "Max time spent", "time_spent_max_short": "Max", "time_spent_total": "Total time spent", "time_spent_total_short": "Total", "title": "Garbage Collection Pauses" }, "health": { "title": "Health" }, "health_group": { "title": "Health Group" }, "info": { "no_info_provided": "No info provided.", "title": "Info" }, "memory": { "max": "Max", "metaspace": "Metaspace", "size": "Size", "title": "Memory", "used": "Used", "committed": "Committed" }, "metadata": { "no_data_provided": "No metadata provided.", "title": "Metadata" }, "process": { "cpus": "CPUs", "pid": "PID", "parent_pid": "Parent PID", "owner": "Owner", "process_cpu_usage": "Process CPU Usage", "process_cpu_usage_short": "CPU", "system_cpu_usage": "System CPU Usage", "system_cpu_usage_short": "Sys", "title": "Process", "uptime": "Uptime", "uptime_short": "Up" }, "threads": { "daemon": "Daemon", "live": "Live", "peak_live": "Peak Live", "title": "Threads" } } } } ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/views/instances/details/i18n.es.json ================================================ { "instances": { "details": { "label": "Detalles", "cache": { "hit_ratio": "Índice de aciertos", "hits": "Aciertos", "misses": "Fallas", "size": "Tamaño" }, "datasource": { "active_connections": "Conexiones activas", "min_connections": "Conexiones Min", "max_connections": "Conexiones Max ", "title": "Fuente de datos: {dataSource}", "unlimited": "Sin límites" }, "gc": { "count": "Cantidad", "time_spent_max": "Tiempo máximo utilizado", "time_spent_total": "Tiempo total utilizado", "title": "Pausas en la recolección de basura" }, "health": { "title": "Salud" }, "health_group": { "title": "Grupo de Salud" }, "info": { "no_info_provided": "No hay información provista.", "title": "Info" }, "memory": { "max": "Max", "metaspace": "Metaspace", "size": "Tamaño", "title": "Memoria", "used": "Usada", "committed": "Comprometida" }, "metadata": { "no_data_provided": "No hay metadatos provistos.", "title": "Metadatos" }, "process": { "cpus": "CPUs", "pid": "PID", "parent_pid": "PID Padre", "owner": "Propietario", "process_cpu_usage": "Uso CPU proceso", "system_cpu_usage": "Uso CPU sistema", "title": "Proceso", "uptime": "Tiempo de actividad" }, "threads": { "daemon": "Daemon", "live": "En vivo", "peak_live": "Pico en vivo", "title": "Hilos" } } } } ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/views/instances/details/i18n.fr.json ================================================ { "instances": { "details": { "label": "Détails", "cache": { "hit_ratio": "Hit ratio", "hits": "Hits", "misses": "Échecs", "size": "Taille" }, "datasource": { "active_connections": "Connexions actives", "min_connections": "Connexions min", "max_connections": "Connexions max", "title": "Datasource: {dataSource}", "unlimited": "illimité" }, "gc": { "count": "Count", "time_spent_max": "Temps max passé", "time_spent_total": "Total du temps passé", "title": "Pauses du Garbage Collector" }, "health": { "title": "État" }, "health_group": { "title": "Groupe d'État" }, "info": { "no_info_provided": "Aucune infos fournies.", "title": "Info" }, "memory": { "max": "Max", "metaspace": "Métaspace", "size": "Taille", "title": "Memoire", "used": "Utilisé", "committed": "Engagé" }, "metadata": { "no_data_provided": "Aucune métadonnées fournies.", "title": "Métadonnées" }, "process": { "cpus": "CPUs", "pid": "PID", "parent_pid": "PID Parent", "owner": "Propriétaire", "process_cpu_usage": "Usage processus du CPU", "system_cpu_usage": "Usage Système du CPU", "title": "Processus", "uptime": "Disponibilité" }, "threads": { "daemon": "Démon", "live": "Actifs", "peak_live": "max actifs", "title": "Threads" } } } } ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/views/instances/details/i18n.is.json ================================================ { "instances": { "details": { "label": "Smáatriði", "cache": { "hit_ratio": "Hit ratio", "hits": "Hits", "misses": "Misses", "size": "Stærð" }, "datasource": { "active_connections": "Virk tengsl", "min_connections": "Lágmarksfjöldi af tengslum", "max_connections": "Hámarksfjöldi af tengslunum", "title": "Gagnagjafi: {dataSource}", "unlimited": "ótakmarkaður" }, "gc": { "count": "Fjöldi", "time_spent_max": "Hámarkstímalengd", "time_spent_total": "Heildartími", "title": "GC hlé" }, "health": { "title": "Staða" }, "health_group": { "title": "Stöðuhópur" }, "info": { "no_info_provided": "Engar upplýsingar eru í boði.", "title": "Upplýsingar" }, "memory": { "max": "Hámark", "metaspace": "Metaspace", "size": "Stærð", "title": "Geymir", "used": "I notkun", "committed": "Skuldbundið" }, "metadata": { "no_data_provided": "Engin lysigögn i boði.", "title": "Lysigögn" }, "process": { "cpus": "CPUs", "pid": "PID", "parent_pid": "Foreldri PID", "owner": "Eigandi", "process_cpu_usage": "CPU notkun (%)", "system_cpu_usage": "Kerfi notkun (%)", "title": "Ferli", "uptime": "Uptime" }, "threads": { "daemon": "Þjónn (daemon)", "live": "Live", "peak_live": "Peak Live", "title": "Þræðir" } } } } ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/views/instances/details/i18n.ko.json ================================================ { "instances": { "details": { "label": "상세", "cache": { "hit_ratio": "히트율", "hits": "히트", "misses": "미스", "size": "사이즈" }, "datasource": { "active_connections": "활성 커넥션", "min_connections": "최소 커넥션", "max_connections": "최대 커넥션", "title": "데이터소스: {dataSource}", "unlimited": "무제한" }, "gc": { "count": "횟수", "time_spent_max": "최대 소요 시간", "time_spent_total": "전체 소요 시간", "title": "가비지 컬렉션" }, "health": { "title": "상태" }, "health_group": { "title": "상태 그룹" }, "info": { "no_info_provided": "정보가 제공되지 않습니다.", "title": "정보" }, "memory": { "max": "최대", "metaspace": "메타스페이스", "size": "사이즈", "title": "메모리", "used": "사용됨", "committed": "커밋됨" }, "metadata": { "no_data_provided": "메타데이터가 제공되지 않습니다.", "title": "메타데이터" }, "process": { "cpus": "CPU", "pid": "PID", "parent_pid": "부모 PID", "owner": "소유자", "process_cpu_usage": "프로세스 CPU 사용량", "system_cpu_usage": "시스템 CPU 사용량", "title": "프로세스", "uptime": "가동시간" }, "threads": { "daemon": "데몬", "live": "활성", "peak_live": "최대 활성", "title": "스레드" } } } } ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/views/instances/details/i18n.pt-BR.json ================================================ { "instances": { "details": { "label": "Detalhes", "cache": { "hit_ratio": "Hit ratio", "hits": "Hits", "misses": "Falhas", "size": "Tamanho" }, "datasource": { "active_connections": "Conexões ativas", "min_connections": "Conexões mínimas", "max_connections": "Conexões máximas", "title": "Datasource: {dataSource}", "unlimited": "ilimitado" }, "gc": { "count": "Contador", "time_spent_max": "Tempo máximo gasto", "time_spent_total": "Tempo total gasto", "title": "Pausas do Garbage Collection" }, "health": { "title": "Integridade" }, "health_group": { "title": "Grupo de Integridade" }, "info": { "no_info_provided": "Nenhuma informação fornecida.", "title": "Info" }, "memory": { "max": "Máximo", "metaspace": "Metaspace", "size": "Tamanho", "title": "Memoria", "used": "Usado", "committed": "Comprometido" }, "metadata": { "no_data_provided": "Nenhum metadado fornecido.", "title": "Metadado" }, "process": { "cpus": "CPUs", "pid": "PID", "parent_pid": "PID Pai", "owner": "Proprietário", "process_cpu_usage": "Uso de Process CPU", "system_cpu_usage": "Uso de System CPU", "title": "Processo", "uptime": "Uptime" }, "threads": { "daemon": "Daemon", "live": "Ativos", "peak_live": "Peak Live", "title": "Threads" } } } } ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/views/instances/details/i18n.ru.json ================================================ { "instances": { "details": { "label": "Подробности", "cache": { "hit_ratio": "Коэффициент попадания", "hits": "Попадания", "misses": "Промахи", "size": "Размер" }, "datasource": { "active_connections": "Активные подключения", "min_connections": "Минимальные подключения", "max_connections": "Максимальные подключения", "title": "Datasource: {dataSource}", "unlimited": "неограниченный" }, "gc": { "count": "Количество", "time_spent_max": "Максимум потраченного времени", "time_spent_total": "Общее потраченное время", "title": "Паузы сборщика мусора" }, "health": { "title": "Здоровье" }, "health_group": { "title": "Группа здоровья" }, "info": { "no_info_provided": "Информация не предоставлена.", "title": "Информация" }, "memory": { "max": "Максимум", "metaspace": "Metaspace", "size": "Размер", "title": "Память", "used": "Используется", "committed": "Зарезервировано" }, "metadata": { "no_data_provided": "Метаданные не предоставлены.", "title": "Метаданные" }, "process": { "cpus": "Процессоры", "pid": "PID (Идентификатор процесса)", "parent_pid": "Родительский PID", "owner": "Владелец", "process_cpu_usage": "Использование процессами", "system_cpu_usage": "Использование системой", "title": "Процесс", "uptime": "Время работы" }, "threads": { "daemon": "Демоны", "live": "Живые потоки", "peak_live": "Максимум живых потоков", "title": "Потоки" } } } } ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/views/instances/details/i18n.zh-CN.json ================================================ { "instances": { "details": { "label": "细节", "cache": { "hit_ratio": "命中率", "hits": "命中次数", "misses": "未命中", "size": "粒度" }, "datasource": { "active_connections": "活动连接数", "min_connections": "最小连接数", "max_connections": "最大连接数", "title": "数据源: {dataSource}", "unlimited": "unlimited" }, "gc": { "count": "总计", "time_spent_max": "最大耗时", "time_spent_total": "总耗时", "title": "垃圾回收" }, "health": { "title": "健康" }, "health_group": { "title": "健康组" }, "info": { "no_info_provided": "未提供任何信息。", "title": "信息" }, "memory": { "max": "最大", "metaspace": "初始空间", "size": "当前可用", "title": "内存", "used": "已用", "committed": "已提交" }, "metadata": { "no_data_provided": "未提供元数据。", "title": "元数据" }, "process": { "cpus": "CPU核心数", "pid": "进程ID", "parent_pid": "父进程ID", "owner": "所有者", "process_cpu_usage": "进程CPU使用率", "system_cpu_usage": "系统CPU使用率", "title": "进程", "uptime": "运行时间" }, "threads": { "daemon": "守护线程", "live": "活动线程", "peak_live": "线程峰值", "title": "线程" } } } } ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/views/instances/details/i18n.zh-TW.json ================================================ { "instances": { "details": { "label": "詳細資訊", "cache": { "hit_ratio": "命中率", "hits": "命中次數", "misses": "未命中次數", "size": "大小" }, "datasource": { "active_connections": "使用中連線數", "min_connections": "最小連線數", "max_connections": "最大連線數", "title": "資料來源:{dataSource}", "unlimited": "無限制" }, "gc": { "count": "次數", "count_short": "#", "time_spent_max": "最大耗時", "time_spent_max_short": "最大", "time_spent_total": "總耗時", "time_spent_total_short": "總計", "title": "垃圾回收暫停" }, "health": { "title": "健康狀態" }, "health_group": { "title": "健康群組" }, "info": { "no_info_provided": "未提供任何資訊。", "title": "資訊" }, "memory": { "max": "最大值", "metaspace": "Metaspace", "size": "大小", "title": "記憶體", "used": "已使用", "committed": "已配置" }, "metadata": { "no_data_provided": "未提供中繼資料。", "title": "中繼資料" }, "process": { "cpus": "CPU 核心數", "pid": "PID", "parent_pid": "父程序 PID", "owner": "擁有者", "process_cpu_usage": "程序 CPU 使用率", "process_cpu_usage_short": "CPU", "system_cpu_usage": "系統 CPU 使用率", "system_cpu_usage_short": "系統", "title": "程序", "uptime": "運作時間", "uptime_short": "運作" }, "threads": { "daemon": "背景執行緒", "live": "執行中", "peak_live": "峰值", "title": "執行緒" } } } } ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/views/instances/details/index.spec.ts ================================================ import { screen, waitFor } from '@testing-library/vue'; import { HttpResponse, http } from 'msw'; import { describe, expect, it, vi } from 'vitest'; import DetailsView from './index.vue'; import { applications } from '@/mocks/applications/data'; import { server } from '@/mocks/server'; import Application from '@/services/application'; import { render } from '@/test-utils'; describe('InstanceDetails', () => { describe('Metrics', () => { it('should hide loading spinner, when network call fails', async () => { const application = new Application(applications[0]); const instance = application.instances[0]; server.use( http.get('/instances/:instanceId/actuator/metrics', () => { return HttpResponse.json({}); }), ); render(DetailsView, { props: { instance, application, }, }); await waitFor(async () => { expect( await screen.queryByTestId('instance-section-loading-spinner'), ).not.toBeInTheDocument(); }); }); it('should hide loading spinner, when metrics endpoint is not exposed', async () => { const application = new Application(applications[0]); const instance = application.instances[0]; instance.hasEndpoint = vi.fn().mockImplementation(function (endpoint) { return endpoint !== 'metrics'; }); render(DetailsView, { props: { instance, application, }, }); await waitFor(async () => { expect( await screen.queryByTestId('instance-section-loading-spinner'), ).not.toBeInTheDocument(); }); }); }); }); ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/views/instances/details/index.vue ================================================ ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/views/instances/details/instance-switcher.vue ================================================ ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/views/instances/details/mem-chart.vue ================================================ ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/views/instances/details/process-uptime.ts ================================================ /* * 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 moment from 'moment'; import { h } from 'vue'; import subscribing from '../../../mixins/subscribing'; import { timer } from '@/utils/rxjs'; export default { props: ['value'], mixins: [subscribing], data: () => ({ startTs: null, offset: null, }), render() { return h('span', this.clock); }, computed: { clock() { if (!this.value) { return null; } const duration = moment.duration(this.value + this.offset); return `${Math.floor( duration.asDays(), )}d ${duration.hours()}h ${duration.minutes()}m ${duration.seconds()}s`; }, }, watch: { value: 'subscribe', }, methods: { createSubscription() { if (this.value) { this.startTs = moment(); this.offset = 0; return timer(0, 1000).subscribe({ next: () => { this.offset = moment().valueOf() - this.startTs.valueOf(); }, }); } }, }, }; ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/views/instances/details/threads-chart.vue ================================================ ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/views/instances/env/busrefresh.spec.ts ================================================ import userEvent from '@testing-library/user-event'; import { screen } from '@testing-library/vue'; import { HttpResponse, http } from 'msw'; import { beforeEach, describe, expect, it, vi } from 'vitest'; import { server } from '@/mocks/server'; import Instance from '@/services/instance'; import { render } from '@/test-utils'; import Busrefresh from '@/views/instances/env/busrefresh.vue'; describe('Busrefresh', () => { const busRefreshInstanceContextMock = vi.fn(); let emitted; function createInstance() { const instance = new Instance({ id: 1233 }); instance.busRefreshContext = busRefreshInstanceContextMock; return instance; } beforeEach(async () => { server.use( http.post('/instances/:instanceId/actuator/busrefresh', () => { return HttpResponse.json({}); }), ); const vm = render(Busrefresh, { props: { instance: createInstance(), }, }); emitted = vm.emitted; }); it('should trigger busrefresh on confirm', async () => { busRefreshInstanceContextMock.mockResolvedValue({}); const busRefreshButton = await screen.findByText( 'instances.env.bus_refresh', ); await userEvent.click(busRefreshButton); const confirmButton = await screen.findByText('Confirm'); await userEvent.click(confirmButton); expect(emitted().refresh[0][0]).toBe(true); }); it('should handle busrefresh failure gracefully', async () => { busRefreshInstanceContextMock.mockRejectedValueOnce(new Error()); const busRefreshButton = await screen.findByText( 'instances.env.bus_refresh', ); await userEvent.click(busRefreshButton); const confirmButton = await screen.findByText('Confirm'); await userEvent.click(confirmButton); expect(emitted().refresh).toBeUndefined(); }); }); ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/views/instances/env/busrefresh.vue ================================================ ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/views/instances/env/env-manager.vue ================================================ ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/views/instances/env/i18n.de.json ================================================ { "instances": { "env": { "label": "Umgebungskonfiguration", "manager": "Umgebungskonfiguration anpassen", "active_profile": "Profil", "context_refresh": "Kontext neu laden", "bus_refresh": "Spring Cloud Bus-refresh", "bus_refresh_success": "Spring Cloud Bus Refresh erfolgreich!", "bus_refresh_failure": "Fehlgeschlagen: Konnte Spring Cloud Bus Refresh nicht ausführen!", "bus_refresh_title": "Sendet eine Refresh Message an den konfigurierten Spring Cloud Bus, der eine Refresh Message an alle verbundenen Nodes sendet", "context_refresh_failed": "Fehlgeschlagen", "context_refreshed": "Erfolreich!", "context_reset": "Zurücksetzen", "context_reset_failed": "Fehlgeschlagen", "context_resetted": "Zurückgesetzt", "context_update": "Aktualisieren", "context_update_failed": "Fehlgeschlagen", "context_updated": "Aktualisiert", "no_properties": "Keine Eigenschaften gesetzt", "refresh": "Umgebungskonfiguration neu laden", "application": "Anwendung", "instance": "Instanz", "refreshed_configurations": "Aktualisierte Konfigurationen:" } } } ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/views/instances/env/i18n.en.json ================================================ { "instances": { "env": { "label": "Environment", "manager": "Environment Manager", "active_profile": "Profile", "context_refresh": "Refresh context", "bus_refresh": "Spring Cloud Bus-refresh", "bus_refresh_success": "Spring Cloud Bus successfully refreshed.", "bus_refresh_failure": "Failure: Unable to trigger Spring Cloud Bus refresh!", "bus_refresh_title": "Sends a refresh message to the configured Spring Cloud Bus which broadcasts a refresh to all nodes listening", "context_refresh_failed": "Failed", "context_refreshed": "Context refreshed", "context_reset": "Reset", "context_reset_failed": "Failed", "context_resetted": "Resetted", "context_update": "Update", "context_update_failed": "Failed", "context_updated": "Updated", "no_properties": "No properties set", "title": "Environment Manager", "refresh": "Refresh environment", "application": "Application", "instance": "Instance", "refreshed_configurations": "Refreshed configurations:" } } } ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/views/instances/env/i18n.es.json ================================================ { "instances": { "env": { "label": "Ambiente", "manager": "Administrador de configuración", "active_profile": "Perfil", "context_refresh": "Refrescar contexto", "context_refresh_failed": "Fallido", "context_refreshed": "Contexto actualizado", "context_reset": "Reiniciar", "context_reset_failed": "Fallido", "context_resetted": "Reiniciado", "context_update": "Actualizar", "context_update_failed": "Fallido", "context_updated": "Actualizado", "no_properties": "No hay propiedades configuradas", "title": "Administrador de configuración de ambiente", "refresh": "Refrescar configuración", "application": "Aplicación", "instance": "Instancia" } } } ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/views/instances/env/i18n.fr.json ================================================ { "instances": { "env": { "label": "Environnement", "manager": "Gestionnaire d'environnement", "active_profile": "Profil", "context_refresh": "Rafraîchir le contexte", "context_refresh_failed": "Echec", "context_refreshed": "Contexte rafraîchi", "context_reset": "Réinitialiser", "context_reset_failed": "Echec", "context_resetted": "Réinitialisé", "context_update": "Mettre à jour", "context_update_failed": "Echec", "context_updated": "Mis à jour", "no_properties": "Pas de propriétés définies", "title": "Gestion d'environnement", "refresh": "Refresh environment", "application": "Application", "instance": "Instance" } } } ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/views/instances/env/i18n.is.json ================================================ { "instances": { "env": { "label": "Umhverfi", "manager": "Umhverfisstjórnandi", "active_profile": "Profile", "context_refresh": "Uppfæra samhengi", "context_refresh_failed": "Mistekist", "context_refreshed": "Samhengi uppfært", "context_reset": "Núllstilla", "context_reset_failed": "Mistekist", "context_resetted": "Núllstillt", "context_update": "Uppfærsla", "context_update_failed": "Mistekist", "context_updated": "Uppfært", "no_properties": "Engir eiginleikar stillt", "title": "Umhverfisstjórnandi", "refresh": "Refresh environment", "application": "Application", "instance": "Instance" } } } ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/views/instances/env/i18n.ko.json ================================================ { "instances": { "env": { "label": "환경", "manager": "환경 관리자", "active_profile": "프로파일", "context_refresh": "컨텍스트 갱신", "context_refresh_failed": "실패", "context_refreshed": "컨텍스트가 갱신되었습니다.", "context_reset": "초기화", "context_reset_failed": "실패", "context_resetted": "초기화됨", "context_update": "갱신", "context_update_failed": "갱신실패", "context_updated": "갱신됨", "no_properties": "프로퍼티 없음", "title": "환경 관리자", "refresh": "환경 새로고침", "application": "애플리케이션", "instance": "인스턴스" } } } ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/views/instances/env/i18n.pt-BR.json ================================================ { "instances": { "env": { "label": "Ambiente", "manager": "Gerenciador de Ambiente", "active_profile": "Perfil", "context_refresh": "Atualizar contexto", "context_refresh_failed": "Falha", "context_refreshed": "Contexto atualizado", "context_reset": "Redefinir", "context_reset_failed": "Falhou", "context_resetted": "Redefinido", "context_update": "Atualizado", "context_update_failed": "Falhou", "context_updated": "Atualizado", "no_properties": "Nenhuma propriedade definida", "title": "Gerenciador de Ambiente", "refresh": "Refresh environment", "application": "Application", "instance": "Instance" } } } ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/views/instances/env/i18n.ru.json ================================================ { "instances": { "env": { "label": "Окружение", "manager": "Менеджер окружения", "active_profile": "Профиль", "context_refresh": "Обновление контекста", "context_refresh_failed": "Ошибка", "context_refreshed": "Контекст обновлен", "context_reset": "Сброс", "context_reset_failed": "Ошибка", "context_resetted": "Перезапущен", "context_update": "Обновить", "context_update_failed": "Ошибка", "context_updated": "Обновлено", "no_properties": "Параметры не установлены", "title": "Менеджер окружения", "refresh": "Refresh environment", "application": "Application", "instance": "Instance" } } } ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/views/instances/env/i18n.zh-CN.json ================================================ { "instances": { "env": { "label": "环境", "manager": "环境管理", "active_profile": "配置文件", "context_refresh": "刷新内容", "context_refresh_failed": "失败", "context_refreshed": "内容已刷新", "context_reset": "重置", "context_reset_failed": "失败", "context_resetted": "已重置", "context_update": "更新", "context_update_failed": "失败", "context_updated": "已更新", "no_properties": "属性未设置。", "title": "环境管理", "refresh": "Refresh environment", "application": "Application", "instance": "Instance" } } } ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/views/instances/env/i18n.zh-TW.json ================================================ { "instances": { "env": { "label": "環境變數", "manager": "環境變數管理", "active_profile": "Profile", "context_refresh": "重新整理 Context", "bus_refresh": "Spring Cloud Bus 重新整理", "bus_refresh_success": "Spring Cloud Bus 已成功重新整理。", "bus_refresh_failure": "失敗:無法觸發 Spring Cloud Bus 重新整理!", "bus_refresh_title": "向已設定的 Spring Cloud Bus 發送重新整理訊息,廣播至所有監聽的節點", "context_refresh_failed": "失敗", "context_refreshed": "Context 已重新整理", "context_reset": "重設", "context_reset_failed": "失敗", "context_resetted": "已重設", "context_update": "更新", "context_update_failed": "失敗", "context_updated": "已更新", "no_properties": "未設定屬性", "title": "環境變數管理", "refresh": "重新整理環境變數", "application": "應用程式", "instance": "執行個體", "refreshed_configurations": "已重新整理的設定:" } } } ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/views/instances/env/index.vue ================================================ ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/views/instances/env/refresh.spec.ts ================================================ import userEvent from '@testing-library/user-event'; import { screen } from '@testing-library/vue'; import { cloneDeep } from 'lodash-es'; import { beforeEach, describe, expect, it, vi } from 'vitest'; import { applications } from '../../../mocks/applications/data'; import Application from '@/services/application'; import Instance from '@/services/instance'; import { render } from '@/test-utils'; import Refresh from '@/views/instances/env/refresh.vue'; describe('Refresh', () => { const refreshInstanceContext = vi.fn().mockResolvedValue({ data: ['spring.redis.host', 'spring.datasource.url'], }); const refreshApplicationContext = vi.fn().mockResolvedValue({ data: [ { instanceId: '123', status: 200, body: '["spring.redis.host","spring.datasource.url"]', contentType: 'application/json', }, { instanceId: '456', status: 200, body: '["spring.redis.host","spring.datasource.url"]', contentType: 'application/json', }, ], }); function createInstance() { const instance = new Instance({ id: 1233 }); instance.refreshContext = refreshInstanceContext; return instance; } function createApplication() { const application = new Application(cloneDeep(applications[1])); application.instances.push(createInstance()); application.refreshContext = refreshApplicationContext; return application; } beforeEach(async () => { render(Refresh, { props: { instance: createInstance(), application: createApplication(), }, }); }); it('shows the changed configurations for the current instance', async () => { const refreshButton = await screen.findByText( 'instances.env.context_refresh', ); await userEvent.click(refreshButton); const confirmButton = await screen.findByText('Confirm'); await userEvent.click(confirmButton); expect(screen.findByText('Refreshed configurations:')).toBeDefined(); expect(screen.findByText('spring.redis.host')).toBeDefined(); expect(screen.findByText('spring.datasource.url')).toBeDefined(); }); it('shows the changed configurations for all instances', async () => { const toggleScopeButton = await screen.getByRole('button', { name: 'Instance', }); await userEvent.click(toggleScopeButton); const refreshButton = await screen.findByText( 'instances.env.context_refresh', ); await userEvent.click(refreshButton); const confirmButton = await screen.findByText('Confirm'); await userEvent.click(confirmButton); expect(screen.findByText('Refreshed configurations:')).toBeDefined(); expect(screen.findByText('spring.redis.host')).toBeDefined(); expect(screen.findByText('spring.datasource.url')).toBeDefined(); expect(screen.findByText('instanceId: 123')).toBeDefined(); expect(screen.findByText('instanceId: 456')).toBeDefined(); }); }); ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/views/instances/env/refresh.vue ================================================ ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/views/instances/flyway/i18n.de.json ================================================ { "instances": { "flyway": { "label": "Flyway", "type": "Typ", "checksum": "Prüfsumme", "version": "Version", "description": "Beschreibung", "script": "Skript", "state": "Status", "installed_by": "Installiert von", "installed_on": "Installiert am", "installed_rank": "Reihenfolge", "execution_time": "Ausführungszeit" } } } ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/views/instances/flyway/i18n.en.json ================================================ { "instances": { "flyway": { "label": "Flyway", "type": "Type", "checksum": "Checksum", "version": "Version", "description": "Description", "script": "Script", "state": "State", "installed_by": "Installed by", "installed_on": "Installed on", "installed_rank": "Installed rank", "execution_time": "Execution Time" } } } ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/views/instances/flyway/i18n.es.json ================================================ { "instances": { "flyway": { "label": "Flyway", "type": "Tipo", "checksum": "Checksum", "version": "Versión", "description": "Descripción", "script": "Script", "state": "Estado", "installed_by": "Instalado por", "installed_on": "Instalado en", "installed_rank": "Rango instalado", "execution_time": "Tiempo ejecución" } } } ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/views/instances/flyway/i18n.fr.json ================================================ { "instances": { "flyway": { "label": "Flyway", "type": "Type", "checksum": "Checksum", "version": "Version", "description": "Description", "script": "Script", "state": "État", "installed_by": "Installé par", "installed_on": "Installé sur", "installed_rank": "Ordre d'installation", "execution_time": "Temps d'éxecution" } } } ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/views/instances/flyway/i18n.is.json ================================================ { "instances": { "flyway": { "label": "Flyway", "type": "Gerð", "checksum": "Checksum", "version": "Útgafa", "description": "Lýsing", "script": "Forskrift", "state": "Staða", "installed_by": " Sett upp af", "installed_on": "Sett upp á", "installed_rank": "Uppsett staða", "execution_time": "Tími framkvæmdarinnar" } } } ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/views/instances/flyway/i18n.ko.json ================================================ { "instances": { "flyway": { "label": "Flyway", "type": "타입", "checksum": "체크섬", "version": "버전", "description": "설명", "script": "스크립트", "state": "상태", "installed_by": "에 의해 설치", "installed_on": "에 설치된", "installed_rank": "설치 순위", "execution_time": "실행시간" } } } ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/views/instances/flyway/i18n.pt-BR.json ================================================ { "instances": { "flyway": { "label": "Flyway", "type": "Tipo", "checksum": "Checksum", "version": "Versão", "description": "Descrição", "script": "Script", "state": "Estado", "installed_by": "Instalado por", "installed_on": "Instalado em", "installed_rank": "Classificação instalada", "execution_time": "Tempo de Execução" } } } ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/views/instances/flyway/i18n.ru.json ================================================ { "instances": { "flyway": { "label": "Flyway", "type": "Тип", "checksum": "Контрольная сумма", "version": "Версия", "description": "Описание", "script": "Скрипт", "state": "Статус", "installed_by": "Кем установлено", "installed_on": "Когда установлено", "installed_rank": "Установленный ранг", "execution_time": "Время выполнения" } } } ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/views/instances/flyway/i18n.zh-CN.json ================================================ { "instances": { "flyway": { "label": "Flyway", "type": "类型", "checksum": "校验码", "version": "版本", "description": "描述", "script": "脚本", "state": "状态", "installed_by": "安装人", "installed_on": "安装位置", "installed_rank": "安装等级", "execution_time": "执行时间" } } } ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/views/instances/flyway/i18n.zh-TW.json ================================================ { "instances": { "flyway": { "label": "Flyway", "type": "類型", "checksum": "檢查碼", "version": "版本", "description": "說明", "script": "指令碼", "state": "狀態", "installed_by": "安裝者", "installed_on": "安裝時間", "installed_rank": "安裝順序", "execution_time": "執行時間" } } } ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/views/instances/flyway/index.vue ================================================ ================================================ FILE: spring-boot-admin-server-ui/src/main/frontend/views/instances/gateway/add-route.vue ================================================