1. Cat简介
CAT(Central Application Tracking)基于Java开发的实时监控平台,主要包括移动端监控,应用侧监控,核心网络层监控,系统层监控等。是一个提供实时监控报警,应用性能分析诊断的工具。
主要有如下功能:
- 机器状态信息:CPU负载、内存信息、磁盘使用率等服务器信息及线程栈、堆、垃圾回收等应用进程信息;
- 请求访问情况:请求个数、响应时间、处理状态等接口访问信息;
- 异常情况:服务无响应、应用Exception等异常信息;
- 业务情况:订单量统计,销售额等业务信息。
2. 应用架构图
springboot应用可以通过client将监控信息传递至Cat服务端处理,生成报表并启动告警。
3. 项目配置
- 创建客户端路由配置
在被监控应用服务器上创建目录/data/appdatas/cat/
和/data/applogs/cat
,
在/data/appdatas/cat/
下新增客户端路由文件client.xml
,用于客户端连接Cat服务端,内容如下:<?xml version="1.0" encoding="utf-8"?> <config mode="client"> <servers> <server ip="10.253.129.2" port="2280" http-port="8080"/> <server ip="10.253.129.8" port="2280" http-port="8080"/> <server ip="10.253.129.17" port="2280" http-port="8080"/> </servers> </config>
- 依赖引入
客户端下载链接:https://github.com/dianping/cat/blob/master/lib/java/jar/cat-client-3.0.0.jar
也可以下载源代码自行编译:https://github.com/dianping/cat/tree/master/lib/java
<dependency> <groupId>com.dianping.cat</groupId> <artifactId>cat-client</artifactId> <version>3.0.0</version> </dependency>
- 应用配置
在resources
目录下创建META-INF
文件夹,在META-INF下创建app.properties
,文件内容如下:app.name=XXXXX #项目domain,应用自定义
- 增加访问URL监控
springboot应用增加如下文件即可(CatFilterConfigure.java
)
效果如图所示package com.test.framework.config; import com.dianping.cat.servlet.CatFilter; import org.springframework.boot.web.servlet.FilterRegistrationBean; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @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; } }
- 增加mybatis监控
新增CatMybatisPlugin.java
文件,内容如下:
修改mybatis配置文件,增加拦截器package com.test.framework.filter; 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.cache.CacheKey; 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; @Intercepts({@Signature(method = "query", type = Executor.class, args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class, CacheKey.class, BoundSql.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) { } }
监控页面显示如下图<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean"> <property name="dataSource" ref="dataSource" /> <property name="plugins"> <array> <bean class="com.test.framework.filter.CatMybatisPlugin"/> #拦截SQL </array> </property> <property name="mapperLocations" value="classpath:com/test/dao/**/*Mapper.xml"/> </bean>
- 增加接口访问频次监控
在springboot应用中新增CatMetricAop.java
文件,开启aop代理配置即可,内容如下:
监控页面显示如下图package com.test.framework.aop; import com.dianping.cat.Cat; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Pointcut; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.core.annotation.Order; import org.springframework.stereotype.Component; import org.springframework.web.context.request.RequestContextHolder; import org.springframework.web.context.request.ServletRequestAttributes; import javax.servlet.http.HttpServletRequest; @Component @Aspect @Order(2) public class CatMetricAop { private static final Logger LOGGER = LoggerFactory.getLogger(CatMetricAop.class); @Pointcut("@annotation(org.springframework.web.bind.annotation.RequestMapping)||@annotation(org.springframework.web.bind.annotation.PostMapping)") public void controllerAspect() { } @Around("controllerAspect()") public Object around(ProceedingJoinPoint joinPoint) { Object value = null; try { String method = joinPoint.getSignature().getName(); Cat.logMetricForCount(method); value = joinPoint.proceed(); } catch (Throwable throwable) { LOGGER.error("CatMetricAop异常", throwable); } return value; } }