去年公司由於不斷髮展,內部自研系統越來越多,所以後來搭建了一個紀錄檔收集平臺,並將紀錄檔收集功能以二方包形式引入各個自研系統,避免每個自研系統都要建立一套自己的紀錄檔模組,節約了開發時間,管理起來也更加容易。
這篇文章主要介紹如何編寫二方包,並整合到各個系統中。
先介紹整個ELK紀錄檔平臺的架構。其中xiaobawang-log就是今天的主角。
先看目錄結構
廢話不多說,上程式碼。
1、首先建立一個springboot專案,引入如下包:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>net.logstash.logback</groupId>
<artifactId>logstash-logback-encoder</artifactId>
<version>7.0.1</version>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-core</artifactId>
<version>1.2.10</version>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.2.10</version>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-access</artifactId>
<version>1.2.10</version>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.7.18</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
<version>1.18.26</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
public class SysLog {
/**
* 紀錄檔名稱
*/
private String logName;
/**
* ip地址
*/
private String ip;
/**
* 請求引數
*/
private String requestParams;
/**
* 請求地址
*/
private String requestUrl;
/**
* 使用者ua資訊
*/
private String userAgent;
/**
* 請求時間
*/
private Long useTime;
/**
* 請求時間
*/
private String exceptionInfo;
/**
* 響應資訊
*/
private String responseInfo;
/**
* 使用者名稱稱
*/
private String username;
/**
* 請求方式
*/
private String requestMethod;
}
建立一個列舉類,包含三種紀錄檔型別。
public enum LogAction {
USER_ACTION("使用者紀錄檔", "user-action"),
SYS_ACTION("系統紀錄檔", "sys-action"),
CUSTON_ACTION("其他紀錄檔", "custom-action");
private final String action;
private final String actionName;
LogAction(String action,String actionName) {
this.action = action;
this.actionName = actionName;
}
public String getAction() {
return action;
}
public String getActionName() {
return actionName;
}
}
更改logstash組態檔,將index名稱更改為log-%{[appname]}-%{+YYYY.MM.dd}-%{[action]},其中appname為系統名稱,action為紀錄檔型別。
整個es索引名稱是以「系統名稱+日期+紀錄檔型別」的形式。比如「mySystem-2023.03.05-system-action」表示這個索引,是由mySystem在2023年3月5日產生的系統級別的紀錄檔。
# 輸入端
input {
stdin { }
#為logstash增加tcp輸入口,後面springboot接入會用到
tcp {
mode => "server"
host => "0.0.0.0"
port => 5043
codec => json_lines
}
}
#輸出端
output {
stdout {
codec => rubydebug
}
elasticsearch {
hosts => ["http://你的虛擬機器器ip地址:9200"]
# 輸出至elasticsearch中的自定義index名稱
index => "log-%{[appname]}-%{+YYYY.MM.dd}-%{[action]}"
}
stdout { codec => rubydebug }
}
使用程式設計式設定logback,AppenderBuilder用於建立appender。
@Component
public class AppenderBuilder {
public static final String SOCKET_ADDRESS = "你的虛擬機器器ip地址";
public static final Integer PORT = 5043;//logstash tcp輸入埠
/**
* logstash通訊Appender
* @param name
* @param action
* @param level
* @return
*/
public LogstashTcpSocketAppender logAppenderBuild(String name, String action, Level level) {
LoggerContext context = (LoggerContext) LoggerFactory.getILoggerFactory();
LogstashTcpSocketAppender appender = new LogstashTcpSocketAppender();
appender.setContext(context);
//設定logstash通訊地址
InetSocketAddress inetSocketAddress = new InetSocketAddress(SOCKET_ADDRESS, PORT);
appender.addDestinations(inetSocketAddress);
LogstashEncoder logstashEncoder = new LogstashEncoder();
//對應前面logstash組態檔裡的引數
logstashEncoder.setCustomFields("{\"appname\":\"" + name + "\",\"action\":\"" + action + "\"}");
appender.setEncoder(logstashEncoder);
//這裡設定級別過濾器
LevelFilter levelFilter = new LevelFilter();
levelFilter.setLevel(level);
levelFilter.setOnMatch(ACCEPT);
levelFilter.setOnMismatch(DENY);
levelFilter.start();
appender.addFilter(levelFilter);
appender.start();
return appender;
}
/**
* 控制列印Appender
* @return
*/
public ConsoleAppender consoleAppenderBuild() {
ConsoleAppender consoleAppender = new ConsoleAppender();
LoggerContext context = (LoggerContext) LoggerFactory.getILoggerFactory();
PatternLayoutEncoder encoder = new PatternLayoutEncoder();
encoder.setContext(context);
//設定格式
encoder.setPattern("%red(%d{yyyy-MM-dd HH:mm:ss}) %green([%thread]) %highlight(%-5level) %boldMagenta(%logger) - %cyan(%msg%n)");
encoder.start();
consoleAppender.setEncoder(encoder);
consoleAppender.start();
return consoleAppender;
}
LoggerBuilder主要用於建立logger類。建立步驟如下:
@Component
public class LoggerBuilder {
@Autowired
AppenderBuilder appenderBuilder;
@Value("${spring.application.name:unknow-system}")
private String appName;
private static final Map<String, Logger> LOGCONTAINER = new ConcurrentHashMap<>();
public Logger getLogger(LogAction logAction) {
Logger logger = LOGCONTAINER.get(logAction.getActionName() + "-" + appName);
if (logger != null) {
return logger;
}
logger = build(logAction);
LOGCONTAINER.put(logAction.getActionName() + "-" + appName, logger);
return logger;
}
public Logger getLogger() {
return getLogger(LogAction.CUSTON_ACTION);
}
private Logger build(LogAction logAction) {
//建立紀錄檔appender
List<LogstashTcpSocketAppender> list = createAppender(appName, logAction.getActionName());
LoggerContext context = (LoggerContext) LoggerFactory.getILoggerFactory();
Logger logger = context.getLogger(logAction.getActionName() + "-" + appName);
logger.setAdditive(false);
//列印控制檯appender
ConsoleAppender consoleAppender = appenderBuilder.consoleAppenderBuild();
logger.addAppender(consoleAppender);
list.forEach(appender -> {
logger.addAppender(appender);
});
return logger;
}
/**
* LoggerContext上下文中的紀錄檔物件加入appender
*/
public void addContextAppender() {
//建立四種型別紀錄檔
String action = LogAction.SYS_ACTION.getActionName();
List<LogstashTcpSocketAppender> list = createAppender(appName, action);
LoggerContext context = (LoggerContext) LoggerFactory.getILoggerFactory();
//列印控制檯
ConsoleAppender consoleAppender = appenderBuilder.consoleAppenderBuild();
context.getLoggerList().forEach(logger -> {
logger.setAdditive(false);
logger.addAppender(consoleAppender);
list.forEach(appender -> {
logger.addAppender(appender);
});
});
}
/**
* 建立連線elk的appender,每一種級別紀錄檔建立一個appender
*
* @param name
* @param action
* @return
*/
public List<LogstashTcpSocketAppender> createAppender(String name, String action) {
List<LogstashTcpSocketAppender> list = new ArrayList<>();
LogstashTcpSocketAppender errorAppender = appenderBuilder.logAppenderBuild(name, action, Level.ERROR);
LogstashTcpSocketAppender infoAppender = appenderBuilder.logAppenderBuild(name, action, Level.INFO);
LogstashTcpSocketAppender warnAppender = appenderBuilder.logAppenderBuild(name, action, Level.WARN);
LogstashTcpSocketAppender debugAppender = appenderBuilder.logAppenderBuild(name, action, Level.DEBUG);
LogstashTcpSocketAppender traceAppender = appenderBuilder.logAppenderBuild(name, action, Level.TRACE);
list.add(errorAppender);
list.add(infoAppender);
list.add(warnAppender);
list.add(debugAppender);
list.add(traceAppender);
return list;
}
}
使用spring aop,實現攔截使用者請求,記錄使用者紀錄檔。比如ip、請求引數、請求使用者等資訊,需要配合下面的XiaoBaWangLog註解使用。
這裡攔截上面所說的第二種紀錄檔型別。
@Aspect
@Component
public class LogAspect {
@Autowired
LoggerBuilder loggerBuilder;
private ThreadLocal<Long> startTime = new ThreadLocal<>();
private SysLog sysLog;
@Pointcut("@annotation(com.xiaobawang.common.log.annotation.XiaoBaWangLog)")
public void pointcut() {
}
/**
* 前置方法執行
*
* @param joinPoint
*/
@Before("pointcut()")
public void before(JoinPoint joinPoint) {
startTime.set(System.currentTimeMillis());
//獲取請求的request
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = attributes.getRequest();
String clientIP = ServletUtil.getClientIP(request, null);
if ("0.0.0.0".equals(clientIP) || "0:0:0:0:0:0:0:1".equals(clientIP) || "localhost".equals(clientIP) || "127.0.0.1".equals(clientIP)) {
clientIP = "127.0.0.1";
}
sysLog = new SysLog();
sysLog.setIp(clientIP);
String requestParams = JSONUtil.toJsonStr(getRequestParams(request));
sysLog.setRequestParams(requestParams.length() > 5000 ? ("請求引數過長,引數長度為:" + requestParams.length()) : requestParams);
MethodSignature ms = (MethodSignature) joinPoint.getSignature();
Method method = ms.getMethod();
String logName = method.getAnnotation(XiaoBaWangLog.class).value();
sysLog.setLogName(logName);
sysLog.setUserAgent(request.getHeader("User-Agent"));
String fullUrl = request.getRequestURL().toString();
if (request.getQueryString() != null && !"".equals(request.getQueryString())) {
fullUrl = request.getRequestURL().toString() + "?" + request.getQueryString();
}
sysLog.setRequestUrl(fullUrl);
sysLog.setRequestMethod(request.getMethod());
//tkSysLog.setUsername(JwtUtils.getUsername());
}
/**
* 方法返回後執行
*
* @param ret
*/
@AfterReturning(returning = "ret", pointcut = "pointcut()")
public void after(Object ret) {
Logger logger = loggerBuilder.getLogger(LogAction.USER_ACTION);
String retJsonStr = JSONUtil.toJsonStr(ret);
if (retJsonStr != null) {
sysLog.setResponseInfo(retJsonStr.length() > 5000 ? ("響應引數過長,引數長度為:" + retJsonStr.length()) : retJsonStr);
}
sysLog.setUseTime(System.currentTimeMillis() - startTime.get());
logger.info(JSONUtil.toJsonStr(sysLog));
}
/**
* 環繞通知,收集方法執行期間的錯誤資訊
*
* @param proceedingJoinPoint
* @return
* @throws Throwable
*/
@Around("pointcut()")
public Object around(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
try {
Object obj = proceedingJoinPoint.proceed();
return obj;
} catch (Exception e) {
e.printStackTrace();
sysLog.setExceptionInfo(e.getMessage());
Logger logger = loggerBuilder.getLogger(LogAction.USER_ACTION);
logger.error(JSONUtil.toJsonStr(sysLog));
throw e;
}
}
/**
* 獲取請求的引數
*
* @param request
* @return
*/
private Map getRequestParams(HttpServletRequest request) {
Map map = new HashMap();
Enumeration paramNames = request.getParameterNames();
while (paramNames.hasMoreElements()) {
String paramName = (String) paramNames.nextElement();
String[] paramValues = request.getParameterValues(paramName);
if (paramValues.length == 1) {
String paramValue = paramValues[0];
if (paramValue.length() != 0) {
map.put(paramName, paramValue);
}
}
}
return map;
}
}
LoggerLoad主要是實現使用者級別紀錄檔的收集功能。
這裡定義了一個註解,在controller方法上加上@XiaoBaWangLog("操作內容"),即可攔截並生成請求紀錄檔。
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component
public @interface XiaoBaWangLog {
String value() default "";
}
LoggerLoad主要是實現系統級別紀錄檔的收集功能。
繼承ApplicationRunner,可以在springboot執行後,自動建立系統級別紀錄檔logger物件。
@Component
@Order(value = 1)
@Slf4j
public class LoggerLoad implements ApplicationRunner {
@Autowired
LoggerBuilder loggerBuilder;
@Override
public void run(ApplicationArguments args) throws Exception {
loggerBuilder.addContextAppender();
log.info("載入紀錄檔模組成功");
}
}
LogConfig主要實現自定義級別紀錄檔的收集功能。
生成一個logger物件交給spring容器管理。後面直接從容器取就可以了。
@Configuration
public class LogConfig {
@Autowired
LoggerBuilder loggerBuilder;
@Bean
public Logger loggerBean(){
return loggerBuilder.getLogger();
}
}
程式碼到現在已經全部完成,怎麼將上述的所有Bean加入到spring呢?這個時候就需要用到spring.factories了。
在EnableAutoConfiguration中加入類的全路徑名,在專案啟動的時候,SpringFactoriesLoader會初始化spring.factories,包括pom中引入的jar包中的設定類。
注意,spring.factories在2.7開始已經不推薦使用,3.X版本的springBoot是不支援使用的。
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.xiaobawang.common.log.config.AppenderBuilder,\
com.xiaobawang.common.log.config.LoggerBuilder,\
com.xiaobawang.common.log.load.LoggerLoad,\
com.xiaobawang.common.log.aspect.LogAspect,\
com.xiaobawang.common.log.config.LogConfig
先將xiaobawang進行打包
新建一個springboot專案,引入打包好的xiaobawang-log.
執行springboot,出現「載入紀錄檔模組成功」表示紀錄檔模組啟動成功。
接著新建一個controller請求
存取請求後,可以看到了三種不同型別的索引了
還有很多需要優化的地方,比如ELK設定使用者名稱密碼登入等,對ELK比較瞭解的童鞋可以自己嘗試優化!
如果這篇文章對你有幫助,記得一鍵三連~