685 lines
20 KiB
Java
685 lines
20 KiB
Java
/*
|
|
* Licensed to the Apache Software Foundation (ASF) under one or more
|
|
* contributor license agreements. See the NOTICE file distributed with
|
|
* this work for additional information regarding copyright ownership.
|
|
* The ASF licenses this file to You 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.apache.catalina.valves;
|
|
|
|
|
|
import java.io.IOException;
|
|
import java.sql.Connection;
|
|
import java.sql.Driver;
|
|
import java.sql.PreparedStatement;
|
|
import java.sql.SQLException;
|
|
import java.sql.Timestamp;
|
|
import java.util.Properties;
|
|
|
|
import javax.servlet.ServletException;
|
|
|
|
import org.apache.catalina.AccessLog;
|
|
import org.apache.catalina.LifecycleException;
|
|
import org.apache.catalina.LifecycleState;
|
|
import org.apache.catalina.connector.Request;
|
|
import org.apache.catalina.connector.Response;
|
|
import org.apache.tomcat.util.ExceptionUtils;
|
|
|
|
/**
|
|
* <p>
|
|
* This Tomcat extension logs server access directly to a database, and can
|
|
* be used instead of the regular file-based access log implemented in
|
|
* AccessLogValve.
|
|
* To use, copy into the server/classes directory of the Tomcat installation
|
|
* and configure in server.xml as:
|
|
* </p>
|
|
* <pre>
|
|
* <Valve className="org.apache.catalina.valves.JDBCAccessLogValve"
|
|
* driverName="<i>your_jdbc_driver</i>"
|
|
* connectionURL="<i>your_jdbc_url</i>"
|
|
* pattern="combined" resolveHosts="false"
|
|
* />
|
|
* </pre>
|
|
* <p>
|
|
* Many parameters can be configured, such as the database connection (with
|
|
* <code>driverName</code> and <code>connectionURL</code>),
|
|
* the table name (<code>tableName</code>)
|
|
* and the field names (corresponding to the get/set method names).
|
|
* The same options as AccessLogValve are supported, such as
|
|
* <code>resolveHosts</code> and <code>pattern</code> ("common" or "combined"
|
|
* only).
|
|
* </p>
|
|
* <p>
|
|
* When Tomcat is started, a database connection is created and used for all the
|
|
* log activity. When Tomcat is shutdown, the database connection is closed.
|
|
* This logger can be used at the level of the Engine context (being shared
|
|
* by all the defined hosts) or the Host context (one instance of the logger
|
|
* per host, possibly using different databases).
|
|
* </p>
|
|
* <p>
|
|
* The database table can be created with the following command:
|
|
* </p>
|
|
* <pre>
|
|
* CREATE TABLE access (
|
|
* id INT UNSIGNED AUTO_INCREMENT NOT NULL,
|
|
* remoteHost CHAR(15) NOT NULL,
|
|
* userName CHAR(15),
|
|
* timestamp TIMESTAMP NOT NULL,
|
|
* virtualHost VARCHAR(64) NOT NULL,
|
|
* method VARCHAR(8) NOT NULL,
|
|
* query VARCHAR(255) NOT NULL,
|
|
* status SMALLINT UNSIGNED NOT NULL,
|
|
* bytes INT UNSIGNED NOT NULL,
|
|
* referer VARCHAR(128),
|
|
* userAgent VARCHAR(128),
|
|
* PRIMARY KEY (id),
|
|
* INDEX (timestamp),
|
|
* INDEX (remoteHost),
|
|
* INDEX (virtualHost),
|
|
* INDEX (query),
|
|
* INDEX (userAgent)
|
|
* );
|
|
* </pre>
|
|
* <p>Set JDBCAccessLogValve attribute useLongContentLength="true" as you have more then 4GB outputs.
|
|
* Please, use long SQL datatype at access.bytes attribute.
|
|
* The datatype of bytes at oracle is <i>number</i> and other databases use <i>bytes BIGINT NOT NULL</i>.
|
|
* </p>
|
|
*
|
|
* <p>
|
|
* If the table is created as above, its name and the field names don't need
|
|
* to be defined.
|
|
* </p>
|
|
* <p>
|
|
* If the request method is "common", only these fields are used:
|
|
* <code>remoteHost, user, timeStamp, query, status, bytes</code>
|
|
* </p>
|
|
* <p>
|
|
* <i>TO DO: provide option for excluding logging of certain MIME types.</i>
|
|
* </p>
|
|
*
|
|
* @author Andre de Jesus
|
|
* @author Peter Rossbach
|
|
*/
|
|
|
|
public final class JDBCAccessLogValve extends ValveBase implements AccessLog {
|
|
|
|
// ----------------------------------------------------------- Constructors
|
|
|
|
|
|
/**
|
|
* Class constructor. Initializes the fields with the default values.
|
|
* The defaults are:
|
|
* <pre>
|
|
* driverName = null;
|
|
* connectionURL = null;
|
|
* tableName = "access";
|
|
* remoteHostField = "remoteHost";
|
|
* userField = "userName";
|
|
* timestampField = "timestamp";
|
|
* virtualHostField = "virtualHost";
|
|
* methodField = "method";
|
|
* queryField = "query";
|
|
* statusField = "status";
|
|
* bytesField = "bytes";
|
|
* refererField = "referer";
|
|
* userAgentField = "userAgent";
|
|
* pattern = "common";
|
|
* resolveHosts = false;
|
|
* </pre>
|
|
*/
|
|
public JDBCAccessLogValve() {
|
|
super(true);
|
|
driverName = null;
|
|
connectionURL = null;
|
|
tableName = "access";
|
|
remoteHostField = "remoteHost";
|
|
userField = "userName";
|
|
timestampField = "timestamp";
|
|
virtualHostField = "virtualHost";
|
|
methodField = "method";
|
|
queryField = "query";
|
|
statusField = "status";
|
|
bytesField = "bytes";
|
|
refererField = "referer";
|
|
userAgentField = "userAgent";
|
|
pattern = "common";
|
|
resolveHosts = false;
|
|
conn = null;
|
|
ps = null;
|
|
currentTimeMillis = new java.util.Date().getTime();
|
|
}
|
|
|
|
|
|
// ----------------------------------------------------- Instance Variables
|
|
|
|
/**
|
|
* Use long contentLength as you have more 4 GB output.
|
|
* @since 6.0.15
|
|
*/
|
|
boolean useLongContentLength = false;
|
|
|
|
/**
|
|
* The connection username to use when trying to connect to the database.
|
|
*/
|
|
String connectionName = null;
|
|
|
|
|
|
/**
|
|
* The connection URL to use when trying to connect to the database.
|
|
*/
|
|
String connectionPassword = null;
|
|
|
|
/**
|
|
* Instance of the JDBC Driver class we use as a connection factory.
|
|
*/
|
|
Driver driver = null;
|
|
|
|
|
|
private String driverName;
|
|
private String connectionURL;
|
|
private String tableName;
|
|
private String remoteHostField;
|
|
private String userField;
|
|
private String timestampField;
|
|
private String virtualHostField;
|
|
private String methodField;
|
|
private String queryField;
|
|
private String statusField;
|
|
private String bytesField;
|
|
private String refererField;
|
|
private String userAgentField;
|
|
private String pattern;
|
|
private boolean resolveHosts;
|
|
|
|
|
|
private Connection conn;
|
|
private PreparedStatement ps;
|
|
|
|
|
|
private long currentTimeMillis;
|
|
|
|
/**
|
|
* Should this valve set request attributes for IP address, hostname,
|
|
* protocol and port used for the request.
|
|
* Default is <code>true</code>.
|
|
* @see #setRequestAttributesEnabled(boolean)
|
|
*/
|
|
boolean requestAttributesEnabled = true;
|
|
|
|
|
|
// ------------------------------------------------------------- Properties
|
|
|
|
/**
|
|
* {@inheritDoc}
|
|
* Default is <code>true</code>.
|
|
*/
|
|
@Override
|
|
public void setRequestAttributesEnabled(boolean requestAttributesEnabled) {
|
|
this.requestAttributesEnabled = requestAttributesEnabled;
|
|
}
|
|
|
|
/**
|
|
* {@inheritDoc}
|
|
*/
|
|
@Override
|
|
public boolean getRequestAttributesEnabled() {
|
|
return requestAttributesEnabled;
|
|
}
|
|
|
|
/**
|
|
* @return the username to use to connect to the database.
|
|
*/
|
|
public String getConnectionName() {
|
|
return connectionName;
|
|
}
|
|
|
|
/**
|
|
* Set the username to use to connect to the database.
|
|
*
|
|
* @param connectionName Username
|
|
*/
|
|
public void setConnectionName(String connectionName) {
|
|
this.connectionName = connectionName;
|
|
}
|
|
|
|
/**
|
|
* Sets the database driver name.
|
|
*
|
|
* @param driverName The complete name of the database driver class.
|
|
*/
|
|
public void setDriverName(String driverName) {
|
|
this.driverName = driverName;
|
|
}
|
|
|
|
/**
|
|
* @return the password to use to connect to the database.
|
|
*/
|
|
public String getConnectionPassword() {
|
|
return connectionPassword;
|
|
}
|
|
|
|
/**
|
|
* Set the password to use to connect to the database.
|
|
*
|
|
* @param connectionPassword User password
|
|
*/
|
|
public void setConnectionPassword(String connectionPassword) {
|
|
this.connectionPassword = connectionPassword;
|
|
}
|
|
|
|
/**
|
|
* Sets the JDBC URL for the database where the log is stored.
|
|
*
|
|
* @param connectionURL The JDBC URL of the database.
|
|
*/
|
|
public void setConnectionURL(String connectionURL) {
|
|
this.connectionURL = connectionURL;
|
|
}
|
|
|
|
|
|
/**
|
|
* Sets the name of the table where the logs are stored.
|
|
*
|
|
* @param tableName The name of the table.
|
|
*/
|
|
public void setTableName(String tableName) {
|
|
this.tableName = tableName;
|
|
}
|
|
|
|
|
|
/**
|
|
* Sets the name of the field containing the remote host.
|
|
*
|
|
* @param remoteHostField The name of the remote host field.
|
|
*/
|
|
public void setRemoteHostField(String remoteHostField) {
|
|
this.remoteHostField = remoteHostField;
|
|
}
|
|
|
|
|
|
/**
|
|
* Sets the name of the field containing the remote user name.
|
|
*
|
|
* @param userField The name of the remote user field.
|
|
*/
|
|
public void setUserField(String userField) {
|
|
this.userField = userField;
|
|
}
|
|
|
|
|
|
/**
|
|
* Sets the name of the field containing the server-determined timestamp.
|
|
*
|
|
* @param timestampField The name of the server-determined timestamp field.
|
|
*/
|
|
public void setTimestampField(String timestampField) {
|
|
this.timestampField = timestampField;
|
|
}
|
|
|
|
|
|
/**
|
|
* Sets the name of the field containing the virtual host information
|
|
* (this is in fact the server name).
|
|
*
|
|
* @param virtualHostField The name of the virtual host field.
|
|
*/
|
|
public void setVirtualHostField(String virtualHostField) {
|
|
this.virtualHostField = virtualHostField;
|
|
}
|
|
|
|
|
|
/**
|
|
* Sets the name of the field containing the HTTP request method.
|
|
*
|
|
* @param methodField The name of the HTTP request method field.
|
|
*/
|
|
public void setMethodField(String methodField) {
|
|
this.methodField = methodField;
|
|
}
|
|
|
|
|
|
/**
|
|
* Sets the name of the field containing the URL part of the HTTP query.
|
|
*
|
|
* @param queryField The name of the field containing the URL part of
|
|
* the HTTP query.
|
|
*/
|
|
public void setQueryField(String queryField) {
|
|
this.queryField = queryField;
|
|
}
|
|
|
|
|
|
/**
|
|
* Sets the name of the field containing the HTTP response status code.
|
|
*
|
|
* @param statusField The name of the HTTP response status code field.
|
|
*/
|
|
public void setStatusField(String statusField) {
|
|
this.statusField = statusField;
|
|
}
|
|
|
|
|
|
/**
|
|
* Sets the name of the field containing the number of bytes returned.
|
|
*
|
|
* @param bytesField The name of the returned bytes field.
|
|
*/
|
|
public void setBytesField(String bytesField) {
|
|
this.bytesField = bytesField;
|
|
}
|
|
|
|
|
|
/**
|
|
* Sets the name of the field containing the referer.
|
|
*
|
|
* @param refererField The referer field name.
|
|
*/
|
|
public void setRefererField(String refererField) {
|
|
this.refererField = refererField;
|
|
}
|
|
|
|
|
|
/**
|
|
* Sets the name of the field containing the user agent.
|
|
*
|
|
* @param userAgentField The name of the user agent field.
|
|
*/
|
|
public void setUserAgentField(String userAgentField) {
|
|
this.userAgentField = userAgentField;
|
|
}
|
|
|
|
|
|
/**
|
|
* Sets the logging pattern. The patterns supported correspond to the
|
|
* file-based "common" and "combined". These are translated into the use
|
|
* of tables containing either set of fields.
|
|
* <P><I>TO DO: more flexible field choices.</I></P>
|
|
*
|
|
* @param pattern The name of the logging pattern.
|
|
*/
|
|
public void setPattern(String pattern) {
|
|
this.pattern = pattern;
|
|
}
|
|
|
|
|
|
/**
|
|
* Determines whether IP host name resolution is done.
|
|
*
|
|
* @param resolveHosts "true" or "false", if host IP resolution
|
|
* is desired or not.
|
|
*/
|
|
public void setResolveHosts(String resolveHosts) {
|
|
this.resolveHosts = Boolean.parseBoolean(resolveHosts);
|
|
}
|
|
|
|
/**
|
|
* @return <code>true</code> if content length should be considered a long
|
|
* rather than an int, defaults to <code>false</code>
|
|
*/
|
|
public boolean getUseLongContentLength() {
|
|
return this.useLongContentLength;
|
|
}
|
|
|
|
/**
|
|
* @param useLongContentLength the useLongContentLength to set
|
|
*/
|
|
public void setUseLongContentLength(boolean useLongContentLength) {
|
|
this.useLongContentLength = useLongContentLength;
|
|
}
|
|
|
|
// --------------------------------------------------------- Public Methods
|
|
|
|
|
|
/**
|
|
* This method is invoked by Tomcat on each query.
|
|
*
|
|
* @param request The Request object.
|
|
* @param response The Response object.
|
|
*
|
|
* @exception IOException Should not be thrown.
|
|
* @exception ServletException Database SQLException is wrapped
|
|
* in a ServletException.
|
|
*/
|
|
@Override
|
|
public void invoke(Request request, Response response) throws IOException,
|
|
ServletException {
|
|
getNext().invoke(request, response);
|
|
}
|
|
|
|
|
|
@Override
|
|
public void log(Request request, Response response, long time) {
|
|
if (!getState().isAvailable()) {
|
|
return;
|
|
}
|
|
|
|
final String EMPTY = "" ;
|
|
|
|
String remoteHost;
|
|
if(resolveHosts) {
|
|
if (requestAttributesEnabled) {
|
|
Object host = request.getAttribute(REMOTE_HOST_ATTRIBUTE);
|
|
if (host == null) {
|
|
remoteHost = request.getRemoteHost();
|
|
} else {
|
|
remoteHost = (String) host;
|
|
}
|
|
} else {
|
|
remoteHost = request.getRemoteHost();
|
|
}
|
|
} else {
|
|
if (requestAttributesEnabled) {
|
|
Object addr = request.getAttribute(REMOTE_ADDR_ATTRIBUTE);
|
|
if (addr == null) {
|
|
remoteHost = request.getRemoteAddr();
|
|
} else {
|
|
remoteHost = (String) addr;
|
|
}
|
|
} else {
|
|
remoteHost = request.getRemoteAddr();
|
|
}
|
|
}
|
|
String user = request.getRemoteUser();
|
|
String query=request.getRequestURI();
|
|
|
|
long bytes = response.getBytesWritten(true);
|
|
if(bytes < 0) {
|
|
bytes = 0;
|
|
}
|
|
int status = response.getStatus();
|
|
String virtualHost = EMPTY;
|
|
String method = EMPTY;
|
|
String referer = EMPTY;
|
|
String userAgent = EMPTY;
|
|
String logPattern = pattern;
|
|
if (logPattern.equals("combined")) {
|
|
virtualHost = request.getServerName();
|
|
method = request.getMethod();
|
|
referer = request.getHeader("referer");
|
|
userAgent = request.getHeader("user-agent");
|
|
}
|
|
synchronized (this) {
|
|
int numberOfTries = 2;
|
|
while (numberOfTries>0) {
|
|
try {
|
|
open();
|
|
|
|
ps.setString(1, remoteHost);
|
|
ps.setString(2, user);
|
|
ps.setTimestamp(3, new Timestamp(getCurrentTimeMillis()));
|
|
ps.setString(4, query);
|
|
ps.setInt(5, status);
|
|
|
|
if(useLongContentLength) {
|
|
ps.setLong(6, bytes);
|
|
} else {
|
|
if (bytes > Integer.MAX_VALUE) {
|
|
bytes = -1 ;
|
|
}
|
|
ps.setInt(6, (int) bytes);
|
|
}
|
|
if (logPattern.equals("combined")) {
|
|
ps.setString(7, virtualHost);
|
|
ps.setString(8, method);
|
|
ps.setString(9, referer);
|
|
ps.setString(10, userAgent);
|
|
}
|
|
ps.executeUpdate();
|
|
return;
|
|
} catch (SQLException e) {
|
|
// Log the problem for posterity
|
|
container.getLogger().error(sm.getString("jdbcAccessLogValve.exception"), e);
|
|
|
|
// Close the connection so that it gets reopened next time
|
|
if (conn != null) {
|
|
close();
|
|
}
|
|
}
|
|
numberOfTries--;
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
|
|
/**
|
|
* Open (if necessary) and return a database connection for use by
|
|
* this AccessLogValve.
|
|
*
|
|
* @exception SQLException if a database error occurs
|
|
*/
|
|
protected void open() throws SQLException {
|
|
|
|
// Do nothing if there is a database connection already open
|
|
if (conn != null) {
|
|
return ;
|
|
}
|
|
|
|
// Instantiate our database driver if necessary
|
|
if (driver == null) {
|
|
try {
|
|
Class<?> clazz = Class.forName(driverName);
|
|
driver = (Driver) clazz.getConstructor().newInstance();
|
|
} catch (Throwable e) {
|
|
ExceptionUtils.handleThrowable(e);
|
|
throw new SQLException(e.getMessage(), e);
|
|
}
|
|
}
|
|
|
|
// Open a new connection
|
|
Properties props = new Properties();
|
|
if (connectionName != null) {
|
|
props.put("user", connectionName);
|
|
}
|
|
if (connectionPassword != null) {
|
|
props.put("password", connectionPassword);
|
|
}
|
|
conn = driver.connect(connectionURL, props);
|
|
conn.setAutoCommit(true);
|
|
String logPattern = pattern;
|
|
if (logPattern.equals("common")) {
|
|
ps = conn.prepareStatement
|
|
("INSERT INTO " + tableName + " ("
|
|
+ remoteHostField + ", " + userField + ", "
|
|
+ timestampField +", " + queryField + ", "
|
|
+ statusField + ", " + bytesField
|
|
+ ") VALUES(?, ?, ?, ?, ?, ?)");
|
|
} else if (logPattern.equals("combined")) {
|
|
ps = conn.prepareStatement
|
|
("INSERT INTO " + tableName + " ("
|
|
+ remoteHostField + ", " + userField + ", "
|
|
+ timestampField + ", " + queryField + ", "
|
|
+ statusField + ", " + bytesField + ", "
|
|
+ virtualHostField + ", " + methodField + ", "
|
|
+ refererField + ", " + userAgentField
|
|
+ ") VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?)");
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Close the specified database connection.
|
|
*/
|
|
protected void close() {
|
|
|
|
// Do nothing if the database connection is already closed
|
|
if (conn == null) {
|
|
return;
|
|
}
|
|
|
|
// Close our prepared statements (if any)
|
|
try {
|
|
ps.close();
|
|
} catch (Throwable f) {
|
|
ExceptionUtils.handleThrowable(f);
|
|
}
|
|
this.ps = null;
|
|
|
|
|
|
|
|
// Close this database connection, and log any errors
|
|
try {
|
|
conn.close();
|
|
} catch (SQLException e) {
|
|
container.getLogger().error(sm.getString("jdbcAccessLogValve.close"), e); // Just log it here
|
|
} finally {
|
|
this.conn = null;
|
|
}
|
|
|
|
}
|
|
|
|
|
|
/**
|
|
* Start this component and implement the requirements
|
|
* of {@link org.apache.catalina.util.LifecycleBase#startInternal()}.
|
|
*
|
|
* @exception LifecycleException if this component detects a fatal error
|
|
* that prevents this component from being used
|
|
*/
|
|
@Override
|
|
protected synchronized void startInternal() throws LifecycleException {
|
|
|
|
try {
|
|
open() ;
|
|
} catch (SQLException e) {
|
|
throw new LifecycleException(e);
|
|
}
|
|
|
|
setState(LifecycleState.STARTING);
|
|
}
|
|
|
|
|
|
/**
|
|
* Stop this component and implement the requirements
|
|
* of {@link org.apache.catalina.util.LifecycleBase#stopInternal()}.
|
|
*
|
|
* @exception LifecycleException if this component detects a fatal error
|
|
* that prevents this component from being used
|
|
*/
|
|
@Override
|
|
protected synchronized void stopInternal() throws LifecycleException {
|
|
|
|
setState(LifecycleState.STOPPING);
|
|
|
|
close() ;
|
|
}
|
|
|
|
|
|
public long getCurrentTimeMillis() {
|
|
long systime = System.currentTimeMillis();
|
|
if ((systime - currentTimeMillis) > 1000) {
|
|
currentTimeMillis = new java.util.Date(systime).getTime();
|
|
}
|
|
return currentTimeMillis;
|
|
}
|
|
|
|
}
|