Cat入门学习笔记整理


本文为美团点评基于 Java 开发的一套开源的分布式实时监控系统Cat的入门学习笔记整理,主要参考以下资料:

本文涉及到的代码仓库链接: https://gitee.com/DaHuYuXiXi/cat-demo-project


链路调用追踪介绍

在微服务架构体系下,微服务系统中各个微服务之间的调用关系链路十分复杂,并且考虑到不同的微服务可能是不同团队进行的开发与维护,因此一旦在某个调用链路节点上出现问题,问题的排查就变得十分困难,困难的原因主要在于我们需要将当前业务的执行原始现场进行还原,从而方便我们对业务执行情况进行分析和问题的定位。

因此调用链路监控是微服务架构体系中非常重要的一环,它除了能帮助我们定位问题以外,还能帮助项目成员清晰的去了解项目部署结构,毕竟一个几十上百的微服务,相信在运行时间久了之后,项目的结构会出现上述非常复杂的调用链,在这种情况下,团队开发者甚至是架构师都不一定能对项目的网络结构有很清晰的了解,那就更别谈系统优化了。

微服务架构下,如果没有一款强大的调用链监控工具,势必会产生如下问题:

  • 问题处理不及时,影响用户的体验
  • 不同应用的负责人不承认是自己的问题导致失败,容易出现“扯皮”
  • 服务之间的调用关系难以梳理,可能会存在很多错误的调用关系
  • 由于没有具体的数据,团队成员对自己的应用性能不在意

由上可知,我们需要一个调用链路监控工具,该工具至少需要帮助我们完成以下几点需求:

  • 线上的服务是否运行正常。是不是有一些服务已经宕机了,但是我们没有发现呢?如何快速发现已经宕机的服务?

  • 来自用户的一笔调用失败了,到底是哪个服务导致的错误,我们需要能够快速定位到才能做到修复。

  • 用户反映,我们的系统很“慢”。如何知道究竟慢在何处?


链路调用监控实现过程简析

在2010年,google发表了一篇名为“Dapper, a Large-Scale Distributed Systems Tracing Infrastructure”的论文,在文中介绍了google生产环境中大规模分布式系统下的跟踪系统Dapper的设计和使用经验。而如今很多的调用链系统如zipkin/pinpoint等系统都是基于这篇文章而实现的。

接下来我们就简单的介绍一下Dapper中调用链监控的原理:
在这里插入图片描述
如上图所示,这是一个查询订单的简单业务,他有如下的步骤:

  1. 前端浏览器发起请求到订单服务,订单服务会从数据库中查询出对应的订单数据。订单数据中包含了商品的ID,所以还需要查询商品信息。
  2. 订单服务发起一笔调用,通过rpc的方式,远程调用商品服务的查询商品信息接口。
  3. 订单服务组装数据,返回给前端。

这几个步骤中,有几个核心概念需要了解:

  • Trace:

    Trace是指一次请求调用的链路过程,trace id 是指这次请求调用的ID。在一次请求中,会在网络的最开始生成一个全局唯一的用于标识此次请求的trace id,这个trace id在这次请求调用过程中无论经过多少个节点都会保持不变,并且在随着每一层的调用不停的传递。最终,可以通过trace id将这一次用户请求在系统中的路径全部串起来。

  • Span:

    Span是指一个模块的调用过程,一般用span id来标识。在一次请求的过程中会调用不同的节点/模块/服务,每一次调用都会生成一个新的span id来记录。这样,就可以通过span id来定位当前请求在整个系统调用链中所处的位置,以及它的上下游节点分别是什么。

那么回到上面的案例中,查询订单数据和查询商品数据这两个过程,就分别是两个span,我们记为span A和B。B的parent也就是父span就是A。这两个span都拥有同一个Trace Id:1。

并且在信息收集过程中,会记录调用的开始时间,结束时间,从中计算出调用的耗时。

这样,就可以清楚的知道,每笔调用:

  • 经过了哪几个服务以及服务的调用顺序
  • 每个服务过程的耗时

常见的链路追踪框架

在这里插入图片描述
CAT是由大众点评开源的一款调用链监控系统,服务端基于JAVA开发的。有很多互联网企业在使用,热度非常高。它有一个非常强大和丰富的可视化报表界面,这一点其实对于一款调用链监控系统而来非常的重要。在CAT提供的报表界面中有非常多的功能,几乎能看到你想要的任何维度的报表数据。

  • 特点:聚合报表丰富,中文支持好,国内案例多

在这里插入图片描述

Pinpoint是由一个韩国团队实现并开源,针对Java编写的大规模分布式系统设计,通过JavaAgent的机制做字节代码植入,实现加入traceid和获取性能数据的目的,对应用代码零侵入。

在这里插入图片描述


在这里插入图片描述
SkyWalking是apache基金会下面的一个开源APM项目,为微服务架构和云原生架构系统设计。它通过探针自动收集所需的指标,并进行分布式追踪。通过这些调用链路以及指标,Skywalking APM会感知应用间关系和服务间关系,并进行相应的指标统计。Skywalking支持链路追踪和监控应用组件基本涵盖主流框架和容器,如国产RPC Dubbo和motan等,国际化的spring boot,spring cloud。

在这里插入图片描述


在这里插入图片描述

Zipkin是由Twitter开源,是分布式链路调用监控系统,聚合各业务系统调用延迟数据,达到链路调用监控跟踪。Zipkin基于Google的Dapper论文实现,主要完成数据的收集、存储、搜索与界面展示。

在这里插入图片描述


CAT报表介绍

本部分内容截取至官方文档报表简介章节,链接:官方文档: 报表介绍篇
在这里插入图片描述

CAT支持如下报表:

报表名称 报表内容
Transaction报表 一段代码的运行时间、次数、比如URL/cache/sql执行次数相应时间
Event报表 一段代码运行次数,比如出现一次异常
Problem报表 根据Transaction/Event数据分析出系统可能出现的一次,慢程序
Heartbeat报表 JVM状态信息
Business报表 业务指标等,用户可以自己定制

Transaction报表

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述


Event报表

在这里插入图片描述
在这里插入图片描述


Problem报表

在这里插入图片描述


Heartbeat报表

在这里插入图片描述


Business报表

在这里插入图片描述


Cat实战

docker安装

#由于仓库的git历史记录众多,对于不关注历史,只关注最新版本或者基于最新版本贡献的新用户,可以在第一次克隆代码时增加--depth=1参数以加快下载速度,如
git clone --depth=1 https://github.com/dianping/cat.git

模块介绍:

  • cat-client: 客户端,上报监控数据
  • cat-consumer: 服务端,收集监控数据进行统计分析,构建丰富的统计报表
  • cat-alarm: 实时告警,提供报表指标的监控告警
  • cat-hadoop: 数据存储,logview 存储至 Hdfs
  • cat-home: 管理端,报表展示、配置管理等

服务端安装:

官方文档: 服务端部署教程

CAT服务端的环境要求如下:

  • Linux 2.6以及之上(2.6内核才可以支持epoll),线上服务端部署请使用Linux环境,Mac以及Windows环境可以作为开发环境,美团点评内部CentOS 6.5
  • Java 6,7,8,服务端推荐使用jdk7的版本,客户端jdk6、7、8都支持
  • Maven 3及以上
  • MySQL 5.6,5.7,更高版本MySQL都不建议使用,不清楚兼容性
  • J2EE容器建议使用tomcat,建议使用推荐版本7.*.或8.0.
  • Hadoop环境可选,一般建议规模较小的公司直接使用磁盘模式,可以申请CAT服务端,500GB磁盘或者更大磁盘,这个磁盘挂载在/data/目录上

数据库安装:

  • 数据库的脚本文件 script/CatApplication.sql

    mysql -uroot -Dcat < CatApplication.sql
    
  • 说明:数据库编码使用utf8mb4,否则可能造成中文乱码等问题

应用打包:

本文安装的cat版本为3.0.0,如果版本与本文安装的不一致,可能会存在各种各样的兼容性问题

cat 3.0.0版本对应的cat.war已经在本文开头给出的代码仓库中给出,大家可以拉取工程获取


