모든 소스코드는 Github에 있습니다. (스타는 사랑입니다)
Index
Example
- 서버 상태가 변하면 WebHooks로 알림
- !command로 서버 상태 체크하기
Heartbeat
(1) Init state --> (heart beat) --> Healthy state
(2) Healthy state --> (heart beat lost) --> Heartbeat lost state
(3) Heartbeat lost state --> (heart beat) --> Healthy state
- 설치 된 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
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 |
---|