Capture request-level metrics (path, status, client platform/version).

This commit is contained in:
Jon Chambers 2020-05-19 13:28:20 -04:00 committed by Jon Chambers
parent 45ad8f8ffb
commit a13c44d81a
4 changed files with 255 additions and 0 deletions

View File

@ -82,6 +82,7 @@ import org.whispersystems.textsecuregcm.mappers.RateLimitExceededExceptionMapper
import org.whispersystems.textsecuregcm.metrics.CpuUsageGauge;
import org.whispersystems.textsecuregcm.metrics.FileDescriptorGauge;
import org.whispersystems.textsecuregcm.metrics.FreeMemoryGauge;
import org.whispersystems.textsecuregcm.metrics.MetricsApplicationEventListener;
import org.whispersystems.textsecuregcm.metrics.NetworkReceivedGauge;
import org.whispersystems.textsecuregcm.metrics.NetworkSentGauge;
import org.whispersystems.textsecuregcm.providers.RedisClientFactory;
@ -316,6 +317,8 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
AuthFilter<BasicCredentials, Account> accountAuthFilter = new BasicCredentialAuthFilter.Builder<Account>().setAuthenticator(accountAuthenticator).buildAuthFilter ();
AuthFilter<BasicCredentials, DisabledPermittedAccount> disabledPermittedAccountAuthFilter = new BasicCredentialAuthFilter.Builder<DisabledPermittedAccount>().setAuthenticator(disabledPermittedAccountAuthenticator).buildAuthFilter();
environment.jersey().register(new MetricsApplicationEventListener());
environment.jersey().register(new PolymorphicAuthDynamicFeature<>(ImmutableMap.of(Account.class, accountAuthFilter,
DisabledPermittedAccount.class, disabledPermittedAccountAuthFilter)));
environment.jersey().register(new PolymorphicAuthValueFactoryProvider.Binder<>(ImmutableSet.of(Account.class, DisabledPermittedAccount.class)));

View File

@ -0,0 +1,23 @@
package org.whispersystems.textsecuregcm.metrics;
import org.glassfish.jersey.server.monitoring.ApplicationEvent;
import org.glassfish.jersey.server.monitoring.ApplicationEventListener;
import org.glassfish.jersey.server.monitoring.RequestEvent;
import org.glassfish.jersey.server.monitoring.RequestEventListener;
/**
* Delegates request events to a listener that captures and reports request-level metrics.
*/
public class MetricsApplicationEventListener implements ApplicationEventListener {
private final MetricsRequestEventListener metricsRequestEventListener = new MetricsRequestEventListener();
@Override
public void onEvent(final ApplicationEvent event) {
}
@Override
public RequestEventListener onRequest(final RequestEvent requestEvent) {
return metricsRequestEventListener;
}
}

View File

