package io.undertow.server.handlers;
import io.undertow.UndertowLogger;
import io.undertow.UndertowMessages;
import io.undertow.security.api.SecurityContext;
import io.undertow.server.ExchangeCompletionListener;
import io.undertow.server.HandlerWrapper;
import io.undertow.server.HttpHandler;
import io.undertow.server.HttpServerExchange;
import io.undertow.server.handlers.builder.HandlerBuilder;
import io.undertow.util.Headers;
import java.net.InetSocketAddress;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.sql.Timestamp;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Deque;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentLinkedDeque;
import java.util.concurrent.Executor;
import java.util.concurrent.atomic.AtomicIntegerFieldUpdater;
import javax.naming.InitialContext;
import javax.naming.NamingException;
import javax.sql.DataSource;
public class JDBCLogHandler implements HttpHandler, Runnable {
private final HttpHandler next;
private final String formatString;
private final ExchangeCompletionListener exchangeCompletionListener = new JDBCLogCompletionListener();
private final Deque<JDBCLogAttribute> pendingMessages;
@SuppressWarnings("unused")
private volatile int state = 0;
@SuppressWarnings("unused")
private volatile Executor executor;
private static final AtomicIntegerFieldUpdater<JDBCLogHandler> stateUpdater = AtomicIntegerFieldUpdater.newUpdater(JDBCLogHandler.class, "state");
protected boolean useLongContentLength = false;
private final DataSource dataSource;
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;
@Deprecated
public JDBCLogHandler(final HttpHandler next, final Executor logWriteExecutor, final String formatString, DataSource dataSource) {
this(next, formatString, dataSource);
}
public JDBCLogHandler(final HttpHandler next, final String formatString, DataSource dataSource) {
this.next = next;
this.formatString = formatString;
this.dataSource = dataSource;
tableName = "access";
remoteHostField = "remoteHost";
userField = "userName";
timestampField = "timestamp";
virtualHostField = "virtualHost";
methodField = "method";
queryField = "query";
statusField = "status";
bytesField = "bytes";
refererField = "referer";
userAgentField = "userAgent";
this.pendingMessages = new ConcurrentLinkedDeque<>();
}
@Override
public void handleRequest(HttpServerExchange exchange) throws Exception {
exchange.addExchangeCompleteListener(exchangeCompletionListener);
next.handleRequest(exchange);
}
private class JDBCLogCompletionListener implements ExchangeCompletionListener {
@Override
public void exchangeEvent(final HttpServerExchange exchange, final NextListener nextListener) {
try {
logMessage(formatString, exchange);
} finally {
nextListener.proceed();
}
}
}
public void logMessage(String pattern, HttpServerExchange exchange) {
JDBCLogAttribute jdbcLogAttribute = new JDBCLogAttribute();
if (pattern.equals("combined")) {
jdbcLogAttribute.pattern = pattern;
}
jdbcLogAttribute.remoteHost = ((InetSocketAddress) exchange.getConnection().getPeerAddress()).getAddress().getHostAddress();
SecurityContext sc = exchange.getSecurityContext();
if (sc == null || !sc.isAuthenticated()) {
jdbcLogAttribute.user = null;
} else {
jdbcLogAttribute.user = sc.getAuthenticatedAccount().getPrincipal().getName();
}
jdbcLogAttribute.query = exchange.getQueryString();
jdbcLogAttribute.bytes = exchange.getResponseContentLength();
if (jdbcLogAttribute.bytes < 0) {
jdbcLogAttribute.bytes = 0;
}
jdbcLogAttribute.status = exchange.getStatusCode();
if (jdbcLogAttribute.pattern.equals("combined")) {
jdbcLogAttribute.virtualHost = exchange.getRequestHeaders().getFirst(Headers.HOST);
jdbcLogAttribute.method = exchange.getRequestMethod().toString();
jdbcLogAttribute.referer = exchange.getRequestHeaders().getFirst(Headers.REFERER);
jdbcLogAttribute.userAgent = exchange.getRequestHeaders().getFirst(Headers.USER_AGENT);
}
this.pendingMessages.add(jdbcLogAttribute);
int state = stateUpdater.get(this);
if (state == 0) {
if (stateUpdater.compareAndSet(this, 0, 1)) {
this.executor = exchange.getConnection().getWorker();
this.executor.execute(this);
}
}
}
@Override
public void run() {
if (!stateUpdater.compareAndSet(this, 1, 2)) {
return;
}
List<JDBCLogAttribute> messages = new ArrayList<>();
JDBCLogAttribute msg = null;
for (int i = 0; i < 1000; ++i) {
msg = pendingMessages.poll();
if (msg == null) {
break;
}
messages.add(msg);
}
try {
if (!messages.isEmpty()) {
writeMessage(messages);
}
} finally {
Executor executor = this.executor;
stateUpdater.set(this, 0);
if (!pendingMessages.isEmpty()) {
if (stateUpdater.compareAndSet(this, 0, 1)) {
executor.execute(this);
}
}
}
}
private void writeMessage(List<JDBCLogAttribute> messages) {
PreparedStatement ps = null;
Connection conn = null;
try {
conn = dataSource.getConnection();
conn.setAutoCommit(true);
ps = prepareStatement(conn);
for (JDBCLogAttribute jdbcLogAttribute : messages) {
int numberOfTries = 2;
while (numberOfTries > 0) {
try {
ps.clearParameters();
ps.setString(1, jdbcLogAttribute.remoteHost);
ps.setString(2, jdbcLogAttribute.user);
ps.setTimestamp(3, jdbcLogAttribute.timestamp);
ps.setString(4, jdbcLogAttribute.query);
ps.setInt(5, jdbcLogAttribute.status);
if (useLongContentLength) {
ps.setLong(6, jdbcLogAttribute.bytes);
} else {
if (jdbcLogAttribute.bytes > Integer.MAX_VALUE) {
jdbcLogAttribute.bytes = -1;
}
ps.setInt(6, (int) jdbcLogAttribute.bytes);
}
ps.setString(7, jdbcLogAttribute.virtualHost);
ps.setString(8, jdbcLogAttribute.method);
ps.setString(9, jdbcLogAttribute.referer);
ps.setString(10, jdbcLogAttribute.userAgent);
ps.executeUpdate();
numberOfTries = 0;
} catch (SQLException e) {
UndertowLogger.ROOT_LOGGER.failedToWriteJdbcAccessLog(e);
}
numberOfTries--;
}
}
ps.close();
} catch (SQLException e) {
UndertowLogger.ROOT_LOGGER.errorWritingJDBCLog(e);
} finally {
if (ps != null) {
try {
ps.close();
} catch (SQLException e) {
UndertowLogger.ROOT_LOGGER.debug("Exception closing prepared statement", e);
}
}
if (conn != null) {
try {
conn.close();
} catch (SQLException e) {
UndertowLogger.ROOT_LOGGER.debug("Exception closing connection", e);
}
}
}
}
void awaitWrittenForTest() throws InterruptedException {
while (!pendingMessages.isEmpty()) {
Thread.sleep(10);
}
while (state != 0) {
Thread.sleep(10);
}
}
private PreparedStatement prepareStatement(Connection conn) throws SQLException {
return conn.prepareStatement("INSERT INTO " + tableName + " ("
+ remoteHostField + ", " + userField + ", "
+ timestampField + ", " + queryField + ", "
+ statusField + ", " + bytesField + ", "
+ virtualHostField + ", " + methodField + ", "
+ refererField + ", " + userAgentField
+ ") VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?)");
}
private static class JDBCLogAttribute {
protected String remoteHost = "";
protected String user = "";
protected String query = "";
protected long bytes = 0;
protected int status = 0;
protected String virtualHost = "";
protected String method = "";
protected String referer = "";
protected String userAgent = "";
protected String pattern = "common";
protected Timestamp timestamp = new Timestamp(System.currentTimeMillis());
}
public boolean isUseLongContentLength() {
return useLongContentLength;
}
public void setUseLongContentLength(boolean useLongContentLength) {
this.useLongContentLength = useLongContentLength;
}
public String getTableName() {
return tableName;
}
public void setTableName(String tableName) {
this.tableName = tableName;
}
public String getRemoteHostField() {
return remoteHostField;
}
public void setRemoteHostField(String remoteHostField) {
this.remoteHostField = remoteHostField;
}
public String getUserField() {
return userField;
}
public void setUserField(String userField) {
this.userField = userField;
}
public String getTimestampField() {
return timestampField;
}
public void setTimestampField(String timestampField) {
this.timestampField = timestampField;
}
public String getVirtualHostField() {
return virtualHostField;
}
public void setVirtualHostField(String virtualHostField) {
this.virtualHostField = virtualHostField;
}
public String getMethodField() {
return methodField;
}
public void setMethodField(String methodField) {
this.methodField = methodField;
}
public String getQueryField() {
return queryField;
}
public void setQueryField(String queryField) {
this.queryField = queryField;
}
public String getStatusField() {
return statusField;
}
public void setStatusField(String statusField) {
this.statusField = statusField;
}
public String getBytesField() {
return bytesField;
}
public void setBytesField(String bytesField) {
this.bytesField = bytesField;
}
public String getRefererField() {
return refererField;
}
public void setRefererField(String refererField) {
this.refererField = refererField;
}
public String getUserAgentField() {
return userAgentField;
}
public void setUserAgentField(String userAgentField) {
this.userAgentField = userAgentField;
}
@Override
public String toString() {
return "JDBCLogHandler{"
+ "formatString='" + formatString + '\''
+ '}';
}
public static class Builder implements HandlerBuilder {
@Override
public String name() {
return "jdbc-access-log";
}
@Override
public Map<String, Class<?>> parameters() {
Map<String, Class<?>> params = new HashMap<>();
params.put("format", String.class);
params.put("datasource", String.class);
params.put("tableName", String.class);
params.put("remoteHostField", String.class);
params.put("userField", String.class);
params.put("timestampField", String.class);
params.put("virtualHostField", String.class);
params.put("methodField", String.class);
params.put("queryField", String.class);
params.put("statusField", String.class);
params.put("bytesField", String.class);
params.put("refererField", String.class);
params.put("userAgentField", String.class);
return params;
}
@Override
public Set<String> requiredParameters() {
return Collections.singleton("datasource");
}
@Override
public String defaultParameter() {
return "datasource";
}
@Override
public HandlerWrapper build(Map<String, Object> config) {
String datasourceName = (String) config.get("datasource");
try {
DataSource ds = (DataSource) new InitialContext().lookup((String) config.get("datasource"));
String format = (String) config.get("format");
return new Wrapper(format, ds, (String)config.get("tableName"), (String)config.get("remoteHostField"), (String)config.get("userField"), (String)config.get("timestampField"), (String)config.get("virtualHostField"), (String)config.get("methodField"), (String)config.get("queryField"), (String)config.get("statusField"), (String)config.get("bytesField"), (String)config.get("refererField"), (String)config.get("userAgentField"));
} catch (NamingException ex) {
throw UndertowMessages.MESSAGES.datasourceNotFound(datasourceName);
}
}
}
private static class Wrapper implements HandlerWrapper {
private final DataSource datasource;
private final String format;
private final String tableName;
private final String remoteHostField;
private final String userField;
private final String timestampField;
private final String virtualHostField;
private final String methodField;
private final String queryField;
private final String statusField;
private final String bytesField;
private final String refererField;
private final String userAgentField;
private Wrapper(String format, DataSource datasource, String tableName, String remoteHostField, String userField, String timestampField, String virtualHostField, String methodField, String queryField, String statusField, String bytesField, String refererField, String userAgentField) {
this.datasource = datasource;
this.tableName = tableName;
this.remoteHostField = remoteHostField;
this.userField = userField;
this.timestampField = timestampField;
this.virtualHostField = virtualHostField;
this.methodField = methodField;
this.queryField = queryField;
this.statusField = statusField;
this.bytesField = bytesField;
this.refererField = refererField;
this.userAgentField = userAgentField;
this.format = "combined".equals(format) ? "combined" : "common";
}
@Override
public HttpHandler wrap(HttpHandler handler) {
JDBCLogHandler jdbc = new JDBCLogHandler(handler, format, datasource);
if(tableName != null) {
jdbc.setTableName(tableName);
}
if(remoteHostField != null) {
jdbc.setRemoteHostField(remoteHostField);
}
if(userField != null) {
jdbc.setUserField(userField);
}
if(timestampField != null) {
jdbc.setTimestampField(timestampField);
}
if(virtualHostField != null) {
jdbc.setVirtualHostField(virtualHostField);
}
if(methodField != null) {
jdbc.setMethodField(methodField);
}
if(queryField != null) {
jdbc.setQueryField(queryField);
}
if(statusField != null) {
jdbc.setStatusField(statusField);
}
if(bytesField != null) {
jdbc.setBytesField(bytesField);
}
if(refererField != null) {
jdbc.setRefererField(refererField);
}
if(userAgentField != null) {
jdbc.setUserAgentField(userAgentField);
}
return jdbc;
}
}
}