🔥스파르타 TIL (MSA)

Grafana Alert Manager와 Kakao Open API 나에게 보내기 연동해보기 (feat. 좋지 않은 결과)

승승장규 2025. 4. 22. 14:38

문득 이메일, slack, discord 등등 다양하게 Grafana에서 제공해주는 기능들을 보다보니 카카오톡 메시지로 알람을 받으면 좋을 것 같다는 생각을 하게 되었다. (하고 싶은 건 다 해보는 스타일...) 사실 좋은 방법은 아니지만 실제로 카카오톡 메시지를 받으면 신기할 것 같아서 진행해보았다. 

 

우선 Discord, Slack에서 Webhook을 사용하듯이 직접 Grafana에서 사용할 Webhook용 Controller를 만들어야 한다.

{
  "client": "Grafana",
  "client_url": "http://grafana.local/alerting",
  "event_type": "alerting",
  "incident_key": "some-unique-key",
  "service_key": "key-for-integration",
  "description": "CPU usage too high",
  "contexts": [
    {
      "type": "link",
      "href": "http://grafana.local/d/abcd1234",
      "text": "View dashboard"
    }
  ],
  "details": {
    "firing": "2",
    "resolved": "1",
    "numFiring": "2",
    "numResolved": "1",
    "severity": "critical"
  }
}

위와 같은 Grafana에서 보내는 JSON 형식을 참고해서 DTO를 구현한 뒤

public class GrafanaAlertManagerDto {
    private String client;
    private String clientUrl;
    private List<Context> contexts;
    private String description;
    private String eventType;
    private String incidentKey;
    private String serviceKey;
    private Details details;

public static class Context {
    private String href;
    private String text;
    private String type;
}

public static class Details {
    private String firing;
    private String lol;
    private String numFiring;
    private String numResolved;
    private String resolved;
    private String severity;
}

 받은 알람 메시지를 사용해서 kakao 나에게 보내기 기능을 사용해보자

@RestController
@RequestMapping("/api/v1/users/webhook")
public class AlertWebhookController {

    private final KaKaoService kaKaoService;

    public AlertWebhookController(KaKaoService kaKaoService) {
        this.kaKaoService = kaKaoService;
    }

    @PostMapping("/grafana")
    public ResponseEntity<String> receiveAlert(@RequestBody GrafanaAlertManagerDto alertDTO) {
        try {
            // 알림 메시지 구성
            String message = buildAlertMessage(alertDTO);
            // 웹 URL (Grafana 대시보드 URL)
            String webUrl = alertDTO.getClientUrl();
            // 카카오톡으로 메시지 전송
            boolean result = kaKaoService.sendMessage(message, webUrl);

            if (result) {
                return ResponseEntity.ok("알림이 성공적으로 처리되었습니다");
            } else {
                return ResponseEntity.internalServerError().body("카카오톡 메시지 전송 실패");
            }
        } catch (Exception e) {
            return ResponseEntity.internalServerError().body("알림 처리 중 오류 발생: " + e.getMessage());
        }
    }

    private String buildAlertMessage(GrafanaAlertManagerDto alertDTO) {
        StringBuilder messageBuilder = new StringBuilder();
        messageBuilder.append("[Grafana 알림]\n");

        messageBuilder.append("이벤트: ").append(alertDTO.getEventType()).append("\n");
        messageBuilder.append("설명: ").append(alertDTO.getDescription()).append("\n");

        if (alertDTO.getDetails() != null) {
            messageBuilder.append("심각도: ").append(alertDTO.getDetails().getSeverity()).append("\n");
            messageBuilder.append("발생 수: ").append(alertDTO.getDetails().getNumFiring()).append("\n");

            if (alertDTO.getDetails().getFiring() != null && !alertDTO.getDetails().getFiring().isEmpty()) {
                String firing = alertDTO.getDetails().getFiring();
                String[] lines = firing.split("\n");
                for (String line : lines) {
                    if (line.trim().startsWith("Labels:") ||
                            line.trim().startsWith("- alertname") ||
                            line.trim().startsWith("Annotations:")) {
                        messageBuilder.append(line.trim()).append("\n");
                    }
                }
            }
        }
        messageBuilder.append("\n자세한 내용은 알림을 터치하여 확인하세요.");

        return messageBuilder.toString();
    }
}

 

public class KaKaoService {

    private final RestTemplate restTemplate;

    @Value("${kakao.api.token}")
    private String accessToken;

    @Value("${kakao.api.url:https://kapi.kakao.com/v2/api/talk/memo/default/send}")
    private String kakaoApiUrl;

    public boolean sendMessage(String message, String webUrl) {
        try {
            // HTTP 헤더 설정
            HttpHeaders headers = new HttpHeaders();
            headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
            headers.set("Authorization", "Bearer " + accessToken);

            // 카카오톡 메시지 템플릿 생성
            String templateObject = String.format(
                    "{\"object_type\":\"text\",\"text\":\"%s\",\"link\":{\"web_url\":\"%s\",\"mobile_web_url\":\"%s\"}}",
                    message.replace("\n", "\\n").replace("\"", "\\\""),
                    webUrl, webUrl);

            // HTTP 요청 파라미터 설정
            MultiValueMap<String, String> parameters = new LinkedMultiValueMap<>();
            parameters.add("template_object", templateObject);

            // HTTP 요청 객체 생성
            HttpEntity<MultiValueMap<String, String>> requestEntity = new HttpEntity<>(parameters, headers);

            // API 호출
            ResponseEntity<String> response = restTemplate.exchange(
                    kakaoApiUrl,
                    HttpMethod.POST,
                    requestEntity,
                    String.class);

            if (response.getStatusCode().is2xxSuccessful()) {
                return true;
            } else {
                return false;
            }
        } catch (Exception e) {
            return false;
        }
    }
}

이제 Grafana에 새로운 Contact point를 생성해주면 되는데 Controller에 설정한 url을 입력해주면 된다. 지금 Grafana가 docker로 띄워져있기 때문에 host.docker.internal로 작성하였다.

이후 Test를 진행하면 정상적으로 잘 전송되는 것을 확인할 수 있다.

 

 

하지만 이 방식을 구현하다 느낀 것은 별로 좋은 방식은 아니라는 것이다.

문제 상황을 빠르게 대응하기 위해 알람 시스템을 적용하는게 목적이지만, 카카오 메시지 API를 사용하려면 우선 로그인 절차가 이루어지고 발급받은 access_token이 존재해야 하기 때문에 Grafana에서 제공하는 slack, discord, jira 등등 사용하는게 좋을 것 같다!