@ -0,0 +1,102 @@
package org.whispersystems.textsecuregcm.metrics;
import com.codahale.metrics.MetricRegistry;
import com.google.common.annotations.VisibleForTesting;
import io.micrometer.core.instrument.MeterRegistry;
import io.micrometer.core.instrument.Metrics;
import io.micrometer.core.instrument.Tag;
import org.glassfish.jersey.server.ExtendedUriInfo;
import org.glassfish.jersey.server.monitoring.RequestEvent;
import org.glassfish.jersey.server.monitoring.RequestEventListener;
import org.whispersystems.textsecuregcm.util.Pair;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Gathers and reports request-level metrics.
*/
class MetricsRequestEventListener implements RequestEventListener {
static final int MAX_VERSIONS = 10_000;
static final String COUNTER_NAME = MetricRegistry.name(MetricsRequestEventListener.class, "request");
static final String PATH_TAG = "path";
static final String STATUS_CODE_TAG = "status";
static final String PLATFORM_TAG = "platform";
static final String VERSION_TAG = "clientVersion";
static final List<Tag> OVERFLOW_TAGS = List.of(Tag.of(PLATFORM_TAG, "overflow"), Tag.of(VERSION_TAG, "overflow"));
static final List<Tag> UNRECOGNIZED_TAGS = List.of(Tag.of(PLATFORM_TAG, "unrecognized"), Tag.of(VERSION_TAG, "unrecognized"));
private static final Pattern USER_AGENT_PATTERN = Pattern.compile("Signal-([^ ]+) ([^ ]+).*$", Pattern.CASE_INSENSITIVE);
private final MeterRegistry meterRegistry;
private final Set<Pair<String, String>> seenVersions = new HashSet<>();
public MetricsRequestEventListener() {
this(Metrics.globalRegistry);
}
@VisibleForTesting
MetricsRequestEventListener(final MeterRegistry meterRegistry) {
this.meterRegistry = meterRegistry;
}
@Override
public void onEvent(final RequestEvent event) {
if (event.getType() == RequestEvent.Type.FINISHED) {
if (!event.getUriInfo().getMatchedTemplates().isEmpty()) {
final List<Tag> tags = new ArrayList<>(4);
tags.add(Tag.of(PATH_TAG, getPathTemplate(event.getUriInfo())));
tags.add(Tag.of(STATUS_CODE_TAG, String.valueOf(event.getContainerResponse().getStatus())));
event.getContainerRequest().getRequestHeader("User-Agent")
.stream()
.findFirst()
.map(this::getUserAgentTags)
.ifPresent(tags::addAll);
meterRegistry.counter(COUNTER_NAME, tags).increment();
}
}
}
@VisibleForTesting
static String getPathTemplate(final ExtendedUriInfo uriInfo) {
final StringBuilder pathBuilder = new StringBuilder();
for (int i = uriInfo.getMatchedTemplates().size() - 1; i >= 0; i--) {
pathBuilder.append(uriInfo.getMatchedTemplates().get(i).getTemplate());
}
return pathBuilder.toString();
}
@VisibleForTesting
List<Tag> getUserAgentTags(final String userAgent) {
final Matcher matcher = USER_AGENT_PATTERN.matcher(userAgent);
final List<Tag> tags;
if (matcher.matches()) {
final Pair<String, String> platformAndVersion = new Pair<>(matcher.group(1).toLowerCase(), matcher.group(2));
final boolean allowVersion;
synchronized (seenVersions) {
allowVersion = seenVersions.contains(platformAndVersion) || (seenVersions.size() < MAX_VERSIONS && seenVersions.add(platformAndVersion));
}
tags = allowVersion ? List.of(Tag.of(PLATFORM_TAG, platformAndVersion.first()), Tag.of(VERSION_TAG, platformAndVersion.second())) : OVERFLOW_TAGS;
} else {
tags = UNRECOGNIZED_TAGS;
}
return tags;
}
}

View File

