微服务场景中多个服务状态的监听变得复杂,如果直接使用Spring Boot Actuator 进行服务的监控也不是很方便,因为集群中的服务肯定不止一个,将这些数据汇总展示很不方便,本文利用Spring Boot Admin简洁快速的实现集群服务健康监控,通过简单的配置实现钉钉机器人通知(你也可以改成其他的类型)

一、环境配置

Springboot 2.1.1
springCloud 2.1.1
Spring Boot Admin 2.1.6
dingtalk 1.0.1

二、服务端搭建

因为此项目是微服务版所以我们此次先从服务端开始搭建

1.导入依赖

<!--SpringBoot web-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <!--SpringCloud Eureka客户端依赖-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        </dependency>

        <!-- https://mvnrepository.com/artifact/de.codecentric/spring-boot-admin-starter-server -->
        <!--这个是 spring-boot-admin 服务端的依赖-->
        <dependency>
            <groupId>de.codecentric</groupId>
            <artifactId>spring-boot-admin-starter-server</artifactId>
            <version>2.1.6</version>
        </dependency>
        <!-- https://mvnrepository.com/artifact/com.aliyun/alibaba-dingtalk-service-sdk -->
        <dependency>
            <groupId>com.aliyun</groupId>
            <artifactId>alibaba-dingtalk-service-sdk</artifactId>
            <version>1.0.1</version>
        </dependency>

2.编写配置

server:
  port: 8788
spring:
  application:
    name: springboot-admin-server
  admin:
    environment: test(环境名称)
    ding-talk-token: 钉钉机器人的token
     check: false #是否开启发送
# eureka相关配置
eureka:
  client:
    service-url:
      defaultZone: http://localhost:5550/eureka
    #客户端每隔30秒从Eureka服务上更新一次服务信息
    registry-fetch-interval-seconds: 30
    #需要将我的服务注册到eureka上
    register-with-eureka: true
    #需要检索服务
    fetch-registry: true
  #心跳检测检测与续约时间
  instance:
    #告诉服务端,如果我10s之内没有给你发心跳,就代表我故障了,将我剔除掉,默认90s
    #Eureka服务端在收到最后一次心跳之后等待的时间上限,单位为秒,超过则剔除(客户端告诉服务端按照此规则等待自己)
    lease-expiration-duration-in-seconds: 10
    #每隔2s向服务端发送一次心跳,证明自已依然活着,默认30s
    #Eureka客户端向服务端发送心跳的时间间隔,单位为秒(客户端告诉服务端自己会按照该规则)
    lease-renewal-interval-in-seconds: 2
    # 这就是我们的监控检查地址
    health-check-url-path: /actuator/health

#使用注册中心后,admin服务端也可以监控自身服务状况
management:
  endpoints:
    web:
      exposure:
        #开放所有页面节点  默认只开启了health、info两个节点
        include: '*'
  endpoint:
    health:
      #显示健康具体信息  默认不会显示详细信息
      show-details: always

3.编写消息通知类

@Component
@Slf4j
public class CustomNotifier extends AbstractEventNotifier {
    @Value("${spring.admin.ding-talk-token}")
    private  String dingTalkToken;
    @Value("${spring.admin.environment}")
    private  String environment;
    
    @Value("${spring.admin.check}")
    private  boolean check;
    /**
     * 消息模板
     */
    private static final String template = "业务报警!\n各位同事大家好,现有 %s 环境 以下服务发生变动:\n服务名:%s(%s) \n状态:%s(%s) \n服务ip:%s \n预警时间:%s";



    protected CustomNotifier(InstanceRepository repository) {
        super(repository);
    }

