Update to Dropwizard 5

Co-authored-by: Chris Eager <chris@signal.org>
This commit is contained in:
ravi-signal
2025-11-04 12:18:56 -06:00
committed by GitHub
parent 24f8f48a26
commit 4dbd564442
36 changed files with 703 additions and 664 deletions

View File

@@ -37,15 +37,12 @@ import io.netty.resolver.ResolvedAddressTypes;
import io.netty.resolver.dns.DnsNameResolver;
import io.netty.resolver.dns.DnsNameResolverBuilder;
import jakarta.servlet.DispatcherType;
import jakarta.servlet.Filter;
import jakarta.servlet.ServletRegistration;
import java.io.ByteArrayInputStream;
import java.net.InetSocketAddress;
import java.net.http.HttpClient;
import java.nio.charset.StandardCharsets;
import java.time.Clock;
import java.time.Duration;
import java.util.ArrayList;
import java.util.Collections;
import java.util.EnumSet;
import java.util.List;
@@ -60,9 +57,9 @@ import java.util.concurrent.SynchronousQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.function.Function;
import java.util.stream.Stream;
import org.eclipse.jetty.ee10.websocket.server.config.JettyWebSocketServletContainerInitializer;
import org.eclipse.jetty.websocket.core.WebSocketComponents;
import org.eclipse.jetty.websocket.core.server.WebSocketServerComponents;
import org.eclipse.jetty.websocket.server.config.JettyWebSocketServletContainerInitializer;
import org.glassfish.jersey.server.ServerProperties;
import org.signal.i18n.HeaderControlledResourceBundleLookup;
import org.signal.libsignal.zkgroup.GenericServerSecretParams;
@@ -136,6 +133,7 @@ import org.whispersystems.textsecuregcm.currency.CurrencyConversionManager;
import org.whispersystems.textsecuregcm.currency.FixerClient;
import org.whispersystems.textsecuregcm.experiment.ExperimentEnrollmentManager;
import org.whispersystems.textsecuregcm.filters.ExternalRequestFilter;
import org.whispersystems.textsecuregcm.filters.PriorityFilter;
import org.whispersystems.textsecuregcm.filters.RemoteAddressFilter;
import org.whispersystems.textsecuregcm.filters.RemoteDeprecationFilter;
import org.whispersystems.textsecuregcm.filters.RequestStatisticsFilter;
@@ -184,7 +182,7 @@ import org.whispersystems.textsecuregcm.metrics.BackupMetrics;
import org.whispersystems.textsecuregcm.metrics.CallQualitySurveyManager;
import org.whispersystems.textsecuregcm.metrics.MessageMetrics;
import org.whispersystems.textsecuregcm.metrics.MetricsApplicationEventListener;
import org.whispersystems.textsecuregcm.metrics.MetricsHttpChannelListener;
import org.whispersystems.textsecuregcm.metrics.MetricsHttpEventHandler;
import org.whispersystems.textsecuregcm.metrics.MetricsUtil;
import org.whispersystems.textsecuregcm.metrics.MicrometerAwsSdkMetricPublisher;
import org.whispersystems.textsecuregcm.metrics.ReportedMessageMetricsListener;
@@ -911,17 +909,6 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
environment.lifecycle().manage(dnsResolutionEventLoopGroup);
environment.lifecycle().manage(exposedGrpcServer);
final List<Filter> filters = new ArrayList<>();
filters.add(remoteDeprecationFilter);
filters.add(new RemoteAddressFilter());
filters.add(new TimestampResponseFilter());
for (Filter filter : filters) {
environment.servlets()
.addFilter(filter.getClass().getSimpleName(), filter)
.addMappingForUrlPatterns(EnumSet.of(DispatcherType.REQUEST), false, "/*");
}
if (!config.getExternalRequestFilterConfiguration().paths().isEmpty()) {
environment.servlets().addFilter(ExternalRequestFilter.class.getSimpleName(),
new ExternalRequestFilter(config.getExternalRequestFilterConfiguration().permittedInternalRanges(),
@@ -938,9 +925,7 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
final String websocketServletPath = "/v1/websocket/";
final String provisioningWebsocketServletPath = "/v1/websocket/provisioning/";
final MetricsHttpChannelListener metricsHttpChannelListener = new MetricsHttpChannelListener(clientReleaseManager,
Set.of(websocketServletPath, provisioningWebsocketServletPath, "/health-check"));
metricsHttpChannelListener.configure(environment);
MetricsHttpEventHandler.configure(environment, Metrics.globalRegistry, clientReleaseManager, Set.of(websocketServletPath, provisioningWebsocketServletPath, "/health-check"));
final MessageMetrics messageMetrics = new MessageMetrics();
final BackupMetrics backupMetrics = new BackupMetrics();
@@ -1130,29 +1115,30 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
webSocketEnvironment.jersey().property(ServerProperties.UNWRAP_COMPLETION_STAGE_IN_WRITER_ENABLE, Boolean.TRUE);
provisioningEnvironment.jersey().property(ServerProperties.UNWRAP_COMPLETION_STAGE_IN_WRITER_ENABLE, Boolean.TRUE);
JettyWebSocketServletContainerInitializer.configure(environment.getApplicationContext(), (context, container) -> {
if (config.getWebSocketConfiguration().isDisablePerMessageDeflate()) {
WebSocketComponents components =
WebSocketServerComponents.getWebSocketComponents(environment.getApplicationContext().getServletContext());
components.getExtensionRegistry().unregister("permessage-deflate");
}
});
WebSocketResourceProviderFactory<AuthenticatedDevice> webSocketServlet = new WebSocketResourceProviderFactory<>(
webSocketEnvironment, AuthenticatedDevice.class, config.getWebSocketConfiguration(),
RemoteAddressFilter.REMOTE_ADDRESS_ATTRIBUTE_NAME);
webSocketEnvironment, AuthenticatedDevice.class, RemoteAddressFilter.REMOTE_ADDRESS_ATTRIBUTE_NAME);
WebSocketResourceProviderFactory<AuthenticatedDevice> provisioningServlet = new WebSocketResourceProviderFactory<>(
provisioningEnvironment, AuthenticatedDevice.class, config.getWebSocketConfiguration(),
RemoteAddressFilter.REMOTE_ADDRESS_ATTRIBUTE_NAME);
provisioningEnvironment, AuthenticatedDevice.class, RemoteAddressFilter.REMOTE_ADDRESS_ATTRIBUTE_NAME);
ServletRegistration.Dynamic websocket = environment.servlets().addServlet("WebSocket", webSocketServlet);
ServletRegistration.Dynamic provisioning = environment.servlets().addServlet("Provisioning", provisioningServlet);
JettyWebSocketServletContainerInitializer.configure(environment.getApplicationContext(),
(servletContext, container) -> {
container.addMapping(websocketServletPath, webSocketServlet);
container.addMapping(provisioningWebsocketServletPath, provisioningServlet);
websocket.addMapping(websocketServletPath);
websocket.setAsyncSupported(true);
PriorityFilter.ensureFilter(servletContext, new TimestampResponseFilter());
PriorityFilter.ensureFilter(servletContext, new RemoteAddressFilter());
PriorityFilter.ensureFilter(servletContext, remoteDeprecationFilter);
provisioning.addMapping(provisioningWebsocketServletPath);
provisioning.setAsyncSupported(true);
container.setMaxBinaryMessageSize(config.getWebSocketConfiguration().getMaxBinaryMessageSize());
container.setMaxTextMessageSize(config.getWebSocketConfiguration().getMaxTextMessageSize());
if (config.getWebSocketConfiguration().isDisablePerMessageDeflate()) {
WebSocketComponents components =
WebSocketServerComponents.getWebSocketComponents(environment.getApplicationContext());
components.getExtensionRegistry().unregister("permessage-deflate");
}
});
environment.admin().addTask(new SetRequestLoggingEnabledTask());
}

