log4j email EmailDailyRollingFileAppender
log4j发送日志邮件, 纠正非网上流传的"达到 BufferSize KB就会发送邮件", 另外重写了一个发送邮件的类DailyRollingFileAppender。 用于定期发送日志邮件。而并不是原始的遇到错误日志就发送邮件,错误率多的情况下。你有的邮箱会爆炸的。
看了网上的几个配置,当中有个 BufferSize , 大家写的都是”缓存文件大小,日志达到512K时发送Email“, 幸亏看了看源代码,先看一下 SMTPAppender 开头凝视:
<p>The number of logging events delivered in this e-mail depend on the value of <b>BufferSize</b> option. The <code>SMTPAppender</code> keeps only the last <code>BufferSize</code> logging events in its cyclic buffer. This keeps memory requirements at a reasonable level while still delivering useful application context.
SMTPAppender仅仅会保留BufferSize个event在内存中。即BufferSize申请的是log事件数量。并不是日志内容大小
在看一下 sendBuffer 是将缓存的log发送邮件的地方 在1.2.17 版本号中仅仅有2个地方用到,一个是 SMTPAppender.close(), 还有一个是 append:
public void append(LoggingEvent event) { if(!checkEntryConditions()) { return; } event.getThreadName(); event.getNDC(); event.getMDCCopy(); if(locationInfo) { event.getLocationInformation(); } event.getRenderedMessage(); event.getThrowableStrRep(); cb.add(event); if(evaluator.isTriggeringEvent(event)) { sendBuffer(); } }
在 append 最后代码中调用了 evaluator.isTriggeringEvent(event) 来推断是否发送邮件
SMTPAppender.evaluator 默认是 DefaultEvaluator
class DefaultEvaluator implements TriggeringEventEvaluator { public boolean isTriggeringEvent(LoggingEvent event) { return event.getLevel().isGreaterOrEqual(Level.ERROR); } }
从代码能够看出仅仅要 event.level >= ERROR 就会返回true,即就会触发邮件发送
再看一下 cb.add(event)
public void add(LoggingEvent event) { ea[last] = event; if(++last == maxSize) last = 0; if(numElems < maxSize) numElems++; else if(++first == maxSize) first = 0; }
这里应和了本文开头提到的,仅仅会在内存中保留 BufferSize 个 LoggingEvent , CyclicBuffer 能够理解为一个循环存储LoggingEvent的内存池
经过分析得知 每次错误日志就会触发发送邮件,而并不是网上流传的达到 BufferSize KB就会发送邮件。将日志发送邮件实际情况是这种:
1. 在内存中仅仅会保留最新的BufferSize个event( 如log.info('test') 就是1个event), 即数组满了后就会循环覆盖旧数据
2. 有错误日志才会触发
3. 每次有错误日志就会触发发送邮件,也就是假设有连续的大量错误日志就会1个event发送一封邮件 ~>_<~
4. 仅仅会将数据保存到内存冲。易丢失
基于以上的特点,假设仅仅是 在非常重要的业务处理中,且错误率非常低的情况写能够考虑使用
另: 因为原始的发送邮件在错误率多的情况下,你有的邮箱会爆炸的。我有又一次写了一个自己定义发送邮件的Appender, 基于 DailyRollingFileAppender 。能够自己定义按分、小时、天、12小时、周、月、年的频率发送邮件,且日志是存储到磁盘的。不易丢失
如配置HTML格式,每天切割日志时将当天的错误日志发送
<appender name="EMAIL_ROLLING_FILE_ERROR" class="com.xxx.logging.log4j.EmailDailyRollingFileAppender"> <param name="Threshold" value="ERROR" /> <param name="Append" value="true" /> <param name="encoding" value="utf-8" /> <param name="File" value="${catalina.base}/logs/ezhe-task-error.html" /> <param name="DatePattern" value="'.'yyyy-MM-dd'.html'" /> <param name="From" value="日志清查<crash_sender@126.com>" /> <param name="nickName" value="日志清查" /> <param name="SmtpHost" value="smtp.126.com" /> <param name="Subject" value="api-错误日志" /> <param name="To" value="myname<crash_xxxx@126.com>,xxx<li_xx@163.com>" /> <param name="SmtpUsername" value="crash_sender" /> <param name="SmtpPassword" value="vqldobvhsssobpfb" /> <layout class="com.xxx.logging.log4j.FormatHTMLLayout"> <param name="title" value="api-错误日志" /> <param name="encoding" value="utf-8"/> </layout> <filter class="org.apache.log4j.varia.LevelRangeFilter"> <param name="LevelMax" value="ERROR" /> <param name="LevelMin" value="DEBUG" /> </filter> </appender>
EmailDailyRollingFileAppender.java
package com.xxx.logging.log4j; import org.apache.log4j.DailyRollingFileAppender; import org.apache.log4j.helpers.CyclicBuffer; import org.apache.log4j.helpers.LogLog; import org.apache.log4j.spi.ErrorCode; import org.apache.log4j.spi.LoggingEvent; import org.apache.log4j.spi.OptionHandler; import org.apache.log4j.spi.TriggeringEventEvaluator; import javax.mail.*; import javax.mail.internet.*; import java.io.*; import java.net.InetAddress; import java.text.SimpleDateFormat; import java.util.*; /** * Created by karl on 2015/7/23. */ public class EmailDailyRollingFileAppender extends DailyRollingFileAppender { private String to; /** * Comma separated list of cc recipients. */ private String cc; /** * Comma separated list of bcc recipients. */ private String bcc; private String from; private String nickName; /** * Comma separated list of replyTo addresses. */ private String replyTo; private String subject; private String smtpHost; private String smtpUsername; private String smtpPassword; private String smtpProtocol; private int smtpPort = -1; private boolean smtpDebug = false; private int bufferSize = 512; private boolean locationInfo = false; private boolean sendOnClose = false; protected CyclicBuffer cb = new CyclicBuffer(bufferSize); protected Message msg; protected TriggeringEventEvaluator evaluator; InternetAddress getAddress(String addressStr) { InternetAddress internetAddress = null; try { internetAddress = new InternetAddress(addressStr); internetAddress.setPersonal(internetAddress.getPersonal()); return internetAddress; }catch (UnsupportedEncodingException e1){ errorHandler.error("Could not parse address ["+addressStr+"].", e1, ErrorCode.ADDRESS_PARSE_FAILURE); } catch(AddressException e) { errorHandler.error("Could not parse address ["+addressStr+"].", e, ErrorCode.ADDRESS_PARSE_FAILURE); } return null; } InternetAddress[] parseAddress(String addressStr) { try { InternetAddress[] as = InternetAddress.parse(addressStr, true); if (as != null && as.length > 0) { for (InternetAddress a : as) { a.setPersonal(a.getPersonal()); } } return as; }catch (UnsupportedEncodingException e1){ errorHandler.error("Could not parse address ["+addressStr+"].", e1, ErrorCode.ADDRESS_PARSE_FAILURE); } catch(AddressException e) { errorHandler.error("Could not parse address ["+addressStr+"].", e, ErrorCode.ADDRESS_PARSE_FAILURE); } return null; } protected void addressMessage(final Message msg) throws MessagingException { if (from != null) { InternetAddress internetAddress = getAddress(from); if(this.nickName!=null) { try{ internetAddress.setPersonal(this.nickName, this.getEncoding()); }catch (UnsupportedEncodingException e1){ errorHandler.error("Could not parse address ["+internetAddress+"].", e1, ErrorCode.ADDRESS_PARSE_FAILURE); } } msg.setFrom(internetAddress); } else { msg.setFrom(); } //Add ReplyTo addresses if defined. if (replyTo != null && replyTo.length() > 0) { msg.setReplyTo(parseAddress(replyTo)); } if (to != null && to.length() > 0) { msg.setRecipients(Message.RecipientType.TO, parseAddress(to)); } //Add CC receipients if defined. if (cc != null && cc.length() > 0) { msg.setRecipients(Message.RecipientType.CC, parseAddress(cc)); } //Add BCC receipients if defined. if (bcc != null && bcc.length() > 0) { msg.setRecipients(Message.RecipientType.BCC, parseAddress(bcc)); } } public void activateOptions() { super.activateOptions(); if(datePattern != null && fileName != null) { now.setTime(System.currentTimeMillis()); sdf = new SimpleDateFormat(datePattern); int type = computeCheckPeriod(); printPeriodicity(type); rc.setType(type); File file = new File(fileName); scheduledFilename = fileName+sdf.format(new Date(file.lastModified())); } else { LogLog.error("Either File or DatePattern options are not set for appender [" +name+"]."); } Session session = createSession(); msg = new MimeMessage(session); try { addressMessage(msg); if(subject != null) { try { msg.setSubject(MimeUtility.encodeText(subject, "UTF-8", null)); } catch(UnsupportedEncodingException ex) { LogLog.error("Unable to encode SMTP subject", ex); } } } catch(MessagingException e) { LogLog.error("Could not activate SMTPAppender options.", e ); } if (evaluator instanceof OptionHandler) { ((OptionHandler) evaluator).activateOptions(); } } protected Session createSession() { Properties props = null; try { props = new Properties (System.getProperties()); } catch(SecurityException ex) { props = new Properties(); } String prefix = "mail.smtp"; if (smtpProtocol != null) { props.put("mail.transport.protocol", smtpProtocol); prefix = "mail." + smtpProtocol; } if (smtpHost != null) { props.put(prefix + ".host", smtpHost); } if (smtpPort > 0) { props.put(prefix + ".port", String.valueOf(smtpPort)); } Authenticator auth = null; if(smtpPassword != null && smtpUsername != null) { props.put(prefix + ".auth", "true"); auth = new Authenticator() { protected PasswordAuthentication getPasswordAuthentication() { return new PasswordAuthentication(smtpUsername, smtpPassword); } }; } Session session = Session.getInstance(props, auth); if (smtpProtocol != null) { session.setProtocolForAddress("rfc822", smtpProtocol); } if (smtpDebug) { session.setDebug(smtpDebug); } return session; } private String getLocalHostInfo(){ try{ String info = InetAddress.getLocalHost().getHostName() + "/" + InetAddress.getLocalHost().getHostAddress(); return info ; }catch (Exception e){ e.printStackTrace(); } return ""; } protected void sendEmail(File file){ try { if(!file.exists()) { LogLog.error( "sendEmail File Not Exist[" + file.getAbsolutePath()+"]"); return; } FileReader fileReader = new FileReader(file); BufferedReader reader = new BufferedReader(fileReader); StringBuilder sb = new StringBuilder(); String host = getLocalHostInfo(); if(host!=null) sb.append(host + "( " + new Date() +" ) " + file.getAbsolutePath() + "\n\n"); msg.setSubject(MimeUtility.encodeText(host + "-" + subject, "UTF-8", null)); do{ String temp = reader.readLine(); if(temp!=null) { sb.append(temp); }else{ break; } }while (true); reader.close(); fileReader.close(); boolean allAscii = false; MimeBodyPart part; if (allAscii) { part = new MimeBodyPart(); part.setContent(sb.toString(), layout.getContentType()); } else { try { ByteArrayOutputStream os = new ByteArrayOutputStream(); Writer writer = new OutputStreamWriter( MimeUtility.encode(os, "quoted-printable"), "UTF-8"); writer.write(sb.toString()); writer.close(); InternetHeaders headers = new InternetHeaders(); headers.setHeader("Content-Type", layout.getContentType() + "; charset=UTF-8"); headers.setHeader("Content-Transfer-Encoding", "quoted-printable"); part = new MimeBodyPart(headers, os.toByteArray()); } catch(Exception ex) { StringBuffer sbuf = new StringBuffer(sb.toString()); for (int i = 0; i < sbuf.length(); i++) { if (sbuf.charAt(i) >= 0x80) { sbuf.setCharAt(i, '?'); } } part = new MimeBodyPart(); part.setContent(sbuf.toString(), layout.getContentType()); } } Multipart mp = new MimeMultipart(); mp.addBodyPart(part); msg.setContent(mp); msg.setSentDate(new Date()); Transport.send(msg); }catch (Exception e){ LogLog.error( "send email error." , e); } } @Override protected void closeFile() { super.closeFile(); } // The code assumes that the following constants are in a increasing // sequence. public static final int TOP_OF_TROUBLE=-1; public static final int TOP_OF_MINUTE = 0; public static final int TOP_OF_HOUR = 1; public static final int HALF_DAY = 2; public static final int TOP_OF_DAY = 3; public static final int TOP_OF_WEEK = 4; public static final int TOP_OF_MONTH = 5; /** The date pattern. By default, the pattern is set to "'.'yyyy-MM-dd" meaning daily rollover. */ protected String datePattern = "'.'yyyy-MM-dd"; /** The log file will be renamed to the value of the scheduledFilename variable when the next interval is entered. For example, if the rollover period is one hour, the log file will be renamed to the value of "scheduledFilename" at the beginning of the next hour. The precise time when a rollover occurs depends on logging activity. */ protected String scheduledFilename; /** The next time we estimate a rollover should occur. */ protected long nextCheck = System.currentTimeMillis () - 1; protected Date now = new Date(); protected SimpleDateFormat sdf; protected RollingCalendar rc = new RollingCalendar(); protected int checkPeriod = TOP_OF_TROUBLE; // The gmtTimeZone is used only in computeCheckPeriod() method. static final TimeZone gmtTimeZone = TimeZone.getTimeZone("GMT"); /** The <b>DatePattern</b> takes a string in the same format as expected by {@link SimpleDateFormat}. This options determines the rollover schedule. */ public void setDatePattern(String pattern) { datePattern = pattern; } /** Returns the value of the <b>DatePattern</b> option. */ public String getDatePattern() { return datePattern; } void printPeriodicity(int type) { switch(type) { case TOP_OF_MINUTE: LogLog.debug("Appender ["+name+"] to be rolled every minute."); break; case TOP_OF_HOUR: LogLog.debug("Appender ["+name +"] to be rolled on top of every hour."); break; case HALF_DAY: LogLog.debug("Appender ["+name +"] to be rolled at midday and midnight."); break; case TOP_OF_DAY: LogLog.debug("Appender ["+name +"] to be rolled at midnight."); break; case TOP_OF_WEEK: LogLog.debug("Appender ["+name +"] to be rolled at start of week."); break; case TOP_OF_MONTH: LogLog.debug("Appender ["+name +"] to be rolled at start of every month."); break; default: LogLog.warn("Unknown periodicity for appender ["+name+"]."); } } // This method computes the roll over period by looping over the // periods, starting with the shortest, and stopping when the r0 is // different from from r1, where r0 is the epoch formatted according // the datePattern (supplied by the user) and r1 is the // epoch+nextMillis(i) formatted according to datePattern. All date // formatting is done in GMT and not local format because the test // logic is based on comparisons relative to 1970-01-01 00:00:00 // GMT (the epoch). protected int computeCheckPeriod() { RollingCalendar rollingCalendar = new RollingCalendar(gmtTimeZone, Locale.getDefault()); // set sate to 1970-01-01 00:00:00 GMT Date epoch = new Date(0); if(datePattern != null) { for(int i = TOP_OF_MINUTE; i <= TOP_OF_MONTH; i++) { SimpleDateFormat simpleDateFormat = new SimpleDateFormat(datePattern); simpleDateFormat.setTimeZone(gmtTimeZone); // do all date formatting in GMT String r0 = simpleDateFormat.format(epoch); rollingCalendar.setType(i); Date next = new Date(rollingCalendar.getNextCheckMillis(epoch)); String r1 = simpleDateFormat.format(next); //System.out.println("Type = "+i+", r0 = "+r0+", r1 = "+r1); if(r0 != null && r1 != null && !r0.equals(r1)) { return i; } } } return TOP_OF_TROUBLE; // Deliberately head for trouble... } /** Rollover the current file to a new file. */ protected void rollOver() throws IOException { /* Compute filename, but only if datePattern is specified */ if (datePattern == null) { errorHandler.error("Missing DatePattern option in rollOver()."); return; } String datedFilename = fileName+sdf.format(now); // It is too early to roll over because we are still within the // bounds of the current interval. Rollover will occur once the // next interval is reached. if (scheduledFilename.equals(datedFilename)) { return; } // close current file, and rename it to datedFilename this.closeFile(); File target = new File(scheduledFilename); if (target.exists()) { target.delete(); } File file = new File(fileName); boolean result = file.renameTo(target); if(result) { LogLog.debug(fileName +" -> "+ scheduledFilename); sendEmail(target); } else { LogLog.error("Failed to rename ["+fileName+"] to ["+scheduledFilename+"]."); } try { // This will also close the file. This is OK since multiple // close operations are safe. this.setFile(fileName, true, this.bufferedIO, this.bufferSize); } catch(IOException e) { errorHandler.error("setFile("+fileName+", true) call failed."); } scheduledFilename = datedFilename; } /** * This method differentiates DailyRollingFileAppender from its * super class. * * <p>Before actually logging, this method will check whether it is * time to do a rollover. If it is, it will schedule the next * rollover time and then rollover. * */ protected void subAppend(LoggingEvent event) { long n = System.currentTimeMillis(); if (n >= nextCheck) { now.setTime(n); nextCheck = rc.getNextCheckMillis(now); try { rollOver(); } catch(IOException ioe) { if (ioe instanceof InterruptedIOException) { Thread.currentThread().interrupt(); } LogLog.error("rollOver() failed.", ioe); } } super.subAppend(event); } public void setTo(String to) { this.to = to; } public void setCc(String cc) { this.cc = cc; } public void setBcc(String bcc) { this.bcc = bcc; } public void setFrom(String from) { this.from = from; } public void setNickName(String nickName){ this.nickName = nickName; } public void setReplyTo(String replyTo) { this.replyTo = replyTo; } public void setSubject(String subject) { this.subject = subject; } public void setSmtpHost(String smtpHost) { this.smtpHost = smtpHost; } public void setSmtpUsername(String smtpUsername) { this.smtpUsername = smtpUsername; } public void setSmtpPassword(String smtpPassword) { this.smtpPassword = smtpPassword; } public void setSmtpProtocol(String smtpProtocol) { this.smtpProtocol = smtpProtocol; } public void setSmtpPort(int smtpPort) { this.smtpPort = smtpPort; } public void setSmtpDebug(boolean smtpDebug) { this.smtpDebug = smtpDebug; } public void setBufferSize(int bufferSize) { this.bufferSize = bufferSize; } public void setLocationInfo(boolean locationInfo) { this.locationInfo = locationInfo; } public void setSendOnClose(boolean sendOnClose) { this.sendOnClose = sendOnClose; } public void setCb(CyclicBuffer cb) { this.cb = cb; } public void setMsg(Message msg) { this.msg = msg; } public void setEvaluator(TriggeringEventEvaluator evaluator) { this.evaluator = evaluator; } } /** * RollingCalendar is a helper class to DailyRollingFileAppender. * Given a periodicity type and the current time, it computes the * start of the next interval. * */ class RollingCalendar extends GregorianCalendar { private static final long serialVersionUID = -3560331770601814177L; int type = EmailDailyRollingFileAppender.TOP_OF_TROUBLE; RollingCalendar() { super(); } RollingCalendar(TimeZone tz, Locale locale) { super(tz, locale); } void setType(int type) { this.type = type; } public long getNextCheckMillis(Date now) { return getNextCheckDate(now).getTime(); } public Date getNextCheckDate(Date now) { this.setTime(now); switch (type) { case EmailDailyRollingFileAppender.TOP_OF_MINUTE: this.set(Calendar.SECOND, 0); this.set(Calendar.MILLISECOND, 0); this.add(Calendar.MINUTE, 1); break; case EmailDailyRollingFileAppender.TOP_OF_HOUR: this.set(Calendar.MINUTE, 0); this.set(Calendar.SECOND, 0); this.set(Calendar.MILLISECOND, 0); this.add(Calendar.HOUR_OF_DAY, 1); break; case EmailDailyRollingFileAppender.HALF_DAY: this.set(Calendar.MINUTE, 0); this.set(Calendar.SECOND, 0); this.set(Calendar.MILLISECOND, 0); int hour = get(Calendar.HOUR_OF_DAY); if (hour < 12) { this.set(Calendar.HOUR_OF_DAY, 12); } else { this.set(Calendar.HOUR_OF_DAY, 0); this.add(Calendar.DAY_OF_MONTH, 1); } break; case EmailDailyRollingFileAppender.TOP_OF_DAY: this.set(Calendar.HOUR_OF_DAY, 0); this.set(Calendar.MINUTE, 0); this.set(Calendar.SECOND, 0); this.set(Calendar.MILLISECOND, 0); this.add(Calendar.DATE, 1); break; case EmailDailyRollingFileAppender.TOP_OF_WEEK: this.set(Calendar.DAY_OF_WEEK, getFirstDayOfWeek()); this.set(Calendar.HOUR_OF_DAY, 0); this.set(Calendar.MINUTE, 0); this.set(Calendar.SECOND, 0); this.set(Calendar.MILLISECOND, 0); this.add(Calendar.WEEK_OF_YEAR, 1); break; case EmailDailyRollingFileAppender.TOP_OF_MONTH: this.set(Calendar.DATE, 1); this.set(Calendar.HOUR_OF_DAY, 0); this.set(Calendar.MINUTE, 0); this.set(Calendar.SECOND, 0); this.set(Calendar.MILLISECOND, 0); this.add(Calendar.MONTH, 1); break; default: throw new IllegalStateException("Unknown periodicity type."); } return getTime(); } }
另外提供一个重写的HTMLLayout, 用于支持中文titile和改动给了时间戳显示:
package com.xxx.logging.log4j; import org.apache.log4j.Layout; import org.apache.log4j.Level; import org.apache.log4j.helpers.Transform; import org.apache.log4j.spi.LocationInfo; import org.apache.log4j.spi.LoggingEvent; import java.text.SimpleDateFormat; /** * Created by karl on 2015/7/24. */ public class FormatHTMLLayout extends Layout { protected final int BUF_SIZE = 256; protected final int MAX_CAPACITY = 1024; static String TRACE_PREFIX = "<br> "; // output buffer appended to when format() is invoked protected StringBuffer sbuf = new StringBuffer(BUF_SIZE); protected String encoding = "utf-8"; /** * A string constant used in naming the option for setting the the * location information flag. Current value of this string * constant is <b>LocationInfo</b>. * <p/> * <p>Note that all option keys are case sensitive. * * @deprecated Options are now handled using the JavaBeans paradigm. * This constant is not longer needed and will be removed in the * <em>near</em> term. */ public static final String LOCATION_INFO_OPTION = "LocationInfo"; /** * A string constant used in naming the option for setting the the * HTML document title. Current value of this string * constant is <b>Title</b>. */ public static final String TITLE_OPTION = "Title"; // Print no location info by default boolean locationInfo = false; String title = "Log4J Log Messages"; /** * The <b>LocationInfo</b> option takes a boolean value. By * default, it is set to false which means there will be no location * information output by this layout. If the the option is set to * true, then the file name and line number of the statement * at the origin of the log statement will be output. * <p/> * <p>If you are embedding this layout within an {@link * org.apache.log4j.net.SMTPAppender} then make sure to set the * <b>LocationInfo</b> option of that appender as well. */ public void setLocationInfo(boolean flag) { locationInfo = flag; } /** * Returns the current value of the <b>LocationInfo</b> option. */ public boolean getLocationInfo() { return locationInfo; } /** * The <b>Title</b> option takes a String value. This option sets the * document title of the generated HTML document. * <p/> * <p>Defaults to 'Log4J 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". */ public String getContentType() { return "text/html"; } public void setEncoding(String encoding) { this.encoding = encoding; } /** * No options to activate. */ public void activateOptions() { } public String format(LoggingEvent event) { if (sbuf.capacity() > MAX_CAPACITY) { sbuf = new StringBuffer(BUF_SIZE); } else { sbuf.setLength(0); } sbuf.append(Layout.LINE_SEP + "<tr>" + Layout.LINE_SEP); sbuf.append("<td >"); // sbuf.append(event.timeStamp - LoggingEvent.getStartTime()); sbuf.append(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new java.util.Date())); sbuf.append("</td>" + Layout.LINE_SEP); String escapedThread = Transform.escapeTags(event.getThreadName()); sbuf.append("<td title=\"" + escapedThread + " thread\">"); sbuf.append(escapedThread); sbuf.append("</td>" + Layout.LINE_SEP); sbuf.append("<td title=\"Level\">"); if (event.getLevel().equals(Level.DEBUG)) { sbuf.append("<font color=\"#339933\">"); sbuf.append(Transform.escapeTags(String.valueOf(event.getLevel()))); sbuf.append("</font>"); } else if (event.getLevel().isGreaterOrEqual(Level.WARN)) { sbuf.append("<font color=\"#993300\"><strong>"); sbuf.append(Transform.escapeTags(String.valueOf(event.getLevel()))); sbuf.append("</strong></font>"); } else { sbuf.append(Transform.escapeTags(String.valueOf(event.getLevel()))); } sbuf.append("</td>" + Layout.LINE_SEP); String escapedLogger = Transform.escapeTags(event.getLoggerName()); sbuf.append("<td title=\"" + escapedLogger + " category\">"); sbuf.append(escapedLogger); sbuf.append("</td>" + Layout.LINE_SEP); if (locationInfo) { LocationInfo locInfo = event.getLocationInformation(); sbuf.append("<td>"); sbuf.append(Transform.escapeTags(locInfo.getFileName())); sbuf.append(':'); sbuf.append(locInfo.getLineNumber()); sbuf.append("</td>" + Layout.LINE_SEP); } sbuf.append("<td title=\"Message\">"); sbuf.append(Transform.escapeTags(event.getRenderedMessage())); sbuf.append("</td>" + Layout.LINE_SEP); sbuf.append("</tr>" + Layout.LINE_SEP); if (event.getNDC() != null) { sbuf.append("<tr><td bgcolor=\"#EEEEEE\" style=\"font-size : xx-small;\" colspan=\"6\" title=\"Nested Diagnostic Context\">"); sbuf.append("NDC: " + Transform.escapeTags(event.getNDC())); sbuf.append("</td></tr>" + Layout.LINE_SEP); } String[] s = event.getThrowableStrRep(); if (s != null) { sbuf.append("<tr><td bgcolor=\"#993300\" style=\"color:White; font-size : xx-small;\" colspan=\"6\">"); appendThrowableAsHTML(s, sbuf); sbuf.append("</td></tr>" + Layout.LINE_SEP); } return sbuf.toString(); } void appendThrowableAsHTML(String[] s, StringBuffer sbuf) { if (s != null) { int len = s.length; if (len == 0) return; sbuf.append(Transform.escapeTags(s[0])); sbuf.append(Layout.LINE_SEP); for (int i = 1; i < len; i++) { sbuf.append(TRACE_PREFIX); sbuf.append(Transform.escapeTags(s[i])); sbuf.append(Layout.LINE_SEP); } } } /** * Returns appropriate HTML headers. */ public String getHeader() { StringBuffer sbuf = new StringBuffer(); sbuf.append("<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.01 Transitional//EN\" \"http://www.w3.org/TR/html4/loose.dtd\">" + Layout.LINE_SEP); sbuf.append("<html>" + Layout.LINE_SEP); sbuf.append("<head>" + Layout.LINE_SEP); sbuf.append("<title>" + title + "</title>" + Layout.LINE_SEP); sbuf.append("<meta http-equiv=\"content-type\" content=\"text/html; charset=" + this.encoding + "\">"); sbuf.append("<style type=\"text/css\">" + Layout.LINE_SEP); sbuf.append("<!--" + Layout.LINE_SEP); sbuf.append("body, table {font-family: arial,sans-serif; font-size: x-small;}" + Layout.LINE_SEP); sbuf.append("th {background: #336699; color: #FFFFFF; text-align: left;}" + Layout.LINE_SEP); sbuf.append("-->" + Layout.LINE_SEP); sbuf.append("</style>" + Layout.LINE_SEP); sbuf.append("</head>" + Layout.LINE_SEP); sbuf.append("<body bgcolor=\"#FFFFFF\" topmargin=\"6\" leftmargin=\"6\">" + Layout.LINE_SEP); sbuf.append("<hr size=\"1\" noshade>" + Layout.LINE_SEP); sbuf.append("Log session start time " + new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new java.util.Date()) + "<br>" + Layout.LINE_SEP); sbuf.append("<br>" + Layout.LINE_SEP); sbuf.append("<table cellspacing=\"0\" cellpadding=\"4\" border=\"1\" bordercolor=\"#224466\" width=\"100%\">" + Layout.LINE_SEP); sbuf.append("<tr>" + Layout.LINE_SEP); sbuf.append("<th min-width='111px'>Time</th>" + Layout.LINE_SEP); sbuf.append("<th min-width='100px' >Thread</th>" + Layout.LINE_SEP); sbuf.append("<th min-width='45px'>Level</th>" + Layout.LINE_SEP); sbuf.append("<th min-width='250px'>Category</th>" + Layout.LINE_SEP); if (locationInfo) { sbuf.append("<th min-width='250px'>File:Line</th>" + Layout.LINE_SEP); } sbuf.append("<th min-width='500px'>Message</th>" + Layout.LINE_SEP); sbuf.append("</tr>" + Layout.LINE_SEP); return sbuf.toString(); } /** * Returns the appropriate HTML footers. */ public String getFooter() { StringBuffer sbuf = new StringBuffer(); sbuf.append("</table>" + Layout.LINE_SEP); sbuf.append("<br>" + Layout.LINE_SEP); sbuf.append("</body></html>"); return sbuf.toString(); } /** * The HTML layout handles the throwable contained in logging * events. Hence, this method return <code>false</code>. */ public boolean ignoresThrowable() { return false; } }