본문 바로가기

Spring Boot/APPLIED

[Heartbeat] Heartbeat로 서버 헬스체크하기 (Spring boot + Slack Web Hooks + Bot)

모든 소스코드는 Github에 있습니다. (스타는 사랑입니다)

Index


Example

  • 서버 상태가 변하면 WebHooks로 알림
  • !command로 서버 상태 체크하기

slack_example


Heartbeat

heartbeat_state

(1) Init state --> (heart beat) --> Healthy state
(2) Healthy state --> (heart beat lost) --> Heartbeat lost state
(3) Heartbeat lost state --> (heart beat) --> Healthy state

heartbeat

    1. 설치 된 Agent에서 각각 Server로 heartbeat를 보냄
  • 2-1) 처음 보낸 Heartbeat의 경우 Register 알림 2-2) 이미 heartbeat를 받은 경우

    • 2-2-1) state가 heart beat lost 상태 인 경우 => Healthy state로 변환 & Restarted 알림
  • 3) Heartbeat monitor에서 각 Service 별 마지막 heartbeat 시간 체크 => 특정 시간이 지나면 Healthy state -> Heartbeat lost state 변환 & Stopped 알림


Java agent

java agent overview
https://speakerdeck.com/shelajev/taming-javaagents-bcn-jug-2015?slide=14

javaagent는 main() 함수보다 먼저 실행되는 premain() 함수를 이용해서 아래와 같이 ClassTransformer를 등록할 수 있습니다.

public static void premain(String agentArgs, Instrumentation inst) {
	if (Agent.instrumentation != null) {
		return;
	}
  try {
		LOGGER.info("start to premain...");
		Agent.instrumentation = inst;
		Agent.instrumentation.addTransformer(new AgentTransformer());
	} catch (Throwable t) {
		LOGGER.error("Failed to premain in Agent", t);
	}
}

=> javaagent를 이용해서 Heartbeat client thread를 실행하여 Server로 heatbeat를 보냅니다
참고로 javaagent로 클래스파일을 변경하여 APM 에이전트를 만들 때 많이 사용합니다.
(Servlet 기반 등 http 프로토콜 통신 및 Jdbc 구현체 등 tracing + jvm metrics) (elasticsearch의 apm-agent-java, scouter, newrelic, pinpoint 등등)


Event bus

Guava의 AsyncEnventBus를 활용해서 아래와 같이 Producer/Consumer 방식으로 이벤트를 생성하고 소비합니다.

Publisher

import com.google.common.eventbus.EventBus;
import java.util.Objects;
import java.util.concurrent.Executors;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import server.events.HostStateChangedEvent;
import server.util.ThreadUtil;

@Slf4j
@Component
public class HostStatePublisher {
    private EventBus asyncEventBus;

    public HostStatePublisher() {        
        this.asyncEventBus = new AsyncEventBus("host-state-publisher", Executors.newCachedThreadPool());
    }

    public void publish(HostStateChangedEvent hostStateChangedEvent) {
        if (hostStateChangedEvent == null || hostStateChangedEvent.getHostEntity() == null) {
            log.warn("Published empty host changed event. stack trace : n{}"
                , ThreadUtil.getStackTraceString(2));
            return;
        }		
        asyncEventBus.post(hostStateChangedEvent);
    }

    public void register(Object listener) {
        Objects.requireNonNull(listener, "listener must be not null");
        log.info("Register hostStateChanged consumer : {}", listener.getClass().getName());
        asyncEventBus.register(listener);
    }
}

Listener

import static server.state.HostState.HEALTHY;
import static server.state.HostState.HEARTBEAT_LOST;
import com.google.common.eventbus.Subscribe;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.stereotype.Component;
import server.configuration.SlackConfiguration;
import server.events.HostStateChangedEvent;
import server.events.publisher.HostStatePublisher;
import server.message.slack.SlackMessageConverter;
import server.message.slack.SlackWebHooks;
import server.state.HostEntity;
import server.state.HostState;

