本文将讲解:SpringBootAdmin服务搭建、集成、日志实时预览、服务告警推送至钉钉群消息

前言

  • 概述
    本篇讲解SpringBoot2.X整合SpringBoot-Admin监控。Spring Boot Admin 就是将 Spring Boot Actuator中提供的endpoint信息可视化表示,并且可以通过钉钉群、邮件、Telegram、Hipchat等发送告警消息。
  • 预览效果
  • 服务说明
  • SpringBootAdmin-Server应用
  • 服务应用A(Application
  • 服务应用B(Application

服务整合

搭建Admin-Server

spring boot admin-server源码在github上,可以通过以下2种方式启动,考虑到后续需要扩展钉钉推送这里我选择第二种

  • 直接使用官方提供的代码构建成jar之后,通过java -jar jar包的方式启动
  • 自己建一个SpringBoot项目,引入官方提供的pom依赖,通过自己的项目的方式来启动

编码实现

  • 加入pom依赖
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>05-boot-admin</artifactId>
        <groupId>com.it235.cloud.example</groupId>
        <version>1.0.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>05-boot-admin-server</artifactId>
    <description>
        SpringBootAdmin的服务端,一般是一个服务端管理多个服务,
        可以采用官方的jar,也可以自己集成,这里我们是自己集成编写服务
    </description>

    <properties>
        <spring-boot-admin.version>2.2.0</spring-boot-admin.version>
        <spring-cloud.version>Hoxton.SR3</spring-cloud.version>
        <spring-cloud-alibaba.version>2.2.1.RELEASE</spring-cloud-alibaba.version>
    </properties>

    <dependencies>
        <!--健康检查-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>de.codecentric</groupId>
            <artifactId>spring-boot-admin-starter-server</artifactId>
        </dependency>
    </dependencies>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-parent</artifactId>
                <version>2.2.5.RELEASE</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>

            <!--spring-boot-admin依赖-->
            <dependency>
                <groupId>de.codecentric</groupId>
                <artifactId>spring-boot-admin-dependencies</artifactId>
                <version>${spring-boot-admin.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>

            <!--SpringCloud-->
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>${spring-cloud.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>

            <!--Spring Alibaba Cloud-->
            <dependency>
                <groupId>com.alibaba.cloud</groupId>
                <artifactId>spring-cloud-alibaba-dependencies</artifactId>
                <version>${spring-cloud-alibaba.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>
</project>
  • 配置yml
server:
  port: 8769
spring:
  application:
    name: it235-boot-admin-server
management:
  endpoints:
    web:
      exposure:
        include: '*'
  endpoint:
    health:
      show-details: always
logging:
  level:
    root: info
  • 打开浏览器访问http://localhost:8769
  • springboot 集成 agent springboot 集成钉钉_springboot 集成 agent

搭建服务应用A、B

服务应用A、B是指当前你已存在的应用服务,用于编写业务的服务,如:订单服务、课程服务等。由于各服务配置基本相同,这里我就以A服务为例进行讲解。

spring-boot-admin提供了spring-boot-admin-starter-client.jar进行admin-server的注册。

  • 添加pom依赖
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>05-boot-admin</artifactId>
        <groupId>com.it235.cloud.example</groupId>
        <version>1.0.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>05-boot-admin-A</artifactId>
    <description>SpringBoot2.X整合spring-boot-admin</description>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-parent</artifactId>
                <version>2.2.5.RELEASE</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>

            <!--SpringCloud-->
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>Hoxton.SR3</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>


    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>

        <!--加入spring-boot-admin连接端-->
        <dependency>
            <groupId>de.codecentric</groupId>
            <artifactId>spring-boot-admin-starter-client</artifactId>
            <version>2.2.0</version>
        </dependency>
    </dependencies>
</project>
  • 编写yml配置
server:
  port: 7056
spring:
  application:
    name: it235-boot-admin
  # 配置spring-boot-admin服务端的地址
  boot:
    admin:
      client:
        enabled: true
        url: http://localhost:8769
# 健康检查访问: http://ip:port/sys/actuator/health
management:
  # 端点信息接口使用的端口,为了和主系统接口使用的端口进行分离
  server:
    port: 7057
    servlet:
      context-path: /sys
  # 端点健康情况,默认值"never",设置为"always"可以显示硬盘使用情况和线程情况
  endpoint:
    health:
      show-details: always
  # 设置端点暴露的哪些内容,默认["health","info"],设置"*"代表暴露所有可访问的端点
  endpoints:
    web:
      exposure:
        include: '*'
logging:
  level:
    root: info
  • 编写服务启动类
@SpringBootApplication
public class BootAdminAApplication {
    public static void main(String[] args) {
        SpringApplication.run(BootAdminAApplication.class , args);
    }
}
  • 启动服务,查看admin-server的面板有无变化
    注意:此处的你有可能在服务面板上发现没有任何服务注册上来,空空如也
    此时你可以将logging.level.root调整为debug级别,会发现异常信息javax.management.InstanceNotFoundException: org.springframework.boot:type=Admin,name=SpringApplication,此时你需要编辑服务参数面板,关闭如下2个勾选项

springboot 集成 agent springboot 集成钉钉_微服务监控_02

  • 重新启动服务,查看admin-server面板
  • 我们以同样的方式构建B应用服务,同时查看admin-server面板
    注意A、B的服务名不要一样,否则会当成多个实例进行注册上来
  • 查看应用详细信息

springboot 集成 agent springboot 集成钉钉_SpringBoot2_03

信息都非常全,这里我就不带大家一一观看了

日志实时预览

SpringBootAdmin预览实时日志也是一个非常强大的功能,接下来我们看如何去实现。

  • 预览效果
  • 原理
    原理非常简单,通过logback记录日志,配置文件位置,springboot-admin定时去抓取某一个位置的日志文件,解析后输出到admin-server所以此处最重要的是集成logback
  • logback集成
    我们在src/main/resources中加入logback-spring.xml文件,输入以下信息
<?xml version="1.0" encoding="UTF-8"?>

<configuration>
    <include resource="org/springframework/boot/logging/logback/defaults.xml"/>

    <property name="CONTEXT_NAME" value="it235-boot-admin"/>
    <property name="LOG_PATH" value="logs"/>
    <property name="MAX_FILE_SIZE" value="100MB"/>
    <property name="MAX_HISTORY" value="30"/>

    <contextName>${CONTEXT_NAME}</contextName>

    <!-- 彩色日志 -->
    <conversionRule conversionWord="clr" converterClass="org.springframework.boot.logging.logback.ColorConverter"/>
    <conversionRule conversionWord="wex"
                    converterClass="org.springframework.boot.logging.logback.WhitespaceThrowableProxyConverter"/>
    <conversionRule conversionWord="wEx"
                    converterClass="org.springframework.boot.logging.logback.ExtendedWhitespaceThrowableProxyConverter"/>

    <!-- 控制台日志样式 -->
    <property name="CONSOLE_LOG_PATTERN"
              value="${CONSOLE_LOG_PATTERN:-%clr(%d{${LOG_DATEFORMAT_PATTERN:-yyyy-MM-dd HH:mm:ss.SSS}}){faint} %clr(${LOG_LEVEL_PATTERN:-%5p}) %clr(${PID:- }){magenta} %clr([%15.15t]){faint} %clr(%-40.40logger{39}){cyan} [%L] %clr(:){faint} %m%n${LOG_EXCEPTION_CONVERSION_WORD:-%wEx}}"/>
    <!-- 文件日志样式 -->
    <property name="FILE_LOG_PATTERN"
              value="${FILE_LOG_PATTERN:-%d{${LOG_DATEFORMAT_PATTERN:-yyyy-MM-dd HH:mm:ss.SSS}} ${LOG_LEVEL_PATTERN:-%5p} ${PID:- } [%t] %-40.40logger{39} %L : %m%n${LOG_EXCEPTION_CONVERSION_WORD:-%wEx}}"/>

    <!-- 禁用logback自身日志输出 -->
    <statusListener class="ch.qos.logback.core.status.NopStatusListener"/>

    <!-- 控制台 -->
    <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <pattern>${CONSOLE_LOG_PATTERN}</pattern>
        </encoder>
    </appender>

    <!-- 运行日志文件 -->
    <appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <encoder>
            <pattern>${FILE_LOG_PATTERN}</pattern>
        </encoder>
        <file>${LOG_PATH}/it235-boot-admin.log</file>
        <rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
            <fileNamePattern>${LOG_PATH}/it235-boot-admin-%d{yyyy-MM-dd}.%i.log</fileNamePattern>
            <maxFileSize>${MAX_FILE_SIZE}</maxFileSize>
            <maxHistory>${MAX_HISTORY}</maxHistory>
        </rollingPolicy>
    </appender>

    <!-- 错误日志文件 -->
    <appender name="ERROR_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <encoder>
            <pattern>${FILE_LOG_PATTERN}</pattern>
        </encoder>
        <file>${LOG_PATH}/it235-bootadmin-a-error.log</file>
        <rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
            <fileNamePattern>${LOG_PATH}/it235-boot-admin-error-%d{yyyy-MM-dd}.%i.log</fileNamePattern>
            <maxFileSize>${MAX_FILE_SIZE}</maxFileSize>
            <maxHistory>${MAX_HISTORY}</maxHistory>
        </rollingPolicy>
        <filter class="ch.qos.logback.classic.filter.LevelFilter">
            <level>ERROR</level>
            <onMatch>ACCEPT</onMatch>
            <onMismatch>DENY</onMismatch>
        </filter>
    </appender>

    <!-- 异步写日志 -->
    <appender name="ASYNC_FILE" class="ch.qos.logback.classic.AsyncAppender">
        <discardingThreshold>0</discardingThreshold>
        <queueSize>1024</queueSize>
        <appender-ref ref="FILE"/>
    </appender>

    <appender name="ASYNC_ERROR_FILE" class="ch.qos.logback.classic.AsyncAppender">
        <discardingThreshold>0</discardingThreshold>
        <queueSize>1024</queueSize>
        <appender-ref ref="ERROR_FILE"/>
    </appender>

    <!-- 不同环境的日志级别配置 -->
    <springProfile name="local">
        <logger name="com.it235" level="DEBUG"/>
    </springProfile>

    <!-- 解决 SpringBootAdmin 错误日志问题 -->
    <logger name="org.apache.catalina.connector.CoyoteAdapter" level="OFF"/>

    <root level="INFO">
        <appender-ref ref="CONSOLE"/>
        <appender-ref ref="ASYNC_FILE"/>
        <appender-ref ref="ASYNC_ERROR_FILE"/>
    </root>

</configuration>
  • 修改yml文件,将日志配置改为如下
logging:
  config: classpath:logback-spring.xml
  level:
    root: info
  #  方便Spring Boot Admin页面上实时查看日志
  file: logs/it235-boot-admin.log
  • 启动应用服务器查看admin-server中对应用的日志管理
    点击-应用-日志,此时你会发现多了日志文件这个子菜单
  • 查看日志是否动态输出
    在应用中加一个Controller,在某个接口中打印几个info或者error,再查看admin-server面板看是否动态输出
@Slf4j
@RestController
@RequestMapping("/demo")
public class DemoController {
    @GetMapping("get")
    public String get(){
        log.info("这里是新日志,日志XXX:{}" , "abc");
        int i = 0;
        int x = 3 / i;
        return "ok";
    }
}
  • 浏览器输入http://localhost:7056/demo/get查看日志是否输出

服务上下线告警

以往我们一般采用zabbix+邮件的方式进行告警,但时效性太低,接下来我们通过spring boot admin来实现服务上下线告警,并将消息推送到钉钉群。

  • 报警通知类继承AbstractStatusChangeNotifier类,重写shouldNotifydoNotify方法
  • 根据获取的状态进行实例的信息获取和封装
  • 接入钉钉群机器人将封装的消息推送到钉钉群中

编码实现

spring-boot-admin-server的服务中编写DingtalkNotifier类用来实现该功能

package com.it235.cloud.example.notifier;


import com.alibaba.fastjson.JSONObject;
import com.alibaba.nacos.client.config.NacosConfigService;
import de.codecentric.boot.admin.server.domain.entities.Instance;
import de.codecentric.boot.admin.server.domain.entities.InstanceRepository;
import de.codecentric.boot.admin.server.domain.events.InstanceEvent;
import de.codecentric.boot.admin.server.domain.events.InstanceStatusChangedEvent;
import de.codecentric.boot.admin.server.notify.AbstractStatusChangeNotifier;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.expression.ParserContext;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Mono;

import java.util.Arrays;
import java.util.Map;

/**
 * @description:
 * @package: com.it235.cloud.example.notifier
 * @author: jianjun.ren
 * @date: Created in 2020/10/16 12:43
 * @copyright: Copyright (c) 2019
 * @modified: jianjun.ren
 */
@Slf4j
@Component
public class DingtalkNotifier extends AbstractStatusChangeNotifier {

    /**
     * 消息模板
     */
    private static final String template = "<<<%s>>> \n 【服务名】: %s(%s) \n 【状态】: %s(%s) \n 【服务ip】: %s \n 【详情】: %s";

    private String titleAlarm = "系统告警";

    private String titleNotice = "系统通知";

    private String[] ignoreChanges = new String[]{"UNKNOWN:UP","DOWN:UP","OFFLINE:UP"};

    public DingtalkNotifier(InstanceRepository repository) {
        super(repository);
    }

    @Override
    protected boolean shouldNotify(InstanceEvent event, Instance instance) {
        if (!(event instanceof InstanceStatusChangedEvent)) {
            return false;
        } else {
            InstanceStatusChangedEvent statusChange = (InstanceStatusChangedEvent)event;
            String from = this.getLastStatus(event.getInstance());
            String to = statusChange.getStatusInfo().getStatus();
            return Arrays.binarySearch(this.ignoreChanges, from + ":" + to) < 0 && Arrays.binarySearch(this.ignoreChanges, "*:" + to) < 0 && Arrays.binarySearch(this.ignoreChanges, from + ":*") < 0;
        }
    }


    @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());

                String status = ((InstanceStatusChangedEvent) event).getStatusInfo().getStatus();
                String messageText = null;
                switch (status) {
                    // 健康检查没通过
                    case "DOWN":
                        log.info("发送 健康检查没通过 的通知!");
                        messageText = String
                                .format(template,titleAlarm, instance.getRegistration().getName(), event.getInstance(),
                                        ((InstanceStatusChangedEvent) event).getStatusInfo().getStatus(), "健康检查没通过通知",
                                        instance.getRegistration().getServiceUrl(), JSONObject.toJSONString(instance.getStatusInfo().getDetails()));
                        //先输出信息在控制台
                        System.out.println(messageText);
                        break;
                    // 服务离线
                    case "OFFLINE":
                        log.info("发送 服务离线 的通知!");
                        messageText = String
                                .format(template,titleAlarm, instance.getRegistration().getName(), event.getInstance(),
                                        ((InstanceStatusChangedEvent) event).getStatusInfo().getStatus(), "服务离线通知",
                                        instance.getRegistration().getServiceUrl(), JSONObject.toJSONString(instance.getStatusInfo().getDetails()));
                        先输出信息在控制台
                        System.out.println(messageText);
                        break;
                    //服务上线
                    case "UP":
                        log.info("发送 服务上线 的通知!");
                        messageText = String
                                .format(template,titleNotice, instance.getRegistration().getName(), event.getInstance(),
                                        ((InstanceStatusChangedEvent) event).getStatusInfo().getStatus(), "服务上线通知",
                                        instance.getRegistration().getServiceUrl(), JSONObject.toJSONString(instance.getStatusInfo().getDetails()));
                        //先输出信息在控制台
                        System.out.println(messageText);
                        break;
                    // 服务未知异常
                    case "UNKNOWN":
                        log.info("发送 服务未知异常 的通知!");
                        messageText = String
                                .format(template,titleAlarm, instance.getRegistration().getName(), event.getInstance(),
                                        ((InstanceStatusChangedEvent) event).getStatusInfo().getStatus(), "服务未知异常通知",
                                        instance.getRegistration().getServiceUrl(), JSONObject.toJSONString(instance.getStatusInfo().getDetails()));
                        先输出信息在控制台
                        System.out.println(messageText);
                        break;
                    default:
                        break;
                }
            } else {
                log.info("Instance {} ({}) {}", instance.getRegistration().getName(), event.getInstance(),
                        event.getType());
            }
        });
    }
}