View File

@@ -10,10 +10,9 @@ import io.micrometer.core.instrument.Counter;
import io.micrometer.core.instrument.Metrics;
import java.time.Clock;
import java.time.Duration;
import java.time.Instant;
import java.util.Optional;
import org.eclipse.jetty.websocket.server.JettyServerUpgradeRequest;
import org.eclipse.jetty.websocket.server.JettyServerUpgradeResponse;
import org.eclipse.jetty.ee10.websocket.server.JettyServerUpgradeRequest;
import org.eclipse.jetty.ee10.websocket.server.JettyServerUpgradeResponse;
import org.whispersystems.textsecuregcm.metrics.MetricsUtil;
import org.whispersystems.textsecuregcm.storage.Device;
import org.whispersystems.websocket.auth.AuthenticatedWebSocketUpgradeFilter;

View File

@@ -0,0 +1,82 @@
/*
* Copyright 2025 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.textsecuregcm.filters;
import jakarta.servlet.DispatcherType;
import jakarta.servlet.Filter;
import jakarta.servlet.ServletContext;
import java.util.EnumSet;
import java.util.Objects;
import org.eclipse.jetty.ee10.servlet.FilterHolder;
import org.eclipse.jetty.ee10.servlet.FilterMapping;
import org.eclipse.jetty.ee10.servlet.ServletContextHandler;
import org.eclipse.jetty.ee10.servlet.ServletHandler;
import org.eclipse.jetty.server.handler.ContextHandler;
import org.eclipse.jetty.util.component.LifeCycle;
public class PriorityFilter {
private PriorityFilter() {}
private static FilterHolder getFilter(ServletContext servletContext, final Class<? extends Filter> filterClass) {
final ContextHandler contextHandler = Objects.requireNonNull(ServletContextHandler.getServletContextHandler(servletContext));
final ServletHandler servletHandler = contextHandler.getDescendant(ServletHandler.class);
return servletHandler.getFilter(filterClass.getName());
}
/**
* Ensure a filter is available on the provided ServletContext, a new filter will added if one does not already
* exist.
* <p>
* If a new filter is added, it will be added before all other filters.
* <p>
* Modeled after {@link org.eclipse.jetty.ee10.websocket.servlet.WebSocketUpgradeFilter#ensureFilter(ServletContext)},
* since its use of {@link org.eclipse.jetty.ee10.servlet.ServletHandler#prependFilter(FilterHolder)} is what makes
* this necessary.
*/
public static void ensureFilter(final ServletContext servletContext, final Filter filter) {
FilterHolder existingFilter = getFilter(servletContext, filter.getClass());
if (existingFilter != null) {
return;
}
final ContextHandler contextHandler = ServletContextHandler.getServletContextHandler(servletContext);
final ServletHandler servletHandler = contextHandler.getDescendant(ServletHandler.class);
final String pathSpec = "/*";
final FilterHolder holder = new FilterHolder(filter);
holder.setName(filter.getClass().getName());
holder.setAsyncSupported(true);
final FilterMapping mapping = new FilterMapping();
mapping.setFilterName(holder.getName());
mapping.setPathSpec(pathSpec);
mapping.setDispatcherTypes(EnumSet.of(DispatcherType.REQUEST));
// Add as the first filter in the list.
servletHandler.prependFilter(holder);
servletHandler.prependFilterMapping(mapping);
// If we create the filter we must also make sure it is removed if the context is stopped.
contextHandler.addEventListener(new LifeCycle.Listener()
{
@Override
public void lifeCycleStopping(LifeCycle event)
{
servletHandler.removeFilterHolder(holder);
servletHandler.removeFilterMapping(mapping);
contextHandler.removeEventListener(this);
}
@Override
public String toString()
{
return String.format("%sCleanupListener", filter.getClass().getSimpleName());
}
});
}
}

