完善系统的最后一公里,增加系统日志功能

当我们在开发一个系统的时候,随着规划的功能越来越多,按照复杂度和稳定性相反的原则,为了保证系统能够按照我们设想的目标运行,我们需要对系统的运行状况进行监控。

那么什么时候介入监控比较好?在系统功能开发的前期(还没有任何实质性的功能),似乎不太合适,那么在系统一切功能开发接近尾声的时候好像也不太合适,最好在这中间,选择一个迭代不是很紧急的阶段,系统已经有那么一个成熟的功能在用的时候,并且随着用户量的不断增大,我们需要对系统的运营情况进行一些了解的时候。

前期我们对系统日志进行设计的时候,可以不必考虑的那么周全,就一些必要的信息进行收集。日志大概分为两种:1. 操作日志;2. 异常日志

操作日志用来监控用户在使用系统时候的一些行为,比如请求了什么接口可以推断出他在系统前台进行了什么操作;异常日志则是用户在请求某个接口的时候,接口内部出现了程序上的错误,这个日志主要提供给程序员进行问题追踪的。随后我们还可以从这些日志中分析出很多有需要的数据,包括系统的健康度,功能的使用频率和频次等。

1. 日志表设计

梳理出一些必要的字段后,我们可以设计出如下的两张不同功能的日志表,它们有些字段是相同的,都需要记录谁在请求某个接口、请求的Uri、请求人的访问IP等信息。

CREATE TABLE `sys_log_operation`  (`id` INT NOT NULL AUTO_INCREMENT COMMENT '自动编号',`opera_module` VARCHAR(64) DEFAULT NULL COMMENT '功能模块',`opera_type` VARCHAR(64) DEFAULT NULL COMMENT '操作类型',`opera_desc` VARCHAR(500) DEFAULT NULL COMMENT '操作描述',`opera_req_param` TEXT DEFAULT NULL COMMENT '请求参数',`opera_resp_param` TEXT DEFAULT NULL COMMENT '返回参数',`opera_employee_account` VARCHAR(11) DEFAULT NULL COMMENT '操作人账号',`opera_method` VARCHAR(255) DEFAULT NULL COMMENT '操作方法',`opera_uri` VARCHAR(255) DEFAULT NULL COMMENT '请求URI',`opera_ip` VARCHAR(64) DEFAULT NULL COMMENT '请求IP',`created_time` DATETIME DEFAULT NULL COMMENT '创建时间',`modified_time` DATETIME DEFAULT NULL COMMENT '修改时间',PRIMARY KEY (`id`) USING BTREE
) COMMENT = '操作日志表' ROW_FORMAT = COMPACT;CREATE TABLE `sys_log_exception`  (`id` INT NOT NULL AUTO_INCREMENT COMMENT '自动编号',`exc_req_param` TEXT DEFAULT NULL COMMENT '请求参数',`exc_name` VARCHAR(255) DEFAULT NULL COMMENT '异常名称',`exc_message` TEXT DEFAULT NULL COMMENT '异常信息',`opera_employee_account` VARCHAR(11) DEFAULT NULL COMMENT '操作人账号',`opera_method` VARCHAR(255) DEFAULT NULL COMMENT '操作方法',`opera_uri` VARCHAR(255) DEFAULT NULL COMMENT '请求URI',`opera_ip` VARCHAR(64) DEFAULT NULL COMMENT '请求IP',`created_time` DATETIME DEFAULT NULL COMMENT '创建时间',`modified_time` DATETIME DEFAULT NULL COMMENT '修改时间',PRIMARY KEY (`id`) USING BTREE
) COMMENT = '异常日志表' ROW_FORMAT = COMPACT;

2. 后台系统日志收集

到了后端编码的阶段,我们来构思下这个代码架构如何去实现?因为日志代码是需要穿插在业务代码中的,这样必然带来一个问题,导致代码过于混乱的问题。有没有一种途径,通过 Spring 里注解的方式,只需要在接口的入口增加携带参数的注解,然后在注解的代码里就实现具体的日志收集功能。

Spring 里的特性 AOP(面向切面)就很适合用来实现日志记录,性能统计,安全控制,事务处理,异常处理等功能,将代码从业务逻辑代码中划分出来。