Liunx源码安装步骤:

  • 创建/data目录用于存放CAT的配置文件和运行时候的数据存储目录

注意:

  • 要求/data/目录能进行读写操作,如果/data/目录不能写,建议使用linux的软链接链接到一个固定可写的目录。所有的客户端集成程序的机器以及CAT服务端机器都需要进行这个权限初始化。
  • CAT支持CAT_HOME环境变量,可以通过JVM参数修改默认的路径。
mkdir /data
chmod -R 777 /data/
  • 配置/data/appdatas/cat/client.xml ($CAT_HOME/client.xml)
mkdir -p /data/appdatas/cat
cd /data/appdatas/cat
vi client.xml

修改client.xml文件:

<?xml version="1.0" encoding="utf-8"?>
<config mode="client">
    <servers>
    	<!--下面的IP地址替换为主机的IP地址-->
        <server ip="192.168.1.101" port="2280" http-port="8080"/>
    </servers>
</config>
  • 配置/data/appdatas/cat/datasources.xml($CAT_HOME/datasources.xml)
vi datasources.xml
<?xml version="1.0" encoding="utf-8"?>

<data-sources>
	<data-source id="cat">
		<maximum-pool-size>3</maximum-pool-size>
		<connection-timeout>1s</connection-timeout>
		<idle-timeout>10m</idle-timeout>
		<statement-cache-size>1000</statement-cache-size>
		<properties>
			<driver>com.mysql.jdbc.Driver</driver>
			<url><![CDATA[jdbc:mysql://127.0.0.1:3306/cat]]></url>  <!-- 请替换为真实数据库URL及Port  -->
			<user>root</user>  <!-- 请替换为真实数据库用户名  -->
			<password>root</password>  <!-- 请替换为真实数据库密码  -->
			<connectionProperties><![CDATA[useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&socketTimeout=120000]]></connectionProperties>
		</properties>
	</data-source>
</data-sources>
  • 创建好名为cat的数据库,并将CatApplication.sql导入其中

在这里插入图片描述

  • 拉取tomcat镜像
#1.拉取镜像
docker pull tomcat:8.5.40
  • 找一个目录,把cat.war包、client.xml、server.xml、datasource.xml都放到目录下,并且在该目录下创建并编辑Dockerfile文件

注意: 将cat-home.war更名为cat.war。

from tomcat:8.5.40 
RUN rm -rf /usr/local/tomcat/webapps/*
COPY cat.war   /usr/local/tomcat/webapps
RUN mkdir -p /data/appdatas/cat
RUN mkdir -p /data/applogs/cat
RUN chmod -R 777 /data/
COPY client.xml   /data/appdatas/cat
COPY datasources.xml   /data/appdatas/cat
COPY server.xml   /data/appdatas/cat
ENV TZ=Asia/Shanghai
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
EXPOSE 8080
EXPOSE 2280
  • 基于tomcat镜像构建cat镜像
docker build -t cat:3.0 .
  • 启动cat容器
docker run -di --name=tomcat -p 8080:8080 -p 2280:2280 -v /root/tomcat/webapps:/usr/local/tomcat/webapps  -v /root/tomcat/conf:/usr/local/tomcat/conf -v /data:/data tomcat:8

记得将防火墙的8080和2280端口都进行开放

  • 为防止中文乱码产生,可以采用docker exec命令进入cat容器内部,修改server.xml配置文件,然后重启容器
#修改/root/tomcat/conf目录下 server.xml配置文件,防止中文乱码产生
<Connector port="8080" protocol="HTTP/1.1"
           URIEncoding="utf-8"    connectionTimeout="20000"
               redirectPort="8443" />  <!-- 增加  URIEncoding="utf-8"  -->  

注意: 修改完下面的配置,去掉注释后,直接粘贴到上面给出的页面中即可。

<?xml version="1.0" encoding="utf-8"?>
<server-config>
   <server id="default">
      <properties>
         <property name="local-mode" value="false"/>
         <property name="job-machine" value="false"/>
         <property name="send-machine" value="false"/>
         <property name="alarm-machine" value="false"/>
         <property name="hdfs-machine" value="false"/>
            #替换为当前cat所在服务器ip,如果cat采用集群部署,这里可以指定各个节点的地址 
         <property name="remote-servers" value="10.1.1.1:8080,10.1.1.2:8080,10.1.1.3:8080"/>
      </properties>
       # 没有使用分布式文件系统HDFS,下面这段配置可以不用管
      <storage  local-base-dir="/data/appdatas/cat/bucket/" max-hdfs-storage-time="15" local-report-storage-time="7" local-logivew-storage-time="7">
        <hdfs id="logview" max-size="128M" server-uri="hdfs://10.1.77.86/" base-dir="user/cat/logview"/>
        <hdfs id="dump" max-size="128M" server-uri="hdfs://10.1.77.86/" base-dir="user/cat/dump"/>
        <hdfs id="remote" max-size="128M" server-uri="hdfs://10.1.77.86/" base-dir="user/cat/remote"/>
      </storage>
      <consumer>
         <long-config default-url-threshold="1000" default-sql-threshold="100" default-service-threshold="50">
            <domain name="cat" url-threshold="500" sql-threshold="500"/>
            <domain name="OpenPlatformWeb" url-threshold="100" sql-threshold="500"/>
         </long-config>
      </consumer>
   </server>
   #替换为当前cat所在服务器ip
   <server id="10.1.1.1">
      <properties>
         <property name="job-machine" value="true"/>
         <property name="alarm-machine" value="true"/>
     <property name="send-machine" value="true"/>
      </properties>
   </server>
</server-config>

在这里插入图片描述

注意: 修改完下面的配置,去掉注释后,直接粘贴到上面给出的页面中即可。

<?xml version="1.0" encoding="utf-8"?>
<router-config backup-server="cat所在服务器IP" backup-server-port="2280">
   <default-server id="cat所在服务器IP" weight="1.0" port="2280" enable="true"/>
   <network-policy id="default" title="默认" block="false" server-group="default_group">
   </network-policy>
   <server-group id="default_group" title="default-group">
      <group-server id="cat所在服务器IP"/>
   </server-group>
   <domain id="cat">
      # 对服务器进行分组,如果配置了多个服务器,下面需要指明这多个服务器的地址
      <group id="default"> 
         <server id="cat所在服务器IP" port="2280" weight="1.0"/>
      </group>
   </domain>
</router-config>

客户端集成

  • 添加maven依赖
        <dependency>
            <groupId>com.dianping.cat</groupId>
            <artifactId>cat-client</artifactId>
            <version>3.0.1</version>
        </dependency>
  • 在你的项目中创建 src/main/resources/META-INF/app.properties 文件, 并添加如下内容:
app.name={
    
    appkey}

appkey 只能包含英文字母 (a-z, A-Z)、数字 (0-9)、下划线 (_) 和中划线 (-)

  • 以下所有文件,如果在windows下,需要创建在启动项目的盘符下。(项目部署在c盘底下,那么就在c盘根目录下创建data目录)

    1. 创建 /data/appdatas/cat 目录—>确保你具有这个目录的读写权限。
    2. 创建 /data/applogs/cat 目录 (可选)—>这个目录是用于存放运行时日志的,这将会对调试提供很大帮助,同样需要读写权限。
    3. 创建 /data/appdatas/cat/client.xml,内容如下
<?xml version="1.0" encoding="utf-8"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema" xsi:noNamespaceSchemaLocation="config.xsd">
    <servers>
        <server ip="127.0.0.1" port="2280" http-port="8080" />
    </servers>
</config>
  • 编写代码
@RestController
public class TestController {
    
    
    @GetMapping("/test")
    public String test() {
    
    
        Transaction t = Cat.newTransaction("URL", "pageName");

        try {
    
    
            Cat.logEvent("URL.Server", "serverIp", Event.SUCCESS, "ip=${serverIp}");
            Cat.logMetricForCount("metric.key");
            Cat.logMetricForDuration("metric.key", 5);

            //让代码抛出异常
            int i = 1 / 0;
            t.setStatus(Transaction.SUCCESS);
        } catch (Exception e) {
    
    
            t.setStatus(e);
            Cat.logError(e);
        } finally {
    
    
            t.complete();
        }

        return "test";
    }
}
  • 启动springboot项目,访问该接口,然后去cat查看结果

在这里插入图片描述
如上图所示,dhy-cat应用已经出现了4次接口调用错误,我们具体查看以下:
在这里插入图片描述
查看具体的错误信息:
在这里插入图片描述
很显然看出上图所示其实是一个除0异常,到此为止SpringBoot客户端集成Cat就完成了。


API介绍

Transaction

Transaction 适合记录跨越系统边界的程序访问行为,比如远程调用,数据库调用,也适合执行时间较长的业务逻辑监控,Transaction用来记录一段代码的执行时间和次数。

现在我们的框架还没有与dubbo、mybatis做集成,所以我们通过手动编写一个本地方法,来测试Transaction的用法,创建TransactionController用于测试。

    @GetMapping("/test1")
    public String test1(){
    
    
        //开启第一个Transaction,类别为URL,名称为test
        Transaction t = Cat.newTransaction("URL", "test");

        try {
    
    
            dubbo();
            t.setStatus(Transaction.SUCCESS);
        } catch (Exception e) {
    
    
            t.setStatus(e);
            Cat.logError(e);
        } finally {
    
    
            t.complete();
        }

        return "test";
    }


    private String dubbo(){
    
    
        //开启第二个Transaction,类别为DUBBO,名称为dubbo
        Transaction t = Cat.newTransaction("DUBBO", "dubbo");

        try {
    
    
            t.setStatus(Transaction.SUCCESS);
        } catch (Exception e) {
    
    
            t.setStatus(e);
            Cat.logError(e);
        } finally {
    
    
            t.complete();
        }

        return "test";
    }

上面的代码中,开启了两个Transaction,其中第一个Transaction为Controller接收到的接口调用,第二个位我们编写的本地方法dubbo用来模拟远程调用。在方法内部,开启第二个Transaction。

启动项目,访问接口http://localhost:9200/test1

在这里插入图片描述
点击左侧菜单Transaction报表,选中URL类型对应的Log View查看调用链关系。
在这里插入图片描述

如图所示调用链已经形成,可以看到类型为URL的test调用了类型为DUBBO的dubbo方法,分别耗时0.47ms和 1.02ms。


扩展API

CAT提供了一系列 API 来对 Transaction 进行修改。

  • addData 添加额外的数据显示
  • setStatus 设置状态,成功可以设置SUCCESS,失败可以设置异常
  • setDurationInMillis 设置执行耗时(毫秒)
  • setTimestamp 设置执行时间
  • complete 结束Transaction

编写如下代码进行测试:

    @GetMapping("/api")
    public String api() {
    
    
        Transaction t = Cat.newTransaction("URL", "pageName");

        try {
    
    
            //设置执行时间1秒
            t.setDurationInMillis(1000);
            t.setTimestamp(System.currentTimeMillis());
            //添加额外数据
            t.addData("content");
            t.setStatus(Transaction.SUCCESS);
        } catch (Exception e) {
    
    
            t.setStatus(e);
            Cat.logError(e);
        } finally {
    
    
            t.complete();
        }

        return "api";
    }

启动项目,访问接口: http://localhost:9200/api

点击左侧菜单Transaction报表,选中URL类型对应的Log View查看调用链关系。

在这里插入图片描述
点击左侧菜单Transaction报表,选中URL类型对应的Log View查看调用链关系。
在这里插入图片描述
如图所示,调用耗时已经被手动修改成了1000ms,并且添加了额外的信息content。

在使用 Transaction API 时,你可能需要注意以下几点:

  1. 你可以调用 addData 多次,添加的数据会被 & 连接起来。
  2. 不要忘记完成 transaction.complete() , 否则你会得到一个毁坏的消息树以及内存泄漏!

Event

Event 用来记录一件事发生的次数,比如记录系统异常,它和transaction相比缺少了时间的统计,开销比transaction要小。

  • Cat.logEvent: 记录一个事件。
Cat.logEvent("URL.Server", "serverIp", Event.SUCCESS, "ip=${serverIp}");
  • Cat.logError: 记录一个带有错误堆栈信息的 Error。

Error 是一种特殊的事件,它的 type 取决于传入的 Throwable e

1. 如果 `e` 是一个 `Error`, `type` 会被设置为 `Error`。
2. 如果 `e` 是一个 `RuntimeException`, `type` 会被设置为 `RuntimeException`。
3. 其他情况下,`type` 会被设置为 `Exception`。

同时错误堆栈信息会被收集并写入 data 属性中。

try {
    
    
    int i = 1 / 0;
} catch (Throwable e) {
    
    
    Cat.logError(e);
}

你可以向错误堆栈顶部添加你自己的错误消息,如下代码所示:

Cat.logError("error(X) := exception(X)", e);

编写案例测试上述API:

@RestController
@RequestMapping("/event")
public class EventController {
    
    
    @RequestMapping("/logEvent")
    public String logEvent(){
    
    
        Cat.logEvent("URL.Server", "serverIp",
                Event.SUCCESS, "ip=127.0.0.1");
        return "test";
    }

    @RequestMapping("/logError")
    public String logError(){
    
    
        try {
    
    
            int i = 1 / 0;
        } catch (Throwable e) {
    
    
            Cat.logError("error(X) := exception(X)", e);
        }
        return "test";
    }
}

启动项目,访问接口http://localhost:9200/event/logEventhttp://localhost:9200/event/logError

在这里插入图片描述
通过上图可以看到,增加了两个事件:URL.Server和RuntimeException。点开LOG查看。

在这里插入图片描述
这里出现了两个Event的详细内容:

1.URL.Server是一个正常的事件,打印出了IP=127.0.0.1的信息。

2.RuntimeException是一个错误Event,不仅打印出了错误堆栈,还将我们打印的

error(X) := exception(X)

内容放到了堆栈的最上方便于查看。


Metric

Metric 用于记录业务指标、指标可能包含对一个指标记录次数、记录平均值、记录总和,业务指标最低统计粒度为1分钟。

# Counter
Cat.logMetricForCount("metric.key");
Cat.logMetricForCount("metric.key", 3);

# Duration
Cat.logMetricForDuration("metric.key", 5);

我们每秒会聚合 metric。

举例来说,如果你在同一秒调用 count 三次(相同的 name),累加他们的值,并且一次性上报给服务端。

duration 的情况下,用平均值来取代累加值。

编写案例测试上述API:

@RestController
@RequestMapping("/metric")
public class MetricController {
    
    

    @RequestMapping("/count")
    public String count(){
    
    
        Cat.logMetricForCount("count");
        return "test";
    }

    @RequestMapping("/duration")
    public String duration(){
    
    
        Cat.logMetricForDuration("duration", 1000);
        return "test";
    }
}

启动项目,访问接口http://localhost:9200/metric/count 点击5次和http://localhost:9200/metric/duration

在这里插入图片描述
通过上图可以看到,count和duration的具体数值。

count一共点击了5次,所以这一分钟内数值为5。而duration不管点击多少次,由于取的是平均值,所以一直是1000。

统计粒度是分钟


CAT监控界面介绍

DashBoard

DashBoard仪表盘显示了每分钟出现错误的系统及其错误的次数和时间。

在这里插入图片描述

  • 点击右上角的时间按钮可以切换不同的展示时间,-7d代表7天前,-1h代表1小时前,now定位到当前时间
  • 上方的时间轴按照分钟进行排布,点击之后可以看到该时间到结束的异常情况
  • 下方标识了出错的系统和出错的时间、次数,点击系统名称可以跳转到Problem报表

Transaction

Transaction报表用来监控一段代码运行情况:运行次数、QPS、错误次数、失败率、响应时间统计(平均影响时间、Tp分位值)等等

  • 应用启动后默认会打点的部分:
打点 来源组件 描述
System cat-client 上报监控数据的打点信息
URL 需要接入cat-filter URL访问的打点信息
  • 小时报表:Type统计界面展示了一个Transaction的第一层分类的视图,可以知道这段时间里面一个分类运行的次数,平均响应时间,延迟,以及分位线。

在这里插入图片描述
从上而下分析报表:

  1. 报表的时间跨度 CAT默认是以一小时为统计时间跨度,点击[切到历史模式],更改查看报表的时间跨度:默认是小时模式;切换为历史模式后,右侧快速导航,变为month(月报表)、week(周报表)、day(天报表),可以点击进行查看,注意报表的时间跨度会有所不同。

  2. 时间选择 通过右上角时间导航栏选择时间:点击[+1h]/[-1h]切换时间为下一小时/上一小时;点击[+1d]/[-1d]切换时间为后一天的同一小时/前一天的同一小时;点击右上角[+7d]/[-7d]切换时间为后一周的同一小时/前一周的同一小时;点击[now]回到当前小时。

  3. 项目选择 输入项目名,查看项目数据;如果需要切换其他项目数据,输入项目名,回车即可。

  4. 机器分组 CAT可以将若干个机器,作为一个分组进行数据统计。默认会有一个All分组,代表所有机器的统计数据,即集群统计数据。

  5. 所有Type汇总表格 第一层分类(Type),点击查看第二级分类(称为name)数据:
    在这里插入图片描述

  • Transaction的埋点的Type和Name由业务自己定义,当打点了Cat.newTransaction(type, name)时,第一层分类是type,第二级分类是name。
  • 第二级分类数据叫是统计相同type下的所有name数据,数据均与第一级(type)一样的展示风格
  1. 单个Type指标图表 点击show,查看Type所有name分钟级统计,如下图:

在这里插入图片描述

  1. 指标说明 显示的是小时粒度第一级分类(type)的次数、错误数、失败率等数据。

  2. 样本logview L代表logview,为一个样例的调用链路。

在这里插入图片描述

  1. 分位线说明 小时粒度的时间第一级分类(type)相关统计
  • 95line表示95%的请求的响应时间比参考值要小,999line表示99.9%的响应时间比参考值要小,95line以及99line,也称之为tp95、tp99

  • 历史报表

Transaction历史报表支持每天、每周、每月的数据统计以及趋势图,点击导航栏的切换历史模式进行查询。Transaction历史报表以响应时间、访问量、错误量三个维度进行展示,以天报表为例:选取一个type,点击show,即可查看天报表。


Event

Event报表监控一段代码运行次数:例如记录程序中一个事件记录了多少次,错误了多少次。Event报表的整体结构与Transaction报表几乎一样,只缺少响应时间的统计。

  • 第一级分类(Type)统计界面

Type统计界面展示了一个Event的第一层分类的视图,Event相对于Transaction少了运行时间统计。可以知道这段时间里面一个分类运行的次数,失败次数,失败率,采样logView,QPS。

在这里插入图片描述

  • 第二级分类(Name)统计界面

第二级分类在Type统计界面中点击具体的Type进入,展示的是相同type下所有的name数据,可以理解为某type下更细化的分类。

在这里插入图片描述


Problem

Problem记录整个项目在运行过程中出现的问题,包括一些异常、错误、访问较长的行为。Problem报表是由logview存在的特征整合而成,方便用户定位问题。 来源:

  1. 业务代码显示调用Cat.logError(e) API进行埋点,具体埋点说明可查看埋点文档。
  2. 与LOG框架集成,会捕获log日志中有异常堆栈的exception日志。
  3. long-url,表示Transaction打点URL的慢请求
  4. long-sql,表示Transaction打点SQL的慢请求
  5. long-service,表示Transaction打点Service或者PigeonService的慢请求
  6. long-call,表示Transaction打点Call或者PigeonCall的慢请求
  7. long-cache,表示Transaction打点Cache.开头的慢请求

所有错误汇总报表 第一层分类(Type),代表错误类型,比如error、long-url等;第二级分类(称为Status),对应具体的错误,比如一个异常类名等。

在这里插入图片描述

错误数分布 点击type和status的show,分别展示type和status的分钟级错误数分布:

在这里插入图片描述


HeartBeat

Heartbeat报表是CAT客户端,以一分钟为周期,定期向服务端汇报当前运行时候的一些状态

  • JVM相关指标

以下所有的指标统计都是1分钟内的值,cat最低统计粒度是一分钟。

JVM GC 相关指标 描述
NewGc Count / PS Scavenge Count 新生代GC次数
NewGc Time / PS Scavenge Time 新生代GC耗时
OldGc Count 老年代GC次数
PS MarkSweepTime 老年代GC耗时
Heap Usage Java虚拟机堆的使用情况
None Heap Usage Java虚拟机Perm的使用情况

在这里插入图片描述

JVM Thread 相关指标 描述
Active Thread 系统当前活动线程
Daemon Thread 系统后台线程
Total Started Thread 系统总共开启线程
Started Thread 系统每分钟新启动的线程
CAT Started Thread 系统中CAT客户端启动线程

在这里插入图片描述
可以参考java.lang.management.ThreadInfo的定义


  • 系统指标
System 相关指标 描述
System Load Average 系统Load详细信息
Memory Free 系统memoryFree情况
FreePhysicalMemory 物理内存剩余空间
/ Free /根的使用情况
/data Free /data盘的使用情况

在这里插入图片描述
在这里插入图片描述


Business

Business报表对应着业务指标,比如订单指标。与Transaction、Event、Problem不同,Business更偏向于宏观上的指标,另外三者偏向于微观代码的执行情况。

场景示例:

1. 我想监控订单数量。
2. 我想监控订单耗时。

在这里插入图片描述

  • 基线: 基线是对业务指标的预测值。
  • 基线生成算法:最近一个月的4个每周几的数据加权求和平均计算得出,秉着更加信任新数据的原则,cat会基于历史数据做异常点的修正,会把一些明显高于以及低于平均值的点剔除。

举例:今天是2018-10-25(周四),今天整天基线数据的算法是最近四个周四(2018-10-18,2018-10-11,2018-10-04,2018-09-27)的每个分钟数据的加权求和或平均,权重值依次为1,2,3,4。如:当前时间为19:56分设为value,前四周对应的19:56分数据(由远及近)分别为A,B,C,D,则value = (A+2B+3C+4D) / 10。

对于刚上线的应用,第一天没有基线,第二天的基线基线是前一天的数据,以此类推。

  • 如何开启基线:只有配置了基线告警的指标,才会自动计算基线。如需基线功能,请配置基线告警。
  • 注意事项:
  1. 打点尽量用纯英文,不要带一些特殊符号,例如 空格( )、分号(:)、竖线(|)、斜线(/)、逗号(,)、与号(&)、星号(*)、左右尖括号(<>)、以及一些奇奇怪怪的字符
  2. 如果有分隔需求,建议用下划线(_)、中划线(-)、英文点号(.)等
  3. 由于数据库不区分大小写,请尽量统一大小写,并且不要对大小写进行改动
  4. 有可能出现小数:趋势图每个点都代表一分钟的值。假设监控区间是10分钟,且10分钟内总共上报5次,趋势图中该点的值为5%10=0.5

State

State报表显示了与CAT相关的信息。

在这里插入图片描述


Cat集成主流框架

完整代码可以参考本文一开始给出的仓库链接

在这里插入图片描述

Cat集成Dubbo

  • 将插件目录下的cat-monitor插件Install到本地仓库中
    在这里插入图片描述

注意: install前,可以先修改pom文件,将cat-client依赖改为3.0.1
在这里插入图片描述
如果出现CatLogger不存在的错误,替换为Cat.logError即可

生产者配置:

  • dubbo项目中使用如下依赖引入dubbo插件
		<dependency>
			<groupId>net.dubboclub</groupId>
			<artifactId>cat-monitor</artifactId>
			<version>0.0.6</version>
		</dependency>
   
        <dependency>
            <groupId>org.apache.dubbo</groupId>
            <artifactId>dubbo-spring-boot-starter</artifactId>
            <version>3.0.7</version>
        </dependency>

        <dependency>
            <groupId>net.dubboclub</groupId>
            <artifactId>cat-monitor</artifactId>
            <version>0.0.6</version>
        </dependency>        
  • dubbo服务端配置
server.port=9300
spring.application.name=dubbo_provider_cat
# 默认为dubbo协议
dubbo.protocol.name=dubbo
# dubbo协议默认通信端口号为20880
dubbo.protocol.port=20880
# 为了简化环境搭建,采用了本地直接调用的方式,所以将注册中心写成N/A表示不注册到注册中心
dubbo.registry.address=N/A
  • META-INF/app.properties勿忘

在这里插入图片描述

  • 设计好接口和实现类
public interface HelloService {
    
    
    String hello();
}

@DubboService(interfaceClass = HelloService.class)
public class HelloServiceImpl implements HelloService {
    
    

    public String hello() {
    
    
        return "hello cat";
    }
}
  • 生产者启动类
@EnableDubbo(scanBasePackages = "org.example.dubbo")
@SpringBootApplication
public class CatProviderDemo {
    
    
    public static void main(String[] args) {
    
    
        SpringApplication.run(CatProviderDemo.class,args);
    }
}

消费者配置:

  • 引入依赖
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.apache.dubbo</groupId>
            <artifactId>dubbo-spring-boot-starter</artifactId>
            <version>3.0.7</version>
        </dependency>
        <dependency>
            <groupId>net.dubboclub</groupId>
            <artifactId>cat-monitor</artifactId>
            <version>0.0.6</version>
        </dependency>
  • 配置
server.port=9500
spring.application.name=dubbo_consumer_cat
  • META-INF/app.properties勿忘

在这里插入图片描述

  • 出于方便,将调用的UserService接口copy一份(注意与生产者中的HelloService全类名一致)
public interface HelloService {
    
    
    public String hello();
}
  • 启动类
@EnableDubbo(scanBasePackages = "org.example.dubbo")
@SpringBootApplication
public class CatConsumerDemo {
    
    
    public static void main(String[] args) {
    
    
        SpringApplication.run(CatConsumerDemo.class,args);
    }
}
  • 编写测试用例,先启动生产者,再运行测试用例
@SpringBootTest(classes = CatConsumerDemo.class)
public class ConsumerTest {
    
    
    //采用直连而非从注册中心获取服务地址的方式,在@Reference注解中声明
    @DubboReference(url = "dubbo://127.0.0.1:20880")
    private HelloService helloService;

    @Test
    public void test(){
    
    
        for (int i = 0; i < 1000; i++) {
    
    
            System.out.println(helloService.hello());   
        }
    }
}

在这里插入图片描述

  • 查看cat页面

在这里插入图片描述
如图所示dubbo的调用已经被正确显示在transaction报表中。点击log view查看详细的调用。
在这里插入图片描述
如图所示,调用的日志已经被成功打印。

dubbo插件的日志打印内容显示的并不是十分的良好,如果在企业中应用,可以基于dubbo插件进行二次开发。


Cat集成Mybaits

  • 查看cat提供的插件目录可知,cat集成mybaits的方式,是利用了mybaits的拦截器,我们创建一个springboot与mybaits整合的项目,将提供的拦截器类copy到项目中即可

在这里插入图片描述

  • 测试表结构
CREATE TABLE `t_user` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `username` varchar(32) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
  `password` varchar(32) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
  • 引入依赖
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>2.1.2</version>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
            <version>5.1.27</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>com.dianping.cat</groupId>
            <artifactId>cat-client</artifactId>
            <version>3.0.1</version>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid</artifactId>
            <version>1.1.10</version>
        </dependency>
    </dependencies>
  • 配置文件
# datasource
spring:
  datasource:
   url: jdbc:mysql://119.91.143.140:3306/test?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=false&serverTimezone=Asia/Shanghai
   username: root
   password: xfx123xfx
   driver-class-name: com.mysql.jdbc.Driver
   type: com.alibaba.druid.pool.DruidDataSource

# mybatis
mybatis:
  mapper-locations: classpath:mapper/*Mapper.xml # mapper映射文件路径
  type-aliases-package: org.example.dao
server:
  port: 9500
  • mapper接口
@Mapper
public interface UserXmlMapper {
    
    
    List<User> findAll();
}
  • userMapper.xml映射文件
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="org.example.mapper.UserXmlMapper">
    <select id="findAll" resultType="user">
        select * from t_user
    </select>
</mapper>
  • 启动类
@SpringBootApplication
public class CatMybaitsDemo {
    
    
    public static void main(String[] args) {
    
    
        SpringApplication.run(CatMybaitsDemo.class,args);
    }
}
  • 测试用例,运行测试用例,确保到这一步为止都没有问题
@SpringBootTest(classes = CatMybaitsDemo.class)
public class CatMybaitsDemoTest {
    
    
    @Resource
    private UserXmlMapper userXmlMapper;

    @Test
    public void testSearchUser() throws InterruptedException {
    
    
        try {
    
    
            userXmlMapper.findAll().forEach(System.out::println);
        } catch (Exception e) {
    
    
            e.printStackTrace();
        }
        Thread.sleep(30000);
    }
}

  • 引入cat提供的mybaits插件
    在这里插入图片描述
import com.alibaba.druid.pool.DruidDataSource;
import com.dianping.cat.Cat;
import com.dianping.cat.message.Message;
import com.dianping.cat.message.Transaction;
import org.apache.ibatis.datasource.pooled.PooledDataSource;
import org.apache.ibatis.datasource.unpooled.UnpooledDataSource;
import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.mapping.*;
import org.apache.ibatis.plugin.*;
import org.apache.ibatis.reflection.MetaObject;
import org.apache.ibatis.session.Configuration;
import org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.session.RowBounds;
import org.apache.ibatis.type.TypeHandlerRegistry;

import javax.sql.DataSource;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.text.DateFormat;
import java.util.Date;
import java.util.List;
import java.util.Locale;
import java.util.Properties;
import java.util.regex.Matcher;
import java.util.regex.Pattern;


/**
 *  1.Cat-Mybatis plugin:  Rewrite on the version of Steven;
 *  2.Support DruidDataSource,PooledDataSource(mybatis Self-contained data source);
 * @author zhanzehui([email protected])
 */

@Intercepts({
    
    
        @Signature(method = "query", type = Executor.class, args = {
    
    
                MappedStatement.class, Object.class, RowBounds.class,
                ResultHandler.class }),
        @Signature(method = "update", type = Executor.class, args = {
    
     MappedStatement.class, Object.class })
})
public class CatMybatisPlugin implements Interceptor {
    
    

    private static final Pattern PARAMETER_PATTERN = Pattern.compile("\\?");
    private static final String MYSQL_DEFAULT_URL = "jdbc:mysql://UUUUUKnown:3306/%s?useUnicode=true";
    private Executor target;

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
    
    
        MappedStatement mappedStatement = this.getStatement(invocation);
        String          methodName      = this.getMethodName(mappedStatement);
        Transaction t = Cat.newTransaction("SQL", methodName);

        String sql = this.getSql(invocation,mappedStatement);
        SqlCommandType sqlCommandType = mappedStatement.getSqlCommandType();
        Cat.logEvent("SQL.Method", sqlCommandType.name().toLowerCase(), Message.SUCCESS, sql);

        String url = this.getSQLDatabaseUrlByStatement(mappedStatement);
        Cat.logEvent("SQL.Database", url);

        return doFinish(invocation,t);
    }

    private MappedStatement getStatement(Invocation invocation) {
    
    
        return (MappedStatement)invocation.getArgs()[0];
    }

    private String getMethodName(MappedStatement mappedStatement) {
    
    
        String[] strArr = mappedStatement.getId().split("\\.");
        String methodName = strArr[strArr.length - 2] + "." + strArr[strArr.length - 1];

        return methodName;
    }

    private String getSql(Invocation invocation, MappedStatement mappedStatement) {
    
    
        Object parameter = null;
        if(invocation.getArgs().length > 1){
    
    
            parameter = invocation.getArgs()[1];
        }

        BoundSql boundSql = mappedStatement.getBoundSql(parameter);
        Configuration configuration = mappedStatement.getConfiguration();
        String sql = sqlResolve(configuration, boundSql);

        return sql;
    }

    private Object doFinish(Invocation invocation,Transaction t) throws InvocationTargetException, IllegalAccessException {
    
    
        Object returnObj = null;
        try {
    
    
            returnObj = invocation.proceed();
            t.setStatus(Transaction.SUCCESS);
        } catch (Exception e) {
    
    
            Cat.logError(e);
            throw e;
        } finally {
    
    
            t.complete();
        }

        return returnObj;
    }


    private String getSQLDatabaseUrlByStatement(MappedStatement mappedStatement) {
    
    
        String url = null;
        DataSource dataSource = null;
        try {
    
    
            Configuration configuration = mappedStatement.getConfiguration();
            Environment environment = configuration.getEnvironment();
            dataSource = environment.getDataSource();

            url = switchDataSource(dataSource);

            return url;
        } catch (NoSuchFieldException|IllegalAccessException|NullPointerException e) {
    
    
            Cat.logError(e);
        }

        Cat.logError(new Exception("UnSupport type of DataSource : "+dataSource.getClass().toString()));
        return MYSQL_DEFAULT_URL;
    }

    private String switchDataSource(DataSource dataSource) throws NoSuchFieldException, IllegalAccessException {
    
    
        String url = null;

        if(dataSource instanceof DruidDataSource) {
    
    
            url = ((DruidDataSource) dataSource).getUrl();
        }else if(dataSource instanceof PooledDataSource) {
    
    
            Field dataSource1 = dataSource.getClass().getDeclaredField("dataSource");
            dataSource1.setAccessible(true);
            UnpooledDataSource dataSource2 = (UnpooledDataSource)dataSource1.get(dataSource);
            url =dataSource2.getUrl();
        }else {
    
    
            //other dataSource expand
        }

        return url;
    }

    public String sqlResolve(Configuration configuration, BoundSql boundSql) {
    
    
        Object parameterObject = boundSql.getParameterObject();
        List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
        String sql = boundSql.getSql().replaceAll("[\\s]+", " ");
        if (parameterMappings.size() > 0 && parameterObject != null) {
    
    
            TypeHandlerRegistry typeHandlerRegistry = configuration.getTypeHandlerRegistry();
            if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
    
    
                sql = sql.replaceFirst("\\?", Matcher.quoteReplacement(resolveParameterValue(parameterObject)));

            } else {
    
    
                MetaObject metaObject = configuration.newMetaObject(parameterObject);
                Matcher matcher = PARAMETER_PATTERN.matcher(sql);
                StringBuffer sqlBuffer = new StringBuffer();
                for (ParameterMapping parameterMapping : parameterMappings) {
    
    
                    String propertyName = parameterMapping.getProperty();
                    Object obj = null;
                    if (metaObject.hasGetter(propertyName)) {
    
    
                        obj = metaObject.getValue(propertyName);
                    } else if (boundSql.hasAdditionalParameter(propertyName)) {
    
    
                        obj = boundSql.getAdditionalParameter(propertyName);
                    }
                    if (matcher.find()) {
    
    
                        matcher.appendReplacement(sqlBuffer, Matcher.quoteReplacement(resolveParameterValue(obj)));
                    }
                }
                matcher.appendTail(sqlBuffer);
                sql = sqlBuffer.toString();
            }
        }
        return sql;
    }

    private String resolveParameterValue(Object obj) {
    
    
        String value = null;
        if (obj instanceof String) {
    
    
            value = "'" + obj.toString() + "'";
        } else if (obj instanceof Date) {
    
    
            DateFormat formatter = DateFormat.getDateTimeInstance(DateFormat.DEFAULT, DateFormat.DEFAULT, Locale.CHINA);
            value = "'" + formatter.format((Date) obj) + "'";
        } else {
    
    
            if (obj != null) {
    
    
                value = obj.toString();
            } else {
    
    
                value = "";
            }

        }
        return value;
    }

    @Override
    public Object plugin(Object target) {
    
    
        if (target instanceof Executor) {
    
    
            this.target = (Executor) target;
            return Plugin.wrap(target, this);
        }
        return target;
    }

    @Override
    public void setProperties(Properties properties) {
    
    
    }

}

