## 前言
* 系统一大,就会拆分成多个独立的进程,比如使用微服务架构,也就成了分布式系统。
* 传统的日志系统比如log4j已经无法满足,我们需要将这些日志合并展示到一个统一的地方。
* 如此一来,分布式日志收集系统就登场了。
* 现在用的较多的技术组合为 ElasticSearch+ logstash(基于java)+kibana(基于JRuby, logstash已自带),也就是大家常说的`ELK`。
* 但是此系统较为重量级并不是很适合轻量级微服务架构,SpringBlade封装了一个相对好拓展的日志系统,下面我们来具体看一下把!~
## 实现思路
### 全局统一异常
* 使用`@RestControllerAdvice`,捕获异常后统一返回封装好的格式。
* 异常分成两种:已知异常与未知异常,未知异常是着重需要关注的,所以会将未知异常入库,方便排查
* 以下为全局异常核心代码
~~~
@Slf4j
@Configuration
@ConditionalOnClass({Servlet.class, DispatcherServlet.class})
@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET)
@RestControllerAdvice
public class BladeRestExceptionTranslator {
@ExceptionHandler(MissingServletRequestParameterException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public R handleError(MissingServletRequestParameterException e) {
log.warn("缺少请求参数", e.getMessage());
String message = String.format("缺少必要的请求参数: %s", e.getParameterName());
return R.failure(ResultCode.PARAM_MISS, message);
}
...............
...............
...............
@ExceptionHandler(ServiceException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public R handleError(ServiceException e) {
log.error("业务异常", e);
return R.failure(e.getResultCode(), e.getMessage());
}
@ExceptionHandler(SecureException.class)
@ResponseStatus(HttpStatus.UNAUTHORIZED)
public R handleError(SecureException e) {
log.error("认证异常", e);
return R.failure(e.getResultCode(), e.getMessage());
}
@ExceptionHandler(Throwable.class)
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public R handleError(Throwable e) {
log.error("服务器异常", e);
//发送服务异常事件
ErrorLogPublisher.publishEvent(e, UrlUtil.getPath(WebUtil.getRequest().getRequestURI()));
return R.failure(ResultCode.INTERNAL_SERVER_ERROR, (Func.isEmpty(e.getMessage()) ? ResultCode.INTERNAL_SERVER_ERROR.getMessage() : e.getMessage()));
}
}
~~~
* 大家可以看下最后几行的 `ErrorLogPublisher.publishEvent`方法,可以看出,该方法使用了Spring的Event事件驱动,进行解耦。异步操作日志的收集、入库等。代码如下
~~~
/**
* 异常信息事件发送
*
* @author Chill
*/
public class ErrorLogPublisher {
public static void publishEvent(Throwable error, String requestUri) {
HttpServletRequest request = WebUtil.getRequest();
LogError logError = new LogError();
logError.setRequestUri(requestUri);
if (Func.isNotEmpty(error)) {
logError.setStackTrace(Exceptions.getStackTraceAsString(error));
logError.setExceptionName(error.getClass().getName());
logError.setMessage(error.getMessage());
StackTraceElement[] elements = error.getStackTrace();
if (Func.isNotEmpty(elements)) {
StackTraceElement element = elements[0];
logError.setMethodName(element.getMethodName());
logError.setMethodClass(element.getClassName());
logError.setFileName(element.getFileName());
logError.setLineNumber(element.getLineNumber());
}
}
Map<String, Object> event = new HashMap<>(16);
event.put(EventConstant.EVENT_LOG, logError);
event.put(EventConstant.EVENT_REQUEST, request);
SpringUtil.publishEvent(new ErrorLogEvent(event));
}
}
~~~
* 跟踪到 `SpringUtil.publishEvent(new ErrorLogEvent(event));`
* 查看 `ErrorLogEvent`代码
~~~
/**
* 错误日志事件
*
* @author Chill
*/
public class ErrorLogEvent extends ApplicationEvent {
public ErrorLogEvent(Map<String, Object> source) {
super(source);
}
}
~~~
* 对应错误日志处理`ErrorLogListener `代码
~~~
/**
* 异步监听错误日志事件
*
* @author Chill
*/
@Slf4j
@AllArgsConstructor
public class ErrorLogListener {
private final ILogClient logService;
private final ServerInfo serverInfo;
private final BladeProperties bladeProperties;
@Async
@Order
@EventListener(ErrorLogEvent.class)
public void saveErrorLog(ErrorLogEvent event) {
Map<String, Object> source = (Map<String, Object>) event.getSource();
LogError logError = (LogError) source.get(EventConstant.EVENT_LOG);
HttpServletRequest request = (HttpServletRequest) source.get(EventConstant.EVENT_REQUEST);
logError.setUserAgent(request.getHeader(WebUtil.USER_AGENT_HEADER));
logError.setMethod(request.getMethod());
logError.setParams(WebUtil.getRequestParamString(request));
logError.setServiceId(bladeProperties.getName());
logError.setServerHost(serverInfo.getHostName());
logError.setServerIp(serverInfo.getIPWithPort());
logError.setEnv(bladeProperties.getEnv());
logError.setCreateBy(SecureUtil.getUserAccount(request));
logError.setCreateTime(LocalDateTime.now());
logService.saveErrorLog(logError);
}
}
~~~
* 到了`ErrorLogListener `其实就可以进行拓展了,可以像现在这样,直接调用日志接口入库,也可以集成消息队列,提高性能。最终,我们可以到数据库中看到如下数据
![](https://box.kancloud.cn/447d37c892af12f167e294b4a7c67cee_1557x845.png)
![](https://box.kancloud.cn/8a4041b5a5fc7ca86502783bcb728f99_1280x747.png)
* 错误产生在哪个服务名,服务的环境,服务的ip,服务的host,异常堆栈详情,异常类,异常所在方法,所在行数,传的参数,操作人,操作时间等等信息一目了然。
* 简单的几处代码,如若集成消息队列或redis,我相信足以满足轻量级微服务架构的需求。
### API日志
* 自动生成错误日志的讲完了,我们需要根据注解主动生成日志。
* 对于这一类我们一般使用AOP来实现功能,切到对应注解后,获取当前请求的信息,然后再通过Event异步入库
* AOP代码如下
~~~
/**
* 操作日志使用spring event异步入库
*
* @author Chill
*/
@Slf4j
@Aspect
public class ApiLogAspect {
@Around("@annotation(apiLog)")
public Object around(ProceedingJoinPoint point, ApiLog apiLog) throws Throwable {
//获取类名
String className = point.getTarget().getClass().getName();
//获取方法
String methodName = point.getSignature().getName();
// 发送异步日志事件
long beginTime = System.currentTimeMillis();
//执行方法
Object result = point.proceed();
//执行时长(毫秒)
long time = System.currentTimeMillis() - beginTime;
//记录日志
ApiLogPublisher.publishEvent(methodName, className, apiLog, time);
return result;
}
}
~~~
* 注解代码如下
~~~
/**
* 操作日志注解
*
* @author Chill
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface ApiLog {
/**
* 日志描述
*
* @return {String}
*/
String value() default "日志记录";
}
~~~
* 日志发送器代码如下
~~~
/**
* API日志信息事件发送
*
* @author Chill
*/
public class ApiLogPublisher {
public static void publishEvent(String methodName, String methodClass, ApiLog apiLog, long time) {
HttpServletRequest request = WebUtil.getRequest();
LogApi logApi = new LogApi();
logApi.setType(BladeConstant.LOG_NORMAL_TYPE);
logApi.setTitle(apiLog.value());
logApi.setTime(String.valueOf(time));
logApi.setMethodClass(methodClass);
logApi.setMethodName(methodName);
Map<String, Object> event = new HashMap<>(16);
event.put(EventConstant.EVENT_LOG, logApi);
event.put(EventConstant.EVENT_REQUEST, request);
SpringUtil.publishEvent(new ApiLogEvent(event));
}
}
~~~
* 日志事件代码如下
~~~
/**
* 系统日志事件
*
* @author Chill
*/
public class ApiLogEvent extends ApplicationEvent {
public ApiLogEvent(Map<String, Object> source) {
super(source);
}
}
~~~
* 日志监听器代码如下
~~~
/**
* 异步监听日志事件
*
* @author Chill
*/
@Slf4j
@Component
@AllArgsConstructor
public class ApiLogListener {
private final ILogClient logService;
private final ServerInfo serverInfo;
private final BladeProperties bladeProperties;
@Async
@Order
@EventListener(ApiLogEvent.class)
public void saveApiLog(ApiLogEvent event) {
Map<String, Object> source = (Map<String, Object>) event.getSource();
LogApi logApi = (LogApi) source.get(EventConstant.EVENT_LOG);
HttpServletRequest request = (HttpServletRequest) source.get(EventConstant.EVENT_REQUEST);
logApi.setServiceId(bladeProperties.getName());
logApi.setServerHost(serverInfo.getHostName());
logApi.setServerIp(serverInfo.getIPWithPort());
logApi.setEnv(bladeProperties.getEnv());
logApi.setRemoteIp(WebUtil.getIP(request));
logApi.setUserAgent(request.getHeader(WebUtil.USER_AGENT_HEADER));
logApi.setRequestUri(UrlUtil.getPath(request.getRequestURI()));
logApi.setMethod(request.getMethod());
logApi.setParams(WebUtil.getRequestParamString(request));
logApi.setCreateBy(SecureUtil.getUserAccount(request));
logApi.setCreateTime(LocalDateTime.now());
logService.saveApiLog(logApi);
}
}
~~~
* 由此可见,思路其实同异常日志一致,都是获取请求相关信息后进行事件驱动解耦,异步将日志入库,若要增加其性能与并发能力,可以采用集成消息队列或者Redis,相信这些都难不倒大家
<br>
- 第零章 序
- 序言
- 系统架构
- 视频公开课
- 开源版介绍
- 商业版介绍
- 功能对比
- 答疑流程
- 第一章 快速开始
- 升级必看
- 环境要求
- 环境准备
- 基础环境安装
- Docker安装基础服务
- Nacos安装
- Sentinel安装
- 插件安装
- 建数据库
- 工程导入
- 导入Cloud版本
- 导入Nacos配置
- 导入Boot版本
- 工程运行
- 运行Cloud版本
- 运行Boot版本
- 工程测试
- 测试Cloud版本
- 测试Boot版本
- 第二章 技术基础
- Java
- Lambda
- Lambda 受检异常处理
- Stream 简介
- Stream API 一览
- Stream API (上)
- Stream API (下)
- Optional 干掉空指针
- 函数式接口
- 新的日期 API
- Lombok
- SpringMVC
- Swagger
- Mybatis
- Mybatis-Plus
- 开发规范
- 第三章 开发初探
- 新建微服务工程
- 第一个API
- API鉴权
- API响应结果
- Redis缓存
- 第一个CRUD
- 建表
- 建Entity
- 建Service和Mapper
- 新增 API
- 修改 API
- 删除 API
- 查询 API
- 单条数据
- 多条数据
- 分页
- 微服务远程调用
- 声明式服务调用 Feign
- 熔断机制 Hystrix
- 第四章 开发进阶
- 聚合文档
- 鉴权配置
- 跨域处理
- Xss防注入
- 自定义启动器
- Secure安全框架
- Token认证简介
- Token认证配置
- PreAuth注解配置
- Token认证实战
- Token认证加密
- 日志系统
- 原理解析
- 功能调用
- Seata分布式事务
- 简介
- 编译包启动
- 配置nacos对接
- docker启动
- 对接微服务
- 代码生成配置
- 前言
- 数据库建表
- 代码生成
- 前端配置
- 优化效果
- 第五章 功能特性
- SaaS多租户
- 概念
- 数据隔离配置
- 线程环境自定义租户ID
- 多终端令牌认证
- 概念
- 系统升级
- 使用
- 第三方系统登录
- 概念说明
- 对接说明
- 对接准备
- 配置说明
- 操作流程
- 后记
- UReport2报表
- 报表简介
- 对接配置
- 报表后记
- 动态数据权限
- 数据权限简介
- 数据权限开发
- 纯注解配置
- Web全自动配置
- 注解半自动配置
- 数据权限注意点
- 动态接口权限
- 乐观锁配置
- 统一服务登陆配置
- Skywalking追踪监控
- Minio分布式对象存储
- Boot版本对接至Cloud
- 第六章 生产部署
- windows部署
- linux部署
- jar部署
- docker部署
- java环境安装
- mysql安装
- docker安装
- docker-compose安装
- harbor安装
- 部署步骤
- 宝塔部署
- 准备工作
- 安装工作
- 部署准备
- 部署后端
- 部署前端
- 部署域名
- 结束工作
- k8s平台部署
- 第七章 版本控制
- Git远程分支合并
- Git地址更换
- 第八章 学习资料
- 第九章 FAQ
- 第十章 联系我们