View File

@@ -26,10 +26,6 @@ public class RemoteAddressFilter implements Filter {
public static final String REMOTE_ADDRESS_ATTRIBUTE_NAME = RemoteAddressFilter.class.getName() + ".remoteAddress";
private static final Logger logger = LoggerFactory.getLogger(RemoteAddressFilter.class);
public RemoteAddressFilter() {
}
@Override
public void doFilter(final ServletRequest request, final ServletResponse response, final FilterChain chain)
throws ServletException, IOException {

View File

@@ -14,7 +14,7 @@ import org.whispersystems.textsecuregcm.storage.ClientReleaseManager;
/**
* Delegates request events to a listener that captures and reports request-level metrics.
*
* @see MetricsHttpChannelListener
* @see MetricsHttpEventHandler
* @see MetricsRequestEventListener
*/
public class MetricsApplicationEventListener implements ApplicationEventListener {
@@ -23,7 +23,7 @@ public class MetricsApplicationEventListener implements ApplicationEventListener
public MetricsApplicationEventListener(final TrafficSource trafficSource, final ClientReleaseManager clientReleaseManager) {
if (trafficSource == TrafficSource.HTTP) {
throw new IllegalArgumentException("Use " + MetricsHttpChannelListener.class.getName() + " for HTTP traffic");
throw new IllegalArgumentException("Use " + MetricsHttpEventHandler.class.getName() + " for HTTP traffic");
}
this.metricsRequestEventListener = new MetricsRequestEventListener(trafficSource, clientReleaseManager);
}

View File

@@ -1,192 +0,0 @@
/*
* Copyright 2024 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.textsecuregcm.metrics;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.net.HttpHeaders;
import io.dropwizard.core.setup.Environment;
import io.micrometer.core.instrument.MeterRegistry;
import io.micrometer.core.instrument.Metrics;
import io.micrometer.core.instrument.Tag;
import io.micrometer.core.instrument.Tags;
import jakarta.ws.rs.container.ContainerRequestContext;
import jakarta.ws.rs.container.ContainerResponseContext;
import jakarta.ws.rs.container.ContainerResponseFilter;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import javax.annotation.Nullable;
import org.eclipse.jetty.server.Connector;
import org.eclipse.jetty.server.HttpChannel;
import org.eclipse.jetty.server.Request;
import org.eclipse.jetty.util.component.Container;
import org.eclipse.jetty.util.component.LifeCycle;
import org.glassfish.jersey.server.ExtendedUriInfo;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.textsecuregcm.storage.ClientReleaseManager;
import org.whispersystems.textsecuregcm.util.logging.UriInfoUtil;
/**
* Gathers and reports HTTP request metrics at the Jetty container level, which sits above Jersey. In order to get
* templated Jersey request paths, it implements {@link jakarta.ws.rs.container.ContainerResponseFilter}, in order to give
* itself access to the template. It is limited to {@link TrafficSource#HTTP} requests.
* <p>
* It implements {@link LifeCycle.Listener} without overriding methods, so that it can be an event listener that
* Dropwizard will attach to the container&mdash;the {@link Container.Listener} implementation is where it attaches
* itself to any {@link Connector}s.
*
* @see MetricsRequestEventListener
*/
public class MetricsHttpChannelListener implements HttpChannel.Listener, Container.Listener, LifeCycle.Listener,
ContainerResponseFilter {
private static final Logger logger = LoggerFactory.getLogger(MetricsHttpChannelListener.class);
private record RequestInfo(String path, String method, int statusCode, @Nullable String userAgent) {
}
private final ClientReleaseManager clientReleaseManager;
private final Set<String> servletPaths;
// Use the same counter namespace as MetricsRequestEventListener for continuity
public static final String REQUEST_COUNTER_NAME = MetricsRequestEventListener.REQUEST_COUNTER_NAME;
public static final String REQUESTS_BY_VERSION_COUNTER_NAME = MetricsRequestEventListener.REQUESTS_BY_VERSION_COUNTER_NAME;
@VisibleForTesting
static final String RESPONSE_BYTES_COUNTER_NAME = MetricsRequestEventListener.RESPONSE_BYTES_COUNTER_NAME;
@VisibleForTesting
static final String REQUEST_BYTES_COUNTER_NAME = MetricsRequestEventListener.REQUEST_BYTES_COUNTER_NAME;
@VisibleForTesting
static final String URI_INFO_PROPERTY_NAME = MetricsHttpChannelListener.class.getName() + ".uriInfo";
@VisibleForTesting
static final String PATH_TAG = "path";
@VisibleForTesting
static final String METHOD_TAG = "method";
@VisibleForTesting
static final String STATUS_CODE_TAG = "status";
@VisibleForTesting
static final String TRAFFIC_SOURCE_TAG = "trafficSource";
private final MeterRegistry meterRegistry;
public MetricsHttpChannelListener(final ClientReleaseManager clientReleaseManager, final Set<String> servletPaths) {
this(Metrics.globalRegistry, clientReleaseManager, servletPaths);
}
@VisibleForTesting
MetricsHttpChannelListener(final MeterRegistry meterRegistry, final ClientReleaseManager clientReleaseManager,
final Set<String> servletPaths) {
this.meterRegistry = meterRegistry;
this.clientReleaseManager = clientReleaseManager;
this.servletPaths = servletPaths;
}
public void configure(final Environment environment) {
// register as ContainerResponseFilter
environment.jersey().register(this);
// hook into lifecycle events, to react to the Connector being added
environment.lifecycle().addEventListener(this);
}
@Override
public void onRequestFailure(final Request request, final Throwable failure) {
if (logger.isDebugEnabled()) {
final RequestInfo requestInfo = getRequestInfo(request);
logger.debug("Request failure: {} {} ({}) [{}] ",
requestInfo.method(),
requestInfo.path(),
requestInfo.userAgent(),
requestInfo.statusCode(), failure);
}
}
@Override
public void onResponseFailure(Request request, Throwable failure) {
if (failure instanceof org.eclipse.jetty.io.EofException) {
// the client disconnected early
return;
}
final RequestInfo requestInfo = getRequestInfo(request);
logger.warn("Response failure: {} {} ({}) [{}] ",
requestInfo.method(),
requestInfo.path(),
requestInfo.userAgent(),
requestInfo.statusCode(), failure);
}
@Override
public void onComplete(final Request request) {
final RequestInfo requestInfo = getRequestInfo(request);
final List<Tag> tags = new ArrayList<>(5);
tags.add(Tag.of(PATH_TAG, requestInfo.path()));
tags.add(Tag.of(METHOD_TAG, requestInfo.method()));
tags.add(Tag.of(STATUS_CODE_TAG, String.valueOf(requestInfo.statusCode())));
tags.add(Tag.of(TRAFFIC_SOURCE_TAG, TrafficSource.HTTP.name().toLowerCase()));
tags.addAll(UserAgentTagUtil.getLibsignalAndPlatformTags(requestInfo.userAgent()));
meterRegistry.counter(REQUEST_COUNTER_NAME, tags).increment();
meterRegistry.counter(RESPONSE_BYTES_COUNTER_NAME, tags).increment(request.getResponse().getContentCount());
meterRegistry.counter(REQUEST_BYTES_COUNTER_NAME, tags).increment(request.getContentRead());
UserAgentTagUtil.getClientVersionTag(requestInfo.userAgent(), clientReleaseManager).ifPresent(
clientVersionTag -> meterRegistry.counter(REQUESTS_BY_VERSION_COUNTER_NAME,
Tags.of(clientVersionTag, UserAgentTagUtil.getPlatformTag(requestInfo.userAgent()))).increment());
}
@Override
public void beanAdded(final Container parent, final Object child) {
if (child instanceof Connector connector) {
connector.addBean(this);
}
}
@Override
public void beanRemoved(final Container parent, final Object child) {
}
@Override
public void filter(final ContainerRequestContext requestContext, final ContainerResponseContext responseContext)
throws IOException {
requestContext.setProperty(URI_INFO_PROPERTY_NAME, requestContext.getUriInfo());
}
private RequestInfo getRequestInfo(Request request) {
final String path = Optional.ofNullable(request.getAttribute(URI_INFO_PROPERTY_NAME))
.map(attr -> UriInfoUtil.getPathTemplate((ExtendedUriInfo) attr))
.orElseGet(() ->
Optional.ofNullable(request.getPathInfo())
.filter(servletPaths::contains)
.orElse("unknown")
);
final String method = Optional.ofNullable(request.getMethod()).orElse("unknown");
// Response cannot be null, but its status might not always reflect an actual response status, since it gets
// initialized to 200
final int status = request.getResponse().getStatus();
@Nullable final String userAgent = request.getHeader(HttpHeaders.USER_AGENT);
return new RequestInfo(path, method, status, userAgent);
}
}

View File

@@ -0,0 +1,245 @@
/*
* Copyright 2024 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.textsecuregcm.metrics;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.net.HttpHeaders;
import io.dropwizard.core.setup.Environment;
import io.micrometer.core.instrument.MeterRegistry;
import io.micrometer.core.instrument.Tag;
import io.micrometer.core.instrument.Tags;
import jakarta.validation.constraints.NotNull;
import jakarta.ws.rs.container.ContainerRequestContext;
import jakarta.ws.rs.container.ContainerResponseContext;
import jakarta.ws.rs.container.ContainerResponseFilter;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import javax.annotation.Nullable;
import org.eclipse.jetty.http.HttpFields;
import org.eclipse.jetty.io.Content;
import org.eclipse.jetty.server.Handler;
import org.eclipse.jetty.server.Request;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.handler.EventsHandler;
import org.eclipse.jetty.util.component.LifeCycle;
import org.glassfish.jersey.server.ExtendedUriInfo;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.textsecuregcm.storage.ClientReleaseManager;
import org.whispersystems.textsecuregcm.util.logging.UriInfoUtil;
/**
* Gathers and reports HTTP request metrics at the Jetty container level, which sits above Jersey. In order to get
* templated Jersey request paths, it adds a {@link jakarta.ws.rs.container.ContainerResponseFilter}, in order to give
* itself access to the template. It is limited to {@link TrafficSource#HTTP} requests.
*
* @see MetricsRequestEventListener
*/
public class MetricsHttpEventHandler extends EventsHandler {
private static final Logger logger = LoggerFactory.getLogger(MetricsHttpEventHandler.class);
private final ClientReleaseManager clientReleaseManager;
private final Set<String> servletPaths;
// Use the same counter namespace as MetricsRequestEventListener for continuity
public static final String REQUEST_COUNTER_NAME = MetricsRequestEventListener.REQUEST_COUNTER_NAME;
public static final String REQUESTS_BY_VERSION_COUNTER_NAME = MetricsRequestEventListener.REQUESTS_BY_VERSION_COUNTER_NAME;
@VisibleForTesting
static final String RESPONSE_BYTES_COUNTER_NAME = MetricsRequestEventListener.RESPONSE_BYTES_COUNTER_NAME;
@VisibleForTesting
static final String REQUEST_BYTES_COUNTER_NAME = MetricsRequestEventListener.REQUEST_BYTES_COUNTER_NAME;
@VisibleForTesting
static final String REQUEST_INFO_PROPERTY_NAME = MetricsHttpEventHandler.class.getName() + ".requestInfo";
@VisibleForTesting
static final String PATH_TAG = "path";
@VisibleForTesting
static final String METHOD_TAG = "method";
@VisibleForTesting
static final String STATUS_CODE_TAG = "status";
@VisibleForTesting
static final String TRAFFIC_SOURCE_TAG = "trafficSource";
private final MeterRegistry meterRegistry;
@VisibleForTesting
MetricsHttpEventHandler(
final Handler handler,
final MeterRegistry meterRegistry,
final ClientReleaseManager clientReleaseManager,
final Set<String> servletPaths) {
super(handler);
this.meterRegistry = meterRegistry;
this.clientReleaseManager = clientReleaseManager;
this.servletPaths = servletPaths;
}
/**
* Configure a {@link MetricsHttpEventHandler}
*
* @param environment A dropwizard {@link org.eclipse.jetty.util.component.Environment}
* @param meterRegistry The meter registry to register metrics with
* @param clientReleaseManager A {@link ClientReleaseManager} that determines what tags to include with metrics
* @param servletPaths An allow-list of paths to include in metric tags for requests that are handled by above
* Jersey
*/
public static void configure(final Environment environment, final MeterRegistry meterRegistry,
final ClientReleaseManager clientReleaseManager, final Set<String> servletPaths) {
// register a filter that will set the initial request info
environment.jersey().register(new SetInfoRequestFilter());
// hook into lifecycle events, to react to the Connector being added
environment.lifecycle().addEventListener(new LifeCycle.Listener() {
@Override
public void lifeCycleStarting(LifeCycle event) {
if (event instanceof Server server) {
server.setHandler(
new MetricsHttpEventHandler(server.getHandler(), meterRegistry, clientReleaseManager, servletPaths));
}
}
});
}
private void onResponseFailure(Request request, int status, Throwable failure) {
if (failure instanceof org.eclipse.jetty.io.EofException) {
// the client disconnected early
return;
}
final RequestInfo requestInfo = getRequestInfo(request);
logger.warn("Response failure: {} {} ({}) [{}] ",
requestInfo.method,
requestInfo.path,
requestInfo.userAgent,
status,
failure);
}
@Override
public void onComplete(Request request, int status, HttpFields headers, Throwable failure) {
super.onComplete(request, status, headers, failure);
if (failure != null) {
onResponseFailure(request, status, failure);
}
final RequestInfo requestInfo = getRequestInfo(request);
final List<Tag> tags = new ArrayList<>(5);
tags.add(Tag.of(PATH_TAG, requestInfo.path));
tags.add(Tag.of(METHOD_TAG, requestInfo.method));
tags.add(Tag.of(STATUS_CODE_TAG, String.valueOf(status)));
tags.add(Tag.of(TRAFFIC_SOURCE_TAG, TrafficSource.HTTP.name().toLowerCase()));
tags.addAll(UserAgentTagUtil.getLibsignalAndPlatformTags(requestInfo.userAgent));
meterRegistry.counter(REQUEST_COUNTER_NAME, tags).increment();
meterRegistry.counter(RESPONSE_BYTES_COUNTER_NAME, tags).increment(requestInfo.responseBytes);
meterRegistry.counter(REQUEST_BYTES_COUNTER_NAME, tags).increment(requestInfo.requestBytes);
UserAgentTagUtil.getClientVersionTag(requestInfo.userAgent, clientReleaseManager).ifPresent(
clientVersionTag -> meterRegistry.counter(REQUESTS_BY_VERSION_COUNTER_NAME,
Tags.of(clientVersionTag, UserAgentTagUtil.getPlatformTag(requestInfo.userAgent))).increment());
}
@Override
protected void onRequestRead(final Request request, final Content.Chunk chunk) {
super.onRequestRead(request, chunk);
if (chunk != null) {
getRequestInfo(request).requestBytes += chunk.remaining();
}
}
@Override
protected void onResponseWrite(final Request request, final boolean last, final ByteBuffer content) {
super.onResponseWrite(request, last, content);
if (content != null) {
getRequestInfo(request).responseBytes += content.remaining();
}
}
private RequestInfo getRequestInfo(Request request) {
Object obj = request.getAttribute(REQUEST_INFO_PROPERTY_NAME);
if (obj != null && obj instanceof RequestInfo requestInfo) {
return requestInfo;
}
// Our ContainerResponseFilter has not run yet. It should eventually run, and will override the path we set here.
// It may not run if this is a websocket upgrade request, a request handled by jetty directly, or a higher priority
// filter aborted the request by throwing an exception, in which case we'll use this path. To avoid giving every
// incorrect path a unique tag we check against a configured list of paths that we know would skip the filter.
final RequestInfo newInfo = new RequestInfo(
Optional.ofNullable(request.getHttpURI().getPath()).filter(servletPaths::contains).orElse("unknown"),
Optional.ofNullable(request.getMethod()).orElse("unknown"),
request.getHeaders().get(HttpHeaders.USER_AGENT));
request.setAttribute(REQUEST_INFO_PROPERTY_NAME, newInfo);
return newInfo;
}
@VisibleForTesting
static class RequestInfo {
private String path;
private final String method;
private final @Nullable String userAgent;
private long requestBytes;
private long responseBytes;
RequestInfo(@NotNull String path, @NotNull String method, @Nullable String userAgent) {
this.path = path;
this.method = method;
this.userAgent = userAgent;
this.requestBytes = 0;
this.responseBytes = 0;
}
@Override
public boolean equals(final Object o) {
if (o == null || getClass() != o.getClass()) {
return false;
}
RequestInfo that = (RequestInfo) o;
return requestBytes == that.requestBytes && responseBytes == that.responseBytes && Objects.equals(path, that.path)
&& Objects.equals(method, that.method) && Objects.equals(userAgent, that.userAgent);
}
@Override
public int hashCode() {
return Objects.hash(path, method, userAgent, requestBytes, responseBytes);
}
}
@VisibleForTesting
static class SetInfoRequestFilter implements ContainerResponseFilter {
@Override
public void filter(final ContainerRequestContext requestContext, final ContainerResponseContext responseContext) {
// Construct the templated URI path. If no matching path is found, this will be ""
final String path = UriInfoUtil.getPathTemplate((ExtendedUriInfo) requestContext.getUriInfo());
final Object obj = requestContext.getProperty(REQUEST_INFO_PROPERTY_NAME);
if (obj != null && obj instanceof RequestInfo requestInfo) {
requestInfo.path = path;
} else {
requestContext.setProperty(REQUEST_INFO_PROPERTY_NAME,
new RequestInfo(path, requestContext.getMethod(), requestContext.getHeaderString(HttpHeaders.USER_AGENT)));
}
}
}
}

View File

@@ -27,7 +27,7 @@ import org.whispersystems.websocket.WebSocketResourceProvider;
/**
* Gathers and reports request-level metrics for WebSocket traffic only.
* For HTTP traffic, use {@link MetricsHttpChannelListener}.
* For HTTP traffic, use {@link MetricsHttpEventHandler}.
*/
public class MetricsRequestEventListener implements RequestEventListener {
@@ -62,7 +62,7 @@ public class MetricsRequestEventListener implements RequestEventListener {
this(trafficSource, Metrics.globalRegistry, clientReleaseManager);
if (trafficSource == TrafficSource.HTTP) {
logger.warn("Use {} for HTTP traffic", MetricsHttpChannelListener.class.getName());
logger.warn("Use {} for HTTP traffic", MetricsHttpEventHandler.class.getName());
}
}

View File

@@ -21,7 +21,7 @@ import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Optional;
import org.eclipse.jetty.util.resource.Resource;
import org.eclipse.jetty.util.resource.PathResourceFactory;
import org.eclipse.jetty.util.security.CertificateUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -37,7 +37,7 @@ public class TlsCertificateExpirationUtil {
final KeyStore keyStore;
try {
keyStore = CertificateUtils.getKeyStore(Resource.newResource(keyStorePath), keyStoreType, keyStoreProvider,
keyStore = CertificateUtils.getKeyStore(new PathResourceFactory().newResource(keyStorePath), keyStoreType, keyStoreProvider,
keyStorePassword);
} catch (Exception e) {

View File

@@ -8,14 +8,14 @@ package org.whispersystems.textsecuregcm.websocket;
import static org.whispersystems.textsecuregcm.util.HeaderUtils.basicCredentialsFromAuthHeader;
import com.google.common.net.HttpHeaders;
import javax.annotation.Nullable;
import io.dropwizard.auth.basic.BasicCredentials;
import org.eclipse.jetty.websocket.api.UpgradeRequest;
import java.util.Optional;
import javax.annotation.Nullable;
import org.eclipse.jetty.ee10.websocket.server.JettyServerUpgradeRequest;
import org.whispersystems.textsecuregcm.auth.AccountAuthenticator;
import org.whispersystems.textsecuregcm.auth.AuthenticatedDevice;
import org.whispersystems.websocket.auth.InvalidCredentialsException;
import org.whispersystems.websocket.auth.WebSocketAuthenticator;
import java.util.Optional;
public class WebSocketAccountAuthenticator implements WebSocketAuthenticator<AuthenticatedDevice> {
@@ -27,7 +27,7 @@ public class WebSocketAccountAuthenticator implements WebSocketAuthenticator<Aut
}
@Override
public Optional<AuthenticatedDevice> authenticate(final UpgradeRequest request)
public Optional<AuthenticatedDevice> authenticate(final JettyServerUpgradeRequest request)
throws InvalidCredentialsException {
@Nullable final String authHeader = request.getHeader(HttpHeaders.AUTHORIZATION);