package com.cwbase.logback; import ch.qos.logback.classic.spi.ILoggingEvent; import ch.qos.logback.classic.spi.IThrowableProxy; import ch.qos.logback.classic.spi.ThrowableProxyUtil; import ch.qos.logback.core.LayoutBase; import java.net.InetAddress; import java.net.UnknownHostException; import java.text.DateFormat; import java.text.SimpleDateFormat; import java.util.*; import java.util.Map.Entry; import java.util.regex.Matcher; import java.util.regex.Pattern; /** * Adapt from XMLLayout * * @author kmtong * */ public class JSONEventLayout extends LayoutBase<ILoggingEvent> { private final int DEFAULT_SIZE = 256; private final int UPPER_LIMIT = 2048; private final static char DBL_QUOTE = '"'; private final static char COMMA = ','; private StringBuilder buf = new StringBuilder(DEFAULT_SIZE); private DateFormat df = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSZ"); private Pattern MDC_VAR_PATTERN = Pattern.compile("\\@\\{([^}^:-]*)(:-([^}]*)?)?\\}"); private boolean locationInfo = false; private int callerStackIdx = 0; private boolean properties = false; String source; String sourceHost; String sourcePath; List<String> tags; List<AdditionalField> additionalFields; String type; public JSONEventLayout() { try { setSourceHost(InetAddress.getLocalHost().getHostName()); } catch (UnknownHostException e) { } } @Override public void start() { super.start(); } /** * 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> * If you are embedding this layout within an * <code>org.apache.log4j.net.SMTPAppender</code> 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; } /** * Sets whether MDC key-value pairs should be output, default false. * * @param flag * new value. * @since 1.2.15 */ public void setProperties(final boolean flag) { properties = flag; } /** * Gets whether MDC key-value pairs should be output. * * @return true if MDC key-value pairs are output. * @since 1.2.15 */ public boolean getProperties() { return properties; } /** * Formats a {@link ILoggingEvent} in conformity with the log4j.dtd. */ public synchronized String doLayout(ILoggingEvent event) { // Reset working buffer. If the buffer is too large, then we need a new // one in order to avoid the penalty of creating a large array. if (buf.capacity() > UPPER_LIMIT) { buf = new StringBuilder(DEFAULT_SIZE); } else { buf.setLength(0); } Map<String, String> mdc = event.getMDCPropertyMap(); buf.append("{"); appendKeyValue(buf, "source", source, mdc); buf.append(COMMA); appendKeyValue(buf, "host", sourceHost, mdc); buf.append(COMMA); appendKeyValue(buf, "path", sourcePath, mdc); buf.append(COMMA); appendKeyValue(buf, "type", type, mdc); buf.append(COMMA); appendKeyValue(buf, "tags", tags, mdc); buf.append(COMMA); appendKeyValue(buf, "message", event.getFormattedMessage(), null); buf.append(COMMA); appendKeyValue(buf, "@timestamp", df.format(new Date(event.getTimeStamp())), null); buf.append(COMMA); // ---- fields ---- appendKeyValue(buf, "logger", event.getLoggerName(), null); buf.append(COMMA); appendKeyValue(buf, "level", event.getLevel().toString(), null); buf.append(COMMA); appendKeyValue(buf, "thread", event.getThreadName(), null); IThrowableProxy tp = event.getThrowableProxy(); if (tp != null) { buf.append(COMMA); String throwable = ThrowableProxyUtil.asString(tp); appendKeyValue(buf, "throwable", throwable, null); } if (locationInfo) { StackTraceElement[] callerDataArray = event.getCallerData(); if (callerDataArray != null && callerDataArray.length > callerStackIdx) { buf.append(COMMA); buf.append("\"location\":{"); StackTraceElement immediateCallerData = callerDataArray[callerStackIdx]; appendKeyValue(buf, "class", immediateCallerData.getClassName(), null); buf.append(COMMA); appendKeyValue(buf, "method", immediateCallerData.getMethodName(), null); buf.append(COMMA); appendKeyValue(buf, "file", immediateCallerData.getFileName(), null); buf.append(COMMA); appendKeyValue(buf, "line", Integer.toString(immediateCallerData.getLineNumber()), null); buf.append("}"); } } /* * <log4j:properties> <log4j:data name="name" value="value"/> * </log4j:properties> */ if (properties) { Map<String, String> propertyMap = event.getMDCPropertyMap(); if ((propertyMap != null) && (propertyMap.size() != 0)) { Set<Entry<String, String>> entrySet = propertyMap.entrySet(); buf.append(COMMA); buf.append("\"properties\":{"); Iterator<Entry<String, String>> i = entrySet.iterator(); while (i.hasNext()) { Entry<String, String> entry = i.next(); appendKeyValue(buf, entry.getKey(), entry.getValue(), null); if (i.hasNext()) { buf.append(COMMA); } } buf.append("}"); } } if(additionalFields != null) { for(AdditionalField field : additionalFields) { buf.append(COMMA); appendKeyValue(buf, field.getKey(), field.getValue(), mdc); } } buf.append("}"); return buf.toString(); } private void appendKeyValue(StringBuilder buf, String key, String value, Map<String, String> mdc) { if (value != null) { buf.append(DBL_QUOTE); buf.append(escape(key)); buf.append(DBL_QUOTE); buf.append(':'); buf.append(DBL_QUOTE); buf.append(escape(mdcSubst(value, mdc))); buf.append(DBL_QUOTE); } else { buf.append(DBL_QUOTE); buf.append(escape(key)); buf.append(DBL_QUOTE); buf.append(':'); buf.append("null"); } } private void appendKeyValue(StringBuilder buf, String key, List<String> values, Map<String, String> mdc) { buf.append(DBL_QUOTE); buf.append(escape(key)); buf.append(DBL_QUOTE); buf.append(':'); buf.append('['); if (values != null) { Iterator<String> i = values.iterator(); while (i.hasNext()) { String v = i.next(); buf.append(DBL_QUOTE); buf.append(escape(mdcSubst(v, mdc))); buf.append(DBL_QUOTE); if (i.hasNext()) { buf.append(','); } } } buf.append(']'); } private String mdcSubst(String v, Map<String, String> mdc) { if (mdc != null && v != null && v.contains("@{")) { Matcher m = MDC_VAR_PATTERN.matcher(v); StringBuffer sb = new StringBuffer(v.length()); while (m.find()) { String val = mdc.get(m.group(1)); if (val == null) { // If a default value exists, use it val = (m.group(3) != null) ? val = m.group(3) : m.group(1) + "_NOT_FOUND"; } m.appendReplacement(sb, Matcher.quoteReplacement(val)); } m.appendTail(sb); return sb.toString(); } return v; } private String escape(String s) { if (s == null) return null; StringBuilder sb = new StringBuilder(); for (int i = 0; i < s.length(); i++) { char ch = s.charAt(i); switch (ch) { case '"': sb.append("\\\""); break; case '\\': sb.append("\\\\"); break; case '\b': sb.append("\\b"); break; case '\f': sb.append("\\f"); break; case '\n': sb.append("\\n"); break; case '\r': sb.append("\\r"); break; case '\t': sb.append("\\t"); break; case '/': sb.append("\\/"); break; default: if (ch >= '\u0000' && ch <= '\u001F') { String ss = Integer.toHexString(ch); sb.append("\\u"); for (int k = 0; k < 4 - ss.length(); k++) { sb.append('0'); } sb.append(ss.toUpperCase()); } else { sb.append(ch); } } }// for return sb.toString(); } @Override public String getContentType() { return "application/json"; } public String getSource() { return source; } public void setSource(String source) { this.source = source; } public String getSourceHost() { return sourceHost; } public void setSourceHost(String sourceHost) { this.sourceHost = sourceHost; } public String getSourcePath() { return sourcePath; } public void setSourcePath(String sourcePath) { this.sourcePath = sourcePath; } public List<String> getTags() { return tags; } public void setTags(List<String> tags) { this.tags = tags; } public String getType() { return type; } public void setType(String type) { this.type = type; } public int getCallerStackIdx() { return callerStackIdx; } /** * Location information dump with respect to call stack level. Some * framework (Play) wraps the original logging method, and dumping the * location always log the file of the wrapper instead of the actual caller. * For PlayFramework, I use 2. * * @param callerStackIdx */ public void setCallerStackIdx(int callerStackIdx) { this.callerStackIdx = callerStackIdx; } public void addAdditionalField(AdditionalField p) { if(additionalFields == null) { additionalFields = new ArrayList<AdditionalField>(); } additionalFields.add(p); } }