场景分析
在上一篇章,讲述了最基本的ELK环境的搭建。这次来讲如何对日志进行处理,首先你需要明白的是elk的作用就是为了做日志的记录。当然es本身的作用是不止这些的,那么如何在JAVA代码中处理日志呢?
通常,你可能会这样处理异常:
try{
...
dosomething
}
catch (Exception e)
{
e.printStackTrace();
log.info("出现的异常:", e);
return "服务异常";
}
将异常进行打印,随后写在日志文件当中。在我们的开发过程中,当你写的try/catch块会有很多的地方都在用,每个地方我们都需要这样log.info操作。
这样一来就变得十分的麻烦,所以我们可以使用全局异常处理。
全局异常处理
全局异常处理其实十分的简单,@RestControllerAdvice+@ExceptionHandler就可以实现全局异常处理
@ExceptionHandler:统一处理某一类异常,从而能够减少代码重复率和复杂度
@RestControllerAdvice:异常集中处理,更好的使业务逻辑与异常处理剥离开
接下来我们看看具体的代码
使用@RestControllerAdvice声明用来进行全局的异常处理,@ExceptionHandler捕获HdException这个异常,savelog()方法进行异常的存储,然后在发给前端ResponseResultVo。这里的HdException和ResponseResultVo都是我自己进行封装的。
在一个系统当中,我们对于返回给前端的信息都是需要进行统一的,一来方便处理,二来优雅美观。
为什么对异常也进行了自定义呢?
1.我们在工作的时候,项目是分模块或者分功能开发的,基本不会是一个人开发一整个项目,使用自定义异常类就统一了对外异常展示的方式。
2.有时候我们遇到某些校验或者问题的时候,需要直接结束掉当前的请求,这时便可以通过抛出自定义异常来结束。
3.自定义异常可以在我们的项目中某些特殊的业务逻辑时抛出异常。
4.使用自定义异常继承相关的异常来抛出处理后的异常信息,可以隐藏底层的异常。这样更安全,异常信息也会更加直观。自定义异常可以抛出我们自己想要抛出的异常,可以通过抛出的信息区分异常发生的位置,根据异常名我们就可以知道哪里有异常,根据异常提示信息对程序进行修改。比如空指针异常NullPointException,我们可以抛出信息为"XXX为空"的定位异常位置,而不用输出堆栈信息(默认异常抛出的信息)。
但是当我们在使用HdException和ResponseResultVo的时候,还有一个必不可缺的东西,那就是自定义的状态码。有了这三样,我们就能很方便的处理系统中业务相关的异常、返回的信息等。
接下来就是异常处理savelog()方法,在这里我为了演示的方便与直观,直接存在了数据库当中,然后logstash直接从数据库中进行读取,在存入es当中。后续会搭建elk+filebeat+中间件的形式走一遍完整的流程! 整个相关的代码我都会放在最下面。
在JAVA这边搭建好之后,我们需要对于logstash的配置进行下改变,之前我们是通过读取文件中的数据写入es当中,接下来,则需要从数据库中读取并存入es当中去。
Logstash配置
新建一个jdbc.conf在bin的目录下
input{
jdbc{
# 数据库驱动包存放路径
jdbc_driver_library => "..\config\mysql-connector-java-5.1.46.jar"
# 数据库驱动器;
jdbc_driver_class => "Java::com.mysql.jdbc.Driver"
# 数据库连接方式
jdbc_connection_string => "jdbc:mysql://localhost:3306/study"
# 数据库用户名
jdbc_user => "admin"
# 数据库密码
jdbc_password => "admin"
# 数据库重连尝试次数
connection_retry_attempts => "3"
# 判断数据库连接是否可用,默认false不开启
jdbc_validate_connection => "true"
# 数据库连接可用校验超时时间,默认3600s
jdbc_validation_timeout => "3600"
# 开启分页查询(默认false不开启)
jdbc_paging_enabled => "true"
# 单次分页查询条数(默认100000,若字段较多且更新频率较高,建议调低此值)
jdbc_page_size => "500"
# statement为查询数据sql,如果sql较复杂,建议通过statement_filepath配置sql文件的存放路径
# statement_filepath => "D:\logstash-5.6.9\config\jdbc.sql"
# sql_last_value为内置的变量,存放上次查询结果中最后一条数据tracking_column的值,此处即为rowid
statement => "select t.* from sys_log_error t where t.create_date > :sql_last_value"
# 是否将字段名转换为小写,默认true(如果有数据序列化、反序列化需求,建议改为false);
lowercase_column_names => false
# 是否记录上次执行结果,true表示会将上次执行结果的tracking_column字段的值保存到last_run_metadata_path指定的文件中
record_last_run => true
# 需要记录查询结果某字段的值时,此字段为true,否则默认tracking_column为timestamp的值
use_column_value => true
# 查询结果某字段的数据类型,仅包括numeric和timestamp,默认为numeric
tracking_column => "create_date"
# 需要记录的字段,用于增量同步,需是数据库字段
tracking_column => "rowid"
# 记录上次执行结果数据的存放位置
last_run_metadata_path => "D:\logstash-5.6.9\logs\last_id.txt"
# 是否清除last_run_metadata_path的记录,需要增量同步时此字段必须为false
clean_run => false
# 同步频率(分 时 天 月 年),默认每分钟同步一次
schedule => "* * * * *"
# ES索引的type
type => "test-log"
}
# 如果同步多个表,可以在复制一个jdbc{}块,修改相应的地方即可
}
output {
elasticsearch {
hosts => "http://localhost:9200"
index => "logs"
document_id => "%{id}"
}
stdout {}
}
注意需要你将数据库相关的jar包进行配置,其余相关字段可以看上面的注释,已经十分的详细了。
logstash -f jdbc.conf
启动logstash,如果遇到提示需要utf-8编码,另存为utf-8格式即可。
代码测试
在Controller层直接抛出一个异常
@RestController
@RequestMapping("test")
public class TestController {
@RequestMapping("log")
public void log(String name) {
throw new HdException(ResultVoStatus.Bad);
}
}
效果查看
大功告成!
下集预告
相关代码
自定义异常代码
package com.springboot.study.common.exception;
import com.springboot.study.common.response.ResultVoStatus;
public class HdException extends RuntimeException {
ResultVoStatus resultVoStatus;
Exception e;
public HdException(ResultVoStatus resultVoStatus){
super(resultVoStatus.getMsg());
this.resultVoStatus=resultVoStatus;
}
public HdException(ResultVoStatus resultVoStatus, Exception e){
super(resultVoStatus.getMsg());
this.resultVoStatus=resultVoStatus;
this.e=e;
}
public Exception getE() {
return e;
}
public void setE(Exception e) {
this.e = e;
}
public ResultVoStatus getResultVoStatus() {
return resultVoStatus;
}
public void setResultVoStatus(ResultVoStatus resultVoStatus) {
this.resultVoStatus = resultVoStatus;
}
}
封装返回数据代码
package com.springboot.study.common.response;
import io.swagger.annotations.ApiModelProperty;
import java.util.HashMap;
import java.util.Map;
public class ResponseResultVo<T> {
private static final long serialVersionUID = 1679552421651455773L;
@ApiModelProperty(value = "是否成功")
private String sucess;
@ApiModelProperty(value = "状态码")
private Integer status;
@ApiModelProperty(value = "数据")
private T data;
@ApiModelProperty(value = "返回信息")
private String msg;
@ApiModelProperty("额外数据")
private Map<String, Object> extra;
public Map<String, Object> getExtra() {
return extra;
}
public ResponseResultVo<T> setExtra(Map<String, Object> extra) {
this.extra = extra;
return this;
}
public String getSucess() {
return sucess;
}
public ResponseResultVo<T> setSucess(String sucess) {
this.sucess = sucess;
return this;
}
public Integer getStatus() {
return status;
}
public ResponseResultVo<T> setStatus(Integer status) {
this.status = status;
return this;
}
public T getData() {
return data;
}
public ResponseResultVo<T> setData(T data) {
this.data = data;
return this;
}
public String getMsg() {
return msg;
}
public ResponseResultVo<T> setMsg(String msg) {
this.msg = msg;
return this;
}
public ResponseResultVo() {
}
public static <T> ResponseResultVo<T> ok(ResultVoStatus resultVoStatus) {
return new ResponseResultVo().setMsg(resultVoStatus.getMsg())
.setStatus(resultVoStatus.getStatus())
.setSucess("success");
}
public static <T> ResponseResultVo<T> sucess(T data) {
return new ResponseResultVo()
.setMsg(ResultVoStatus.OK.getMsg())
.setData(data)
.setStatus(ResultVoStatus.OK.getStatus())
.setSucess("true");
}
public static <T> ResponseResultVo<T> sucess() {
return new ResponseResultVo()
.setMsg(ResultVoStatus.OK.getMsg())
.setData("你的请求非常完美,我说的!")
.setStatus(ResultVoStatus.OK.getStatus())
.setSucess("true");
}
public static ResponseResultVo faild(ResultVoStatus resultVoStatus) {
return new ResponseResultVo()
.setSucess("false")
.setStatus(resultVoStatus.getStatus())
.setMsg(resultVoStatus.getMsg());
}
public static ResponseResultVo faild(Integer status, String msg) {
return new ResponseResultVo()
.setMsg(msg)
.setStatus(status)
.setSucess("false");
}
public ResponseResultVo<T> addExtraIfTrue(boolean bool, String key, Object value) {
if (bool) {
addExtra(key, value);
}
return this;
}
public ResponseResultVo<T> addExtra(String key, Object value) {
extra = extra == null ? new HashMap<>(16) : extra;
extra.put(key, value);
return this;
}
}
自定义状态码
package com.springboot.study.common.response;
public enum ResultVoStatus {
//枚举字段以逗号隔开,结尾是分号,必须写在前面
OK(200,"请求成功"),
Bad(400,"请求失败"),
Faild(400,"exportTPdf的异常"),
wordTopdf(400,"word生成pdf异常"),
FileOK(200,"文件生成成功!"),
Success(200,"保存成功"),
Error(400,"服务异常,插入失败!"),
ErrorUp(400,"OSS项目上传成功,更新数据失败"),
FileNotFound(400,"文件未生成!") ,
ErrorException(500,"内部服务异常"),
test(500,"test");
;
private Integer status;
private String msg;
private ResultVoStatus(Integer status, String msg){
this.status=status;
this.msg=msg;
}
public Integer getStatus() {
return status;
}
public void setStatus(Integer status) {
this.status = status;
}
public String getMsg() {
return msg;
}
public void setMsg(String msg) {
this.msg = msg;
}
}
全局异常处理类
@RestControllerAdvice
public class GlobalExceptionHandler {
@Autowired
SysLogErrorMapper sysLogErrorMapper;
@ExceptionHandler(HdException.class)
public ResponseResultVo handleException(HttpServletRequest request,
HttpServletResponse response, HdException e) {
savelog(request, e);
return ResponseResultVo.faild(ResultVoStatus.ErrorException)
.addExtra("stackTrace", e.getStackTrace())
.addExtra("exceptionMessage", e.getClass().getName() + ": " + e.getMessage());
}
private void savelog(HttpServletRequest request,
HdException e) {
SysLogError sysLogError = new SysLogError();
//获取IP
String IP = IpUtil.getIpAddrByRequest(request);
//获取请求路径
String requestURI = request.getRequestURI();
//获取请求参数
Map<String, String[]> maps = request.getParameterMap();
String cs = null;
for (Map.Entry<String, String[]> entry : maps.entrySet()) {
cs = entry.getKey() + ":" + Arrays.toString(entry.getValue()) + ";";
}
Exception f = null;
if (e.getE() != null) {
f = e.getE();
} else {
f = e;
}
StackTraceElement stackTraceElement = f.getStackTrace()[0];
String errorMsg = "文件名:" + stackTraceElement.getFileName() +
"\r\n类名:" + stackTraceElement.getClassName() +
"\r\n方法名:" + stackTraceElement.getMethodName() +
"\r\n抛出异常行号:" + stackTraceElement.getLineNumber() +
"\r\n产生的异常为:" + e;
//获取请求类型
String type = request.getMethod();
//头信息
String header = request.getHeader(HttpHeaders.USER_AGENT);
sysLogError.setCreateDate(new Date());
sysLogError.setIp(IP);
sysLogError.setRequestMethod(type);
sysLogError.setRequestParams(cs);
sysLogError.setRequestUri(requestURI);
sysLogError.setUserAgent(header);
sysLogError.setErrorInfo(errorMsg);
sysLogErrorMapper.insertSelective(sysLogError);
}
}
获取IP的工具类
public class IpUtil {
/**
* @param 获取远程IP
* @return IP Address
*/
public static String getIpAddrByRequest(HttpServletRequest request) {
String ip = request.getHeader("x-forwarded-for");
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("Proxy-Client-IP");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("WL-Proxy-Client-IP");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getRemoteAddr();
}
return ip;
}
/**
* @return 获取本机IP
* @throws SocketException
*/
public static String getRealIp() throws SocketException {
String localip = null;// 本地IP,如果没有配置外网IP则返回它
String netip = null;// 外网IP
Enumeration<NetworkInterface> netInterfaces =
NetworkInterface.getNetworkInterfaces();
InetAddress ip = null;
boolean finded = false;// 是否找到外网IP
while (netInterfaces.hasMoreElements() && !finded) {
NetworkInterface ni = netInterfaces.nextElement();
Enumeration<InetAddress> address = ni.getInetAddresses();
while (address.hasMoreElements()) {
ip = address.nextElement();
if (!ip.isSiteLocalAddress()
&& !ip.isLoopbackAddress()
&& ip.getHostAddress().indexOf(":") == -1) {// 外网IP
netip = ip.getHostAddress();
finded = true;
break;
} else if (ip.isSiteLocalAddress()
&& !ip.isLoopbackAddress()
&& ip.getHostAddress().indexOf(":") == -1) {// 内网IP
localip = ip.getHostAddress();
}
}
}
if (netip != null && !"".equals(netip)) {
return netip;
} else {
return localip;
}
}
}
建立表的SQL
/*
Navicat MySQL Data Transfer
Source Server : local
Source Server Version : 50726
Source Host : localhost:3306
Source Database : study
Target Server Type : MYSQL
Target Server Version : 50726
File Encoding : 65001
Date: 2019-12-24 16:37:43
*/
SET FOREIGN_KEY_CHECKS=0;
-- ----------------------------
-- Table structure for sys_log_error
-- ----------------------------
DROP TABLE IF EXISTS `sys_log_error`;
CREATE TABLE `sys_log_error` (
`id` int(20) NOT NULL COMMENT 'id',
`request_uri` varchar(200) DEFAULT NULL COMMENT '请求URI',
`request_method` varchar(20) DEFAULT NULL COMMENT '请求方式',
`request_params` text COMMENT '请求参数',
`user_agent` varchar(500) DEFAULT NULL COMMENT '用户代理',
`ip` varchar(32) DEFAULT NULL COMMENT '操作IP',
`error_info` text COMMENT '异常信息',
`creator` bigint(20) DEFAULT NULL COMMENT '创建者',
`create_date` datetime DEFAULT NULL COMMENT '创建时间',
PRIMARY KEY (`id`),
KEY `idx_create_date` (`create_date`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='异常日志';