我们在项目代码的 Utils 目录创建注解接口 OperLog

package com.lead.utils;import java.lang.annotation.Retention;
import java.lang.annotation.ElementType;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Documented;
import java.lang.annotation.Target;/*** 自定义操作日志注解* @author Fan*/@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface OperLog {String operModule() default ""; // 操作模块String operType() default ""; // 操作类型String operDesc() default ""; // 操作说明
}

具体的功能,我们创建一个 OperLogAspect 切面处理类来实现。这里面通过 @Pointcut注解定义了日志的切入点和执行范围

package com.lead.utils;import com.alibaba.fastjson.JSON;
import com.lead.entity.System.ExceptionLog;
import com.lead.entity.System.OperationLog;
import com.lead.service.System.IExceptionLogService;
import com.lead.service.System.IOperationLogService;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.AfterThrowing;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;
import org.springframework.util.Assert;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;/*** 切面处理类,操作日志异常日志记录处理* @author Fan*/@Aspect
@Component
public class OperLogAspect {private final IOperationLogService operationLogService;private final IExceptionLogService exceptionLogService;public OperLogAspect(IOperationLogService operationLogService,IExceptionLogService exceptionLogService) {Assert.notNull(operationLogService, "operationLogService must not be null!");Assert.notNull(exceptionLogService, "exceptionLogService must not be null!");this.operationLogService = operationLogService;this.exceptionLogService = exceptionLogService;}/*** 设置操作日志切入点 记录操作日志 在注解的位置切入代码*/@Pointcut("@annotation(com.lead.utils.OperLog)")public void operLogPoinCut() {}/*** 设置操作异常切入点记录异常日志 扫描所有controller包下操作*/@Pointcut("execution(* com.lead.controller..*.*(..))")public void exceptionLogPoinCut() {}/*** 正常返回通知,拦截用户操作日志,连接点正常执行完成后执行, 如果连接点抛出异常,则不会执行* @param joinPoint 切入点* @param keys 返回结果*/@AfterReturning(value = "operLogPoinCut()", returning = "keys")public void saveOperLog(JoinPoint joinPoint, Object keys) {RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();HttpServletRequest request = (HttpServletRequest) requestAttributes.resolveReference(RequestAttributes.REFERENCE_REQUEST);OperationLog operlog = new OperationLog();String token = request.getHeader("accessToken");String employeeAccount = "";Object[] args = joinPoint.getArgs();Object params = args[0]; // 请求参数对象if (token != null && !token.equals("")) {employeeAccount = JwtUtil.getUserId(token);} else {Map argMap = (Map) params;String resp = JSON.toJSONString(keys);if (argMap.get("account") != null && argMap.get("passWord") != null && resp.indexOf("用户登录成功") > -1) {employeeAccount = argMap.get("account").toString();}}try {MethodSignature signature = (MethodSignature) joinPoint.getSignature();Method method = signature.getMethod();OperLog opLog = method.getAnnotation(OperLog.class);if (opLog != null) {String operaModule = opLog.operModule();String operaType = opLog.operType();String operaDesc = opLog.operDesc();operlog.setOperaModule(operaModule);operlog.setOperaType(operaType);operlog.setOperaDesc(operaDesc);}// 获取请求的类名String className = joinPoint.getTarget().getClass().getName();// 获取请求的方法名String methodName = method.getName();methodName = className + '.' + methodName;operlog.setOperaMethod(methodName);// Map<String, String> rtnMap = converMap(request.getParameterMap());// 将参数所在的数组转换成jsonoperlog.setOperaReqParam(JSON.toJSONString(params));operlog.setOperaRespParam(JSON.toJSONString(keys));operlog.setOperaEmployeeAccount(employeeAccount);operlog.setOperaUri(request.getRequestURI());operlog.setOperaIp(IPUtil.getIpAddress(request));operationLogService.addOperationLog(operlog);} catch (Exception e) {e.printStackTrace();}}/*** 异常返回通知,用于拦截异常日志信息 连接点抛出异常后执行*/@AfterThrowing(pointcut = "exceptionLogPoinCut()", throwing = "e")public void saveExceptionLog(JoinPoint joinPoint, Throwable e) {RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();HttpServletRequest request = (HttpServletRequest) requestAttributes.resolveReference(RequestAttributes.REFERENCE_REQUEST);ExceptionLog exceptLog = new ExceptionLog();String token = request.getHeader("accessToken");String employeeAccount = "";Object[] args = joinPoint.getArgs();Object params = args[0]; // 请求参数对象if (token != null && !token.equals("")) {employeeAccount = JwtUtil.getUserId(token);} else {employeeAccount = request.getParameter("account");}try {MethodSignature signature = (MethodSignature) joinPoint.getSignature();Method method = signature.getMethod();// 获取请求的类名String className = joinPoint.getTarget().getClass().getName();// 获取请求的方法名String methodName = method.getName();methodName = className + '.' + methodName;exceptLog.setExcReqParam(JSON.toJSONString(params)); // 请求参数exceptLog.setOperaMethod(methodName); // 请求方法名exceptLog.setExcName(e.getClass().getName()); // 异常名称exceptLog.setExcMessage(stackTraceToString(e.getClass().getName(), e.getMessage(), e.getStackTrace())); // 异常信息exceptLog.setOperaEmployeeAccount(employeeAccount);exceptLog.setOperaUri(request.getRequestURI());exceptLog.setOperaIp(IPUtil.getIpAddress(request));exceptionLogService.addExceptionLog(exceptLog);} catch (Exception e2) {e2.printStackTrace();}}/*** 转换request 请求参数* @param paramMap request获取的参数数组*/public Map<String, String> converMap(Map<String, String[]> paramMap) {Map<String, String> rtnMap = new HashMap<String, String>();for (String key : paramMap.keySet()) {rtnMap.put(key, paramMap.get(key)[0]);}return rtnMap;}/*** 转换异常信息为字符串* @param exceptionName    异常名称* @param exceptionMessage 异常信息* @param elements         堆栈信息*/public String stackTraceToString(String exceptionName, String exceptionMessage, StackTraceElement[] elements) {StringBuffer strbuff = new StringBuffer();for (StackTraceElement stet : elements) {strbuff.append(stet + "\n");}String message = exceptionName + ":" + exceptionMessage + "\n\t" + strbuff.toString();return message;}
}

具体的日志数据处理分别封装在 saveOperLogsaveExceptionLog方法中,这里主要说下他们共同的一些字段数据的获取。用户的账号是通过请求头参数 accessToken获取的 token信息然后解析出来的,这里为了获取未登录情况也就是请求登录接口的时候,直接获取登录提交的用户账号

RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
HttpServletRequest request = (HttpServletRequest) requestAttributes.resolveReference(RequestAttributes.REFERENCE_REQUEST);
OperationLog operlog = new OperationLog();
String token = request.getHeader("accessToken");
String employeeAccount = "";if (token != null && !token.equals("")) {employeeAccount = JwtUtil.getUserId(token);
} else {Map argMap = (Map) params;String resp = JSON.toJSONString(keys);if (argMap.get("account") != null && argMap.get("passWord") != null && resp.indexOf("用户登录成功") > -1) {employeeAccount = argMap.get("account").toString();}
}

获取接口类名和方法名

MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
// 获取请求的类名
String className = joinPoint.getTarget().getClass().getName();
// 获取请求的方法名
String methodName = method.getName();
methodName = className + '.' + methodName;
operlog.setOperaMethod(methodName);

这里直接获取的注解携带的参数,也就是我们在接口端添加的代码

OperLog opLog = method.getAnnotation(OperLog.class);
if (opLog != null) {String operaModule = opLog.operModule();String operaType = opLog.operType();String operaDesc = opLog.operDesc();operlog.setOperaModule(operaModule);operlog.setOperaType(operaType);operlog.setOperaDesc(operaDesc);
}

在业务代码中添加日志注解,给每个参数赋予带有一定含义的值:

@OperLog(operModule = "培训模块", operType = "用户获取培训课程列表", operDesc = "用户获取培训课程列表")
@RequestMapping(value = "/get-training", method = RequestMethod.POST)
public Result getTrainings(@RequestBody Map<String, Object> params, HttpServletRequest request) {}

3. 前台日志查询

前台拿到日志接口请求过来的数据,我们用一张表格展示就好,在该接口中提供了操作人、操作模块、操作类型和起始时间等查询参数,前台可以通过输入参数值进行过滤。

为了展示的信息更全,我们在表格中增加展开行的功能,点击表格中的行任何位置,出现该条日志的详细信息。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/354522.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

java泛型面试_Java泛型面试问题

java泛型面试Java面试中的通用面试问题在相当长的时间内在Java 5周围越来越普遍&#xff0c;许多应用程序都转移到Java 5上&#xff0c;并且几乎所有新的Java开发都发生在Tiger&#xff08;Java 5的代号&#xff09;上。 泛型和Java 5功能&#xff08;例如Enum&#xff09;的重…

Vue3 实现网页背景水印功能

经常有一些公司和组织出于系统文件或信息安全保密的需要&#xff0c;需要在系统网页上增加带有个人标识&#xff08;系统账号或个人信息&#xff09;的水印&#xff0c;可以简单防止截图外传首先我们来看这样一个水印功能的实现思路&#xff0c;通常是在我们原有的网页上附上一…

部署微服务– Spring Boot fatjar到Amazon Elastic Beanstalk

最近&#xff0c;我正在研究概念验证的Web应用程序&#xff0c;我想将其部署到公共云以进行快速演示。 我决定使用Amazon&#xff0c;因为我已经有过使用它的经验。 亚马逊提供了几种不同的方式来部署Java Web应用程序。 EC2使我们可以灵活地在机箱上安装和配置任何我们想要的…

在线答卷系统的前端设计与数据库系统的设计与实现

如果要你实现一个在线的答题系统&#xff0c;你能想到它该具有哪些功能&#xff1f;当我接到这样一个需求的时候&#xff0c;脑海中立马能想到的就是它有录入题库的功能&#xff0c;创建试卷后可以从题库选择试题&#xff0c;并且可以针对试题进行分数的设置和排序。试卷发布后…

jgroups传输消息_使用JGroups进行ElasticMQ消息复制

jgroups传输消息ElasticMQ是一个消息服务器&#xff0c;具有Scala&#xff0c;Java和与Amazon SQS兼容的接口。 它通过跨服务器群集复制消息来支持有保证的消息传递&#xff0c;并通过日志记录实现消息持久性。 消息复制是ElasticMQ的核心功能之一。 但是&#xff0c;如果您看一…

使用 Element 组件搭建在线学习的课程卡片设计

假如我们要做一个在线课程学习的系统&#xff0c;其中我们需要做的一个功能就是课程信息流的一个展示&#xff0c;以等高卡片列表或者瀑布流的方式呈现。首先我们来罗列下这个卡片内应该包括哪些信息点&#xff1a;课程名称课程简介分类信息课程评分等级课程文件课时观看学习人…

12个很棒的Spring数据教程来启动您的数据项目

Spring Data的任务是为数据访问提供一个熟悉且一致的&#xff0c;基于Spring的编程模型&#xff0c;同时仍保留基础数据存储的特​​殊特征。 它使使用数据访问技术&#xff0c;关系和非关系数据库&#xff0c;map-reduce框架以及基于云的数据服务变得容易。 这是一个总括项目…

python多线程编程(3): 使用互斥锁同步线程

问题的提出 上一节的例子中&#xff0c;每个线程互相独立&#xff0c;相互之间没有任何关系。现在假设这样一个例子&#xff1a;有一个全局的计数num&#xff0c;每个线程获取这个全局的计数&#xff0c;根据num进行一些处理&#xff0c;然后将num加1。很容易写出这样的代码&am…

如何防止水印被恶意删除或者隐藏?

继上篇 Vue3 实现网页背景水印功能 我们了解了常见的网页水印功能是如何实现的&#xff0c;懂原理的都知道水印是通过在网页中添加代码绘制 DOM 元素覆盖在原有的网页上而来的&#xff0c;一旦你打开浏览器中的元素审查&#xff0c;可以通过删除元素或者在元素的样式上操作属性…

jmc线程转储_如何分析线程转储– IBM VM

jmc线程转储本文是我们的线程转储分析系列的第4部分&#xff0c;它将为您提供什么是IBM VM的JVM线程转储以及您将找到的不同线程和数据点的概述。 您将看到和学习​​到&#xff0c;IBM VM Thread Dump格式是不同的&#xff0c;但是提供了更多现成的故障排除数据。 在这一点上&…

Java 8:使用交替接口公开的类型安全地图生成器

动态展示您的课程 当我是Java新手时&#xff0c;我记得当时想过应该有一种方法可以删除或隐藏我不想公开的类中的方法。 就像用private方法或类似方法覆盖public方法一样&#xff08;哪种情况是不可能的&#xff0c;也不应该是不可能的&#xff09;。 显然&#xff0c;今天&…

nodejs面试题

1、为什么用Nodejs,它有哪些缺点&#xff1f; 事件驱动&#xff0c;通过闭包很容易实现客户端的生命活期。不用担心多线程&#xff0c;锁&#xff0c;并行计算的问题V8引擎速度非常快对于游戏来说&#xff0c;写一遍游戏逻辑代码&#xff0c;前端后端通用当然Nodejs也有一些缺点…

sts-bundle的使用_使用WS-Trust / STS采样器扩展JMeter

sts-bundle的使用JMeter没有对WS-Security或WS-Trust的任何内置支持&#xff0c;这使我为JMeter开发了此STS采样器–可以在负载测试STS时使任何人的生活变得更好。 首先&#xff0c;您需要拥有Apache JMeter发行版。 我正在使用v2.7。 然后&#xff0c;您可以从此处下载sts.sam…

001_jdk配置

配置JAVA_HOME,CLASSPATH,PATH 其中JAVA_HOME必须的 JAVA_HOMEE:\java\jdk1.8.0_77 CLASSPATH(告诉java程序运行时&#xff0c;你的类或者类库在哪里) .; E:\java\jdk1.8.0_77\lib\dt.jar;E:\java\jdk1.8.0_77\lib\tools.jar;E:\java\jdk1.8.0_77\jre\lib\rt.jar 改成变量 .;%J…

python -- join()

python -- join()pythonjoinos月似当时&#xff0c;人似当时否&#xff1f;总 在 python 中&#xff0c;一共有两个 join 方法&#xff0c;一个是 str.join(),另一个是 os.path.join() &#xff0c;这里只了解前一种 str.join(iterable) 官方文档 Return a string which is the…

Spark数据倾斜解决方案(转)

本文转发自技术世界&#xff0c;原文链接 http://www.jasongj.com/spark/skew/ Spark性能优化之道——解决Spark数据倾斜&#xff08;Data Skew&#xff09;的N种姿势 发表于 2017-02-28 | 更新于 2017-10-17 | 本文结合实例详细阐明了Spark数据倾斜的几种场景以及对应的解…

JavaParser入门:以编程方式分析Java代码

我最喜欢的事情之一是解析代码并对其执行自动操作。 因此&#xff0c;我开始为JavaParser做出贡献&#xff0c;并创建了两个相关项目&#xff1a; java-symbol-solver和Effectivejava 。 作为JavaParser的贡献者&#xff0c;我反复阅读了一些非常类似的问题&#xff0c;这些问…

GoldenGate Logdump基本使用

Logdump是GoldenGate复制软件中附带的一个工具软件&#xff0c;在OGG的目录下可以找到。这个工具主要用于分析OGG生成的队列文件&#xff0c;查找记录、统计队列文件中的数据等。 在OGG安装目录下执行logdump.exe or ./logdump即可进入命令行。 开始查找记录之前&#xff0c;先…

.bam.bai的意义_业务活动监视器(BAM)2.0带来的革命

.bam.bai的意义生产兼具精益和企业价值的中间件是一项艰巨的工作。 它要么不存在&#xff0c;要么需要创新的思维&#xff08;很多&#xff09;&#xff0c;并且需要在实现中反复进行。 业务风险很大&#xff0c;但是如果您做对了&#xff0c;它就会使您领先于其他任何公司。 这…

数据结构和算法之排序五:选择排序

我们上一篇谈到了冒泡排序&#xff0c;其实我也说了&#xff0c;这两个排序方式何其相似&#xff0c;如果掌握了冒泡排序再来进行选择排序的理解我觉得完全没有太大的问题。那么什么叫做选择排序呢&#xff1f;我们可以理解为矮子里面挑高个&#xff0c;比如说呀有一个富翁来到…