今天我们设计一个实际的业务场景,监控后台服务的所有接口,显然,结合AOP切面是最好的方案。
1、jar包引入、.yml配置文件配置参考上一篇文章。
2、切面核心业务代码:
import com.alibaba.fastjson.JSON;
import com.nandao.demo.common.R;
import com.nandao.demo.common.ResultCode;
import com.nandao.demo.exception.BizException;
import com.nandao.demo.exception.ErrorException;
import io.micrometer.core.instrument.Counter;
import io.micrometer.core.instrument.MeterRegistry;
import io.micrometer.core.instrument.Metrics;
import io.micrometer.core.instrument.Timer;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.AfterThrowing;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.dao.DuplicateKeyException;
import org.springframework.stereotype.Component;
import org.springframework.validation.BindException;
import org.springframework.validation.ObjectError;
import org.springframework.web.HttpRequestMethodNotSupportedException;
import org.springframework.web.multipart.MultipartFile;
import javax.annotation.PostConstruct;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;
/**
* @author wanghuainan
* @date 2020/9/2
*/
@Component
@Slf4j
@Aspect
public class PrometheusAspect {
/**
* 定义注册器
*/
@Autowired
MeterRegistry registry;
private Counter counter_all_total;
private AtomicInteger pc_online_count;
private ConcurrentHashMap<String,Counter> counterMap = new ConcurrentHashMap();
ThreadLocal<Long> startTime = new ThreadLocal<>();
/**
* 监控controller层的接口
*/
@Pointcut("execution(public * com.nandao.demo.controller.*.*(..))")
private void pointCut(){}
/**
* 启动时初始化指标,所有接口的请求次数统计
*/
@PostConstruct
public void init(){
counter_all_total = registry.counter("pc_aop_all_requests_count", "aop_all_method", "count");
pc_online_count = registry.gauge("pc_all_online_count", new AtomicInteger(0));
}
/**
* 有些业务场景也可以在这里监控接口
* @param joinPoint
* @throws Throwable
*/
// @Before("pointCut()")
public void doBefore(JoinPoint joinPoint )throws Throwable {
Counter counter_total = null;
String classMethod = getClassMethodName(joinPoint);
if(counterMap.containsKey(classMethod)){
counter_total = counterMap.get(classMethod);
}else {
counter_total = registry.counter("pc_aop_requests_count", "aop_method", classMethod);
counterMap.put(classMethod,counter_total);
}
startTime.set(System.currentTimeMillis());
counter_total.increment();
counter_all_total.increment();
}
/**
* 获取请求的接口名
* @param joinPoint
* @return
*/
private String getClassMethodName(JoinPoint joinPoint) {
//类方法
log.info("classMethod={}", joinPoint.getSignature().getDeclaringTypeName());
String declaringTypeName = joinPoint.getSignature().getDeclaringTypeName();
String className = declaringTypeName.substring(declaringTypeName.lastIndexOf(".")+1);
log.info("className={}", className);
log.info("Method={}", joinPoint.getSignature().getName());
return className +"."+ joinPoint.getSignature().getName();
}
/**
* 获取请求的参数
* @param joinPoint
* @return
*/
private String getMethodArgs(ProceedingJoinPoint joinPoint){
// 构造参数组集合
List<Object> argList = new ArrayList<>();
for (Object arg : joinPoint.getArgs()) {
// request/response无法使用toJSON
if (arg instanceof HttpServletRequest) {
argList.add("request");
} else if (arg instanceof HttpServletResponse) {
argList.add("response");
} else if (arg instanceof MultipartFile){
argList.add("file");
} else {
argList.add(JSON.toJSON(arg));
}
}
return JSON.toJSON(argList).toString();
}
//@Before("pointcutImage()")
/* public void doBefore(JoinPoint joinPoint)throws Throwable {
startTime.set(System.currentTimeMillis());
counter_total.increment();
}*/
/**
* 每个接口的耗时数据采集
* @param joinPoint
* @return
* @throws Throwable
*/
@Around(value = "pointCut()")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
/**
* 改服务请求的总次数
*/
counter_all_total.increment();
String classMethod = getClassMethodName(joinPoint);
Timer timer = Metrics.timer("pc_reponse_usedtime", "method_name", classMethod);
/**
* 该服务中每个接口请求过程没有抛异常的次数、最大响应时间、总的请求时间
*/
Object result = timer.recordCallable(() -> {
try {
Object proceed = joinPoint.proceed();
/**
* 该服务中每个接口请求后返回错误码的次数
*/
if(proceed instanceof R) {
R r = (R) proceed;
if (r.getCode() != ResultCode.SUCCESS.getCode()) {
Counter reponse_error_total = registry.counter("pc_reponse_error_count", "aop_method", classMethod);
reponse_error_total.increment();
String methodArgs = getMethodArgs(joinPoint);
log.info("接口返回错误码[{}],接口返回错误信息[{}],错误接口名是[{}],所传参数[{}]",r.getCode(),r.getMsg(),classMethod,methodArgs);
}
}
return proceed;
} catch (Throwable e) {
/**
* 该服务中每个接口请求过程中抛异常的次数
*/
Counter reponse_throw_total = registry.counter("pc_reponse_throw_count", "aop_method", classMethod);
reponse_throw_total.increment();
String methodArgs = getMethodArgs(joinPoint);
log.info("接口抛异常方法名[{}],所传参数[{}]",classMethod,methodArgs);
return getThrowDetail(e);
}
});
return result;
}
/**
* 获取自定义异常的详情
* @param e
* @return
*/
private R getThrowDetail(Throwable e) {
if(e instanceof BizException){
BizException eb = (BizException) e;
log.info("业务异常: {} {}", eb.getErrorEnum().getCode(), eb.getErrorEnum().getMsg());
return new R(eb.getErrorEnum());
}else if(e instanceof ErrorException){
ErrorException ee = (ErrorException) e;
log.error("应用异常: {}", ee);
return R.error(ee.getErrorEnum());
}else if(e instanceof HttpRequestMethodNotSupportedException){
// HttpRequestMethodNotSupportedException eh = (HttpRequestMethodNotSupportedException) e;
log.error("不支持方法请求类型异常: {}", e);
return R.error(ResultCode.ACCOUNT_LOCKED);
}else if(e instanceof DuplicateKeyException){
log.error("唯一约束异常:{} {}", e);
return R.error(ResultCode.DEVICE_BANNED);
}else if(e instanceof BindException){
BindException eb = (BindException) e;
log.error("系统异常:{} {}" , e);
if (eb.hasErrors()) {
ObjectError objectError = eb.getAllErrors().get(0);
String message = objectError.getDefaultMessage();
return new R(ResultCode.PARAM_ERROR).setMsg(message);
}
return R.error();
} else {
log.error("系统异常: {}" , e);
return R.error();
}
}
// @AfterReturning(returning = "returnVal", pointcut = "pointCut()")
public void doAftereReturning(Object returnVal){
AtomicLong app_reponse_usedtime = null;
app_reponse_usedtime = registry.gauge("pc_reponse_usedtime", new AtomicLong(0));
log.info("开始时间:[{}]存的时间[{}]",startTime.get(),app_reponse_usedtime.get());
app_reponse_usedtime.set((System.currentTimeMillis() - startTime.get()));
System.out.println("请求执行时间:" + (System.currentTimeMillis() - startTime.get()));
}
/**
* 接口抛异常时走这里
* @param joinPoint
* @param exception
*/
@AfterThrowing(value = "pointCut()" ,throwing = "exception")
public void logTestAfterReturing3(JoinPoint joinPoint, Throwable exception){
log.info("采集接口访问次数报错的接口名[{}],类名[{}]",joinPoint.getSignature().getName(),joinPoint.getSignature().getDeclaringTypeName());
log.info("采集接口访问信息报错日志{}",exception.getStackTrace());
exception.printStackTrace();
/* if(exception instanceof NullPointerException){
log.info("发生了空指针异常!!!!!");
}*/
}
}
此段伪代码的优点是集中处理业务,松耦合性、可读性、可扩展性好,一般情况所有的业务切面都是采用这种结构处理,稍微不同的地方是,自定义注解,扫描有注解的接口。
3、启动服务后调用采集数据的接口,就可以实时调用最新的数据。下篇我们分享监控平台统计的指标类型,及可以从哪些纬度监控数据,敬请期待!