将此文件和所有其他cat插件一同打包放到私有仓库上是一种更好的选择。

  • 编写mybatis-config.xml配置文件,将文件放置在resources/mybatis文件夹下:
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
    <plugins>
        <plugin interceptor="org.example.cat.CatMybatisPlugin"/>
    </plugins>
</configuration>
  • 修改application.yml文件:
# mybatis
mybatis:
  mapper-locations: classpath:mapper/*Mapper.xml # mapper映射文件路径
  type-aliases-package: org.example.dao
  # config-location:  # 指定mybatis的核心配置文件
  config-location: classpath:mybatis/mybatis-config.xml
  • 配置META-INF/app.properties
    在这里插入图片描述

  • 测试: 如果我们将sql语句修改为错误的语句,启动后,再进行测试

在这里插入图片描述
已经能够显示出有部分语句执行错误,如果要查看具体的错误,点击Log View查看:

在这里插入图片描述
图中不止能看到具体的sql语句,也可以看到报错的堆栈信息。


Cat集成日志框架

CAT集成日志框架的思路大体上都类似,所以课程中采用Spring Boot默认的logback日志框架来进行讲解,如果使用了log4j、log4j2处理方式也是类似的。

  • 引入依赖
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>

        <dependency>
            <groupId>com.dianping.cat</groupId>
            <artifactId>cat-client</artifactId>
            <version>3.0.1</version>
        </dependency>
  • application.yml配置
logging:
  level:
    root: info
  path: ./logs
  config: classpath:logback-spring.xml
server:
  port: 9600
  • 编写配置文件logback-spring.xml,放在resources目录下:
<?xml version="1.0" encoding="UTF-8" ?>
<configuration>
    <!-- 属性文件:在properties文件中找到对应的配置项 -->
    <springProperty scope="context" name="logging.path" source="logging.path"/>
    <contextName>cat</contextName>
    <appender name="consoleLog" class="ch.qos.logback.core.ConsoleAppender">
        <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
            <!--格式化输出(配色):%d表示日期,%thread表示线程名,%-5level:级别从左显示5个字符宽度%msg:日志消息,%n是换行符-->
            <pattern>%yellow(%d{yyyy-MM-dd HH:mm:ss}) %red([%thread]) %highlight(%-5level) %cyan(%logger{50}) -
                %magenta(%msg) %n
            </pattern>
            <charset>UTF-8</charset>
        </encoder>
    </appender>

    <!--根据日志级别分离日志,分别输出到不同的文件-->
    <appender name="fileInfoLog" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <filter class="ch.qos.logback.classic.filter.LevelFilter">
            <level>ERROR</level>
            <onMatch>DENY</onMatch>
            <onMismatch>ACCEPT</onMismatch>
        </filter>
        <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
            <pattern>
                %d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{50} - %msg%n
            </pattern>
            <charset>UTF-8</charset>
        </encoder>
        <!--滚动策略-->
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <!--按时间保存日志 修改格式可以按小时、按天、月来保存-->
            <fileNamePattern>${logging.path}/cat.info.%d{yyyy-MM-dd}.log</fileNamePattern>
            <!--保存时长-->
            <MaxHistory>90</MaxHistory>
            <!--文件大小-->
            <totalSizeCap>1GB</totalSizeCap>
        </rollingPolicy>
    </appender>

    <appender name="fileErrorLog" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <filter class="ch.qos.logback.classic.filter.ThresholdFilter">
            <level>ERROR</level>
        </filter>
        <encoder>
            <pattern>
                %d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{50} - %msg%n
            </pattern>
        </encoder>
        <!--滚动策略-->
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <!--路径-->
            <fileNamePattern>${logging.path}/cat.error.%d{yyyy-MM-dd}.log</fileNamePattern>
            <MaxHistory>90</MaxHistory>
        </rollingPolicy>
    </appender>
    <root level="info">
        <appender-ref ref="consoleLog"/>
        <appender-ref ref="fileInfoLog"/>
        <appender-ref ref="fileErrorLog"/>
    </root>
</configuration>
  • 查看cat官方提供的相关日志框架集成插件

在这里插入图片描述

  • 将CatLogbackAppender copy到我们自己的项目下
package org.example.appender;

import ch.qos.logback.classic.Level;
import ch.qos.logback.classic.spi.ILoggingEvent;
import ch.qos.logback.classic.spi.ThrowableProxy;
import ch.qos.logback.core.AppenderBase;
import ch.qos.logback.core.LogbackException;
import com.dianping.cat.Cat;

import java.io.PrintWriter;
import java.io.StringWriter;

public class CatLogbackAppender extends AppenderBase<ILoggingEvent> {
    
    

	@Override
	protected void append(ILoggingEvent event) {
    
    
		try {
    
    
			boolean isTraceMode = Cat.getManager().isTraceMode();
			Level level = event.getLevel();
			if (level.isGreaterOrEqual(Level.ERROR)) {
    
    
				logError(event);
			} else if (isTraceMode) {
    
    
				logTrace(event);
			}
		} catch (Exception ex) {
    
    
			throw new LogbackException(event.getFormattedMessage(), ex);
		}
	}

	private void logError(ILoggingEvent event) {
    
    
		ThrowableProxy info = (ThrowableProxy) event.getThrowableProxy();
		if (info != null) {
    
    
			Throwable exception = info.getThrowable();

			Object message = event.getFormattedMessage();
			if (message != null) {
    
    
				Cat.logError(String.valueOf(message), exception);
			} else {
    
    
				Cat.logError(exception);
			}
		}
	}

	private void logTrace(ILoggingEvent event) {
    
    
		String type = "Logback";
		String name = event.getLevel().toString();
		Object message = event.getFormattedMessage();
		String data;
		if (message instanceof Throwable) {
    
    
			data = buildExceptionStack((Throwable) message);
		} else {
    
    
			data = event.getFormattedMessage().toString();
		}

		ThrowableProxy info = (ThrowableProxy) event.getThrowableProxy();
		if (info != null) {
    
    
			data = data + '\n' + buildExceptionStack(info.getThrowable());
		}

		Cat.logTrace(type, name, "0", data);
	}

	private String buildExceptionStack(Throwable exception) {
    
    
		if (exception != null) {
    
    
			StringWriter writer = new StringWriter(2048);
			exception.printStackTrace(new PrintWriter(writer));
			return writer.toString();
		} else {
    
    
			return "";
		}
	}

}
  • 修改logback-spring.xml配置文件:
    <appender name="CatAppender" class="org.example.appender.CatLogbackAppender"/>
    <root level="info">
        <appender-ref ref="consoleLog"/>
        <appender-ref ref="fileInfoLog"/>
        <appender-ref ref="fileErrorLog"/>
        <appender-ref ref="CatAppender" />
    </root>
</configuration>
  • META-INF/app.properties勿忘

在这里插入图片描述

  • 编写测试用例
@SpringBootTest(classes = CatDemoMain.class)
@Slf4j
public class CatLogBackDemoTest {
    
    

    @Test
    public void testLog() throws InterruptedException {
    
    
        //需要开启跟踪模式,才会生效--可以参考CatLogbackAppender的append方法,一看便知        
        Cat.getManager().setTraceMode(true);
        log.info("cat info");
        try {
    
    
            int i = 1/0;
        }catch (Exception e){
    
    
            log.error("cat error",e);
        }
        //睡眠一会,让cat客户端有时间上报错误
        Thread.sleep(100000);
    }
}

  • 运行查看cat界面

在这里插入图片描述

Cat列出的信息还是相对详细的,有INFO级别的日志与ERROR级别的日志,其中ERROR级别的日志显示出了所有的堆栈信息方便分析问题。

在这里插入图片描述


Cat集成SprinBoot

Spring Boot的集成方式相对比较简单,我们使用已经搭建完的Mybatis框架来进行测试。
在这里插入图片描述

  • 添加如下配置类到config包中:
@Configuration
public class CatFilterConfigure {
    
    

    @Bean
    public FilterRegistrationBean catFilter() {
    
    
        FilterRegistrationBean registration = new FilterRegistrationBean();
        CatFilter filter = new CatFilter();
        registration.setFilter(filter);
        registration.addUrlPatterns("/*");
        registration.setName("cat-filter");
        registration.setOrder(1);
        return registration;
    }
}
  • 访问测试

在这里插入图片描述
图中的调用先经过了Controller,所以打印出了相关信息:

  • /mybatis 接口地址
  • URL.Server 服务器、浏览器等相关信息
  • URL.Method 调用方法(GET、POST等)和URL

Cat集成Spring AOP

使用Spring AOP技术可以简化我们的埋点操作,通过添加统一注解的方式,使得指定方法被能被CAT监控起来。
在这里插入图片描述

  • 使用比较简单,这里将两个类copy到我们项目中,将注解标注在我们希望监控的方法上即可
@Retention(RUNTIME)
@Target(ElementType.METHOD)
public @interface CatAnnotation {
    
    
}
@Component
@Aspect
public class CatAopService {
    
    

	@Around(value = "@annotation(CatAnnotation)")
	public Object aroundMethod(ProceedingJoinPoint pjp) throws Throwable {
    
    
		MethodSignature joinPointObject = (MethodSignature) pjp.getSignature();
		Method method = joinPointObject.getMethod();

		Transaction t = Cat.newTransaction("method", method.getName());

		try {
    
    
			Object res = pjp.proceed();
			t.setSuccessStatus();
			return res;
		} catch (Throwable e) {
    
    
			t.setStatus(e);
			Cat.logError(e);
			throw e;
		} finally {
    
    
			t.complete();
		}

	}

}
  • 注解使用演示
@RestController
public class AopController {
    
    

    @RequestMapping("aop")
    @CatAnnotation
    public String aop1(){
    
    

        return  "aop";
    }
}

Cat集成SpringMVC

Spring MVC的集成方式,官方提供的是使用AOP来进行集成,源码如下:

  • AOP接口:
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface CatTransaction {
    
    
    String type() default "Handler";//"URL MVC Service SQL" is reserved for Cat Transaction Type
    String name() default "";
}
  • AOP处理代码:
 @Around("@annotation(catTransaction)")
    public Object catTransactionProcess(ProceedingJoinPoint pjp, CatTransaction catTransaction) throws Throwable {
    
    
        String transName = pjp.getSignature().getDeclaringType().getSimpleName() + "." + pjp.getSignature().getName();
        if(StringUtils.isNotBlank(catTransaction.name())){
    
    
            transName = catTransaction.name();
        }
        Transaction t = Cat.newTransaction(catTransaction.type(), transName);
        try {
    
    
            Object result = pjp.proceed();
            t.setStatus(Transaction.SUCCESS);
            return result;
        } catch (Throwable e) {
    
    
            t.setStatus(e);
            throw e;
        }finally{
    
    
            t.complete();
        }
    }

Cat告警

CAT提供给我们完善的告警功能。合理、灵活的监控规则可以帮助更快、更精确的发现业务线上故障。

告警通用配置

  • 告警服务器配置

只有配置为告警服务器的机器,才会执行告警逻辑;只有配置为发送服务器的机器,才会发送告警。

进入功能 全局系统配置-服务端配置,修改服务器类型,对告警服务器增加<property name="alarm-machine" value="true"/>配置、以及<property name="send-machine" value="true"/>配置。如下图所示:

在这里插入图片描述


告警策略

告警策略:配置某种告警类型、某个项目、某个错误级别,对应的告警发送渠道,以及暂停时间。

在这里插入图片描述
举例:下述配置示例,说明对于Transaction告警,当告警项目名为demo_project:

  • 当告警级别为error时,发送渠道为邮件、短信、微信,连续告警之间的间隔为5分钟
  • 当告警级别为warning时,发送渠道为邮件、微信,连续告警之间的间隔为10分钟
<alert-policy>
	<type id="Transaction">
          <group id="default">
             <level id="error" send="mail,weixin" suspendMinute="5"/>
             <level id="warning" send="mail,weixin" suspendMinute="5"/>
          </group>
          <group id="demo-project">
             <level id="error" send="mail,weixin,sms" suspendMinute="5"/>
             <level id="warning" send="mail,weixin" suspendMinute="10"/>
          </group>
    </type>
</alert-policy>
  • 配置说明:

    • type:告警的类型,可选:Transaction、Event、Business、Heartbeat
    • group id属性:group可以为default,代表默认,即所有项目;也可以为项目名,代表某个项目的策略,此时default策略不会生效
    • level id属性:错误级别,分为warning代表警告、error代表错误
    • level send属性:告警渠道,分为mail-邮箱、weixin-微信、sms-短信
    • level suspendMinute属性:连续告警的暂停时间

告警接收人

  • 告警接收人,为告警所属项目的联系人:

    • 项目组邮件:项目负责人邮件,或项目组产品线邮件,多个邮箱由英文逗号分割,不要留有空格;作为发送告警邮件、微信的依据
    • 项目组号码:项目负责人手机号;多个号码由英文逗号分隔,不要留有空格;作为发送告警短信的依据
      在这里插入图片描述

告警服务端

告警发送中心的配置。(什么是告警发送中心:提供发送短信、邮件、微信功能,且提供Http API的服务)

CAT在生成告警后,调用告警发送中心的Http接口发送告警。CAT自身并不集成告警发送中心,请自己搭建告警发送中心。

在这里插入图片描述

  • 配置示例
<sender-config>
   <sender id="mail" url="http://test/" type="post" successCode="200" batchSend="true">
      <par id="type=1500"/>
      <par id="key=title,body"/>
      <par id="[email protected]"/>
      <par id="to=${receiver}"/>
      <par id="value=${title},${content}"/>
   </sender>
   <sender id="weixin" url="http://test/" type="post" successCode="success" batchSend="true">
      <par id="domain=${domain}"/>
      <par id="email=${receiver}"/>
      <par id="title=${title}"/>
      <par id="content=${content}"/>
      <par id="type=${type}"/>
   </sender>
   <sender id="sms" url="http://test/" type="post" successCode="200" batchSend="false">
      <par id="jsonm={type:808,mobile:'${receiver}',pair:{body='${content}'}}"/>
   </sender>
</sender-config>
  • 配置说明:

    • sender id属性:告警的类型,可选:mail、sms、weixin
    • sender url属性:告警中心的URL
    • sender batchSend属性:是否支持批量发送告警信息
    • par:告警中心所需的Http参数。${argument}代表构建告警对象时,附带的动态参数;此处需要根据告警发送中心的需求,将动态参数加入到代码AlertEntity中的m_paras

告警规则

目前CAT的监控规则有五个要素

  • 告警时间段。同一项业务指标在每天不同的时段可能有不同的趋势。设定该项,可让CAT在每天不同的时间段执行不同的监控规则。注意:告警时间段,不是监控数据的时间段,只是告警从这一刻开始进行检查数据

  • 规则组合。在一个时间段中,可能指标触发了多个监控规则中的一个规则就要发出警报,也有可能指标要同时触发了多个监控规则才需要发出警报。

  • 监控规则类型。通过以下六种类型对指标进行监控:最大值、最小值、波动上升百分比、波动下降百分比、总和最大值、总和最小值

  • 监控最近分钟数。设定时间后(单位为分钟),当指标在设定的最近的时间长度内连续触发了监控规则,才会发出警报。比如最近分钟数为3,表明连续三分钟的数组都满足条件才告警。如果分钟数为1,表示最近的一分钟满足条件就告警

  • 规则与被监控指标的匹配。监控规则可以按照名称、正则表达式与监控的对象(指标)进行匹配

子条件类型:

有六种类型。子条件的内容为对应的阈值,请注意阈值只能由数字组成,当阈值表达百分比时,不能在最后加上百分号。八种类型如下:

类型 说明
MaxVal 最大值(当前值) 当前实际值 最大值,比如检查最近3分钟数据,3分钟数据会有3个value,是表示(>=N)个值都必须同时>=设定值
MinVal 最小值(当前值) 当前实际值 最小值,比如检查最近3分钟数据,3分钟数据会有3个value,是表示(>=N)个值都必须同时比<=设定值
FluAscPer 波动上升百分比(当前值) 波动百分比最大值。即当前最后(N)分钟值比监控周期内其它分钟值(M-N个)的增加百分比都>=设定的百分比时触发警报,比如检查最近10分钟数据,触发个数为3;10分钟内数据会算出7个百分比数据,是表示最后3分钟值分别相比前面7分钟值,3组7次比较的上升波动百分比全部>=配置阈值。比如下降50%,阈值填写50。
FluDescPer 波动下降百分比(当前值) 波动百分比最小值。当前最后(N)分钟值比监控周期内其它(M-N个)分钟值的减少百分比都大于设定的百分比时触发警报,比如检查最近10分钟数据,触发个数为3;10分钟数据会算出7个百分比数据,是表示最后3分钟值分别相比前面7分钟值,3组7次比较的下降波动百分比全部>=配置阈值。比如下降50%,阈值填写50。
SumMaxVal 总和最大值(当前值) 当前值总和最大值,比如检查最近3分钟数据,表示3分钟内的总和>=设定值就告警。
SumMinVal 总和最小值(当前值) 当前值总和最小值,比如检查最近3分钟数据,表示3分钟内的总和<=设定值就告警。

Transaction告警

对Transaction的告警,支持的指标有次数、延时、失败率;监控周期:一分钟

  • 如下图所示,配置了springboot-cat项目的Transaction监控规则。

在这里插入图片描述
配置说明:

  • 项目名:要监控的项目名

  • type:被监控transaction的type

  • name:被监控transaction的name;如果为All,代表全部name

  • 监控指标:次数、延时、失败率

  • 告警规则:详情见告警规则部分


Event告警

对Event的个数进行告警;监控周期:一分钟

在这里插入图片描述
配置说明:

  • 项目名:要监控的项目名
  • type:被监控event的type
  • name:被监控event的name;如果为All,代表全部name
  • 告警规则:详情见告警规则部分

心跳告警

心跳告警是对服务器当前状态的监控,如监控系统负载、GC数量等信息;监控周期:一分钟

在这里插入图片描述
配置说明:

  • 项目名:要监控的项目名
  • 指标:被监控的心跳指标名称;心跳告警是由两级匹配的:首先匹配项目,然后按照指标匹配
  • 告警规则:详情见告警规则部分

异常告警

对异常的个数进行告警;监控周期:一分钟

在这里插入图片描述
配置说明:

  • 项目名:要监控的项目名
  • 异常名称:被监控异常名称;当设置为“Total”时,是针对当前项目组所有异常总数阈值进行设置;当设置为特定异常名称时,针对当前项目组所有同名的异常阈值进行设定
  • warning阈值:到达该阈值,发送warning级别告警;当异常数小于该阈值时,不做任何警报
  • error阈值:到达该阈值,发送error级别告警
  • 总数大于Warning阈值,小于Error阈值,进行Warning级别告警;大于Error阈值,进行Error级别告警

告警接口编写

  • 编写controller接口:
@RestController
public class AlertController {
    
    

    @RequestMapping(value = "/alert/msg")
    public String sendAlert(@RequestParam String to) {
    
    
        System.out.println("告警了" +to);
        return "200";
    }
}
  • 修改告警服务端的配置,填写接口地址,以邮件为例:
 <sender id="mail" url="http://localhost:8085/alert/msg" type="post" successCode="200" batchSend="true">
      <par id="type=1500"/>
      <par id="key=title,body"/>
      <par id="[email protected]"/>
      <par id="to=${receiver}"/>
      <par id="value=${title},${content}"/>
   </sender>
  • 测试结果,输出内容如下:
告警了testUser1@test.com,testUser2@test.com

猜你喜欢

转载自blog.csdn.net/m0_53157173/article/details/130041284