首先,應用日志直接寫入數據庫(關系型、NoSQL)的話,會極大地影響應用的性能和並發能力。本人做過壓測實驗,並發數到達一定量后,業務接口沒受到什么影響,反倒是應用日志由於生產速度過快,導致日志數據大量堆積,無法寫入數據庫,成為應用的瓶頸。互聯網軟件行業對性能、並發要求比較高,通常使用的日志收集系統架構有如下幾種: ElasticSearch + Logstash + Kibana(ELK)、ElasticSearch + Filebeat + Kibana(EFK)、Kafka + ELK、 Kafka + EFK。每個應用服務器都要安裝agent客戶端從日志文件中收集日志,ElasticSearch做存儲,Kibana做展示。
但是,傳統軟件行業很多對性能、並發性要求並不高,很多軟件項目可能只有一個管理后台,如果硬上互聯網那一套日志收集系統,無疑會增加項目的部署和維護難度。這種情況下,應用info級別的日志可以在項目中定義一個AOP切面異步寫入數據庫。本文主要介紹錯誤日志的統一存儲。
在spring boot項目中,默認使用的是slf4j + logback日志框架。只需實現logback的Appender接口,自定義一個錯誤日志處理類即可對錯誤日志進行統一存儲。
錯誤日志數據庫表設計
添加錯誤日志實體類
@Data
public class ErrorLogPO {
private Integer logId;
private String className;
private String methodName;
private String exceptionName;
private String errMsg;
private String stackTrace;
private Date createTime;
}
添加錯誤日志寫數據庫自定義Appender類
@Component
public class DbErrorLogAppender extends UnsynchronizedAppenderBase<ILoggingEvent> {
/**
* 錯誤日志數據庫增刪改查服務
*/
@Autowired
private ILogService logService;
/**
* DbErrorLogAppender初始化
*/
@PostConstruct
public void init() {
LoggerContext context = (LoggerContext) LoggerFactory.getILoggerFactory();
ThresholdFilter filter = new ThresholdFilter();
filter.setLevel("ERROR");
filter.setContext(context);
filter.start();
this.addFilter(filter);
this.setContext(context);
context.getLogger("ROOT").addAppender(DbErrorLogAppender.this);
super.start();
}
/**
* 錯誤日志拼裝成實體類,寫入數據庫
*/
@Override
protected void append(ILoggingEvent loggingEvent) {
IThrowableProxy tp = loggingEvent.getThrowableProxy();
// ErrorLogPO數據表實體類
ErrorLogPO errorLog = new ErrorLogPO();
errorLog.setErrMsg(loggingEvent.getMessage());
errorLog.setCreateTime(new Date(loggingEvent.getTimeStamp()));
if (loggingEvent.getCallerData() != null && loggingEvent.getCallerData().length > 0) {
StackTraceElement element = loggingEvent.getCallerData()[0];
errorLog.setClassName(element.getClassName());
errorLog.setMethodName(element.getMethodName());
}
if (tp != null) {
errorLog.setExceptionName(tp.getClassName());
errorLog.setStackTrace(getStackTraceMsg(tp));
}
try {
// 錯誤日志實體類寫入數據庫
logService.addErrorLog(errorLog);
} catch (Exception ex) {
this.addError("上報錯誤日志失敗:" + ex.getMessage());
}
}
/**
* 拼裝堆棧跟蹤信息
*/
private String getStackTraceMsg(IThrowableProxy tp) {
StringBuilder buf = new StringBuilder();
if (tp != null) {
while (tp != null) {
this.renderStackTrace(buf, tp);
tp = tp.getCause();
}
}
return buf.toString();
}
/**
* 堆棧跟蹤信息拼裝成html字符串
*/
private void renderStackTrace(StringBuilder sbuf, IThrowableProxy tp) {
this.printFirstLine(sbuf, tp);
int commonFrames = tp.getCommonFrames();
StackTraceElementProxy[] stepArray = tp.getStackTraceElementProxyArray();
for (int i = 0; i < stepArray.length - commonFrames; ++i) {
StackTraceElementProxy step = stepArray[i];
sbuf.append("<br /> ");
sbuf.append(Transform.escapeTags(step.toString()));
sbuf.append(CoreConstants.LINE_SEPARATOR);
}
if (commonFrames > 0) {
sbuf.append("<br /> ");
sbuf.append("\t... ").append(commonFrames).append(" common frames omitted").append(CoreConstants.LINE_SEPARATOR);
}
}
/**
* 拼裝堆棧跟蹤信息第一行
*/
public void printFirstLine(StringBuilder sb, IThrowableProxy tp) {
int commonFrames = tp.getCommonFrames();
if (commonFrames > 0) {
sb.append("<br />").append("Caused by: ");
}
sb.append(tp.getClassName()).append(": ").append(Transform.escapeTags(tp.getMessage()));
sb.append(CoreConstants.LINE_SEPARATOR);
}
}
添加到數據庫暫不展示