//======================================================================== //Copyright 1997-2006 Mort Bay Consulting Pty. Ltd. //------------------------------------------------------------------------ //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 org.mortbay.jetty; import java.util.Locale; import javax.servlet.http.Cookie; import org.mortbay.util.TypeUtil; import org.mortbay.util.DateCache; import org.mortbay.jetty.Log4JRequestLog; import org.mortbay.jetty.servlet.PathMap; import org.mortbay.component.AbstractLifeCycle; import org.apache.log4j.Logger; import org.apache.log4j.Level; /* ------------------------------------------------------------ */ /** * This {@link RequestLog} implementation outputs log messages in same * pseudo-standard NCSA common log format as the * org.mortbay.jetty.NCSARequestLog implementation, but logs to a Log4J Logger * rather than writing directly to file. Most of the functionality of the * NCSARequestLog was copied directly into this implementation (especially the * log message building), with the exception of the file rotation format which * would now be configured in the log4j config (or not). * * Other configuration options are unchanged from NCSARequestLog as most logic * was copied directly from that Class. * * This log format can be output by most web servers, and almost all web log * analysis software can understand these formats. * * @author Greg Wilkins * @author Nigel Canonizado * @author Chance Yeoman */ public class Log4JRequestLogImpl extends AbstractLifeCycle implements Log4JRequestLog { // Log4JRequestLog default values and constants. private final static int INITIAL_LOG_MESSAGE_BUFFER_SIZE = 160; private final static String DEFAULT_LOG_DATE_FORMAT = "dd/MMM/yyyy:HH:mm:ss Z"; private final static String DEFAULT_LOG_TIME_ZONE = "GMT"; // Logger and its name private String accessLoggerName; private Logger accessRequestLogger; // configuration properties from NCSARequestLog private boolean logServer; private boolean preferProxiedForAddress; private String logDateFormat; private String logTimeZone; private Locale logLocale; private String[] ignorePaths; private boolean extended; private boolean logCookies; private boolean logLatency; // derived from set properties private DateCache logDateCache; private PathMap ignorePathMap; /* ------------------------------------------------------------ */ /** * Constructor for Log4JRequestLog sets default name to null. Note that a * null name only affects the name appended to the fully qualified name of * this class. Leaving this null just causes it to get the default request * logger and use default appenders. */ public Log4JRequestLogImpl() { accessLoggerName = null; accessRequestLogger = null; logServer = false; preferProxiedForAddress = false; logDateFormat = DEFAULT_LOG_DATE_FORMAT; logTimeZone = DEFAULT_LOG_TIME_ZONE; logLocale = Locale.getDefault(); ignorePaths = null; extended = true; logCookies = false; logLatency = false; logDateCache = null; ignorePathMap = null; } /* ------------------------------------------------------------ */ /** * @param accessLoggerName * String name for the Logger instance. Used to configure * logger-specific appenders. */ public void setAccessLoggerName(final String accessLoggerName) { this.accessLoggerName = accessLoggerName; } public void setLogServer(boolean logServer) { this.logServer = logServer; } public void setPreferProxiedForAddress(boolean preferProxiedForAddress) { this.preferProxiedForAddress = preferProxiedForAddress; } public void setLogDateFormat(String logDateFormat) { this.logDateFormat = logDateFormat; } public void setLogTimeZone(String logTimeZone) { this.logTimeZone = logTimeZone; } public void setIgnorePaths(String[] ignorePaths) { this.ignorePaths = ignorePaths; } public void setExtended(boolean extended) { this.extended = extended; } public void setLogCookies(boolean logCookies) { this.logCookies = logCookies; } public void setLogLatency(boolean logLatency) { this.logLatency = logLatency; } /* ------------------------------------------------------------ */ /* * The following group of methods override methods in the following abstract * class: org.mortbay.component.AbstractLifeCycle */ /* ------------------------------------------------------------ */ /** * Executed when making the transition to the STARTED LifeCycle State. * * Constructs a logger name from the package/class name and custom appended * name if supplied. The Logger of that name is then retrieved. * * @see org.mortbay.component.AbstractLifeCycle#doStart() */ protected void doStart() throws Exception { // if not set, default name is just class name String completeLoggerName = this.getClass().getName(); if (accessLoggerName != null) { completeLoggerName = completeLoggerName + "." + accessLoggerName; } accessRequestLogger = Logger.getLogger(completeLoggerName); // while the logger will always be created, it is possible that this // particular logger was configured to have no appenders and not log at // all. // if this is the case, the Logger should be nulled to prevent // unnecessary // message building and log processing. if (accessRequestLogger.getAllAppenders().hasMoreElements()) { // the level of this logger is overridden to log everything. // if access requests are to be filtered, it should be done at the // appender level. accessRequestLogger.setLevel((Level) Level.ALL); // log date format if set if (logDateFormat != null) { logDateCache = new DateCache(logDateFormat, logLocale); logDateCache.setTimeZoneID(logTimeZone); } // paths to ignore if set if (ignorePaths != null && ignorePaths.length > 0) { ignorePathMap = new PathMap(); for (int i = 0; i < ignorePaths.length; i++) { ignorePathMap.put(ignorePaths[i], ignorePaths[i]); } } else { ignorePathMap = null; } } else { accessRequestLogger = null; } super.doStart(); } /* ------------------------------------------------------------ */ /** * Executed when making the transition to the STOPPED LifeCycle State. * * Simply nulls resources. * * @see org.mortbay.component.AbstractLifeCycle#doStop() */ protected void doStop() throws Exception { super.doStop(); accessRequestLogger = null; logDateCache = null; } /* ------------------------------------------------------------ */ /* * The following method implements the following interface: * org.mortbay.jetty.RequestLog */ /* ------------------------------------------------------------ */ /** * Log method executed by the container handling the http request. The * message building code was brought directly in from the * org.mortbay.jetty.NCSARequestLog implementation. * * @param request * - incoming http Request * @param response * - outgoing http Response */ public void log(Request request, Response response) { if (!this.isStarted()) { return; } // quit if the logger was not created correctly. if (accessRequestLogger == null) { return; } // quit if this is an ignored path. if (ignorePathMap != null && ignorePathMap.getMatch(request.getRequestURI()) != null) { return; } StringBuffer buffer = new StringBuffer(INITIAL_LOG_MESSAGE_BUFFER_SIZE); String logMessage = null; int status = 0; synchronized (buffer) { if (this.logServer) { buffer.append(request.getServerName()); buffer.append(' '); } String address = null; if (preferProxiedForAddress) { address = request.getHeader(HttpHeaders.X_FORWARDED_FOR); } if (address == null) { address = request.getRemoteAddr(); } buffer.append(address); buffer.append(" - "); String user = request.getRemoteUser(); if (user == null) { buffer.append(" - "); } else { buffer.append(user); } buffer.append(" ["); if (logDateCache != null) { buffer.append(logDateCache.format(request.getTimeStamp())); } else { buffer.append(request.getTimeStampBuffer().toString()); } buffer.append("] \""); buffer.append(request.getMethod()); buffer.append(' '); buffer.append(request.getUri()); buffer.append(' '); buffer.append(request.getProtocol()); buffer.append("\" "); status = response.getStatus(); if (status <= 0) { status = 404; } buffer.append((char) ('0' + ((status / 100) % 10))); buffer.append((char) ('0' + ((status / 10) % 10))); buffer.append((char) ('0' + (status % 10))); long responseLength = response.getContentCount(); if (responseLength >= 0) { buffer.append(' '); if (responseLength > 99999) { buffer.append(Long.toString(responseLength)); } else { if (responseLength > 9999) { buffer .append((char) ('0' + ((responseLength / 10000) % 10))); } if (responseLength > 999) { buffer .append((char) ('0' + ((responseLength / 1000) % 10))); } if (responseLength > 99) { buffer .append((char) ('0' + ((responseLength / 100) % 10))); } if (responseLength > 9) { buffer .append((char) ('0' + ((responseLength / 10) % 10))); } buffer.append((char) ('0' + (responseLength) % 10)); } buffer.append(' '); } else { buffer.append(" - "); } if (extended) { String referer = request.getHeader(HttpHeaders.REFERER); if (referer == null) { buffer.append("\"-\" "); } else { buffer.append('"'); buffer.append(referer); buffer.append("\" "); } String agent = request.getHeader(HttpHeaders.USER_AGENT); if (agent == null) { buffer.append("\"-\" "); } else { buffer.append('"'); buffer.append(agent); buffer.append('"'); } } if (logCookies) { Cookie[] cookies = request.getCookies(); if (cookies == null || cookies.length == 0) { buffer.append(" -"); } else { buffer.append(" \""); for (int i = 0; i < cookies.length; i++) { if (i != 0) { buffer.append(';'); } buffer.append(cookies[i].getName()); buffer.append('='); buffer.append(cookies[i].getValue()); } buffer.append("\""); } } if (logLatency) { buffer.append(" "); buffer.append(TypeUtil.toString(System.currentTimeMillis() - request.getTimeStamp())); } } logMessage = buffer.toString(); // switch on the category of response code and set the log level // accordingly. // This allows appenders to filter based on response code. The threshold // of the logger itself is not modified. // Default appender configuration simply logs all messages, this // only provides the configuration option. switch (status / 100) { case 1: // 1xx level response code accessRequestLogger.info(logMessage); break; case 2: // 2xx level response code accessRequestLogger.info(logMessage); break; case 3: // 3xx level response code accessRequestLogger.info(logMessage); break; case 4: // 4xx level response code accessRequestLogger.warn(logMessage); break; case 5: // 5xx level response code accessRequestLogger.error(logMessage); break; default: // unknown response code accessRequestLogger.error(logMessage); break; } } }