一、使用log4j记录日志
新建log4j配置文件
#log4j.rootLogger=CONSOLE,info,error,DEBUG log4j.rootLogger=info,error,CONSOLE,DEBUG log4j.appender.CONSOLE=org.apache.log4j.ConsoleAppender log4j.appender.CONSOLE.layout=org.apache.log4j.PatternLayout log4j.appender.CONSOLE.layout.ConversionPattern=%d{yyyy-MM-dd-HH-mm} [%t] [%c] [%p] - %m%n log4j.logger.info=info log4j.appender.info=org.apache.log4j.DailyRollingFileAppender log4j.appender.info.layout=org.apache.log4j.PatternLayout log4j.appender.info.layout.ConversionPattern=%d{yyyy-MM-dd-HH-mm} [%t] [%c] [%p] - %m%n log4j.appender.info.datePattern='.'yyyy-MM-dd log4j.appender.info.Threshold = info log4j.appender.info.append=true #log4j.appender.info.File=/home/admin/pms-api-services/logs/info/api_services_info log4j.appender.info.File=/Users/dddd/Documents/testspace/pms-api-services/logs/info/api_services_info log4j.logger.error=error log4j.appender.error=org.apache.log4j.DailyRollingFileAppender log4j.appender.error.layout=org.apache.log4j.PatternLayout log4j.appender.error.layout.ConversionPattern=%d{yyyy-MM-dd-HH-mm} [%t] [%c] [%p] - %m%n log4j.appender.error.datePattern='.'yyyy-MM-dd log4j.appender.error.Threshold = error log4j.appender.error.append=true #log4j.appender.error.File=/home/admin/pms-api-services/logs/error/api_services_error log4j.appender.error.File=/Users/dddd/Documents/testspace/pms-api-services/logs/error/api_services_error log4j.logger.DEBUG=DEBUG log4j.appender.DEBUG=org.apache.log4j.DailyRollingFileAppender log4j.appender.DEBUG.layout=org.apache.log4j.PatternLayout log4j.appender.DEBUG.layout.ConversionPattern=%d{yyyy-MM-dd-HH-mm} [%t] [%c] [%p] - %m%n log4j.appender.DEBUG.datePattern='.'yyyy-MM-dd log4j.appender.DEBUG.Threshold = DEBUG log4j.appender.DEBUG.append=true #log4j.appender.DEBUG.File=/home/admin/pms-api-services/logs/debug/api_services_debug log4j.appender.DEBUG.File=/Users/dddd/Documents/testspace/pms-api-services/logs/debug/api_services_debug
|
二、使用logback记录日志
在resources 文件夹下,新建logback-spring.xml 文件,将文件文本内容复制进去。
<?xml version="1.0" encoding="UTF-8"?> <configuration> <!-- 说明: 1、日志级别及文件 日志记录采用分级记录,级别与日志文件名相对应,不同级别的日志信息记录到不同的日志文件中 例如:error级别记录到log_error_xxx.log或log_error.log(该文件为当前记录的日志文件),而log_error_xxx.log为归档日志, 日志文件按日期记录,同一天内,若日志文件大小等于或大于2M,则按0、1、2...顺序分别命名 例如log-level-2013-12-21.0.log 其它级别的日志也是如此。 2、文件路径 若开发、测试用,在Eclipse中运行项目,则到Eclipse的安装路径查找logs文件夹,以相对路径../logs。 若部署到Tomcat下,则在Tomcat下的logs文件中 3、Appender FILEERROR对应error级别,文件名以log-error-xxx.log形式命名 FILEWARN对应warn级别,文件名以log-warn-xxx.log形式命名 FILEINFO对应info级别,文件名以log-info-xxx.log形式命名 FILEDEBUG对应debug级别,文件名以log-debug-xxx.log形式命名 stdout将日志信息输出到控制上,为方便开发测试使用 --> <contextName>logback</contextName> <!--生成日志目录,dubug模式生成在项目根目录,运行jar模式生成目录在jar的同级目录--> <property name="LOG_PATH" value="logs" /> <!-- 日志记录器,日期滚动记录 --> <appender name="FILEERROR" class="ch.qos.logback.core.rolling.RollingFileAppender"> <!-- 正在记录的日志文件的路径及文件名 --> <file>${LOG_PATH}/log_error.log</file> <!-- 日志记录器的滚动策略,按日期,按大小记录 --> <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy"> <!-- 归档的日志文件的路径,例如今天是2013-12-21日志,当前写的日志文件路径为file节点指定,可以将此文件与file指定文件路径设置为不同路径,从而将当前日志文件或归档日志文件置不同的目录。 而2013-12-21的日志文件在由fileNamePattern指定。%d{yyyy-MM-dd}指定日期格式,%i指定索引 --> <fileNamePattern>${LOG_PATH}/error/log-error-%d{yyyy-MM-dd}.%i.log</fileNamePattern> <!-- 除按日志记录之外,还配置了日志文件不能超过2M,若超过2M,日志文件会以索引0开始, 命名日志文件,例如log-error-2013-12-21.0.log --> <timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP"> <maxFileSize>2MB</maxFileSize> </timeBasedFileNamingAndTriggeringPolicy> <!--只保留最近n天的日志--> <maxHistory>7</maxHistory> <!--用来指定日志文件的上限大小,那么到了这个值,就会删除旧的日志--> <totalSizeCap>1GB</totalSizeCap> <!--启动清除超过上限的历史日志--> <cleanHistoryOnStart>true</cleanHistoryOnStart> </rollingPolicy> <!-- 追加方式记录日志 --> <append>true</append> <!-- 日志文件的格式 --> <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder"> <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} %-5level %logger Line:%-3L - %msg%n</pattern> <charset>utf-8</charset> </encoder> <!-- 此日志文件只记录info级别的 --> <filter class="ch.qos.logback.classic.filter.LevelFilter"> <level>error</level> <onMatch>ACCEPT</onMatch> <onMismatch>DENY</onMismatch> </filter> </appender> <!-- 日志记录器,日期滚动记录 --> <appender name="FILEWARN" class="ch.qos.logback.core.rolling.RollingFileAppender"> <!-- 正在记录的日志文件的路径及文件名 --> <file>${LOG_PATH}/log_warn.log</file> <!-- 日志记录器的滚动策略,按日期,按大小记录 --> <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy"> <!-- 归档的日志文件的路径,例如今天是2013-12-21日志,当前写的日志文件路径为file节点指定,可以将此文件与file指定文件路径设置为不同路径,从而将当前日志文件或归档日志文件置不同的目录。 而2013-12-21的日志文件在由fileNamePattern指定。%d{yyyy-MM-dd}指定日期格式,%i指定索引 --> <fileNamePattern>${LOG_PATH}/warn/log-warn-%d{yyyy-MM-dd}.%i.log </fileNamePattern> <!-- 除按日志记录之外,还配置了日志文件不能超过2M,若超过2M,日志文件会以索引0开始, 命名日志文件,例如log-error-2013-12-21.0.log --> <timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP"> <maxFileSize>2MB</maxFileSize> </timeBasedFileNamingAndTriggeringPolicy> <!--只保留最近n天的日志--> <maxHistory>7</maxHistory> <!--用来指定日志文件的上限大小,那么到了这个值,就会删除旧的日志--> <totalSizeCap>1GB</totalSizeCap> <!--启动清除超过上限的历史日志--> <cleanHistoryOnStart>true</cleanHistoryOnStart> </rollingPolicy> <!-- 追加方式记录日志 --> <append>true</append> <!-- 日志文件的格式 --> <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder"> <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} %-5level %logger Line:%-3L - %msg%n</pattern> <charset>utf-8</charset> </encoder> <!-- 此日志文件只记录info级别的 --> <filter class="ch.qos.logback.classic.filter.LevelFilter"> <level>warn</level> <level>debug</level> <onMatch>ACCEPT</onMatch> <onMismatch>DENY</onMismatch> </filter> </appender> <!-- 日志记录器,日期滚动记录 --> <appender name="FILEINFO" class="ch.qos.logback.core.rolling.RollingFileAppender"> <!-- 正在记录的日志文件的路径及文件名 --> <file>${LOG_PATH}/log_info.log</file> <!-- 日志记录器的滚动策略,按日期,按大小记录 --> <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy"> <!-- 归档的日志文件的路径,例如今天是2013-12-21日志,当前写的日志文件路径为file节点指定,可以将此文件与file指定文件路径设置为不同路径,从而将当前日志文件或归档日志文件置不同的目录。 而2013-12-21的日志文件在由fileNamePattern指定。%d{yyyy-MM-dd}指定日期格式,%i指定索引 --> <fileNamePattern>${LOG_PATH}/info/log-info-%d{yyyy-MM-dd}.%i.log </fileNamePattern> <!-- 除按日志记录之外,还配置了日志文件不能超过2M,若超过2M,日志文件会以索引0开始, 命名日志文件,例如log-error-2013-12-21.0.log --> <timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP"> <maxFileSize>2MB</maxFileSize> </timeBasedFileNamingAndTriggeringPolicy> <!--只保留最近n天的日志--> <maxHistory>7</maxHistory> <!--用来指定日志文件的上限大小,那么到了这个值,就会删除旧的日志--> <totalSizeCap>1GB</totalSizeCap> <!--启动清除超过上限的历史日志--> <cleanHistoryOnStart>true</cleanHistoryOnStart> </rollingPolicy> <!-- 追加方式记录日志 --> <append>true</append> <!-- 日志文件的格式 --> <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder"> <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} %-5level %logger Line:%-3L - %msg%n</pattern> <charset>utf-8</charset> </encoder> <!-- 此日志文件只记录info级别的 --> <filter class="ch.qos.logback.classic.filter.LevelFilter"> <level>info</level> <onMatch>ACCEPT</onMatch> <onMismatch>DENY</onMismatch> </filter> </appender> <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender"> <!--encoder 默认配置为PatternLayoutEncoder--> <encoder> <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} %-5level %logger Line:%-3L - %msg%n</pattern> <charset>utf-8</charset> </encoder> <!--此日志appender是为开发使用,只配置最底级别,控制台输出的日志级别是大于或等于此级别的日志信息--> <filter class="ch.qos.logback.classic.filter.ThresholdFilter"> <level>debug</level> </filter> </appender> <logger name="org.springframework" level="WARN" /> <logger name="org.hibernate" level="WARN" /> <logger name="org.hibernate.type.descriptor.sql.BasicBinder" level="TRACE"/> <logger name="org.hibernate.type.descriptor.sql.BasicExtractor" level="TRACE"/> <logger name="org.hibernate.SQL" level="DEBUG"/> <logger name="org.hibernate.engine.QueryParameters" level="DEBUG"/> <logger name="org.hibernate.engine.query.HQLQueryPlan" level="DEBUG"/> <!-- 生产环境下,将此级别配置为适合的级别,以免日志文件太多或影响程序性能 --> <root level="INFO"> <appender-ref ref="FILEERROR" /> <appender-ref ref="FILEWARN" /> <appender-ref ref="FILEINFO" /> <!-- 生产环境将请stdout,testfile去掉 --> <appender-ref ref="STDOUT" /> </root> </configuration> |
---|
三、使用AOP统一处理Web请求日志。
POM文件新增依赖
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId> </dependency> |
---|
切面拦截器,增加日志记录内容。
@Aspect @Component public class WebLogAspect { private Logger logger = LoggerFactory.getLogger(getClass()); @Pointcut("execution(public * com.itmayiedu.controller..*.*(..))") public void webLog() { } @Before("webLog()") public void doBefore(JoinPoint joinPoint) throws Throwable { // 接收到请求,记录请求内容 ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); HttpServletRequest request = attributes.getRequest(); // 记录下请求内容 logger.info("URL : " + request.getRequestURL().toString()); logger.info("HTTP_METHOD : " + request.getMethod()); logger.info("IP : " + request.getRemoteAddr()); Enumeration<String> enu = request.getParameterNames(); while (enu.hasMoreElements()) { String name = (String) enu.nextElement(); logger.info("name:{},value:{}", name, request.getParameter(name)); } } @AfterReturning(returning = "ret", pointcut = "webLog()") public void doAfterReturning(Object ret) throws Throwable { // 处理完请求,返回内容 logger.info("RESPONSE : " + ret); } } |
四、记录每个接口的响应时间。
记录每个接口的响应时间,在实际使用中可以用来查询接口的运行效率。也可以通过elk的方式,将接口的响应的响应效率通过es等工具进行展示,监控接口的运行情况。
ElkFormatLog 类
//ELK日志记录实体类 public class ElkFormatLog { //接口名称 public String interfacename; //日志级别,info信息,errinfo,错误信息 public String logLevel; //日志记录时间 public String logTime; //访问者Ip public String Ip ; //服务端Ip public String serverIp ; //服务端计算机名称 public String servercomputername ; //调用的接口路径 public String function ; //执行开始时间 public String starttime; //执行结束时间,接口返回时间 public String endtime; //接口方式执行耗时 public long usetime; //接口入参 public String inputParameters; //接口出参 public String outputParameters; //接口信息,出错记录错误信息 public String messageinfo; } |
---|
切面拦截器优化
package com.winning.sx.microframework.config; import com.alibaba.fastjson.JSON; import com.winning.sx.microframework.common.constant.BaseConstant; import com.winning.sx.microframework.common.util.ElkFormatLog; import com.winning.sx.microframework.common.util.LogInfo; import lombok.extern.slf4j.Slf4j; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Pointcut; import org.joda.time.DateTime; import org.joda.time.Days; import org.springframework.stereotype.Component; import org.springframework.web.context.request.RequestContextHolder; import org.springframework.web.context.request.ServletRequestAttributes; import javax.servlet.ServletRequest; import javax.servlet.ServletResponse; import javax.servlet.http.HttpServletRequest; import java.text.SimpleDateFormat; import java.util.Date; /* 使用记录ELK日志 */ @Aspect @Slf4j @Component public class AccessLogAspect { @Pointcut("execution(public * com.winning..controller.*.*(..))") public void accessLog() { } @Around("accessLog()") public Object around(ProceedingJoinPoint joinPoint){ DateTime startTime =DateTime.now(); // 定义返回对象、得到方法需要的参数 Object resultData = null; Object[] args = joinPoint.getArgs(); String uri = ""; String methodName = joinPoint.getSignature().getName(); ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); HttpServletRequest request = attributes.getRequest(); try { uri = request.getServletPath(); if (joinPoint != null && joinPoint.getArgs() != null) { StringBuffer buf = new StringBuffer("["); for (int i = 0; i < joinPoint.getArgs().length; i++) { Object arg = joinPoint.getArgs()[i]; if (arg != null && !(arg instanceof ServletRequest) && !(arg instanceof ServletResponse)) { String argStr = arg.toString(); //针对登录密码日志,做特殊处理 if (argStr.contains("password") && argStr.contains("LoginForm")){ int pwIndex = argStr.indexOf("password="); argStr = argStr.substring(0,pwIndex+10)+"****"+argStr.substring(argStr.indexOf("'",pwIndex+11)); } buf.append(argStr); } } // log.info("args{}", buf.append("]").toString()); } resultData = joinPoint.proceed(args); DateTime endTime = DateTime.now(); long diff = endTime.getMillis() - startTime.getMillis(); //log.info("======>用户:{},完成请求[{}],开始时间:{},结束时间:{},耗时:{},返回:{}", "", uri,startTime.toString(),endTime.toString(), diff, resultData.toString()); ElkFormatLog elk = new ElkFormatLog(); elk.interfacename="JAVAVue系统框架"; elk.logLevel = "loginfo"; Date date = new Date(); SimpleDateFormat sd = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS"); elk.logTime = sd.format(date); elk.Ip = request.getRemoteAddr(); elk.serverIp = request.getLocalAddr(); elk.servercomputername =request.getLocalName(); elk.function = request.getRequestURL().toString(); elk.starttime = startTime.toString("yyyy-MM-dd HH:mm:ss.SSS"); elk.endtime = endTime.toString("yyyy-MM-dd HH:mm:ss.SSS"); elk.usetime = diff; elk.inputParameters = request.getQueryString(); elk.outputParameters = JSON.toJSONString(resultData); elk.messageinfo = ""; // 记录ELK日志信息 LogInfo.writeLog(elk); } catch (Throwable e) { e.printStackTrace(); DateTime endTime = DateTime.now(); long diff = endTime.getMillis() - startTime.getMillis(); ElkFormatLog elk = new ElkFormatLog(); elk.interfacename="JAVA系统框架"; elk.logLevel = "errinfo"; Date date = new Date(); SimpleDateFormat sd = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS"); elk.logTime = sd.format(date); elk.Ip = request.getRemoteAddr(); elk.serverIp = request.getLocalAddr(); elk.servercomputername =request.getLocalName(); elk.function = request.getRequestURL().toString(); elk.starttime = startTime.toString("yyyy-MM-dd HH:mm:ss.SSS"); elk.endtime = DateTime.now().toString("yyyy-MM-dd HH:mm:ss.SSS"); elk.usetime = diff; elk.inputParameters = request.getQueryString(); elk.outputParameters = JSON.toJSONString(BaseConstant.ERR_MSG); elk.messageinfo = e.toString(); // 记录异常信息 LogInfo.writeLog(elk); //log.error("======>用户:{},完成请求[{}],开始时间:{},结束时间:{},耗时:{},返回:{}", "", uri,startTime.toString(),endTime.toString(), diff, resultData.toString()); // log.error("======>请求[{}]异常!耗时:{}", uri, (endTime - startTime)); } return resultData; } } |
---|
记录日志类
package com.winning.sx.microframework.common.util; import com.alibaba.fastjson.JSON; import com.winning.sx.microframework.common.util.ElkFormatLog; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.RandomAccessFile; import java.text.SimpleDateFormat; import java.util.Date; /* 记录ELK可分析接口日志,方便问题追踪 */ public class LogInfo { /** * 将信息写入到日志 * * @param * @return * @throws IOException */ public static boolean writeLog(ElkFormatLog elk) { //先进行日志内容拼接 String content = info(elk); //创建文件夹及日志文件,每天一个文件夹,1小时一个日志文件 Date date = new Date(); SimpleDateFormat dtnow = new SimpleDateFormat("yyyyMMdd"); SimpleDateFormat dtnowhour = new SimpleDateFormat("HH"); String logfilepath = dtnow.format(date); String logfiel = dtnowhour.format(date); File file1 = new File(System.getProperty("user.dir") + "/WinningLog"); //如果文件夹不存在则创建 if (!file1.exists() && !file1.isDirectory()) { file1.mkdir(); } File file = new File(System.getProperty("user.dir") + "/WinningLog/" + logfilepath); //如果文件夹不存在则创建 if (!file.exists() && !file.isDirectory()) { file.mkdir(); } File fileName = new File(System.getProperty("user.dir") + "/WinningLog/" + logfilepath + "/" + logfiel + ".wnlog"); RandomAccessFile mm = null; boolean flag = false; FileOutputStream o = null; try { o = new FileOutputStream(fileName, true); o.write(content.getBytes("utf-8")); o.close(); flag = true; } catch (IOException e) { e.printStackTrace(); } finally { if (mm != null) { try { mm.close(); } catch (IOException e) { e.printStackTrace(); } } } try { //使用线程进行删除过期的日志文件 MyThread myThread = new MyThread(); Thread t1 = new Thread(myThread); t1.start(); } catch (Exception e) { e.printStackTrace(); } return flag; } /* 使用线程进行删除过期日志文件,保留2天 */ public static class MyThread extends Thread { public void run() { removeOldFile(System.getProperty("user.dir") + "/WinningLog/", 2); } } /* 拼接日志消息 */ private static String info(ElkFormatLog elk) { StringBuffer buffer = new StringBuffer(); buffer.append(JSON.toJSONString(elk) + "\r\n"); return buffer.toString(); } private static void removeOldFile(String filepath, int keepDays) { File file = new File(filepath); File[] filelist = file.listFiles(); for (int i = 0; i < filelist.length; i++) { File filedetail = new File(filelist[i].getPath()); File[] filedetaillist = filedetail.listFiles(); for (int j = 0; j < filedetaillist.length; j++) { long newtime = System.currentTimeMillis(); long txttime = filedetaillist[j].lastModified(); long time = newtime - txttime; time=time / (1000 * 60 * 60 * 24); if (time >= keepDays) { filedetaillist[j].delete(); } } //最后删除空文件夹 filedetaillist = filedetail.listFiles(); if(filedetaillist.length==0) { filelist[i].delete(); } } } } |
---|
五、将日志信息信息存储到数据库表中。
代码优化
六、关于接口日志的细节思考。
1、接口必须进行日志存储,否则无法排查问题和处理问题。
2、接口日志一般建议保存7-30天,再长占用空间过多。
3、接口一定要进行性能分析,即存储服务的响应时间进行分析。
4、服务如果文件存储,建议按照小时存储,将一天的日志文件按照一个小时或者半个小时进行存储,方便排查问题。
1、日志存储在数据库还是通过文件格式存储。
存储在数据库的优势在于方便进行结构化查询和分析。
缺陷在于需要对于日志进行定时清理,要不然数据库日志越来越大。
2、使用elk的方案。
将日志通过文件json格式进行存储,再异步进行文件提出,抽取到日志分析的数据库进行分析。因为是异步的即使日志平台崩溃了也不影响正常业务的使用。