启动服务可以看到控制台输出的监课检查通知

  • 服务上线通知
  • 服务下线通知
  • 加入钉钉群消息推送功能

springboot 集成 agent springboot 集成钉钉_SpringBootAdmin_04

springboot 集成 agent springboot 集成钉钉_SpringBootAdmin_05

springboot 集成 agent springboot 集成钉钉_SpringClou_06

springboot 集成 agent springboot 集成钉钉_微服务监控_07

  • 推送代码编写
    钉钉推送需要发送http请求触发webhook机器人,我们先加入pom.xml依赖
<dependency>
          <groupId>org.apache.httpcomponents</groupId>
          <artifactId>httpclient</artifactId>
          <version>4.5.13</version>
      </dependency>

      <dependency>
          <groupId>org.apache.httpcomponents</groupId>
          <artifactId>httpcore</artifactId>
          <version>4.4.13</version>
      </dependency>

编写DingtalkUtils代码

package com.it235.cloud.example.notifier;

import com.alibaba.fastjson.JSON;
import lombok.extern.slf4j.Slf4j;
import org.apache.http.HttpResponse;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.util.EntityUtils;
import org.springframework.http.HttpStatus;

import java.util.HashMap;

/**
 * @description:
 * @package: com.it235.cloud.example.notifier
 * @author: jianjun.ren
 * @date: Created in 2020/10/17 0:11
 * @copyright: Copyright (c) 2019
 * @modified: jianjun.ren
 */
