diff --git a/pom.xml b/pom.xml index c8a4e42..815011f 100644 --- a/pom.xml +++ b/pom.xml @@ -109,6 +109,12 @@ spring-boot-starter-test test + + org.springframework.cloud + spring-cloud-contract-wiremock + 4.1.4 + test + diff --git a/src/main/java/dev/vality/alerting/tg/bot/controller/WebhookController.java b/src/main/java/dev/vality/alerting/tg/bot/controller/WebhookController.java index 5bc942c..3f22fab 100644 --- a/src/main/java/dev/vality/alerting/tg/bot/controller/WebhookController.java +++ b/src/main/java/dev/vality/alerting/tg/bot/controller/WebhookController.java @@ -3,6 +3,7 @@ import com.fasterxml.jackson.databind.ObjectMapper; import dev.vality.alerting.tg.bot.config.properties.AlertmanagerWebhookProperties; import dev.vality.alerting.tg.bot.model.Webhook; +import dev.vality.alerting.tg.bot.service.AlertBot; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.web.bind.annotation.PostMapping; @@ -22,7 +23,7 @@ public class WebhookController { private final AlertmanagerWebhookProperties alertmanagerWebhookProperties; private final ObjectMapper objectMapper; - public static final String FIRING = "firing"; + private final AlertBot alertBot; @PostMapping(value = "/webhook", consumes = MediaType.APPLICATION_JSON_VALUE) public ResponseEntity processWebhook(HttpServletRequest servletRequest) { @@ -30,6 +31,7 @@ public ResponseEntity processWebhook(HttpServletRequest servletRequest) var webhookBody = servletRequest.getReader().lines().collect(Collectors.joining(" ")); log.info("Received webhook from alertmanager: {}", webhookBody); var webhook = objectMapper.readValue(webhookBody, Webhook.class); + alertBot.sendAlertMessage(webhook); } catch (Exception e) { log.error("Unexpected error during webhook parsing:", e); return ResponseEntity.internalServerError().build(); diff --git a/src/main/java/dev/vality/alerting/tg/bot/model/Webhook.java b/src/main/java/dev/vality/alerting/tg/bot/model/Webhook.java index 19253ed..b426c5f 100644 --- a/src/main/java/dev/vality/alerting/tg/bot/model/Webhook.java +++ b/src/main/java/dev/vality/alerting/tg/bot/model/Webhook.java @@ -1,6 +1,8 @@ package dev.vality.alerting.tg.bot.model; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import lombok.Data; +import lombok.NoArgsConstructor; import java.util.List; import java.util.Map; @@ -9,6 +11,8 @@ * Alertmanager webhook body */ @Data +@NoArgsConstructor +@JsonIgnoreProperties(ignoreUnknown = true) public class Webhook { private String status; @@ -16,6 +20,8 @@ public class Webhook { private List alerts; @Data + @NoArgsConstructor + @JsonIgnoreProperties(ignoreUnknown = true) public static class Alert { private String status; diff --git a/src/main/java/dev/vality/alerting/tg/bot/service/AlertBot.java b/src/main/java/dev/vality/alerting/tg/bot/service/AlertBot.java index 86b7e93..912ab21 100644 --- a/src/main/java/dev/vality/alerting/tg/bot/service/AlertBot.java +++ b/src/main/java/dev/vality/alerting/tg/bot/service/AlertBot.java @@ -1,6 +1,7 @@ package dev.vality.alerting.tg.bot.service; import dev.vality.alerting.tg.bot.config.properties.AlertBotProperties; +import dev.vality.alerting.tg.bot.model.Webhook; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.scheduling.annotation.EnableScheduling; @@ -68,6 +69,11 @@ public void onUpdateReceived(Update update) { } } + public void sendAlertMessage(Webhook webhook) { + sendResponse(properties.getChatId(), properties.getTopics().getCommands(), webhook.getAlerts().toString(), + null); + } + public void sendScheduledMetrics() { send5xxErrorsMetrics(properties.getChatId()); sendFailedMachinesMetrics(properties.getChatId()); diff --git a/src/test/java/dev/vality/alerting/tg/bot/WebhookControllerTest.java b/src/test/java/dev/vality/alerting/tg/bot/WebhookControllerTest.java new file mode 100644 index 0000000..35582e4 --- /dev/null +++ b/src/test/java/dev/vality/alerting/tg/bot/WebhookControllerTest.java @@ -0,0 +1,108 @@ +package dev.vality.alerting.tg.bot; + +import com.fasterxml.jackson.databind.ObjectMapper; +import dev.vality.alerting.tg.bot.config.AlertBotConfig; +import dev.vality.alerting.tg.bot.config.properties.AlertmanagerWebhookProperties; +import dev.vality.alerting.tg.bot.controller.WebhookController; +import dev.vality.alerting.tg.bot.model.Webhook; +import dev.vality.alerting.tg.bot.service.AlertBot; +import lombok.val; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.MediaType; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.test.context.TestPropertySource; +import org.springframework.test.context.bean.override.mockito.MockitoBean; + +import java.nio.charset.StandardCharsets; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; +import static org.mockito.Mockito.verify; + +@SpringBootTest +@TestPropertySource(properties = { + "spring.mvc.pathmatch.matching-strategy=ant_path_matcher", + "bot.token=test", + "bot.name=vality_alerting_bot", + "bot.chatId=1", + "bot.topics.commands=1", + "bot.topics.errors5xx=2", + "bot.topics.altpay-conversion=3", + "bot.topics.failed-machines=4", + "bot.topics.pending-payments=5" +}) +public class WebhookControllerTest { + + @MockitoBean + AlertmanagerWebhookProperties webhookProperties; + + @MockitoBean + AlertBot alertBot; + + @MockitoBean + AlertBotConfig alertBotConfig; + + String webhookJson = """ + { + "status": "firing", + "receiver": "telegram", + "alerts": [ + { + "status": "firing", + "labels": { + "alertname": "Errors5xxHigh", + "severity": "critical", + "job": "payments", + "namespace": "prod", + "service": "payments-api", + "instance": "payments-api-1", + "pod": "payments-api-1-abc123" + }, + "annotations": { + "summary": "HTTP 5xx rate is too high", + "description": "Payments API is returning >5% 5xx responses for 5m", + "runbook_url": "https://runbook.company/alerts/errors5xx" + } + }, + { + "status": "resolved", + "labels": { + "alertname": "AltpayConversionLow", + "severity": "warning", + "job": "altpay", + "namespace": "prod", + "service": "altpay-conversion", + "pod": "altpay-0-xzy987" + }, + "annotations": { + "summary": "Altpay conversion dropped", + "description": "Altpay conversion < 2% in last 10m" + } + } + ] + } + """; + + @Test + public void sendTgMessageTest() { + ObjectMapper objectMapper = new ObjectMapper(); + WebhookController webhookController = new WebhookController(webhookProperties, objectMapper, alertBot); + + MockHttpServletRequest req = new MockHttpServletRequest(); + req.setMethod("POST"); + req.setRequestURI("/alertmanager/webhook"); + req.setContentType(MediaType.APPLICATION_JSON_VALUE); + req.setCharacterEncoding(StandardCharsets.UTF_8.name()); + req.setContent(webhookJson.getBytes(StandardCharsets.UTF_8)); + + val response = webhookController.processWebhook(req); + assertThat(response.getStatusCode().value()).isEqualTo(200); + + ArgumentCaptor webhookCaptor = ArgumentCaptor.forClass(Webhook.class); + verify(alertBot).sendAlertMessage(webhookCaptor.capture()); + Webhook passed = webhookCaptor.getValue(); + assertThat(passed).isNotNull(); + assertThat(passed.getAlerts()).isNotNull(); + } +}