From 5bce1bfa46ca49e714a11b5f83fb35e3b9cd1829 Mon Sep 17 00:00:00 2001 From: Subhash Rawat <19619948+Shubzz-02@users.noreply.github.com> Date: Fri, 8 Mar 2024 13:14:43 +0530 Subject: [PATCH] Added Webex Notifier (#3186) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Added Webex Notifier * Added Webex Notifier doc * updated webex notifier doc --------- Co-authored-by: Stephan Köninger --- .../site/asciidoc/server-notifications.adoc | 28 +++ .../AdminServerNotifierAutoConfiguration.java | 18 +- .../admin/server/notify/WebexNotifier.java | 191 ++++++++++++++++++ ...inServerNotifierAutoConfigurationTest.java | 10 +- .../server/notify/WebexNotifierTest.java | 135 +++++++++++++ 5 files changed, 380 insertions(+), 2 deletions(-) create mode 100644 spring-boot-admin-server/src/main/java/de/codecentric/boot/admin/server/notify/WebexNotifier.java create mode 100644 spring-boot-admin-server/src/test/java/de/codecentric/boot/admin/server/notify/WebexNotifierTest.java diff --git a/spring-boot-admin-docs/src/site/asciidoc/server-notifications.adoc b/spring-boot-admin-docs/src/site/asciidoc/server-notifications.adoc index aa7194b2b6f..1d040d4f2f3 100644 --- a/spring-boot-admin-docs/src/site/asciidoc/server-notifications.adoc +++ b/spring-boot-admin-docs/src/site/asciidoc/server-notifications.adoc @@ -356,6 +356,34 @@ To enable https://telegram.org/[Telegram] notifications you need to create and a | `+++"<strong>#{instance.registration.name}</strong>/#{instance.id} is <strong>#{event.statusInfo.status}</strong>"+++` |=== +[[webex-notifications]] +=== Webex Notifications === +To enable https://www.webex.com/[Webex] notifications, you need to set the appropriate configuration properties for `auth-token` and `room-id`. + +.Webex notifications configuration options +|=== +| Property name |Description |Default value + +| spring.boot.admin.notify.webex.enabled +| Enable Webex notifications +| `true` + +| spring.boot.admin.notify.webex.url +| The Webex server url to send the notifications to. +| "https://webexapis.com/v1/messages" + +| spring.boot.admin.notify.webex.auth-token +| The authentication token for your Webex account (e.g. `123456-ascbhuwbtzzk-abtabhixta-788654`). +| + +| spring.boot.admin.notify.webex.room-id +| Unique identifier for the target room in Webex. +| + +| spring.boot.admin.notify.webex.message +| Text to send. SpEL-expressions are supported. By default, messages will be sent as Markdown, so you can include Markdown formatting. +| `+++"*#{instance.registration.name}* (#{instance.id}) is *#{event.statusInfo.status}*"+++` +|=== [[discord-notifications]] === Discord Notifications === diff --git a/spring-boot-admin-server/src/main/java/de/codecentric/boot/admin/server/config/AdminServerNotifierAutoConfiguration.java b/spring-boot-admin-server/src/main/java/de/codecentric/boot/admin/server/config/AdminServerNotifierAutoConfiguration.java index 9a3190d9328..23fdc2f9165 100644 --- a/spring-boot-admin-server/src/main/java/de/codecentric/boot/admin/server/config/AdminServerNotifierAutoConfiguration.java +++ b/spring-boot-admin-server/src/main/java/de/codecentric/boot/admin/server/config/AdminServerNotifierAutoConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2023 the original author or authors. + * 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. @@ -69,6 +69,7 @@ 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; @@ -362,4 +363,19 @@ public FeiShuNotifier feiShuNotifier(InstanceRepository repository, NotifierProx } + @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)); + } + + } + } diff --git a/spring-boot-admin-server/src/main/java/de/codecentric/boot/admin/server/notify/WebexNotifier.java b/spring-boot-admin-server/src/main/java/de/codecentric/boot/admin/server/notify/WebexNotifier.java new file mode 100644 index 00000000000..1de59b22467 --- /dev/null +++ b/spring-boot-admin-server/src/main/java/de/codecentric/boot/admin/server/notify/WebexNotifier.java @@ -0,0 +1,191 @@ +/* + * 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.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.context.expression.MapAccessor; +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.SimpleEvaluationContext; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.lang.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; + +// 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 AbstractStatusChangeNotifier { + + 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 = "#{instance.registration.name}/#{instance.id} is #{event.statusInfo.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; + + private final SpelExpressionParser parser = new SpelExpressionParser(); + + /** + * Template for the message to be sent + */ + private Expression message; + + /** + * 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; + this.message = parser.parseExpression(DEFAULT_MESSAGE, ParserContext.TEMPLATE_EXPRESSION); + } + + /** + * 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", getText(event, instance)); + return parameters; + } + + /** + * Retrieves the text for the given event and instance. + * @param event the instance event for which the text is being retrieved + * @param instance the instance associated with the event + * @return the text for the event and instance, or null if not available + */ + @Nullable + protected String getText(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 message.getValue(context, String.class); + } + + 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; + } + + public Expression getMessage() { + return message; + } + + public void setMessage(String message) { + this.message = parser.parseExpression(message, ParserContext.TEMPLATE_EXPRESSION); + } + +} diff --git a/spring-boot-admin-server/src/test/java/de/codecentric/boot/admin/server/config/AdminServerNotifierAutoConfigurationTest.java b/spring-boot-admin-server/src/test/java/de/codecentric/boot/admin/server/config/AdminServerNotifierAutoConfigurationTest.java index f3130b2dfde..cb47af4e668 100644 --- a/spring-boot-admin-server/src/test/java/de/codecentric/boot/admin/server/config/AdminServerNotifierAutoConfigurationTest.java +++ b/spring-boot-admin-server/src/test/java/de/codecentric/boot/admin/server/config/AdminServerNotifierAutoConfigurationTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2023 the original author or authors. + * 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. @@ -44,6 +44,7 @@ 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; @@ -131,6 +132,13 @@ public void test_rocketchat() { .run((context) -> assertThat(context).hasSingleBean(RocketChatNotifier.class)); } + @Test + public 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 public void test_multipleNotifiers() { this.contextRunner.withUserConfiguration(TestMultipleNotifierConfig.class).run((context) -> { diff --git a/spring-boot-admin-server/src/test/java/de/codecentric/boot/admin/server/notify/WebexNotifierTest.java b/spring-boot-admin-server/src/test/java/de/codecentric/boot/admin/server/notify/WebexNotifierTest.java new file mode 100644 index 00000000000..3562dac2229 --- /dev/null +++ b/spring-boot-admin-server/src/test/java/de/codecentric/boot/admin/server/notify/WebexNotifierTest.java @@ -0,0 +1,135 @@ +/* + * 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; + +public class WebexNotifierTest { + + private final Instance instance = Instance.create(InstanceId.of("-id-")) + .register(Registration.create("webex", "http://health").build()); + + private InstanceRepository repository; + + private WebexNotifier notifier; + + private RestTemplate restTemplate; + + @BeforeEach + public void setUp() { + 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 + public 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 DEFAULT_URL = URI.create("https://webexapis.com/v1/messages"); + + HttpEntity entity = new HttpEntity<>(createMessage("UP"), createHeaders()); + + verify(restTemplate).postForEntity(eq(DEFAULT_URL), eq(entity), eq(Void.class)); + } + + @Test + public 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 DEFAULT_URL = URI.create("https://webexapis.com/v1/messages"); + + HttpEntity entity = new HttpEntity<>(createMessage("DOWN"), createHeaders()); + + verify(restTemplate).postForEntity(eq(DEFAULT_URL), eq(entity), eq(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 + ""; + } + +}