    @Override
    protected Mono<Void> doNotify(InstanceEvent event, Instance instance) {
        return Mono.fromRunnable(() -> {
            if (event instanceof InstanceStatusChangedEvent) {
                log.info("Instance {} ({}) is {}", instance.getRegistration().getName(), event.getInstance(),
                        ((InstanceStatusChangedEvent) event).getStatusInfo().getStatus());
                Date date = new Date();
                SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
                String format = simpleDateFormat.format(date);

                String status = ((InstanceStatusChangedEvent) event).getStatusInfo().getStatus();
                String messageText = null;
                switch (status) {
//                    // 服务离线
                    case "OFFLINE":
                        log.info("发送 服务离线 的通知!");
                        messageText = String.format(template, environment, instance.getRegistration().getName(),event.getInstance(), ((InstanceStatusChangedEvent) event).getStatusInfo().getStatus(),"服务离线",instance.getRegistration().getServiceUrl(),format);
                        this.sendMessage(messageText);
                        break;
                    //服务上线
                    case "UP":
                        log.info("发送 服务上线 的通知!");
                        messageText = String.format(template,environment, instance.getRegistration().getName(),event.getInstance(), ((InstanceStatusChangedEvent) event).getStatusInfo().getStatus(),"服务上线",instance.getRegistration().getServiceUrl(),format);
                        this.sendMessage(messageText);
                        break;
                    // 服务未知异常
                    case "UNKNOWN":
                        log.info("发送 服务未知异常 的通知!");
                        messageText = String.format(template,environment, instance.getRegistration().getName(),event.getInstance(), ((InstanceStatusChangedEvent) event).getStatusInfo().getStatus(),"服务未知异常",instance.getRegistration().getServiceUrl(),format);
                        this.sendMessage(messageText);
                        break;
                    default:
                        break;
                }
            } else {
                log.info("Instance {} ({}) {}", instance.getRegistration().getName(), event.getInstance(),
                        event.getType());
            }
        });
    }

    /**
     * 发送消息
     * @param messageText
     */
    private void sendMessage(String messageText)  {
        Long timestamp = System.currentTimeMillis();
        String secret = "钉钉机器人密钥";

        String stringToSign = timestamp + "\n" + secret;
        Mac mac = null;
        String sign = null;
        try {
            mac = Mac.getInstance("HmacSHA256");
            mac.init(new SecretKeySpec(secret.getBytes("UTF-8"), "HmacSHA256"));
            byte[] signData = mac.doFinal(stringToSign.getBytes("UTF-8"));
            //签名数据
            sign = URLEncoder.encode(new String(Base64.encodeBase64(signData)),"UTF-8");
        } catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
        } catch (InvalidKeyException e) {
            e.printStackTrace();
        } catch (UnsupportedEncodingException e) {
            e.printStackTrace();
        }
        System.out.println(messageText);
        DingTalkClient client = new DefaultDingTalkClient("https://oapi.dingtalk.com/robot/send?access_token="+dingTalkToken+"×tamp="+timestamp+"&sign="+sign);
        OapiRobotSendRequest request = new OapiRobotSendRequest();

        request.setMsgtype("text");
        OapiRobotSendRequest.Text text = new OapiRobotSendRequest.Text();
        text.setContent(messageText);
        request.setText(text);

        try {
            if (check) {
                client.execute(request);
            }
        } catch (ApiException e) {
            log.info("[ERROR] sendMessage", e);
        }
    }

}

三、客户端搭建

在微服务场景下我们只需要引入依赖,配置一下就可以了

1.导入依赖

<!--SpringCloud Eureka客户端依赖-->
<dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        </dependency>
<dependency>
            <groupId>de.codecentric</groupId>
            <artifactId>spring-boot-admin-starter-client</artifactId>
            <version>2.1.6</version>
        </dependency>

2.编写配置

eureka:
  client:
    serviceUrl:
      defaultZone: http://localhost:5550/eureka/
    registry-fetch-interval-seconds: 60    # 每隔60秒就去注册中心拉取注册信息
  instance:
    lease-renewal-interval-in-seconds: 2  # 每隔2秒,向服务端发送一次心跳
    lease-expiration-duration-in-seconds: 6   # 如果6秒之内没给我发送心跳,则表示服务挂了,剔除该服务
management:
  endpoints:
    web:
      exposure:
        include: "*"
  endpoint:
    health:
      show-details: ALWAYS

最终效果

界面

springboot 集成redisonclient springboot 集成钉钉_java


通知效果

springboot 集成redisonclient springboot 集成钉钉_服务端_02