@ -0,0 +1,127 @@
package org.whispersystems.textsecuregcm.metrics;
import io.micrometer.core.instrument.Counter;
import io.micrometer.core.instrument.MeterRegistry;
import io.micrometer.core.instrument.Tag;
import org.bouncycastle.ocsp.Req;
import org.glassfish.jersey.server.ContainerRequest;
import org.glassfish.jersey.server.ContainerResponse;
import org.glassfish.jersey.server.ExtendedUriInfo;
import org.glassfish.jersey.server.monitoring.RequestEvent;
import org.glassfish.jersey.uri.UriTemplate;
import org.junit.Before;
import org.junit.Test;
import org.mockito.ArgumentCaptor;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import static org.junit.Assert.*;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
public class MetricsRequestEventListenerTest {
private MeterRegistry meterRegistry;
private Counter counter;
private MetricsRequestEventListener listener;
@Before
public void setup() {
meterRegistry = mock(MeterRegistry.class);
counter = mock(Counter.class);
listener = new MetricsRequestEventListener(meterRegistry);
}
@Test
@SuppressWarnings("unchecked")
public void testOnEvent() {
final String path = "/test";
final int statusCode = 200;
final ExtendedUriInfo uriInfo = mock(ExtendedUriInfo.class);
when(uriInfo.getMatchedTemplates()).thenReturn(Collections.singletonList(new UriTemplate(path)));
final ContainerRequest request = mock(ContainerRequest.class);
when(request.getRequestHeader("User-Agent")).thenReturn(Collections.singletonList("Signal-Android 4.53.7 (Android 8.1)"));
final ContainerResponse response = mock(ContainerResponse.class);
when(response.getStatus()).thenReturn(statusCode);
final RequestEvent event = mock(RequestEvent.class);
when(event.getType()).thenReturn(RequestEvent.Type.FINISHED);
when(event.getUriInfo()).thenReturn(uriInfo);
when(event.getContainerRequest()).thenReturn(request);
when(event.getContainerResponse()).thenReturn(response);
final ArgumentCaptor<Iterable<Tag>> tagCaptor = ArgumentCaptor.forClass(Iterable.class);
when(meterRegistry.counter(eq(MetricsRequestEventListener.COUNTER_NAME), any(Iterable.class))).thenReturn(counter);
listener.onEvent(event);
verify(meterRegistry).counter(eq(MetricsRequestEventListener.COUNTER_NAME), tagCaptor.capture());
final Iterable<Tag> tagIterable = tagCaptor.getValue();
final Set<Tag> tags = new HashSet<>();
for (final Tag tag : tagIterable) {
tags.add(tag);
}
assertEquals(4, tags.size());
assertTrue(tags.contains(Tag.of(MetricsRequestEventListener.PATH_TAG, path)));
assertTrue(tags.contains(Tag.of(MetricsRequestEventListener.STATUS_CODE_TAG, String.valueOf(statusCode))));
assertTrue(tags.contains(Tag.of(MetricsRequestEventListener.PLATFORM_TAG, "android")));
assertTrue(tags.contains(Tag.of(MetricsRequestEventListener.VERSION_TAG, "4.53.7")));
}
@Test
public void testGetPathTemplate() {
final UriTemplate firstComponent = new UriTemplate("/first");
final UriTemplate secondComponent = new UriTemplate("/second");
final UriTemplate thirdComponent = new UriTemplate("/{param}/{moreDifferentParam}");
final ExtendedUriInfo uriInfo = mock(ExtendedUriInfo.class);
when(uriInfo.getMatchedTemplates()).thenReturn(Arrays.asList(thirdComponent, secondComponent, firstComponent));
assertEquals("/first/second/{param}/{moreDifferentParam}", MetricsRequestEventListener.getPathTemplate(uriInfo));
}
@Test
public void testGetUserAgentTags() {
assertEquals(MetricsRequestEventListener.UNRECOGNIZED_TAGS,
listener.getUserAgentTags("This is obviously not a reasonable User-Agent string."));
final List<Tag> tags = listener.getUserAgentTags("Signal-Android 4.53.7 (Android 8.1)");
assertEquals(2, tags.size());
assertTrue(tags.contains(Tag.of(MetricsRequestEventListener.PLATFORM_TAG, "android")));
assertTrue(tags.contains(Tag.of(MetricsRequestEventListener.VERSION_TAG, "4.53.7")));
}
@Test
public void testGetUserAgentTagsFlooded() {
for (int i = 0; i < MetricsRequestEventListener.MAX_VERSIONS; i++) {
listener.getUserAgentTags(String.format("Signal-Android 1.0.%d (Android 8.1)", i));
}
assertEquals(MetricsRequestEventListener.OVERFLOW_TAGS,
listener.getUserAgentTags("Signal-Android 2.0.0 (Android 8.1)"));
final List<Tag> tags = listener.getUserAgentTags("Signal-Android 1.0.0 (Android 8.1)");
assertEquals(2, tags.size());
assertTrue(tags.contains(Tag.of(MetricsRequestEventListener.PLATFORM_TAG, "android")));
assertTrue(tags.contains(Tag.of(MetricsRequestEventListener.VERSION_TAG, "1.0.0")));
}
}