001: /*
002: * Copyright 1999-2001,2004 The Apache Software Foundation.
003: *
004: * Licensed under the Apache License, Version 2.0 (the "License");
005: * you may not use this file except in compliance with the License.
006: * You may obtain a copy of the License at
007: *
008: * http://www.apache.org/licenses/LICENSE-2.0
009: *
010: * Unless required by applicable law or agreed to in writing, software
011: * distributed under the License is distributed on an "AS IS" BASIS,
012: * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013: * See the License for the specific language governing permissions and
014: * limitations under the License.
015: */
016:
017: package org.apache.catalina.valves;
018:
019: import java.io.IOException;
020: import java.sql.Connection;
021: import java.sql.Driver;
022: import java.sql.PreparedStatement;
023: import java.sql.SQLException;
024: import java.sql.Timestamp;
025: import java.util.Properties;
026:
027: import javax.servlet.ServletException;
028: import javax.servlet.ServletRequest;
029: import javax.servlet.http.HttpServletRequest;
030:
031: import org.apache.catalina.HttpResponse;
032: import org.apache.catalina.Lifecycle;
033: import org.apache.catalina.LifecycleException;
034: import org.apache.catalina.LifecycleListener;
035: import org.apache.catalina.Request;
036: import org.apache.catalina.Response;
037: import org.apache.catalina.ValveContext;
038: import org.apache.catalina.util.LifecycleSupport;
039: import org.apache.catalina.util.StringManager;
040: import org.apache.catalina.Logger;
041:
042: /**
043: * <p>
044: * This Tomcat extension logs server access directly to a database, and can
045: * be used instead of the regular file-based access log implemented in
046: * AccessLogValve.
047: * To use, copy into the server/classes directory of the Tomcat installation
048: * and configure in server.xml as:
049: * <pre>
050: * <Valve className="AccessLogDBValve"
051: * driverName="<i>your_jdbc_driver</i>"
052: * connectionURL="<i>your_jdbc_url</i>"
053: * pattern="combined" resolveHosts="false"
054: * />
055: * </pre>
056: * </p>
057: * <p>
058: * Many parameters can be configured, such as the database connection (with
059: * <code>driverName</code> and <code>connectionURL</code>),
060: * the table name (<code>tableName</code>)
061: * and the field names (corresponding to the get/set method names).
062: * The same options as AccessLogValve are supported, such as
063: * <code>resolveHosts</code> and <code>pattern</code> ("common" or "combined"
064: * only).
065: * </p>
066: * <p>
067: * When Tomcat is started, a database connection (with autoReconnect option)
068: * is created and used for all the log activity. When Tomcat is shutdown, the
069: * database connection is closed.
070: * This logger can be used at the level of the Engine context (being shared
071: * by all the defined hosts) or the Host context (one instance of the logger
072: * per host, possibly using different databases).
073: * </p>
074: * <p>
075: * The database table can be created with the following command:
076: * </p>
077: * <pre>
078: * CREATE TABLE access (
079: * id INT UNSIGNED AUTO_INCREMENT NOT NULL,
080: * ts TIMESTAMP NOT NULL,
081: * remoteHost CHAR(15) NOT NULL,
082: * user CHAR(15),
083: * timestamp TIMESTAMP NOT NULL,
084: * virtualHost VARCHAR(64) NOT NULL,
085: * method VARCHAR(8) NOT NULL,
086: * query VARCHAR(255) NOT NULL,
087: * status SMALLINT UNSIGNED NOT NULL,
088: * bytes INT UNSIGNED NOT NULL,
089: * referer VARCHAR(128),
090: * userAgent VARCHAR(128),
091: * PRIMARY KEY (id),
092: * INDEX (ts),
093: * INDEX (remoteHost),
094: * INDEX (virtualHost),
095: * INDEX (query),
096: * INDEX (userAgent)
097: * );
098: * </pre>
099: * <p>
100: * If the table is created as above, its name and the field names don't need
101: * to be defined.
102: * </p>
103: * <p>
104: * If the request method is "common", only these fields are used:
105: * <code>remoteHost, user, timeStamp, query, status, bytes</code>
106: * </p>
107: * <p>
108: * <i>TO DO: provide option for excluding logging of certain MIME types.</i>
109: * </p>
110: *
111: * @author Andre de Jesus
112: * @author Peter Rossbach
113: */
114:
115: public final class JDBCAccessLogValve extends ValveBase implements
116: Lifecycle {
117:
118: // ----------------------------------------------------------- Constructors
119:
120: /**
121: * Class constructor. Initializes the fields with the default values.
122: * The defaults are:
123: * <pre>
124: * driverName = null;
125: * connectionURL = null;
126: * tableName = "access";
127: * remoteHostField = "remoteHost";
128: * userField = "user";
129: * timestampField = "timestamp";
130: * virtualHostField = "virtualHost";
131: * methodField = "method";
132: * queryField = "query";
133: * statusField = "status";
134: * bytesField = "bytes";
135: * refererField = "referer";
136: * userAgentField = "userAgent";
137: * pattern = "common";
138: * resolveHosts = false;
139: * </pre>
140: */
141: public JDBCAccessLogValve() {
142: super ();
143: driverName = null;
144: connectionURL = null;
145: tableName = "access";
146: remoteHostField = "remoteHost";
147: userField = "user";
148: timestampField = "timestamp";
149: virtualHostField = "virtualHost";
150: methodField = "method";
151: queryField = "query";
152: statusField = "status";
153: bytesField = "bytes";
154: refererField = "referer";
155: userAgentField = "userAgent";
156: pattern = "common";
157: resolveHosts = false;
158: conn = null;
159: ps = null;
160: currentTimeMillis = new java.util.Date().getTime();
161: }
162:
163: // ----------------------------------------------------- Instance Variables
164:
165: /**
166: * The connection username to use when trying to connect to the database.
167: */
168: protected String connectionName = null;
169:
170: /**
171: * The connection URL to use when trying to connect to the database.
172: */
173: protected String connectionPassword = null;
174:
175: /**
176: * Instance of the JDBC Driver class we use as a connection factory.
177: */
178: protected Driver driver = null;
179:
180: private String driverName;
181: private String connectionURL;
182: private String tableName;
183: private String remoteHostField;
184: private String userField;
185: private String timestampField;
186: private String virtualHostField;
187: private String methodField;
188: private String queryField;
189: private String statusField;
190: private String bytesField;
191: private String refererField;
192: private String userAgentField;
193: private String pattern;
194: private boolean resolveHosts;
195:
196: private Connection conn;
197: private PreparedStatement ps;
198:
199: private long currentTimeMillis;
200:
201: /**
202: * The descriptive information about this implementation.
203: */
204: protected static String info = "org.apache.catalina.valves.JDBCAccessLogValve/1.0";
205:
206: /**
207: * The lifecycle event support for this component.
208: */
209: protected LifecycleSupport lifecycle = new LifecycleSupport(this );
210:
211: /**
212: * The string manager for this package.
213: */
214: private StringManager sm = StringManager
215: .getManager(Constants.Package);
216:
217: /**
218: * Has this component been started yet?
219: */
220: private boolean started = false;
221:
222: // ------------------------------------------------------------- Properties
223:
224: /**
225: * Return the username to use to connect to the database.
226: *
227: */
228: public String getConnectionName() {
229: return connectionName;
230: }
231:
232: /**
233: * Set the username to use to connect to the database.
234: *
235: * @param connectionName Username
236: */
237: public void setConnectionName(String connectionName) {
238: this .connectionName = connectionName;
239: }
240:
241: /**
242: * Sets the database driver name.
243: *
244: * @param driverName The complete name of the database driver class.
245: */
246: public void setDriverName(String driverName) {
247: this .driverName = driverName;
248: }
249:
250: /**
251: * Return the password to use to connect to the database.
252: *
253: */
254: public String getConnectionPassword() {
255: return connectionPassword;
256: }
257:
258: /**
259: * Set the password to use to connect to the database.
260: *
261: * @param connectionPassword User password
262: */
263: public void setConnectionPassword(String connectionPassword) {
264: this .connectionPassword = connectionPassword;
265: }
266:
267: /**
268: * Sets the JDBC URL for the database where the log is stored.
269: *
270: * @param connectionURL The JDBC URL of the database.
271: */
272: public void setConnectionURL(String connectionURL) {
273: this .connectionURL = connectionURL;
274: }
275:
276: /**
277: * Sets the name of the table where the logs are stored.
278: *
279: * @param tableName The name of the table.
280: */
281: public void setTableName(String tableName) {
282: this .tableName = tableName;
283: }
284:
285: /**
286: * Sets the name of the field containing the remote host.
287: *
288: * @param remoteHostField The name of the remote host field.
289: */
290: public void setRemoteHostField(String remoteHostField) {
291: this .remoteHostField = remoteHostField;
292: }
293:
294: /**
295: * Sets the name of the field containing the remote user name.
296: *
297: * @param userField The name of the remote user field.
298: */
299: public void setUserField(String userField) {
300: this .userField = userField;
301: }
302:
303: /**
304: * Sets the name of the field containing the server-determined timestamp.
305: *
306: * @param timestampField The name of the server-determined timestamp field.
307: */
308: public void setTimestampField(String timestampField) {
309: this .timestampField = timestampField;
310: }
311:
312: /**
313: * Sets the name of the field containing the virtual host information
314: * (this is in fact the server name).
315: *
316: * @param virtualHostField The name of the virtual host field.
317: */
318: public void setVirtualHostField(String virtualHostField) {
319: this .virtualHostField = virtualHostField;
320: }
321:
322: /**
323: * Sets the name of the field containing the HTTP request method.
324: *
325: * @param methodField The name of the HTTP request method field.
326: */
327: public void setMethodField(String methodField) {
328: this .methodField = methodField;
329: }
330:
331: /**
332: * Sets the name of the field containing the URL part of the HTTP query.
333: *
334: * @param queryField The name of the field containing the URL part of
335: * the HTTP query.
336: */
337: public void setQueryField(String queryField) {
338: this .queryField = queryField;
339: }
340:
341: /**
342: * Sets the name of the field containing the HTTP response status code.
343: *
344: * @param statusField The name of the HTTP response status code field.
345: */
346: public void setStatusField(String statusField) {
347: this .statusField = statusField;
348: }
349:
350: /**
351: * Sets the name of the field containing the number of bytes returned.
352: *
353: * @param bytesField The name of the returned bytes field.
354: */
355: public void setBytesField(String bytesField) {
356: this .bytesField = bytesField;
357: }
358:
359: /**
360: * Sets the name of the field containing the referer.
361: *
362: * @param refererField The referer field name.
363: */
364: public void setRefererField(String refererField) {
365: this .refererField = refererField;
366: }
367:
368: /**
369: * Sets the name of the field containing the user agent.
370: *
371: * @param userAgentField The name of the user agent field.
372: */
373: public void setUserAgentField(String userAgentField) {
374: this .userAgentField = userAgentField;
375: }
376:
377: /**
378: * Sets the logging pattern. The patterns supported correspond to the
379: * file-based "common" and "combined". These are translated into the use
380: * of tables containing either set of fields.
381: * <P><I>TO DO: more flexible field choices.</I></P>
382: *
383: * @param pattern The name of the logging pattern.
384: */
385: public void setPattern(String pattern) {
386: this .pattern = pattern;
387: }
388:
389: /**
390: * Determines whether IP host name resolution is done.
391: *
392: * @param resolveHosts "true" or "false", if host IP resolution
393: * is desired or not.
394: */
395: public void setResolveHosts(String resolveHosts) {
396: this .resolveHosts = new Boolean(resolveHosts).booleanValue();
397: }
398:
399: // --------------------------------------------------------- Public Methods
400:
401: /**
402: * This method is invoked by Tomcat on each query.
403: *
404: * @param request The Request object.
405: * @param response The Response object.
406: * @param context The ValveContext object.
407: * @exception IOException Should not be thrown.
408: * @exception ServletException Database SQLException is wrapped
409: * in a ServletException.
410: */
411: public void invoke(Request request, Response response,
412: ValveContext context) throws IOException, ServletException {
413:
414: context.invokeNext(request, response);
415:
416: ServletRequest req = request.getRequest();
417: HttpServletRequest hreq = null;
418: if (req instanceof HttpServletRequest)
419: hreq = (HttpServletRequest) req;
420: String remoteHost = "";
421: if (resolveHosts)
422: remoteHost = req.getRemoteHost();
423: else
424: remoteHost = req.getRemoteAddr();
425: String user = "";
426: if (hreq != null)
427: user = hreq.getRemoteUser();
428: String query = "";
429: if (hreq != null)
430: query = hreq.getRequestURI();
431: int bytes = response.getContentCount();
432: if (bytes < 0)
433: bytes = 0;
434: int status = ((HttpResponse) response).getStatus();
435: if (pattern.equals("combined")) {
436: String virtualHost = "";
437: if (hreq != null)
438: virtualHost = hreq.getServerName();
439: String method = "";
440: if (hreq != null)
441: method = hreq.getMethod();
442: String referer = "";
443: if (hreq != null)
444: referer = hreq.getHeader("referer");
445: String userAgent = "";
446: if (hreq != null)
447: userAgent = hreq.getHeader("user-agent");
448: }
449: synchronized (this ) {
450: int numberOfTries = 2;
451: while (numberOfTries > 0) {
452: try {
453: open();
454:
455: ps.setString(1, remoteHost);
456: ps.setString(2, user);
457: ps.setTimestamp(3, new Timestamp(
458: getCurrentTimeMillis()));
459: ps.setString(4, query);
460: ps.setInt(5, status);
461: ps.setInt(6, bytes);
462: if (pattern.equals("combined")) {
463:
464: String virtualHost = "";
465: if (hreq != null)
466: virtualHost = hreq.getServerName();
467: String method = "";
468: if (hreq != null)
469: method = hreq.getMethod();
470: String referer = "";
471: if (hreq != null)
472: referer = hreq.getHeader("referer");
473: String userAgent = "";
474: if (hreq != null)
475: userAgent = hreq.getHeader("user-agent");
476: ps.setString(7, virtualHost);
477: ps.setString(8, method);
478: ps.setString(9, referer);
479: ps.setString(10, userAgent);
480: }
481: ps.executeUpdate();
482: return;
483: } catch (SQLException e) {
484: // Log the problem for posterity
485: log(sm.getString("jdbcAccessLogValve.exception"), e);
486:
487: // Close the connection so that it gets reopened next time
488: if (conn != null)
489: close();
490: }
491: numberOfTries--;
492: }
493: }
494:
495: }
496:
497: /**
498: * Adds a Lifecycle listener.
499: *
500: * @param listener The listener to add.
501: */
502: public void addLifecycleListener(LifecycleListener listener) {
503:
504: lifecycle.addLifecycleListener(listener);
505:
506: }
507:
508: /**
509: * Get the lifecycle listeners associated with this lifecycle. If this
510: * Lifecycle has no listeners registered, a zero-length array is returned.
511: */
512: public LifecycleListener[] findLifecycleListeners() {
513:
514: return lifecycle.findLifecycleListeners();
515:
516: }
517:
518: /**
519: * Removes a Lifecycle listener.
520: *
521: * @param listener The listener to remove.
522: */
523: public void removeLifecycleListener(LifecycleListener listener) {
524:
525: lifecycle.removeLifecycleListener(listener);
526:
527: }
528:
529: /**
530: * Open (if necessary) and return a database connection for use by
531: * this AccessLogValve.
532: *
533: * @exception SQLException if a database error occurs
534: */
535: protected void open() throws SQLException {
536:
537: // Do nothing if there is a database connection already open
538: if (conn != null)
539: return;
540:
541: // Instantiate our database driver if necessary
542: if (driver == null) {
543: try {
544: Class clazz = Class.forName(driverName);
545: driver = (Driver) clazz.newInstance();
546: } catch (Throwable e) {
547: throw new SQLException(e.getMessage());
548: }
549: }
550:
551: // Open a new connection
552: Properties props = new Properties();
553: props.put("autoReconnect", "true");
554: if (connectionName != null)
555: props.put("user", connectionName);
556: if (connectionPassword != null)
557: props.put("password", connectionPassword);
558: conn = driver.connect(connectionURL, props);
559: conn.setAutoCommit(true);
560: if (pattern.equals("common")) {
561: ps = conn.prepareStatement("INSERT INTO " + tableName
562: + " (" + remoteHostField + ", " + userField + ", "
563: + timestampField + ", " + queryField + ", "
564: + statusField + ", " + bytesField
565: + ") VALUES(?, ?, ?, ?, ?, ?)");
566: } else if (pattern.equals("combined")) {
567: ps = conn.prepareStatement("INSERT INTO " + tableName
568: + " (" + remoteHostField + ", " + userField + ", "
569: + timestampField + ", " + queryField + ", "
570: + statusField + ", " + bytesField + ", "
571: + virtualHostField + ", " + methodField + ", "
572: + refererField + ", " + userAgentField
573: + ") VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?)");
574: }
575: }
576:
577: /**
578: * Close the specified database connection.
579: */
580: protected void close() {
581:
582: // Do nothing if the database connection is already closed
583: if (conn == null)
584: return;
585:
586: // Close our prepared statements (if any)
587: try {
588: ps.close();
589: } catch (Throwable f) {
590: ;
591: }
592: this .ps = null;
593:
594: // Close this database connection, and log any errors
595: try {
596: conn.close();
597: } catch (SQLException e) {
598: log(sm.getString("jdbcAccessLogValeve.close"), e); // Just log it here
599: } finally {
600: this .conn = null;
601: }
602:
603: }
604:
605: /**
606: * Invoked by Tomcat on startup. The database connection is set here.
607: *
608: * @exception LifecycleException Can be thrown on lifecycle
609: * inconsistencies or on database errors (as a wrapped SQLException).
610: */
611: public void start() throws LifecycleException {
612:
613: if (started)
614: throw new LifecycleException(sm
615: .getString("accessLogValve.alreadyStarted"));
616: lifecycle.fireLifecycleEvent(START_EVENT, null);
617: started = true;
618:
619: try {
620: open();
621: } catch (SQLException e) {
622: throw new LifecycleException(e);
623: }
624:
625: }
626:
627: /**
628: * Invoked by tomcat on shutdown. The database connection is closed here.
629: *
630: * @exception LifecycleException Can be thrown on lifecycle
631: * inconsistencies or on database errors (as a wrapped SQLException).
632: */
633: public void stop() throws LifecycleException {
634:
635: if (!started)
636: throw new LifecycleException(sm
637: .getString("accessLogValve.notStarted"));
638: lifecycle.fireLifecycleEvent(STOP_EVENT, null);
639: started = false;
640:
641: close();
642:
643: }
644:
645: public long getCurrentTimeMillis() {
646: long systime = System.currentTimeMillis();
647: if ((systime - currentTimeMillis) > 1000) {
648: currentTimeMillis = new java.util.Date(systime).getTime();
649: }
650: return currentTimeMillis;
651: }
652:
653: /**
654: * Log a message on the Logger associated with our Container (if any)
655: *
656: * @param message Message to be logged
657: */
658: protected void log(String message) {
659:
660: Logger logger = null;
661: String name = null;
662: if (container != null) {
663: logger = container.getLogger();
664: name = container.getName();
665: }
666:
667: if (logger != null) {
668: logger.log(connectionURL + "[" + name + "]: " + message);
669: } else {
670: System.out.println(connectionURL + "[" + name + "]: "
671: + message);
672: }
673:
674: }
675:
676: /**
677: * Log a message on the Logger associated with our Container (if any)
678: *
679: * @param message Message to be logged
680: * @param throwable Associated exception
681: */
682: protected void log(String message, Throwable throwable) {
683:
684: Logger logger = null;
685: String name = null;
686: if (container != null) {
687: logger = container.getLogger();
688: name = container.getName();
689: }
690:
691: if (logger != null) {
692: logger.log(connectionURL + "[" + name + "]: " + message,
693: throwable);
694: } else {
695: System.out.println(connectionURL + "[" + name + "]: "
696: + message);
697: throwable.printStackTrace(System.out);
698: }
699: }
700:
701: }
|