aboutsummaryrefslogtreecommitdiffstats
path: root/container-core/src/main/java/com/yahoo/jdisc/http/server/jetty/JettyConnectionLogger.java
diff options
context:
space:
mode:
Diffstat (limited to 'container-core/src/main/java/com/yahoo/jdisc/http/server/jetty/JettyConnectionLogger.java')
-rw-r--r--container-core/src/main/java/com/yahoo/jdisc/http/server/jetty/JettyConnectionLogger.java373
1 files changed, 373 insertions, 0 deletions
diff --git a/container-core/src/main/java/com/yahoo/jdisc/http/server/jetty/JettyConnectionLogger.java b/container-core/src/main/java/com/yahoo/jdisc/http/server/jetty/JettyConnectionLogger.java
new file mode 100644
index 00000000000..cd1ca490f61
--- /dev/null
+++ b/container-core/src/main/java/com/yahoo/jdisc/http/server/jetty/JettyConnectionLogger.java
@@ -0,0 +1,373 @@
+// Copyright Verizon Media. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.jdisc.http.server.jetty;
+
+import com.yahoo.container.logging.ConnectionLog;
+import com.yahoo.container.logging.ConnectionLogEntry;
+import com.yahoo.container.logging.ConnectionLogEntry.SslHandshakeFailure.ExceptionEntry;
+import com.yahoo.io.HexDump;
+import com.yahoo.jdisc.http.ServerConfig;
+import org.eclipse.jetty.io.Connection;
+import org.eclipse.jetty.io.EndPoint;
+import org.eclipse.jetty.io.SocketChannelEndPoint;
+import org.eclipse.jetty.io.ssl.SslConnection;
+import org.eclipse.jetty.io.ssl.SslHandshakeListener;
+import org.eclipse.jetty.server.HttpChannel;
+import org.eclipse.jetty.server.HttpConnection;
+import org.eclipse.jetty.server.ProxyConnectionFactory;
+import org.eclipse.jetty.server.Request;
+import org.eclipse.jetty.util.component.AbstractLifeCycle;
+
+import javax.net.ssl.ExtendedSSLSession;
+import javax.net.ssl.SNIHostName;
+import javax.net.ssl.SNIServerName;
+import javax.net.ssl.SSLEngine;
+import javax.net.ssl.SSLHandshakeException;
+import javax.net.ssl.SSLPeerUnverifiedException;
+import javax.net.ssl.SSLSession;
+import javax.net.ssl.StandardConstants;
+import java.net.InetSocketAddress;
+import java.security.cert.X509Certificate;
+import java.time.Instant;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.List;
+import java.util.Objects;
+import java.util.UUID;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+/**
+ * Jetty integration for jdisc connection log ({@link ConnectionLog}).
+ *
+ * @author bjorncs
+ */
+class JettyConnectionLogger extends AbstractLifeCycle implements Connection.Listener, HttpChannel.Listener, SslHandshakeListener {
+
+ static final String CONNECTION_ID_REQUEST_ATTRIBUTE = "jdisc.request.connection.id";
+
+ private static final Logger log = Logger.getLogger(JettyConnectionLogger.class.getName());
+
+ private final ConcurrentMap<IdentityKey<SocketChannelEndPoint>, ConnectionInfo> connectionInfo = new ConcurrentHashMap<>();
+ private final ConcurrentMap<IdentityKey<SSLEngine>, ConnectionInfo> sslToConnectionInfo = new ConcurrentHashMap<>();
+
+ private final boolean enabled;
+ private final ConnectionLog connectionLog;
+
+ JettyConnectionLogger(ServerConfig.ConnectionLog config, ConnectionLog connectionLog) {
+ this.enabled = config.enabled();
+ this.connectionLog = connectionLog;
+ log.log(Level.FINE, () -> "Jetty connection logger is " + (config.enabled() ? "enabled" : "disabled"));
+ }
+
+ //
+ // AbstractLifeCycle methods start
+ //
+ @Override
+ protected void doStop() {
+ handleListenerInvocation("AbstractLifeCycle", "doStop", "", List.of(), () -> {
+ log.log(Level.FINE, () -> "Jetty connection logger is stopped");
+ });
+ }
+
+ @Override
+ protected void doStart() {
+ handleListenerInvocation("AbstractLifeCycle", "doStart", "", List.of(), () -> {
+ log.log(Level.FINE, () -> "Jetty connection logger is started");
+ });
+ }
+ //
+ // AbstractLifeCycle methods stop
+ //
+
+ //
+ // Connection.Listener methods start
+ //
+ @Override
+ public void onOpened(Connection connection) {
+ handleListenerInvocation("Connection.Listener", "onOpened", "%h", List.of(connection), () -> {
+ SocketChannelEndPoint endpoint = findUnderlyingSocketEndpoint(connection.getEndPoint());
+ var endpointKey = IdentityKey.of(endpoint);
+ ConnectionInfo info = connectionInfo.get(endpointKey);
+ if (info == null) {
+ info = ConnectionInfo.from(endpoint);
+ connectionInfo.put(IdentityKey.of(endpoint), info);
+ }
+ if (connection instanceof SslConnection) {
+ SSLEngine sslEngine = ((SslConnection) connection).getSSLEngine();
+ sslToConnectionInfo.put(IdentityKey.of(sslEngine), info);
+ }
+ if (connection.getEndPoint() instanceof ProxyConnectionFactory.ProxyEndPoint) {
+ InetSocketAddress remoteAddress = connection.getEndPoint().getRemoteAddress();
+ info.setRemoteAddress(remoteAddress);
+ }
+ });
+ }
+
+ @Override
+ public void onClosed(Connection connection) {
+ handleListenerInvocation("Connection.Listener", "onClosed", "%h", List.of(connection), () -> {
+ SocketChannelEndPoint endpoint = findUnderlyingSocketEndpoint(connection.getEndPoint());
+ var endpointKey = IdentityKey.of(endpoint);
+ ConnectionInfo info = connectionInfo.get(endpointKey);
+ if (info == null) return; // Closed connection already handled
+ if (connection instanceof HttpConnection) {
+ info.setHttpBytes(connection.getBytesIn(), connection.getBytesOut());
+ }
+ if (!endpoint.isOpen()) {
+ info.setClosedAt(System.currentTimeMillis());
+ connectionLog.log(info.toLogEntry());
+ connectionInfo.remove(endpointKey);
+ }
+ });
+ }
+ //
+ // Connection.Listener methods end
+ //
+
+ //
+ // HttpChannel.Listener methods start
+ //
+ @Override
+ public void onRequestBegin(Request request) {
+ handleListenerInvocation("HttpChannel.Listener", "onRequestBegin", "%h", List.of(request), () -> {
+ SocketChannelEndPoint endpoint = findUnderlyingSocketEndpoint(request.getHttpChannel().getEndPoint());
+ ConnectionInfo info = Objects.requireNonNull(connectionInfo.get(IdentityKey.of(endpoint)));
+ info.incrementRequests();
+ request.setAttribute(CONNECTION_ID_REQUEST_ATTRIBUTE, info.uuid());
+ });
+ }
+
+ @Override
+ public void onResponseBegin(Request request) {
+ handleListenerInvocation("HttpChannel.Listener", "onResponseBegin", "%h", List.of(request), () -> {
+ SocketChannelEndPoint endpoint = findUnderlyingSocketEndpoint(request.getHttpChannel().getEndPoint());
+ ConnectionInfo info = Objects.requireNonNull(connectionInfo.get(IdentityKey.of(endpoint)));
+ info.incrementResponses();
+ });
+ }
+ //
+ // HttpChannel.Listener methods end
+ //
+
+ //
+ // SslHandshakeListener methods start
+ //
+ @Override
+ public void handshakeSucceeded(Event event) {
+ SSLEngine sslEngine = event.getSSLEngine();
+ handleListenerInvocation("SslHandshakeListener", "handshakeSucceeded", "sslEngine=%h", List.of(sslEngine), () -> {
+ ConnectionInfo info = sslToConnectionInfo.remove(IdentityKey.of(sslEngine));
+ info.setSslSessionDetails(sslEngine.getSession());
+ });
+ }
+
+ @Override
+ public void handshakeFailed(Event event, Throwable failure) {
+ SSLEngine sslEngine = event.getSSLEngine();
+ handleListenerInvocation("SslHandshakeListener", "handshakeFailed", "sslEngine=%h,failure=%s", List.of(sslEngine, failure), () -> {
+ log.log(Level.FINE, failure, failure::toString);
+ ConnectionInfo info = sslToConnectionInfo.remove(IdentityKey.of(sslEngine));
+ info.setSslHandshakeFailure((SSLHandshakeException)failure);
+ });
+ }
+ //
+ // SslHandshakeListener methods end
+ //
+
+ private void handleListenerInvocation(
+ String listenerType, String methodName, String methodArgumentsFormat, List<Object> methodArguments, ListenerHandler handler) {
+ if (!enabled) return;
+ try {
+ log.log(Level.FINE, () -> String.format(listenerType + "." + methodName + "(" + methodArgumentsFormat + ")", methodArguments.toArray()));
+ handler.run();
+ } catch (Exception e) {
+ log.log(Level.WARNING, String.format("Exception in %s.%s listener: %s", listenerType, methodName, e.getMessage()), e);
+ }
+ }
+
+ /**
+ * Protocol layers are connected through each {@link Connection}'s {@link EndPoint} reference.
+ * This methods iterates through the endpoints recursively to find the underlying socket endpoint.
+ */
+ private static SocketChannelEndPoint findUnderlyingSocketEndpoint(EndPoint endpoint) {
+ if (endpoint instanceof SocketChannelEndPoint) {
+ return (SocketChannelEndPoint) endpoint;
+ } else if (endpoint instanceof SslConnection.DecryptedEndPoint) {
+ var decryptedEndpoint = (SslConnection.DecryptedEndPoint) endpoint;
+ return findUnderlyingSocketEndpoint(decryptedEndpoint.getSslConnection().getEndPoint());
+ } else if (endpoint instanceof ProxyConnectionFactory.ProxyEndPoint) {
+ var proxyEndpoint = (ProxyConnectionFactory.ProxyEndPoint) endpoint;
+ return findUnderlyingSocketEndpoint(proxyEndpoint.unwrap());
+ } else {
+ throw new IllegalArgumentException("Unknown connection endpoint type: " + endpoint.getClass().getName());
+ }
+ }
+
+ @FunctionalInterface private interface ListenerHandler { void run() throws Exception; }
+
+ private static class ConnectionInfo {
+ private final UUID uuid;
+ private final long createdAt;
+ private final InetSocketAddress localAddress;
+ private final InetSocketAddress peerAddress;
+
+ private long closedAt = 0;
+ private long httpBytesReceived = 0;
+ private long httpBytesSent = 0;
+ private long requests = 0;
+ private long responses = 0;
+ private InetSocketAddress remoteAddress;
+ private byte[] sslSessionId;
+ private String sslProtocol;
+ private String sslCipherSuite;
+ private String sslPeerSubject;
+ private Date sslPeerNotBefore;
+ private Date sslPeerNotAfter;
+ private List<SNIServerName> sslSniServerNames;
+ private SSLHandshakeException sslHandshakeException;
+
+ private ConnectionInfo(UUID uuid, long createdAt, InetSocketAddress localAddress, InetSocketAddress peerAddress) {
+ this.uuid = uuid;
+ this.createdAt = createdAt;
+ this.localAddress = localAddress;
+ this.peerAddress = peerAddress;
+ }
+
+ static ConnectionInfo from(SocketChannelEndPoint endpoint) {
+ return new ConnectionInfo(
+ UUID.randomUUID(),
+ endpoint.getCreatedTimeStamp(),
+ endpoint.getLocalAddress(),
+ endpoint.getRemoteAddress());
+ }
+
+ synchronized UUID uuid() { return uuid; }
+
+ synchronized ConnectionInfo setClosedAt(long closedAt) {
+ this.closedAt = closedAt;
+ return this;
+ }
+
+ synchronized ConnectionInfo setHttpBytes(long received, long sent) {
+ this.httpBytesReceived = received;
+ this.httpBytesSent = sent;
+ return this;
+ }
+
+ synchronized ConnectionInfo incrementRequests() { ++this.requests; return this; }
+
+ synchronized ConnectionInfo incrementResponses() { ++this.responses; return this; }
+
+ synchronized ConnectionInfo setRemoteAddress(InetSocketAddress remoteAddress) {
+ this.remoteAddress = remoteAddress;
+ return this;
+ }
+
+ synchronized ConnectionInfo setSslSessionDetails(SSLSession session) {
+ this.sslCipherSuite = session.getCipherSuite();
+ this.sslProtocol = session.getProtocol();
+ this.sslSessionId = session.getId();
+ if (session instanceof ExtendedSSLSession) {
+ ExtendedSSLSession extendedSession = (ExtendedSSLSession) session;
+ this.sslSniServerNames = extendedSession.getRequestedServerNames();
+ }
+ try {
+ this.sslPeerSubject = session.getPeerPrincipal().getName();
+ X509Certificate peerCertificate = (X509Certificate) session.getPeerCertificates()[0];
+ this.sslPeerNotBefore = peerCertificate.getNotBefore();
+ this.sslPeerNotAfter = peerCertificate.getNotAfter();
+ } catch (SSLPeerUnverifiedException e) {
+ // Throw if peer is not authenticated (e.g when client auth is disabled)
+ // JSSE provides no means of checking for client authentication without catching this exception
+ }
+ return this;
+ }
+
+ synchronized ConnectionInfo setSslHandshakeFailure(SSLHandshakeException exception) {
+ this.sslHandshakeException = exception;
+ return this;
+ }
+
+ synchronized ConnectionLogEntry toLogEntry() {
+ ConnectionLogEntry.Builder builder = ConnectionLogEntry.builder(uuid, Instant.ofEpochMilli(createdAt));
+ if (closedAt > 0) {
+ builder.withDuration((closedAt - createdAt) / 1000D);
+ }
+ if (httpBytesReceived > 0) {
+ builder.withHttpBytesReceived(httpBytesReceived);
+ }
+ if (httpBytesSent > 0) {
+ builder.withHttpBytesSent(httpBytesSent);
+ }
+ if (requests > 0) {
+ builder.withRequests(requests);
+ }
+ if (responses > 0) {
+ builder.withResponses(responses);
+ }
+ if (peerAddress != null) {
+ builder.withPeerAddress(peerAddress.getHostString())
+ .withPeerPort(peerAddress.getPort());
+ }
+ if (localAddress != null) {
+ builder.withLocalAddress(localAddress.getHostString())
+ .withLocalPort(localAddress.getPort());
+ }
+ if (remoteAddress != null) {
+ builder.withRemoteAddress(remoteAddress.getHostString())
+ .withRemotePort(remoteAddress.getPort());
+ }
+ if (sslProtocol != null && sslCipherSuite != null && sslSessionId != null) {
+ builder.withSslProtocol(sslProtocol)
+ .withSslCipherSuite(sslCipherSuite)
+ .withSslSessionId(HexDump.toHexString(sslSessionId));
+ }
+ if (sslSniServerNames != null) {
+ sslSniServerNames.stream()
+ .filter(name -> name instanceof SNIHostName && name.getType() == StandardConstants.SNI_HOST_NAME)
+ .map(name -> ((SNIHostName) name).getAsciiName())
+ .findAny()
+ .ifPresent(builder::withSslSniServerName);
+ }
+ if (sslPeerSubject != null && sslPeerNotAfter != null && sslPeerNotBefore != null) {
+ builder.withSslPeerSubject(sslPeerSubject)
+ .withSslPeerNotAfter(sslPeerNotAfter.toInstant())
+ .withSslPeerNotBefore(sslPeerNotBefore.toInstant());
+ }
+ if (sslHandshakeException != null) {
+ List<ExceptionEntry> exceptionChain = new ArrayList<>();
+ Throwable cause = sslHandshakeException;
+ while (cause != null) {
+ exceptionChain.add(new ExceptionEntry(cause.getClass().getName(), cause.getMessage()));
+ cause = cause.getCause();
+ }
+ String type = SslHandshakeFailure.fromSslHandshakeException(sslHandshakeException)
+ .map(SslHandshakeFailure::failureType)
+ .orElse("UNKNOWN");
+ builder.withSslHandshakeFailure(new ConnectionLogEntry.SslHandshakeFailure(type, exceptionChain));
+ }
+ return builder.build();
+ }
+
+ }
+
+ private static class IdentityKey<T> {
+ final T instance;
+
+ IdentityKey(T instance) { this.instance = instance; }
+
+ static <T> IdentityKey<T> of(T instance) { return new IdentityKey<>(instance); }
+
+ @Override public int hashCode() { return System.identityHashCode(instance); }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) return true;
+ if (!(obj instanceof IdentityKey<?>)) return false;
+ IdentityKey<?> other = (IdentityKey<?>) obj;
+ return this.instance == other.instance;
+ }
+ }
+}