@Slf4j
public class DingtalkUtils {

    public static void main(String[] args) {
        pushInfoToDingding("测试消息通知", "b240b227f5add0fsdfsf54721bf08d7ee17114");
    }

    public static Boolean pushInfoToDingding(String textMsg, String dingURL) {

        HashMap<String, Object> resultMap = new HashMap<>(8);
        resultMap.put("msgtype", "text");

        HashMap<String, String> textItems = new HashMap<>(8);
        textItems.put("content", textMsg);
        resultMap.put("text", textItems);

        HashMap<String, Object> atItems = new HashMap<>(8);
        atItems.put("atMobiles", null);
        atItems.put("isAtAll", false);
        resultMap.put("at", atItems);

        
        dingURL = "https://oapi.dingtalk.com/robot/send?access_token=" + dingURL;
        try {
            HttpClient httpClient = HttpClients.createDefault();
            StringEntity stringEntity = new StringEntity(JSON.toJSONString(resultMap), "utf-8");

            HttpPost httpPost = createConnectivity(dingURL);
            httpPost.setEntity(stringEntity);
            HttpResponse response = httpClient.execute(httpPost);
            if (response.getStatusLine().getStatusCode() == HttpStatus.OK.value()) {
                String result = EntityUtils.toString(response.getEntity(), "utf-8");
                System.out.println(result);
                log.info("执行结果:{}" , result);
            }
            return Boolean.TRUE;
        } catch (Exception e) {
            e.printStackTrace();
            return Boolean.FALSE;
        }
    }


    static HttpPost createConnectivity(String restUrl) {
        HttpPost post = new HttpPost(restUrl);
        post.setHeader("Content-Type", "application/json");
        post.setHeader("Accept", "application/json");
        post.setHeader("X-Stream", "true");
        return post;
    }
}

更换token后直接运行测试,看是否发送成功,我这了显示发送成功

springboot 集成 agent springboot 集成钉钉_springboot 集成 agent_08

改造DingtalkNotifier中sout输出的消息为钉钉工具类推送

DingtalkUtils.pushInfoToDingding(messageText , "b240b227f5add0ffba1d04d017b53019f5sdfsfsds12317ee17114");
  • 改造完成后,再次启动服务,查看钉钉消息群的信息
  • 格式美化
    此时的格式仅仅是字符串,钉钉提供markdown及更多的小卡片元素,请大家自行扩展,我的github源码整合库中也有讲解。