@Slf4j
@Component
@ConditionalOnBean(SlackConfiguration.class)
public class SlackNotifierListener {

    private SlackWebHooks slackWebHooks;
    private SlackMessageConverter messageConverter;

    @Autowired
    public SlackNotifierListener(HostStatePublisher hostStatePublisher, SlackWebHooks slackWebHooks,
        SlackMessageConverter messageConverter) {

        this.slackWebHooks = slackWebHooks;
        this.messageConverter = messageConverter;
        hostStatePublisher.register(this);
    }

    @Subscribe
    public void onHostStateChanged(HostStateChangedEvent event) {

        HostState prevState = event.getPrevState();
        HostEntity hostEntity = event.getHostEntity();		

        switch (prevState) {
            case UNKNOWN:
                // register
                if (hostEntity.getHostState() == HEALTHY) {
                    sendHostStateChangedMessage("Register", hostEntity);
                }
                break;
            case HEALTHY:
                // stopped
                if (hostEntity.getHostState() == HEARTBEAT_LOST) {
                    sendHostStateChangedMessage("Stopped", hostEntity);
                }
                break;				
            case HEARTBEAT_LOST:
                // started
                if (hostEntity.getHostState() == HEALTHY) {
                    sendHostStateChangedMessage("Restarted", hostEntity);
                }
                break;
        }
    }
}

Slack Web Hooks, Bot

Slack 관련된 것은 JBot 라이브러리를 활용하였습니다.
Bot 관련해서 잘 구현되어있길래 정말 쉽게 쓸수가 있습니다
아래와 같이 spring config 파일에 enabled flag를 주어서 SlackConfiguration의 활성화를 결정하고
true일 경우 jbot의 패키지를 ComponentScan하여서 jbot 라이브러리의 bean들을 가져왔습니다

SlackConfiguration

// application.yaml 파일
slack:
  enabled: true

// slack 관련 Spring Configuration
@Configuration
@ConditionalOnProperty(name = "slack.enabled", havingValue = "true")
@ComponentScan(basePackages = {"me.ramswaroop.jbot"})
public class SlackConfiguration {
}

Web Hooks

아래와 같이 @ConditionalOnBean어노테이션을 활용해서 SlackWebHooks를 빈으로 등록할 지 말지를 결정합니다.
그리고 slack에서 등록된 webhook url과 RichMessage를 포함하면 손쉽게 WebHooks를 이용할 수 있습니다.

@Slf4j
@Component
@ConditionalOnBean(SlackConfiguration.class)
public class SlackWebHooks {

    private RestTemplate restTemplate;
    private SlackProperties slackProperties;

    @Autowired
    public SlackWebHooks(SlackProperties slackProperties, RestTemplate restTemplate) {
        this.slackProperties = slackProperties;
        this.restTemplate = restTemplate;
    }

    public void invokeSlackWebHooks(RichMessage message) {
        try {
            restTemplate.postForEntity(
                slackProperties.getSlackIncomingWebhookUrl(),
                message.encodedMessage(),
                String.class
            );
        } catch (Exception e) {
            log.error("Exception occur while invoke web hooks", e);
        }
    }
}

Slack Bot

SlackBot의 경우 @ConditionalOnBean을 똑같이 활용하였습니다.

그리고 별도의 @Controller 라는 어노테이션을 이용해서 slack channel의 메시지들을 처리할 수 있습니다.

pattern값을 통해 정규식도 사용할 수 있습니다.

@Slf4j
@JBot
@Component
@ConditionalOnBean(SlackConfiguration.class)
public class SlackBot extends Bot {
	...

	@Controller(events = EventType.MESSAGE, pattern = "^![a-zA-Z0-9]*.?")

	public void onReceiveMessage(WebSocketSession session, Event event, Matcher matcher) {

        log.info("## Receive command : {}", event.getText());

        handleCommand(session, event, event.getText().substring(1));

    }
	...
}

'Spring Boot > APPLIED' 카테고리의 다른 글

Spring async + stomp websocket 랜덤 채팅 구현 예제  (3) 2018.08.25