一、重写HTMLLayout
两个自定义类:LzhHTMLLayoutBase和LzhHTMLLayout
LzhHTMLLayoutBase代码如下:
package top.liaozhenghan.logback.study.layout; import static ch.qos.logback.core.CoreConstants.LINE_SEPARATOR; import java.util.Date; import java.util.HashMap; import java.util.Map; import com.liaozhenghan.util.dateutil.DateStyle; import com.liaozhenghan.util.dateutil.DateUtil; import ch.qos.logback.core.Context; import ch.qos.logback.core.CoreConstants; import ch.qos.logback.core.LayoutBase; import ch.qos.logback.core.html.CssBuilder; import ch.qos.logback.core.pattern.Converter; import ch.qos.logback.core.pattern.ConverterUtil; import ch.qos.logback.core.pattern.parser.Node; import ch.qos.logback.core.pattern.parser.Parser; import ch.qos.logback.core.spi.ScanException; public abstract class LzhHTMLLayoutBase<E> extends LayoutBase<E>{ protected String pattern; protected Converter<E> head; protected String title = "Logback Log Messages"; // It is the responsibility of derived classes to set // this variable in their constructor to a default value. protected CssBuilder cssBuilder; // counter keeping track of the rows output protected long counter = 0; /** * Set the <b>ConversionPattern </b> option. This is the string which controls * formatting and consists of a mix of literal content and conversion * specifiers. */ public void setPattern(String conversionPattern) { pattern = conversionPattern; } /** * Returns the value of the <b>ConversionPattern </b> option. */ public String getPattern() { return pattern; } public CssBuilder getCssBuilder() { return cssBuilder; } public void setCssBuilder(CssBuilder cssBuilder) { this.cssBuilder = cssBuilder; } /** * Parses the pattern and creates the Converter linked list. */ @Override public void start() { int errorCount = 0; try { Parser<E> p = new Parser<E>(pattern); p.setContext(getContext()); Node t = p.parse(); this.head = p.compile(t, getEffectiveConverterMap()); ConverterUtil.startConverters(this.head); } catch (ScanException ex) { addError("Incorrect pattern found", ex); errorCount++; } if (errorCount == 0) { super.started = true; } } protected abstract Map<String, String> getDefaultConverterMap(); /** * Returns a map where the default converter map is merged with the map * contained in the context. */ public Map<String, String> getEffectiveConverterMap() { Map<String, String> effectiveMap = new HashMap<String, String>(); // add the least specific map fist Map<String, String> defaultMap = getDefaultConverterMap(); if (defaultMap != null) { effectiveMap.putAll(defaultMap); } // contextMap is more specific than the default map Context context = getContext(); if (context != null) { @SuppressWarnings("unchecked") Map<String, String> contextMap = (Map<String, String>) context.getObject(CoreConstants.PATTERN_RULE_REGISTRY); if (contextMap != null) { effectiveMap.putAll(contextMap); } } return effectiveMap; } /** * The <b>Title </b> option takes a String value. This option sets the * document title of the generated HTML document. * * <p> * Defaults to 'Logback Log Messages'. */ public void setTitle(String title) { this.title = title; } /** * Returns the current value of the <b>Title </b> option. */ public String getTitle() { return title; } /** * Returns the content type output by this layout, i.e "text/html". */ @Override public String getContentType() { return "text/html"; } /** * Returns appropriate HTML headers. */ @Override public String getFileHeader() { StringBuilder sbuf = new StringBuilder(); sbuf.append("<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN""); sbuf.append(" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">"); sbuf.append(LINE_SEPARATOR); sbuf.append("<html>"); sbuf.append(LINE_SEPARATOR); sbuf.append(" <head>"); sbuf.append(LINE_SEPARATOR); sbuf.append(" <title>"); sbuf.append(title); sbuf.append("</title>"); sbuf.append(LINE_SEPARATOR); cssBuilder.addCss(sbuf); sbuf.append(LINE_SEPARATOR); sbuf.append(" </head>"); sbuf.append(LINE_SEPARATOR); sbuf.append("<body>"); sbuf.append(LINE_SEPARATOR); return sbuf.toString(); } public String getPresentationHeader() { StringBuilder sbuf = new StringBuilder(); sbuf.append("<hr/>"); sbuf.append(LINE_SEPARATOR); sbuf.append("<p><span style='color:red;'>日志启动时间:</span> "); sbuf.append(DateUtil.DateToString(new Date(), DateStyle.YYYY_MM_DD_HH_MM_SS_CN)); sbuf.append("</p><p></p>"); sbuf.append(LINE_SEPARATOR); sbuf.append(LINE_SEPARATOR); sbuf.append("<table cellspacing="0">"); sbuf.append(LINE_SEPARATOR); buildHeaderRowForTable(sbuf); return sbuf.toString(); } private void buildHeaderRowForTable(StringBuilder sbuf) { Converter c = head; String name; sbuf.append("<tr class="header">"); sbuf.append(LINE_SEPARATOR); while (c != null) { name = computeConverterName(c); if (name == null) { c = c.getNext(); continue; } sbuf.append("<td class=""); sbuf.append(name); sbuf.append("">"); sbuf.append(filterTableTitleName(name)); sbuf.append("</td>"); sbuf.append(LINE_SEPARATOR); c = c.getNext(); } sbuf.append("</tr>"); sbuf.append(LINE_SEPARATOR); } private String filterTableTitleName(String name) { String str = name.toLowerCase(); if ("level".equals(str)) { name = "级别"; } else if ("date".equals(str)) { name = "执行时间"; } else if ("callerdata".equals(str)) { name = "所在行"; } else if ("message".equals(str)) { name = "信息"; } return name; } public String getPresentationFooter() { StringBuilder sbuf = new StringBuilder(); sbuf.append("</table>"); return sbuf.toString(); } /** * Returns the appropriate HTML footers. */ @Override public String getFileFooter() { StringBuilder sbuf = new StringBuilder(); sbuf.append(LINE_SEPARATOR); sbuf.append("</body></html>"); return sbuf.toString(); } protected void startNewTableIfLimitReached(StringBuilder sbuf) { if (this.counter >= CoreConstants.TABLE_ROW_LIMIT) { counter = 0; sbuf.append("</table>"); sbuf.append(LINE_SEPARATOR); sbuf.append("<p></p>"); sbuf.append("<table cellspacing="0">"); sbuf.append(LINE_SEPARATOR); buildHeaderRowForTable(sbuf); } } protected String computeConverterName(Converter c) { String name = null; String className = c.getClass().getSimpleName(); int index = className.indexOf("Converter"); if (index == -1) { name = className; } else { name = className.substring(0, index); } return name; } }
LzhHTMLLayout代码如下:
package top.liaozhenghan.logback.study.layout; import static ch.qos.logback.core.CoreConstants.LINE_SEPARATOR; import java.util.Map; import ch.qos.logback.classic.PatternLayout; import ch.qos.logback.classic.html.DefaultCssBuilder; import ch.qos.logback.classic.html.DefaultThrowableRenderer; import ch.qos.logback.classic.pattern.MDCConverter; import ch.qos.logback.classic.spi.ILoggingEvent; import ch.qos.logback.core.helpers.Transform; import ch.qos.logback.core.html.IThrowableRenderer; import ch.qos.logback.core.pattern.Converter; public class LzhHTMLLayout extends LzhHTMLLayoutBase<ILoggingEvent>{ /** * Default pattern string for log output. */ static final String DEFAULT_CONVERSION_PATTERN = "%date{HH:mm:ss}%level%caller{1}%msg"; IThrowableRenderer<ILoggingEvent> throwableRenderer; /** * Constructs a PatternLayout using the DEFAULT_LAYOUT_PATTERN. * * The default pattern just produces the application supplied message. */ public LzhHTMLLayout() { pattern = DEFAULT_CONVERSION_PATTERN; throwableRenderer = new DefaultThrowableRenderer(); cssBuilder = new DefaultCssBuilder(); } @Override public void start() { int errorCount = 0; if (throwableRenderer == null) { addError("ThrowableRender cannot be null."); errorCount++; } if (errorCount == 0) { super.start(); } } protected Map<String, String> getDefaultConverterMap() { return PatternLayout.defaultConverterMap; } public String doLayout(ILoggingEvent event) { StringBuilder buf = new StringBuilder(); startNewTableIfLimitReached(buf); boolean odd = true; if (((counter++) & 1) == 0) { odd = false; } String level = event.getLevel().toString().toLowerCase(); buf.append(LINE_SEPARATOR); buf.append("<tr class=""); buf.append(level); if (odd) { buf.append(" odd">"); } else { buf.append(" even">"); } buf.append(LINE_SEPARATOR); Converter<ILoggingEvent> c = head; while (c != null) { appendEventToBuffer(buf, c, event); c = c.getNext(); } buf.append("</tr>"); buf.append(LINE_SEPARATOR); if (event.getThrowableProxy() != null) { throwableRenderer.render(buf, event); } return buf.toString(); } private void appendEventToBuffer(StringBuilder buf, Converter<ILoggingEvent> c, ILoggingEvent event) { buf.append("<td class=""); buf.append(computeConverterName(c)); buf.append("">"); buf.append(Transform.escapeTags(c.convert(event))); buf.append("</td>"); buf.append(LINE_SEPARATOR); } public IThrowableRenderer getThrowableRenderer() { return throwableRenderer; } public void setThrowableRenderer(IThrowableRenderer<ILoggingEvent> throwableRenderer) { this.throwableRenderer = throwableRenderer; } @Override protected String computeConverterName(Converter c) { if (c instanceof MDCConverter) { MDCConverter mc = (MDCConverter) c; String key = mc.getFirstOption(); if (key != null) { return key; } else { return "MDC"; } } else { return super.computeConverterName(c); } } }
二、logback.xml配置
<?xml version="1.0" encoding="UTF-8"?> <configuration scan="true" scanPeriod="3 seconds"> <!-- 定义变量 --> <if condition='p("os.name").contains("Windows")'> <then> <property name="LOG_PATH" value="c:/logs" /> </then> </if> <if condition='p("os.name").contains("Linux")'> <then> <property name="LOG_PATH" value="logs" /> </then> </if> <property name="APP_NAME" value="study" /> <property name="LOG_ROOT_LEVEL" value="debug" /> <property name="maxHistory" value="30" /> <contextName>${APP_NAME}</contextName> <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender"> <encoder> <pattern> <!-- 设置日志输出格式 --> [%-5level] %d{yyyy-MM-dd HH:mm:ss} [%thread] %logger{36} - %m%n </pattern> </encoder> </appender> <!-- 按照每天生成日志文件 --> <appender name="FILE_ERROR" class="ch.qos.logback.core.rolling.RollingFileAppender"> <file>${LOG_PATH}/${APP_NAME}/ERROR.log</file> <encoder class="ch.qos.logback.core.encoder.LayoutWrappingEncoder"> <layout class="top.liaozhenghan.logback.study.layout.LzhHTMLLayout"/> </encoder> <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy"> <FileNamePattern>${LOG_PATH}/${APP_NAME}/ERROR.%d.log </FileNamePattern> <MaxHistory>${maxHistory}</MaxHistory> </rollingPolicy> <filter class="ch.qos.logback.classic.filter.LevelFilter"> <level>ERROR</level> <onMatch>ACCEPT</onMatch> <onMismatch>DENY</onMismatch> </filter> </appender> <appender name="FILE_WARN" class="ch.qos.logback.core.rolling.RollingFileAppender"> <file>${LOG_PATH}/${APP_NAME}/WARN.log</file> <encoder class="ch.qos.logback.core.encoder.LayoutWrappingEncoder"> <layout class="top.liaozhenghan.logback.study.layout.LzhHTMLLayout"/> </encoder> <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy"> <FileNamePattern>${LOG_PATH}/${APP_NAME}/WARN.%d.log </FileNamePattern> <MaxHistory>${maxHistory}</MaxHistory> </rollingPolicy> <filter class="ch.qos.logback.classic.filter.LevelFilter"> <level>WARN</level> <onMatch>ACCEPT</onMatch> <onMismatch>DENY</onMismatch> </filter> </appender> <appender name="FILE_INFO" class="ch.qos.logback.core.rolling.RollingFileAppender"> <file>${LOG_PATH}/${APP_NAME}/INFO.log</file> <encoder class="ch.qos.logback.core.encoder.LayoutWrappingEncoder"> <layout class="top.liaozhenghan.logback.study.layout.LzhHTMLLayout"/> </encoder> <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy"> <FileNamePattern>${LOG_PATH}/${APP_NAME}/INFO.%d.log </FileNamePattern> <MaxHistory>${maxHistory}</MaxHistory> </rollingPolicy> <filter class="ch.qos.logback.classic.filter.LevelFilter"> <level>INFO</level> <onMatch>ACCEPT</onMatch> <onMismatch>DENY</onMismatch> </filter> </appender> <appender name="FILE_DEBUG" class="ch.qos.logback.core.rolling.RollingFileAppender"> <file>${LOG_PATH}/${APP_NAME}/DEBUG.log</file> <encoder class="ch.qos.logback.core.encoder.LayoutWrappingEncoder"> <layout class="top.liaozhenghan.logback.study.layout.LzhHTMLLayout"/> </encoder> <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy"> <FileNamePattern>${LOG_PATH}/${APP_NAME}/DEBUG.%d.log </FileNamePattern> <MaxHistory>${maxHistory}</MaxHistory> </rollingPolicy> <filter class="ch.qos.logback.classic.filter.LevelFilter"> <level>DEBUG</level> <onMatch>ACCEPT</onMatch> <onMismatch>DENY</onMismatch> </filter> </appender> <root level="${LOG_ROOT_LEVEL}"> <!-- 控制台输出 --> <appender-ref ref="STDOUT" /> <!-- 文件输出 --> <appender-ref ref="FILE_ERROR" /> <appender-ref ref="FILE_WARN" /> <appender-ref ref="FILE_INFO" /> <appender-ref ref="FILE_DEBUG" /> </root> </configuration>
三、查看日志的jsp页面
用了三个页面:登录页面loglogin.jsp,日志级别选择页面logselect.jsp,日志显示页面log.jsp
<%@ page language="java" import="java.util.*" pageEncoding="UTF-8"%> <%@ page isELIgnored="false"%> <% String path = request.getContextPath(); String basePath = request.getScheme()+"://"+request.getServerName()+":"+request.getServerPort()+path+"/"; %> <!DOCTYPE html> <html lang="en"> <head> <title>日志系统登录</title> <meta charset="UTF-8"> <!-- meta信息,可维护 --> <meta name="format-detection" content="telephone=no"> <meta name="viewport" content="width=device-width, initial-scale=1.0,user-scalable=no"> <meta name="apple-mobile-web-app-status-bar-style" content="black-translucent"> <meta name="apple-touch-fullscreen" content="yes"> </head> <body style=" 100%;"> <form action="" method="post"> <p>密码:</p><input name="password"><br> <input type="submit" value="登录日志系统"> </form> </body> </html>
<%@ page language="java" import="java.util.*" pageEncoding="UTF-8"%> <%@ page isELIgnored="false"%> <% String path = request.getContextPath(); String basePath = request.getScheme()+"://"+request.getServerName()+":"+request.getServerPort()+path+"/"; %> <!DOCTYPE html> <html lang="en"> <head> <title>日志记录</title> <meta charset="UTF-8"> <!-- meta信息,可维护 --> <meta name="format-detection" content="telephone=no"> <meta name="viewport" content="width=device-width, initial-scale=1.0,user-scalable=no"> <meta name="apple-mobile-web-app-status-bar-style" content="black-translucent"> <meta name="apple-touch-fullscreen" content="yes"> <script src="js/jquery.min.js" type="text/javascript"></script> <style type="text/css"> .lzh_div td{ } </style> </head> <body style=" 100%;"> <form action="" method="post"> <input id="date" type="date" name="date"> <select id="levels" name="level"> <!-- <option value="TRACE">TRACE</option> --> <option value="DEBUG">DEBUG</option> <option value="INFO">INFO</option> <option value="WARN">WARN</option> <option value="ERROR">ERROR</option> </select> <input type="submit" value="查看日志"> </form> <script type="text/javascript"> var level = "${level}"; var date = "${date}"; $("#levels").val(level); $("#date").val(date); </script> <div class="lzh_div" style=" 100%;">${html}</div> </body> </html>
<%@ page language="java" import="java.util.*" pageEncoding="UTF-8"%> <%@ page isELIgnored="false"%> <% String path = request.getContextPath(); String basePath = request.getScheme()+"://"+request.getServerName()+":"+request.getServerPort()+path+"/"; %> ${html}
四、LogController.java
package top.liaozhenghan.logback.study.controller; import java.io.File; import java.io.IOException; import java.util.Date; import javax.servlet.http.HttpServletRequest; import org.apache.commons.io.FileUtils; import org.apache.commons.lang.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.RequestMapping; import com.liaozhenghan.util.dateutil.DateStyle; import com.liaozhenghan.util.dateutil.DateUtil; import com.liaozhenghan.util.httputil.SrcUtil; @Controller public class LogController { final static Logger LOG = LoggerFactory.getLogger(LogController.class); final static String LOG_LOGIN = "LOG_LOGIN"; @RequestMapping(value="/log") public String log(HttpServletRequest request, Model model) { String date = request.getParameter("date"); String level = request.getParameter("level"); String password = request.getParameter("password"); String html = null; // 判断是否登录 Object login = request.getSession().getAttribute(LOG_LOGIN); if (login == null) { String PASSWORD = "baofa2018"; if (PASSWORD.equals(password)) { request.getSession().setAttribute(LOG_LOGIN, LOG_LOGIN); } else { return "logs/loglogin"; } } // 1.判断参数是否为空 if (StringUtils.isEmpty(level) && StringUtils.isEmpty(date)) { date = DateUtil.DateToString(new Date(), DateStyle.YYYY_MM_DD); level = "ERROR"; model.addAttribute("date", date); model.addAttribute("level", level); return "logs/logselect"; } // 2.判断路径 String filepath = "c:/logs/study/"; String osName = System.getProperty("os.name"); if (StringUtils.isEmpty(osName)) { html = "系统错误"; model.addAttribute("date", date); model.addAttribute("level", level); model.addAttribute("html", html); return "logs/logselect"; } else if (osName.contains("Window")) { filepath = "c:/logs/study/"; } else if (osName.contains("Linux")) { filepath = "logs/study/"; } else { html = "未识别的操作系统:" + osName; model.addAttribute("date", date); model.addAttribute("level", level); model.addAttribute("html", html); return "logs/logselect"; } if (StringUtils.isNotEmpty(date) && !DateUtil.DateToString(new Date(), DateStyle.YYYY_MM_DD).equals(date)) { filepath += level + "." + date + ".log"; } else { filepath += level + ".log"; date = DateUtil.DateToString(new Date(), DateStyle.YYYY_MM_DD); } File file = new File(filepath); html = "无日志记录"; if (file.exists()) { try { html = FileUtils.readFileToString(file, "UTF-8"); } catch (IOException e) { LOG.error(DateUtil.DateToString(new Date(), DateStyle.YYYY_MM_DD_HH_MM_SS) + "【文件名】" + filepath, e); } } LOG.debug("测试"); LOG.info("测试"); LOG.warn("测试"); LOG.error("测试"); model.addAttribute("html", html); model.addAttribute("date", date); model.addAttribute("level", level); return "logs/log"; } }
注:logback.xml在tomcat环境下的日志目录可改为:
<property name="LOG_PATH" value="${catalina.base}/logs" />
在java代码中获取文件目录改为:
String catalina_base = System.getProperty("catalina.base");
String filepath = catalina_base + "/logs/study/";