// Copyright (C) 2009 Google Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package com.google.enterprise.connector.logging;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.logging.Formatter;
import java.util.logging.LogManager;
import java.util.logging.LogRecord;
/**
* A custom log {@code Formatter} that generates XML format log events.
* The style of the generated XML can either resemble
* {@code java.util.logging.XMLFormatter} output (the default) or
* {@code Log4j XMLLayout} output (viewable in the Chainsaw log analyzer).
* <p>
* To select this Formatter, configure it via the
* {@code FileHandler.formatter} property in {@code logging.properties}.
* For instance: <pre><code>
* java.util.logging.FileHandler.formatter=com.google.enterprise.connector.logging.XmlFormatter
* </code></pre>
* <p>
* To generate Log4j-compatible XML output, configure it via the
* {@code XmlFormatter.format} property in {@code logging.properties}.
* A property value of {@code log4j} or {@code chainsaw} will trigger
* the generation of Log4j-compatible XML output. Otherwise, java.util.logging
* style output will be produced.
* For instance: <pre><code>
* com.google.enterprise.connector.logging.XmlFormatter.format=log4j
* </code></pre>
*/
/* TODO: Add MDC logging.
* TODO: Use XmlUtils when it is extracted into a utility jar. But it, too,
* must then get added to the system classpath to be available to logging.jar.
*/
public class XmlFormatter extends Formatter {
private static final String NL = System.getProperty("line.separator");
// Constants used by the java.util.logging compatible formatter.
private static final String RECORD_TAG = "<record>";
private static final int RECORD_TAG_LEN = RECORD_TAG.length();
// Constants used by the log4j compatible formatter.
private static final String EVENT_TAG = "log4j:event";
private static final String LOCATION_TAG = "log4j:locationInfo";
private static final String MESSAGE_TAG = "log4j:message";
private static final String NDC_TAG = "log4j:NDC";
private static final String THROWABLE_TAG = "log4j:throwable";
private static final String CDATA_START = "<![CDATA[";
private static final String CDATA_END = "]]>";
// Contains the underlying formatter being used; either the log4j compatible
// formatter or the java.util.logging compatible formatter.
private Formatter formatter = null;
public XmlFormatter() {
// Load the format from logging.properties.
// If requested format is "log4j" or "chainsaw", generate log4j-style XML,
// otherwise generate java.util.logging.XMLFormat-style XML.
String propName = getClass().getName() + ".format";
String format = LogManager.getLogManager().getProperty(propName);
if (format != null && format.trim().length() > 0) {
format = format.trim().toLowerCase();
if ("log4j".equals(format) || "chainsaw".equals(format)) {
formatter = new Log4jXmlFormatter();
}
}
if (formatter == null) {
formatter = new UtilLoggingXmlFormatter();
}
}
@Override
public String format(LogRecord record) {
return formatter.format(record);
}
/**
* A log formatter resembling java.util.logging.XMLFormatter,
* adding NDC Logging capabilities. The output is easier to read
* than log4jFormatter, but NDC data doesn't work in Chainsaw.
*/
private static class UtilLoggingXmlFormatter
extends java.util.logging.XMLFormatter {
@Override
public String format(LogRecord record) {
String output = super.format(record);
String ndc = NDC.peek();
if (ndc != null && ndc.length() > 0) {
int point = output.indexOf(RECORD_TAG);
if (point >= 0) {
point += RECORD_TAG_LEN;
if (ndc.indexOf('&') >= 0) {
ndc = ndc.replaceAll("&", "&");
}
if (ndc.indexOf('<') >= 0) {
ndc = ndc.replaceAll("<", "<");
}
if (ndc.indexOf('>') >= 0) {
ndc = ndc.replaceAll(">", ">");
}
output = output.substring(0, point) + NL + " <ndc>" + ndc + "</ndc>"
+ output.substring(point);
}
}
return output;
}
}
/**
* A log formatter resembling the Log4j XMLFormatter,
* based upon java.util.logging.LogRecord rather than
* log4j LoggerEvents. This log Formatter generates
* XML output that resembles log4j output, so that it
* is viewable using Chainsaw.
*/
private static class Log4jXmlFormatter extends SimpleFormatter {
@Override
public String format(LogRecord record) {
StringBuilder buf = new StringBuilder();
// Start event element.
buf.append('<').append(EVENT_TAG);
appendAttr(buf, "logger", record.getLoggerName());
appendAttr(buf, "timestamp", Long.toString(record.getMillis()));
appendAttr(buf, "level", record.getLevel().getName());
appendAttr(buf, "thread", Thread.currentThread().getName());
buf.append('>').append(NL);
// Add NDC element.
String ndc = NDC.peek();
if (ndc != null && ndc.length() > 0) {
appendCdata(buf, NDC_TAG, ndc);
}
// Add location element.
buf.append('<').append(LOCATION_TAG);
appendAttr(buf, "class", record.getSourceClassName());
appendAttr(buf, "method", record.getSourceMethodName());
buf.append(" file=\"\" line=\"\"/>").append(NL);
// Add message element.
appendCdata(buf, MESSAGE_TAG, super.formatMessage(record));
// Add throwable element.
Throwable thrown = record.getThrown();
if (thrown != null) {
StringWriter sw = new StringWriter();
PrintWriter pw = new PrintWriter(sw);
thrown.printStackTrace(pw);
pw.flush();
appendCdata(buf, THROWABLE_TAG, sw.toString());
pw.close();
}
// Close the event element.
buf.append("</").append(EVENT_TAG).append('>').append(NL).append(NL);
return buf.toString();
}
}
/**
* Append the supplied attribute to the XML element.
*
* @param buf {@code StringBuilder} we are appending to.
* @param attr The attribute name.
* @param value The attribute value.
*/
private static void appendAttr(StringBuilder buf, String attr, String value) {
if (value != null && value.length() > 0) {
buf.append(' ').append(attr).append("=\"");
appendAttrValue(buf, value);
buf.append('"');
}
}
/**
* XML encodes an attribute value, escaping some characters as
* character entities, and dropping invalid control characters.
* <p>
* Only four characters need to be encoded, according to
* http://www.w3.org/TR/REC-xml/#NT-AttValue: < & " '.
* Actually, we could only encode one of the quote characters if
* we knew that that was the one used to wrap the value, but we'll
* play it safe and encode both.
* <p>
* We drop invalid XML characters, following
* http://www.w3.org/TR/2000/REC-xml-20001006#NT-Char :
* <pre>
* Char ::= #x9 | #xA | #xD | [#x20-#xD7FF] | [#xE000-#xFFFD] | [#x10000-#x10FFFF]
* </pre>
* Java uses UTF-16 internally, so Unicode characters U+10000 to
* U+10FFFF are encoded using the surrogate characters excluded
* above, 0xD800 to 0xDFFF. So we allow just 0x09, 0x0A, 0x0D,
* and the range 0x20 to 0xFFFD.
*
* @param buf the {@code StringBuffer} to which to append the attribute value.
* @param attrValue the attribute value.
*/
private static void appendAttrValue(StringBuilder buf, String attrValue) {
for (int i = 0; i < attrValue.length(); i++) {
char c = attrValue.charAt(i);
switch (c) {
case '<':
buf.append("<");
break;
case '&':
buf.append("&");
break;
case '"':
buf.append(""");
break;
case '\'':
buf.append("'");
break;
case '\t':
case '\n':
case '\r':
buf.append(' ');
break;
default:
if (c >= 0x20 && c <= 0xFFFD) {
buf.append(c);
}
break;
}
}
}
/**
* Append the supplied content in a CDATA block.
* Escape the contents of a CDATA block, if necessary.
* CDATA blocks cannot be nested, so we will remove the
* CDATA start and end tags.
*
* @param buf {@code StringBuilder} we are appending to.
* @param tag XML element tag to wrap around CDATA.
* @param str String to enclose in CDATA tags.
*/
private static void appendCdata(StringBuilder buf, String tag, String str) {
if (str.indexOf(CDATA_START) >= 0) {
str = str.replaceAll(CDATA_START, " ");
}
if (str.indexOf(CDATA_END) >= 0) {
str = str.replaceAll(CDATA_END, " ");
}
buf.append('<').append(tag).append('>');
buf.append(CDATA_START).append(str).append(CDATA_END);
buf.append("</").append(tag).append('>').append(NL);
}
}