001: /*
002: * ====================================================================
003: *
004: * The Apache Software License, Version 1.1
005: *
006: * Copyright (c) 1999-2001 The Apache Software Foundation. All rights
007: * reserved.
008: *
009: * Redistribution and use in source and binary forms, with or without
010: * modification, are permitted provided that the following conditions
011: * are met:
012: *
013: * 1. Redistributions of source code must retain the above copyright
014: * notice, this list of conditions and the following disclaimer.
015: *
016: * 2. Redistributions in binary form must reproduce the above copyright
017: * notice, this list of conditions and the following disclaimer in
018: * the documentation and/or other materials provided with the
019: * distribution.
020: *
021: * 3. The end-user documentation included with the redistribution, if
022: * any, must include the following acknowlegement:
023: * "This product includes software developed by the
024: * Apache Software Foundation (http://www.apache.org/)."
025: * Alternately, this acknowlegement may appear in the software itself,
026: * if and wherever such third-party acknowlegements normally appear.
027: *
028: * 4. The names "The Jakarta Project", "Tomcat", and "Apache Software
029: * Foundation" must not be used to endorse or promote products derived
030: * from this software without prior written permission. For written
031: * permission, please contact apache@apache.org.
032: *
033: * 5. Products derived from this software may not be called "Apache"
034: * nor may "Apache" appear in their names without prior written
035: * permission of the Apache Group.
036: *
037: * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED
038: * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
039: * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
040: * DISCLAIMED. IN NO EVENT SHALL THE APACHE SOFTWARE FOUNDATION OR
041: * ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
042: * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
043: * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
044: * USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
045: * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
046: * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
047: * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
048: * SUCH DAMAGE.
049: * ====================================================================
050: *
051: * This software consists of voluntary contributions made by many
052: * individuals on behalf of the Apache Software Foundation. For more
053: * information on the Apache Software Foundation, please see
054: * <http://www.apache.org/>.
055: *
056: * [Additional notices, if required by prior licensing conditions]
057: *
058: */
059:
060: package org.apache.catalina.valves;
061:
062: import java.io.IOException;
063: import javax.servlet.ServletException;
064: import javax.servlet.ServletRequest;
065: import javax.servlet.http.HttpServletRequest;
066: import org.apache.catalina.Request;
067: import org.apache.catalina.Response;
068: import org.apache.catalina.HttpResponse;
069: import org.apache.catalina.ValveContext;
070: import org.apache.catalina.Lifecycle;
071: import org.apache.catalina.LifecycleEvent;
072: import org.apache.catalina.LifecycleException;
073: import org.apache.catalina.LifecycleListener;
074: import org.apache.catalina.util.LifecycleSupport;
075: import org.apache.catalina.util.StringManager;
076: import org.apache.catalina.valves.ValveBase;
077: import org.apache.catalina.valves.Constants;
078: import java.util.Properties;
079: import java.util.Date;
080: import java.sql.Timestamp;
081: import java.sql.SQLException;
082: import java.sql.DriverManager;
083: import java.sql.Connection;
084: import java.sql.PreparedStatement;
085:
086: /**
087: * <p>
088: * This Tomcat extension logs server access directly to a database, and can
089: * be used instead of the regular file-based access log implemented in
090: * AccessLogValve.
091: * To use, copy into the server/classes directory of the Tomcat installation
092: * and configure in server.xml as:
093: * <pre>
094: * <Valve className="AccessLogDBValve"
095: * driverName="<i>your_jdbc_driver</i>"
096: * connectionURL="<i>your_jdbc_url</i>"
097: * pattern="combined" resolveHosts="false"
098: * />
099: * </pre>
100: * </p>
101: * <p>
102: * Many parameters can be configured, such as the database connection (with
103: * <code>driverName</code> and <code>connectionURL</code>),
104: * the table name (<code>tableName</code>)
105: * and the field names (corresponding to the get/set method names).
106: * The same options as AccessLogValve are supported, such as
107: * <code>resolveHosts</code> and <code>pattern</code> ("common" or "combined"
108: * only).
109: * </p>
110: * <p>
111: * When Tomcat is started, a database connection (with autoReconnect option)
112: * is created and used for all the log activity. When Tomcat is shutdown, the
113: * database connection is closed.
114: * This logger can be used at the level of the Engine context (being shared
115: * by all the defined hosts) or the Host context (one instance of the logger
116: * per host, possibly using different databases).
117: * </p>
118: * <p>
119: * The database table can be created with the following command:
120: * </p>
121: * <pre>
122: * CREATE TABLE access (
123: * id INT UNSIGNED AUTO_INCREMENT NOT NULL,
124: * ts TIMESTAMP NOT NULL,
125: * remoteHost CHAR(15) NOT NULL,
126: * user CHAR(15),
127: * timestamp TIMESTAMP NOT NULL,
128: * virtualHost VARCHAR(64) NOT NULL,
129: * method VARCHAR(8) NOT NULL,
130: * query VARCHAR(255) NOT NULL,
131: * status SMALLINT UNSIGNED NOT NULL,
132: * bytes INT UNSIGNED NOT NULL,
133: * referer VARCHAR(128),
134: * userAgent VARCHAR(128),
135: * PRIMARY KEY (id),
136: * INDEX (ts),
137: * INDEX (remoteHost),
138: * INDEX (virtualHost),
139: * INDEX (query),
140: * INDEX (userAgent)
141: * );
142: * </pre>
143: * <p>
144: * If the table is created as above, its name and the field names don't need
145: * to be defined.
146: * </p>
147: * <p>
148: * If the request method is "common", only these fields are used:
149: * <code>remoteHost, user, timeStamp, query, status, bytes</code>
150: * </p>
151: * <p>
152: * <i>TO DO: provide option for excluding logging of certain MIME types.</i>
153: * </p>
154: *
155: * @version 1.0
156: * @author Andre de Jesus
157: */
158:
159: public final class JDBCAccessLogValve extends ValveBase implements
160: Lifecycle {
161:
162: // ----------------------------------------------------------- Constructors
163:
164: /**
165: * Class constructor. Initializes the fields with the default values.
166: * The defaults are:
167: * <pre>
168: * driverName = null;
169: * connectionURL = null;
170: * tableName = "access";
171: * remoteHostField = "remoteHost";
172: * userField = "user";
173: * timestampField = "timestamp";
174: * virtualHostField = "virtualHost";
175: * methodField = "method";
176: * queryField = "query";
177: * statusField = "status";
178: * bytesField = "bytes";
179: * refererField = "referer";
180: * userAgentField = "userAgent";
181: * pattern = "common";
182: * resolveHosts = false;
183: * </pre>
184: */
185: public JDBCAccessLogValve() {
186: super ();
187: driverName = null;
188: connectionURL = null;
189: tableName = "access";
190: remoteHostField = "remoteHost";
191: userField = "user";
192: timestampField = "timestamp";
193: virtualHostField = "virtualHost";
194: methodField = "method";
195: queryField = "query";
196: statusField = "status";
197: bytesField = "bytes";
198: refererField = "referer";
199: userAgentField = "userAgent";
200: pattern = "common";
201: resolveHosts = false;
202: conn = null;
203: ps = null;
204: currentTimeMillis = new java.util.Date().getTime();
205: }
206:
207: // ----------------------------------------------------- Instance Variables
208:
209: private String driverName;
210: private String connectionURL;
211: private String tableName;
212: private String remoteHostField;
213: private String userField;
214: private String timestampField;
215: private String virtualHostField;
216: private String methodField;
217: private String queryField;
218: private String statusField;
219: private String bytesField;
220: private String refererField;
221: private String userAgentField;
222: private String pattern;
223: private boolean resolveHosts;
224:
225: private Connection conn;
226: private PreparedStatement ps;
227:
228: private long currentTimeMillis;
229:
230: /**
231: * The descriptive information about this implementation.
232: */
233: protected static String info = "org.apache.catalina.valves.JDBCAccessLogValve/1.0";
234:
235: /**
236: * The lifecycle event support for this component.
237: */
238: protected LifecycleSupport lifecycle = new LifecycleSupport(this );
239:
240: /**
241: * The string manager for this package.
242: */
243: private StringManager sm = StringManager
244: .getManager(Constants.Package);
245:
246: /**
247: * Has this component been started yet?
248: */
249: private boolean started = false;
250:
251: // ------------------------------------------------------------- Properties
252:
253: /**
254: * Sets the database driver name.
255: *
256: * @param driverName The complete name of the database driver class.
257: */
258: public void setDriverName(String driverName) {
259: this .driverName = driverName;
260: }
261:
262: /**
263: * Sets the JDBC URL for the database where the log is stored.
264: *
265: * @param connectionURL The JDBC URL of the database.
266: */
267: public void setConnectionURL(String connectionURL) {
268: this .connectionURL = connectionURL;
269: }
270:
271: /**
272: * Sets the name of the table where the logs are stored.
273: *
274: * @param tableName The name of the table.
275: */
276: public void setTableName(String tableName) {
277: this .tableName = tableName;
278: }
279:
280: /**
281: * Sets the name of the field containing the remote host.
282: *
283: * @param remoteHostField The name of the remote host field.
284: */
285: public void setRemoteHostField(String remoteHostField) {
286: this .remoteHostField = remoteHostField;
287: }
288:
289: /**
290: * Sets the name of the field containing the remote user name.
291: *
292: * @param userField The name of the remote user field.
293: */
294: public void setUserField(String userField) {
295: this .userField = userField;
296: }
297:
298: /**
299: * Sets the name of the field containing the server-determined timestamp.
300: *
301: * @param timestampField The name of the server-determined timestamp field.
302: */
303: public void setTimestampField(String timestampField) {
304: this .timestampField = timestampField;
305: }
306:
307: /**
308: * Sets the name of the field containing the virtual host information
309: * (this is in fact the server name).
310: *
311: * @param virtualHostField The name of the virtual host field.
312: */
313: public void setVirtualHostField(String virtualHostField) {
314: this .virtualHostField = virtualHostField;
315: }
316:
317: /**
318: * Sets the name of the field containing the HTTP request method.
319: *
320: * @param methodField The name of the HTTP request method field.
321: */
322: public void setMethodField(String methodField) {
323: this .methodField = methodField;
324: }
325:
326: /**
327: * Sets the name of the field containing the URL part of the HTTP query.
328: *
329: * @param queryField The name of the field containing the URL part of
330: * the HTTP query.
331: */
332: public void setQueryField(String queryField) {
333: this .queryField = queryField;
334: }
335:
336: /**
337: * Sets the name of the field containing the HTTP response status code.
338: *
339: * @param statusField The name of the HTTP response status code field.
340: */
341: public void setStatusField(String statusField) {
342: this .statusField = statusField;
343: }
344:
345: /**
346: * Sets the name of the field containing the number of bytes returned.
347: *
348: * @param bytesField The name of the returned bytes field.
349: */
350: public void setBytesField(String bytesField) {
351: this .bytesField = bytesField;
352: }
353:
354: /**
355: * Sets the name of the field containing the referer.
356: *
357: * @param refererField The referer field name.
358: */
359: public void setRefererField(String refererField) {
360: this .refererField = refererField;
361: }
362:
363: /**
364: * Sets the name of the field containing the user agent.
365: *
366: * @param userAgentField The name of the user agent field.
367: */
368: public void setUserAgentField(String userAgentField) {
369: this .userAgentField = userAgentField;
370: }
371:
372: /**
373: * Sets the logging pattern. The patterns supported correspond to the
374: * file-based "common" and "combined". These are translated into the use
375: * of tables containing either set of fields.
376: * <P><I>TO DO: more flexible field choices.</I></P>
377: *
378: * @param pattern The name of the logging pattern.
379: */
380: public void setPattern(String pattern) {
381: this .pattern = pattern;
382: }
383:
384: /**
385: * Determines whether IP host name resolution is done.
386: *
387: * @param resolveHosts "true" or "false", if host IP resolution
388: * is desired or not.
389: */
390: public void setResolveHosts(String resolveHosts) {
391: this .resolveHosts = new Boolean(resolveHosts).booleanValue();
392: }
393:
394: // --------------------------------------------------------- Public Methods
395:
396: /**
397: * This method is invoked by Tomcat on each query.
398: *
399: * @param request The Request object.
400: * @param response The Response object.
401: * @param context The ValveContext object.
402: * @exception IOException Should not be thrown.
403: * @exception ServletException Database SQLException is wrapped
404: * in a ServletException.
405: */
406: public void invoke(Request request, Response response,
407: ValveContext context) throws IOException, ServletException {
408:
409: context.invokeNext(request, response);
410:
411: ServletRequest req = request.getRequest();
412: HttpServletRequest hreq = null;
413: if (req instanceof HttpServletRequest)
414: hreq = (HttpServletRequest) req;
415: String remoteHost = "";
416: if (resolveHosts)
417: remoteHost = req.getRemoteHost();
418: else
419: remoteHost = req.getRemoteAddr();
420: String user = "";
421: if (hreq != null)
422: user = hreq.getRemoteUser();
423: String query = "";
424: if (hreq != null)
425: query = hreq.getRequestURI();
426: int bytes = response.getContentCount();
427: if (bytes < 0)
428: bytes = 0;
429: int status = ((HttpResponse) response).getStatus();
430:
431: synchronized (ps) {
432: try {
433: ps.setString(1, remoteHost);
434: ps.setString(2, user);
435: ps.setTimestamp(3,
436: new Timestamp(getCurrentTimeMillis()));
437: ps.setString(4, query);
438: ps.setInt(5, status);
439: ps.setInt(6, bytes);
440: } catch (SQLException e) {
441: throw new ServletException(e);
442: }
443: if (pattern.equals("common")) {
444: try {
445: ps.executeUpdate();
446: } catch (SQLException e) {
447: throw new ServletException(e);
448: }
449: } else if (pattern.equals("combined")) {
450: String virtualHost = "";
451: if (hreq != null)
452: virtualHost = hreq.getServerName();
453: String method = "";
454: if (hreq != null)
455: method = hreq.getMethod();
456: String referer = "";
457: if (hreq != null)
458: referer = hreq.getHeader("referer");
459: String userAgent = "";
460: if (hreq != null)
461: userAgent = hreq.getHeader("user-agent");
462: try {
463: ps.setString(7, virtualHost);
464: ps.setString(8, method);
465: ps.setString(9, referer);
466: ps.setString(10, userAgent);
467: ps.executeUpdate();
468: } catch (SQLException e) {
469: throw new ServletException(e);
470: }
471: }
472: }
473:
474: }
475:
476: /**
477: * Adds a Lifecycle listener.
478: *
479: * @param listener The listener to add.
480: */
481: public void addLifecycleListener(LifecycleListener listener) {
482:
483: lifecycle.addLifecycleListener(listener);
484:
485: }
486:
487: /**
488: * Get the lifecycle listeners associated with this lifecycle. If this
489: * Lifecycle has no listeners registered, a zero-length array is returned.
490: */
491: public LifecycleListener[] findLifecycleListeners() {
492:
493: return lifecycle.findLifecycleListeners();
494:
495: }
496:
497: /**
498: * Removes a Lifecycle listener.
499: *
500: * @param listener The listener to remove.
501: */
502: public void removeLifecycleListener(LifecycleListener listener) {
503:
504: lifecycle.removeLifecycleListener(listener);
505:
506: }
507:
508: /**
509: * Invoked by Tomcat on startup. The database connection is set here.
510: *
511: * @exception LifecycleException Can be thrown on lifecycle
512: * inconsistencies or on database errors (as a wrapped SQLException).
513: */
514: public void start() throws LifecycleException {
515:
516: if (started)
517: throw new LifecycleException(sm
518: .getString("accessLogValve.alreadyStarted"));
519: lifecycle.fireLifecycleEvent(START_EVENT, null);
520: started = true;
521:
522: try {
523: Class.forName(driverName).newInstance();
524: } catch (ClassNotFoundException e) {
525: throw new LifecycleException(e);
526: } catch (InstantiationException e) {
527: throw new LifecycleException(e);
528: } catch (IllegalAccessException e) {
529: throw new LifecycleException(e);
530: }
531: Properties info = new Properties();
532: info.setProperty("autoReconnect", "true");
533: try {
534: conn = DriverManager.getConnection(connectionURL, info);
535: if (pattern.equals("common")) {
536: ps = conn.prepareStatement("INSERT INTO " + tableName
537: + " (" + remoteHostField + ", " + userField
538: + ", " + timestampField + ", " + queryField
539: + ", " + statusField + ", " + bytesField
540: + ") VALUES(?, ?, ?, ?, ?, ?)");
541: } else if (pattern.equals("combined")) {
542: ps = conn.prepareStatement("INSERT INTO " + tableName
543: + " (" + remoteHostField + ", " + userField
544: + ", " + timestampField + ", " + queryField
545: + ", " + statusField + ", " + bytesField + ", "
546: + virtualHostField + ", " + methodField + ", "
547: + refererField + ", " + userAgentField
548: + ") VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?)");
549: }
550: } catch (SQLException e) {
551: throw new LifecycleException(e);
552: }
553:
554: }
555:
556: /**
557: * Invoked by tomcat on shutdown. The database connection is closed here.
558: *
559: * @exception LifecycleException Can be thrown on lifecycle
560: * inconsistencies or on database errors (as a wrapped SQLException).
561: */
562: public void stop() throws LifecycleException {
563:
564: if (!started)
565: throw new LifecycleException(sm
566: .getString("accessLogValve.notStarted"));
567: lifecycle.fireLifecycleEvent(STOP_EVENT, null);
568: started = false;
569:
570: try {
571: if (ps != null)
572: ps.close();
573: if (conn != null)
574: conn.close();
575: } catch (SQLException e) {
576: throw new LifecycleException(e);
577: }
578:
579: }
580:
581: public long getCurrentTimeMillis() {
582: long systime = System.currentTimeMillis();
583: if ((systime - currentTimeMillis) > 1000) {
584: currentTimeMillis = new java.util.Date(systime).getTime();
585: }
586: return currentTimeMillis;
587: }
588:
589: }
|