SpringBoot(审计) 统计接口调用次数及成功率
介绍:
很多时候会需要提供一些统计记录的,比如某个服务一个月的被调用量、接口的调用次数、成功调用次数等等。
优点:
使用AOP+Hendler对业务逻辑代码无侵入,完全解耦。通过spring boot自带的健康检查接口(/health)方便、安全。
注意:
数据没有被持久化,只保存在内存中,重启后数据将被重置。可按需自己实现
代码:
AOP:在AOP中调用Handler
@Component @Aspect public class ControllerAdvice { private static ILogger log = LoggerFactory.getLogger(ControllerAdvice.class); @Around("execution(public * *..*controller.*.*(..))") public Object handle(ProceedingJoinPoint proceedingJoinPoint) throws Throwable { Object result; try { Function<ProceedingJoinPoint, AbstractControllerHandler> build = AbstractControllerHandler.getBuild(); if (null == build) { AbstractControllerHandler.registerBuildFunction(DefaultControllerHandler::new); } build = AbstractControllerHandler.getBuild(); AbstractControllerHandler controllerHandler = build.apply(proceedingJoinPoint); if (null == controllerHandler) { log.warn(String.format("The method(%s) do not be handle by controller handler.", proceedingJoinPoint.getSignature().getName())); result = proceedingJoinPoint.proceed(); } else { result = controllerHandler.handle(); } } catch (Throwable throwable) { RuntimeHealthIndicator.failedRequestCount++; log.error(new Exception(throwable), "Unknown exception- -!"); throw throwable; } return result; } }
Handler:执行记录的逻辑
抽象类:AbstractControllerHandler
public abstract class AbstractControllerHandler { private static ILogger log = LoggerFactory.getLogger(AbstractControllerHandler.class); private static Function<ProceedingJoinPoint, AbstractControllerHandler> build; public static Function<ProceedingJoinPoint, AbstractControllerHandler> getBuild() { return build; } public static void registerBuildFunction(Function<ProceedingJoinPoint, AbstractControllerHandler> build) { Assert.isNotNull(build, "build"); AbstractControllerHandler.build = build; } protected ProceedingJoinPoint proceedingJoinPoint; protected HttpServletRequest httpServletRequest; protected String methodName; protected String uri; protected String requestBody; protected String ip; protected Method method; protected boolean inDataMasking; protected boolean outDataMasking; public AbstractControllerHandler(ProceedingJoinPoint proceedingJoinPoint) { Assert.isNotNull(proceedingJoinPoint, "proceedingJoinPoint"); this.proceedingJoinPoint = proceedingJoinPoint; Signature signature = this.proceedingJoinPoint.getSignature(); this.httpServletRequest = this.getHttpServletRequest(this.proceedingJoinPoint.getArgs()); this.methodName = signature.getName(); this.uri = null == this.httpServletRequest ? null : this.httpServletRequest.getRequestURI(); this.requestBody = this.formatParameters(this.proceedingJoinPoint.getArgs()); this.ip = null == this.httpServletRequest ? "" : CommonHelper.getIp(this.httpServletRequest); this.inDataMasking = false; this.outDataMasking = false; if (signature instanceof MethodSignature) { MethodSignature methodSignature = (MethodSignature) signature; try { this.method = proceedingJoinPoint.getTarget().getClass().getMethod(this.methodName, methodSignature.getParameterTypes()); if (null != this.method) { LogDataMasking dataMasking = this.method.getDeclaredAnnotation(LogDataMasking.class); if (null != dataMasking) { this.inDataMasking = dataMasking.in(); this.outDataMasking = dataMasking.out(); } } } catch (NoSuchMethodException e) { e.printStackTrace(); } } } public abstract Object handle() throws Throwable; protected void logIn() { String requestBody = this.requestBody; if (this.inDataMasking) { requestBody = "Data Masking"; } log.info(String.format("Start-[%s][%s][%s][body: %s]", this.ip, this.uri, this.methodName, requestBody)); } protected void logOut(long elapsedMilliseconds, boolean success, String responseBody) { if (success) { if (this.outDataMasking) { responseBody = "Data Masking"; } log.info( String.format( "Success(%s)-[%s][%s][%s][response body: %s]", elapsedMilliseconds, this.ip, this.uri, this.methodName, responseBody)); } else { log.warn( String.format( "Failed(%s)-[%s][%s][%s][request body: %s][response body: %s]", elapsedMilliseconds, this.ip, this.uri, this.methodName, this.requestBody, responseBody)); } } protected HttpServletRequest getHttpServletRequest(Object[] parameters) { try { if (null != parameters) { for (Object parameter : parameters) { if (parameter instanceof HttpServletRequest) { return (HttpServletRequest) parameter; } } } return ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest(); } catch (Exception e) { log.error(e); return null; } } protected String formatParameters(Object[] parameters) { if (null == parameters) { return null; } else { StringBuilder stringBuilder = new StringBuilder(); for (int i = 0; i < parameters.length; i++) { Object parameter = parameters[i]; if (parameter instanceof HttpServletRequest || parameter instanceof HttpServletResponse) { continue; } stringBuilder.append(String.format("[%s]: %s.", i, JSON.toJSONString(parameter))); } return stringBuilder.toString(); } }
实现类:
public class DefaultControllerHandler extends AbstractControllerHandler { private static ILogger log = LoggerFactory.getLogger(DefaultControllerHandler.class); private static int currentMonth = Calendar.getInstance().get(Calendar.MONTH) + 1; public DefaultControllerHandler(ProceedingJoinPoint proceedingJoinPoint) { super(proceedingJoinPoint); } @Override public Object handle() throws Throwable { long timestamp = System.currentTimeMillis(); this.logIn(); ResponseDto responseDto; boolean success = false; try { Object result = proceedingJoinPoint.proceed(); if (result instanceof ResponseDto) { responseDto = (ResponseDto) result; } else { responseDto = ResponseDto.success(result); } success = true; RuntimeHealthIndicator.successRequestCount++; } catch (BusinessException e) { // RuntimeHealthIndicator.failedRequestCount++; if (this.isDebugLogLevel()) { log.error(e); } responseDto = new ResponseDto<>(e.getCode(), e.getMessage(), null); } catch (Exception e) { RuntimeHealthIndicator.failedRequestCount++; if (this.isDebugLogLevel()) { log.error(e); } responseDto = ResponseDto.failed(ExceptionDefinitions.ServerError, e.getMessage(), null); } finally { Calendar cale = Calendar.getInstance(); if (currentMonth != (cale.get(Calendar.MONTH) + 1)) { String recodeKey = String.format("%d年%d月", cale.get(Calendar.YEAR), cale.get(Calendar.MONTH) + 1); String recodeValue = "successCount:" + RuntimeHealthIndicator.successRequestCount + " failedCount:" + RuntimeHealthIndicator.failedRequestCount; RuntimeHealthIndicator.historyRequestRecode.put(recodeKey, recodeValue); RuntimeHealthIndicator.successRequestCount = 0; RuntimeHealthIndicator.failedRequestCount = 0; currentMonth = cale.get(Calendar.MONTH); } } long duration = System.currentTimeMillis() - timestamp; RuntimeHealthIndicator.markRestApiInvoked(this.methodName, (int) duration); this.logOut(duration, success, JSON.toJSONString(responseDto)); return responseDto; } public boolean isDebugLogLevel() { return log.isEnabled(LogLevel.DEBUG); } }
Health接口
@Component public class RuntimeHealthIndicator extends AbstractHealthIndicator { private static ILogger log = LoggerFactory.getLogger(ApplicationInstanceManager.class); private static Map<String, RestApiInvokeStatus> restApiInvokeStatuses = new HashMap<>(); public static long failedRequestCount = 0; public static long successRequestCount = 0; public static Map<String, Object> historyRequestRecode; private Map<String, Object> details; public RuntimeHealthIndicator() { this.details = new HashMap<>(); RuntimeHealthIndicator.historyRequestRecode = new HashMap<>(); this.details.put("startTime", new Date(ManagementFactory.getRuntimeMXBean().getStartTime())); this.details.put("path", RuntimeHealthIndicator.class.getClassLoader().getResource("").getPath()); this.details.put("osName", System.getProperty("os.name")); this.details.put("osVersion", System.getProperty("os.version")); this.details.put("javaVersion", System.getProperty("java.version")); try { this.details.put("ip", ZGHelper.getIpV4()); } catch (SocketException e) { log.error(e, "Failed to get Ipv4."); } } @Override protected void doHealthCheck(Health.Builder builder) throws Exception { ThreadGroup threadGroup = Thread.currentThread().getThreadGroup(); while (null != threadGroup.getParent()) { threadGroup = threadGroup.getParent(); } this.details.put("threadCount", threadGroup.activeCount()); OperatingSystemMXBean operatingSystemMXBean = (OperatingSystemMXBean) ManagementFactory.getOperatingSystemMXBean(); this.details.put("cpuUsageRate", operatingSystemMXBean.getSystemCpuLoad()); this.details.put( "memoryUsageRate", (float) (operatingSystemMXBean.getTotalPhysicalMemorySize() - operatingSystemMXBean.getFreePhysicalMemorySize()) / (float) operatingSystemMXBean.getTotalPhysicalMemorySize()); this.details.put("failedRequestCount", RuntimeHealthIndicator.failedRequestCount); this.details.put("successRequestCount", RuntimeHealthIndicator.successRequestCount); this.details.put("restApiInvokeStatuses", RuntimeHealthIndicator.restApiInvokeStatuses); this.details.put("historyRequestRecode",RuntimeHealthIndicator.historyRequestRecode); for (Map.Entry<String, Object> detail : this.details.entrySet()) { builder.withDetail(detail.getKey(), detail.getValue()); } builder.up(); } public static void markRestApiInvoked(String name, int duration) { if (StringUtils.isBlank(name)) { return; } if (!RuntimeHealthIndicator.restApiInvokeStatuses.containsKey(name)) { RuntimeHealthIndicator.restApiInvokeStatuses.put(name, new RestApiInvokeStatus(name)); } RestApiInvokeStatus restApiInvokeStatus = RuntimeHealthIndicator.restApiInvokeStatuses.get(name); restApiInvokeStatus.setDuration(duration); } }
public class RestApiInvokeStatus { private String name; private Date startDate; private Date latestDate; private long times; private float averageDuration; private int minDuration; private int maxDuration; private int[] durations; public String getName() { return name; } public Date getStartDate() { return startDate; } public Date getLatestDate() { return latestDate; } public long getTimes() { return times; } public int getMinDuration() { return minDuration; } public int getMaxDuration() { return maxDuration; } public RestApiInvokeStatus(String name) { Assert.isNotBlank(name, "name"); this.name = name; this.durations = new int[1000]; this.minDuration = Integer.MAX_VALUE; this.maxDuration = Integer.MIN_VALUE; Date now = new Date(); this.startDate = now; this.latestDate = now; } public void setDuration(int duration) { this.durations[(int) (this.times % this.durations.length)] = duration; this.maxDuration = this.maxDuration > duration ? this.maxDuration : duration; this.minDuration = this.minDuration < duration ? this.minDuration : duration; this.latestDate = new Date(); this.times++; } public float getAverageDuration() { long length = this.times < this.durations.length ? this.times : this.durations.length; int count = 0; for (int i = 0; i < length; i++) { count += this.durations[i]; } this.averageDuration = (float) count / (float) length; return this.averageDuration; } }