001: /*
002: * Licensed to the Apache Software Foundation (ASF) under one or more
003: * contributor license agreements. See the NOTICE file distributed with
004: * this work for additional information regarding copyright ownership.
005: * The ASF licenses this file to You under the Apache License, Version 2.0
006: * (the "License"); you may not use this file except in compliance with
007: * the License. You may obtain a copy of the License at
008: *
009: * http://www.apache.org/licenses/LICENSE-2.0
010: *
011: * Unless required by applicable law or agreed to in writing, software
012: * distributed under the License is distributed on an "AS IS" BASIS,
013: * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014: * See the License for the specific language governing permissions and
015: * limitations under the License.
016: */
017:
018: package org.apache.catalina.valves;
019:
020: import java.io.IOException;
021: import java.sql.Connection;
022: import java.sql.Driver;
023: import java.sql.PreparedStatement;
024: import java.sql.SQLException;
025: import java.sql.Timestamp;
026: import java.util.Properties;
027:
028: import javax.servlet.ServletException;
029:
030: import org.apache.catalina.Lifecycle;
031: import org.apache.catalina.LifecycleException;
032: import org.apache.catalina.LifecycleListener;
033: import org.apache.catalina.connector.Request;
034: import org.apache.catalina.connector.Response;
035: import org.apache.catalina.util.LifecycleSupport;
036: import org.apache.catalina.util.StringManager;
037:
038: /**
039: * <p>
040: * This Tomcat extension logs server access directly to a database, and can
041: * be used instead of the regular file-based access log implemented in
042: * AccessLogValve.
043: * To use, copy into the server/classes directory of the Tomcat installation
044: * and configure in server.xml as:
045: * <pre>
046: * <Valve className="org.apache.catalina.valves.JDBCAccessLogValve"
047: * driverName="<i>your_jdbc_driver</i>"
048: * connectionURL="<i>your_jdbc_url</i>"
049: * pattern="combined" resolveHosts="false"
050: * />
051: * </pre>
052: * </p>
053: * <p>
054: * Many parameters can be configured, such as the database connection (with
055: * <code>driverName</code> and <code>connectionURL</code>),
056: * the table name (<code>tableName</code>)
057: * and the field names (corresponding to the get/set method names).
058: * The same options as AccessLogValve are supported, such as
059: * <code>resolveHosts</code> and <code>pattern</code> ("common" or "combined"
060: * only).
061: * </p>
062: * <p>
063: * When Tomcat is started, a database connection (with autoReconnect option)
064: * is created and used for all the log activity. When Tomcat is shutdown, the
065: * database connection is closed.
066: * This logger can be used at the level of the Engine context (being shared
067: * by all the defined hosts) or the Host context (one instance of the logger
068: * per host, possibly using different databases).
069: * </p>
070: * <p>
071: * The database table can be created with the following command:
072: * </p>
073: * <pre>
074: * CREATE TABLE access (
075: * id INT UNSIGNED AUTO_INCREMENT NOT NULL,
076: * remoteHost CHAR(15) NOT NULL,
077: * userName CHAR(15),
078: * timestamp TIMESTAMP NOT NULL,
079: * virtualHost VARCHAR(64) NOT NULL,
080: * method VARCHAR(8) NOT NULL,
081: * query VARCHAR(255) NOT NULL,
082: * status SMALLINT UNSIGNED NOT NULL,
083: * bytes INT UNSIGNED NOT NULL,
084: * referer VARCHAR(128),
085: * userAgent VARCHAR(128),
086: * PRIMARY KEY (id),
087: * INDEX (timestamp),
088: * INDEX (remoteHost),
089: * INDEX (virtualHost),
090: * INDEX (query),
091: * INDEX (userAgent)
092: * );
093: * </pre>
094: * <p>
095: * If the table is created as above, its name and the field names don't need
096: * to be defined.
097: * </p>
098: * <p>
099: * If the request method is "common", only these fields are used:
100: * <code>remoteHost, user, timeStamp, query, status, bytes</code>
101: * </p>
102: * <p>
103: * <i>TO DO: provide option for excluding logging of certain MIME types.</i>
104: * </p>
105: *
106: * @author Andre de Jesus
107: * @author Peter Rossbach
108: */
109:
110: public final class JDBCAccessLogValve extends ValveBase implements
111: Lifecycle {
112:
113: // ----------------------------------------------------------- Constructors
114:
115: /**
116: * Class constructor. Initializes the fields with the default values.
117: * The defaults are:
118: * <pre>
119: * driverName = null;
120: * connectionURL = null;
121: * tableName = "access";
122: * remoteHostField = "remoteHost";
123: * userField = "userName";
124: * timestampField = "timestamp";
125: * virtualHostField = "virtualHost";
126: * methodField = "method";
127: * queryField = "query";
128: * statusField = "status";
129: * bytesField = "bytes";
130: * refererField = "referer";
131: * userAgentField = "userAgent";
132: * pattern = "common";
133: * resolveHosts = false;
134: * </pre>
135: */
136: public JDBCAccessLogValve() {
137: super ();
138: driverName = null;
139: connectionURL = null;
140: tableName = "access";
141: remoteHostField = "remoteHost";
142: userField = "userName";
143: timestampField = "timestamp";
144: virtualHostField = "virtualHost";
145: methodField = "method";
146: queryField = "query";
147: statusField = "status";
148: bytesField = "bytes";
149: refererField = "referer";
150: userAgentField = "userAgent";
151: pattern = "common";
152: resolveHosts = false;
153: conn = null;
154: ps = null;
155: currentTimeMillis = new java.util.Date().getTime();
156: }
157:
158: // ----------------------------------------------------- Instance Variables
159:
160: /**
161: * The connection username to use when trying to connect to the database.
162: */
163: protected String connectionName = null;
164:
165: /**
166: * The connection URL to use when trying to connect to the database.
167: */
168: protected String connectionPassword = null;
169:
170: /**
171: * Instance of the JDBC Driver class we use as a connection factory.
172: */
173: protected Driver driver = null;
174:
175: private String driverName;
176: private String connectionURL;
177: private String tableName;
178: private String remoteHostField;
179: private String userField;
180: private String timestampField;
181: private String virtualHostField;
182: private String methodField;
183: private String queryField;
184: private String statusField;
185: private String bytesField;
186: private String refererField;
187: private String userAgentField;
188: private String pattern;
189: private boolean resolveHosts;
190:
191: private Connection conn;
192: private PreparedStatement ps;
193:
194: private long currentTimeMillis;
195:
196: /**
197: * The descriptive information about this implementation.
198: */
199: protected static String info = "org.apache.catalina.valves.JDBCAccessLogValve/1.1";
200:
201: /**
202: * The lifecycle event support for this component.
203: */
204: protected LifecycleSupport lifecycle = new LifecycleSupport(this );
205:
206: /**
207: * The string manager for this package.
208: */
209: private StringManager sm = StringManager
210: .getManager(Constants.Package);
211:
212: /**
213: * Has this component been started yet?
214: */
215: private boolean started = false;
216:
217: // ------------------------------------------------------------- Properties
218:
219: /**
220: * Return the username to use to connect to the database.
221: *
222: */
223: public String getConnectionName() {
224: return connectionName;
225: }
226:
227: /**
228: * Set the username to use to connect to the database.
229: *
230: * @param connectionName Username
231: */
232: public void setConnectionName(String connectionName) {
233: this .connectionName = connectionName;
234: }
235:
236: /**
237: * Sets the database driver name.
238: *
239: * @param driverName The complete name of the database driver class.
240: */
241: public void setDriverName(String driverName) {
242: this .driverName = driverName;
243: }
244:
245: /**
246: * Return the password to use to connect to the database.
247: *
248: */
249: public String getConnectionPassword() {
250: return connectionPassword;
251: }
252:
253: /**
254: * Set the password to use to connect to the database.
255: *
256: * @param connectionPassword User password
257: */
258: public void setConnectionPassword(String connectionPassword) {
259: this .connectionPassword = connectionPassword;
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: *
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: throws IOException, ServletException {
408:
409: getNext().invoke(request, response);
410:
411: String remoteHost = "";
412: if (resolveHosts)
413: remoteHost = request.getRemoteHost();
414: else
415: remoteHost = request.getRemoteAddr();
416: String user = "";
417: if (request != null)
418: user = request.getRemoteUser();
419: String query = "";
420: if (request != null)
421: query = request.getRequestURI();
422: int bytes = response.getContentCount();
423: if (bytes < 0)
424: bytes = 0;
425: int status = response.getStatus();
426: if (pattern.equals("combined")) {
427: String virtualHost = "";
428: if (request != null)
429: virtualHost = request.getServerName();
430: String method = "";
431: if (request != null)
432: method = request.getMethod();
433: String referer = "";
434: if (request != null)
435: referer = request.getHeader("referer");
436: String userAgent = "";
437: if (request != null)
438: userAgent = request.getHeader("user-agent");
439: }
440: synchronized (this ) {
441: int numberOfTries = 2;
442: while (numberOfTries > 0) {
443: try {
444: open();
445:
446: ps.setString(1, remoteHost);
447: ps.setString(2, user);
448: ps.setTimestamp(3, new Timestamp(
449: getCurrentTimeMillis()));
450: ps.setString(4, query);
451: ps.setInt(5, status);
452: ps.setInt(6, bytes);
453: if (pattern.equals("combined")) {
454:
455: String virtualHost = "";
456: if (request != null)
457: virtualHost = request.getServerName();
458: String method = "";
459: if (request != null)
460: method = request.getMethod();
461: String referer = "";
462: if (request != null)
463: referer = request.getHeader("referer");
464: String userAgent = "";
465: if (request != null)
466: userAgent = request.getHeader("user-agent");
467: ps.setString(7, virtualHost);
468: ps.setString(8, method);
469: ps.setString(9, referer);
470: ps.setString(10, userAgent);
471: }
472: ps.executeUpdate();
473: return;
474: } catch (SQLException e) {
475: // Log the problem for posterity
476: container
477: .getLogger()
478: .error(
479: sm
480: .getString("jdbcAccessLogValve.exception"),
481: e);
482:
483: // Close the connection so that it gets reopened next time
484: if (conn != null)
485: close();
486: }
487: numberOfTries--;
488: }
489: }
490:
491: }
492:
493: /**
494: * Adds a Lifecycle listener.
495: *
496: * @param listener The listener to add.
497: */
498: public void addLifecycleListener(LifecycleListener listener) {
499:
500: lifecycle.addLifecycleListener(listener);
501:
502: }
503:
504: /**
505: * Get the lifecycle listeners associated with this lifecycle. If this
506: * Lifecycle has no listeners registered, a zero-length array is returned.
507: */
508: public LifecycleListener[] findLifecycleListeners() {
509:
510: return lifecycle.findLifecycleListeners();
511:
512: }
513:
514: /**
515: * Removes a Lifecycle listener.
516: *
517: * @param listener The listener to remove.
518: */
519: public void removeLifecycleListener(LifecycleListener listener) {
520:
521: lifecycle.removeLifecycleListener(listener);
522:
523: }
524:
525: /**
526: * Open (if necessary) and return a database connection for use by
527: * this AccessLogValve.
528: *
529: * @exception SQLException if a database error occurs
530: */
531: protected void open() throws SQLException {
532:
533: // Do nothing if there is a database connection already open
534: if (conn != null)
535: return;
536:
537: // Instantiate our database driver if necessary
538: if (driver == null) {
539: try {
540: Class clazz = Class.forName(driverName);
541: driver = (Driver) clazz.newInstance();
542: } catch (Throwable e) {
543: throw new SQLException(e.getMessage());
544: }
545: }
546:
547: // Open a new connection
548: Properties props = new Properties();
549: props.put("autoReconnect", "true");
550: if (connectionName != null)
551: props.put("user", connectionName);
552: if (connectionPassword != null)
553: props.put("password", connectionPassword);
554: conn = driver.connect(connectionURL, props);
555: conn.setAutoCommit(true);
556: if (pattern.equals("common")) {
557: ps = conn.prepareStatement("INSERT INTO " + tableName
558: + " (" + remoteHostField + ", " + userField + ", "
559: + timestampField + ", " + queryField + ", "
560: + statusField + ", " + bytesField
561: + ") VALUES(?, ?, ?, ?, ?, ?)");
562: } else if (pattern.equals("combined")) {
563: ps = conn.prepareStatement("INSERT INTO " + tableName
564: + " (" + remoteHostField + ", " + userField + ", "
565: + timestampField + ", " + queryField + ", "
566: + statusField + ", " + bytesField + ", "
567: + virtualHostField + ", " + methodField + ", "
568: + refererField + ", " + userAgentField
569: + ") VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?)");
570: }
571: }
572:
573: /**
574: * Close the specified database connection.
575: */
576: protected void close() {
577:
578: // Do nothing if the database connection is already closed
579: if (conn == null)
580: return;
581:
582: // Close our prepared statements (if any)
583: try {
584: ps.close();
585: } catch (Throwable f) {
586: ;
587: }
588: this .ps = null;
589:
590: // Close this database connection, and log any errors
591: try {
592: conn.close();
593: } catch (SQLException e) {
594: container.getLogger().error(
595: sm.getString("jdbcAccessLogValeve.close"), e); // Just log it here
596: } finally {
597: this .conn = null;
598: }
599:
600: }
601:
602: /**
603: * Invoked by Tomcat on startup. The database connection is set here.
604: *
605: * @exception LifecycleException Can be thrown on lifecycle
606: * inconsistencies or on database errors (as a wrapped SQLException).
607: */
608: public void start() throws LifecycleException {
609:
610: if (started)
611: throw new LifecycleException(sm
612: .getString("accessLogValve.alreadyStarted"));
613: lifecycle.fireLifecycleEvent(START_EVENT, null);
614: started = true;
615:
616: try {
617: open();
618: } catch (SQLException e) {
619: throw new LifecycleException(e);
620: }
621:
622: }
623:
624: /**
625: * Invoked by tomcat on shutdown. The database connection is closed here.
626: *
627: * @exception LifecycleException Can be thrown on lifecycle
628: * inconsistencies or on database errors (as a wrapped SQLException).
629: */
630: public void stop() throws LifecycleException {
631:
632: if (!started)
633: throw new LifecycleException(sm
634: .getString("accessLogValve.notStarted"));
635: lifecycle.fireLifecycleEvent(STOP_EVENT, null);
636: started = false;
637:
638: close();
639:
640: }
641:
642: public long getCurrentTimeMillis() {
643: long systime = System.currentTimeMillis();
644: if ((systime - currentTimeMillis) > 1000) {
645: currentTimeMillis = new java.util.Date(systime).getTime();
646: }
647: return currentTimeMillis;
648: }
649:
650: }
|