Record OS versions for desktop and SDK versions for Android.

This commit is contained in:
Jon Chambers 2020-12-07 18:24:11 -05:00 committed by Jon Chambers
parent 0de3a400eb
commit 34a11c2338
2 changed files with 130 additions and 12 deletions

View File

@ -11,22 +11,37 @@ 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.internal.process.MappableException;
import org.glassfish.jersey.server.monitoring.RequestEvent;
import org.glassfish.jersey.server.monitoring.RequestEventListener;
import org.whispersystems.textsecuregcm.util.ua.ClientPlatform;
import org.whispersystems.textsecuregcm.util.ua.UnrecognizedUserAgentException;
import org.whispersystems.textsecuregcm.util.ua.UserAgent;
import org.whispersystems.textsecuregcm.util.ua.UserAgentUtil;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
/**
* Gathers and reports request-level metrics.
*/
class MetricsRequestEventListener implements RequestEventListener {
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 TRAFFIC_SOURCE_TAG = "trafficSource";
static final String REQUEST_COUNTER_NAME = MetricRegistry.name(MetricsRequestEventListener.class, "request");
static final String PATH_TAG = "path";
static final String STATUS_CODE_TAG = "status";
static final String TRAFFIC_SOURCE_TAG = "trafficSource";
static final String ANDROID_REQUEST_COUNTER_NAME = MetricRegistry.name(MetricsRequestEventListener.class, "androidRequest");
static final String DESKTOP_REQUEST_COUNTER_NAME = MetricRegistry.name(MetricsRequestEventListener.class, "desktopRequest");
static final String OS_TAG = "os";
static final String SDK_TAG = "sdkVersion";
private static final Set<String> ACCEPTABLE_DESKTOP_OS_STRINGS = Set.of("linux", "macos", "windows");
private static final String ANDROID_SDK_PREFIX = "Android/";
private static final int MIN_ANDROID_SDK_VERSION = 19;
private static final int MAX_ANDROID_SDK_VERSION = 50;
private final TrafficSource trafficSource;
private final MeterRegistry meterRegistry;
@ -53,11 +68,46 @@ class MetricsRequestEventListener implements RequestEventListener {
final List<String> userAgentValues = event.getContainerRequest().getRequestHeader("User-Agent");
tags.addAll(UserAgentTagUtil.getUserAgentTags(userAgentValues != null ? userAgentValues.stream().findFirst().orElse(null) : null));
meterRegistry.counter(COUNTER_NAME, tags).increment();
meterRegistry.counter(REQUEST_COUNTER_NAME, tags).increment();
try {
final UserAgent userAgent = UserAgentUtil.parseUserAgentString(userAgentValues != null ? userAgentValues.stream().findFirst().orElse(null) : null);
recordDesktopOperatingSystem(userAgent);
recordAndroidSdkVersion(userAgent);
} catch (final UnrecognizedUserAgentException ignored) {
}
}
}
}
@VisibleForTesting
void recordDesktopOperatingSystem(final UserAgent userAgent) {
if (userAgent.getPlatform() == ClientPlatform.DESKTOP) {
if (userAgent.getAdditionalSpecifiers().map(String::toLowerCase).map(ACCEPTABLE_DESKTOP_OS_STRINGS::contains).orElse(false)) {
meterRegistry.counter(DESKTOP_REQUEST_COUNTER_NAME, OS_TAG, userAgent.getAdditionalSpecifiers().get().toLowerCase()).increment();
}
}
}
@VisibleForTesting
void recordAndroidSdkVersion(final UserAgent userAgent) {
if (userAgent.getPlatform() == ClientPlatform.ANDROID) {
userAgent.getAdditionalSpecifiers().ifPresent(additionalSpecifiers -> {
if (additionalSpecifiers.startsWith(ANDROID_SDK_PREFIX)) {
try {
final int sdkVersion = Integer.parseInt(additionalSpecifiers, ANDROID_SDK_PREFIX.length(), additionalSpecifiers.length(), 10);
if (sdkVersion >= MIN_ANDROID_SDK_VERSION && sdkVersion <= MAX_ANDROID_SDK_VERSION) {
meterRegistry.counter(ANDROID_REQUEST_COUNTER_NAME, SDK_TAG, String.valueOf(sdkVersion));
}
} catch (final NumberFormatException ignored) {
}
}
});
}
}
@VisibleForTesting
static String getPathTemplate(final ExtendedUriInfo uriInfo) {
final StringBuilder pathBuilder = new StringBuilder();

View File

@ -7,11 +7,14 @@ package org.whispersystems.textsecuregcm.metrics;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.protobuf.InvalidProtocolBufferException;
import com.vdurmont.semver4j.Semver;
import io.dropwizard.jersey.DropwizardResourceConfig;
import io.dropwizard.jersey.jackson.JacksonMessageBodyProvider;
import io.micrometer.core.instrument.Counter;
import io.micrometer.core.instrument.MeterRegistry;
import io.micrometer.core.instrument.Tag;
import junitparams.JUnitParamsRunner;
import junitparams.Parameters;
import org.eclipse.jetty.websocket.api.RemoteEndpoint;
import org.eclipse.jetty.websocket.api.Session;
import org.eclipse.jetty.websocket.api.UpgradeRequest;
@ -24,7 +27,10 @@ import org.glassfish.jersey.server.monitoring.RequestEvent;
import org.glassfish.jersey.uri.UriTemplate;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.whispersystems.textsecuregcm.util.ua.ClientPlatform;
import org.whispersystems.textsecuregcm.util.ua.UserAgent;
import org.whispersystems.websocket.WebSocketResourceProvider;
import org.whispersystems.websocket.auth.WebsocketAuthValueFactoryProvider;
import org.whispersystems.websocket.logging.WebsocketRequestLog;
@ -40,6 +46,7 @@ import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Optional;
import java.util.Set;
@ -47,11 +54,15 @@ import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyIterable;
import static org.mockito.ArgumentMatchers.anyVararg;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@RunWith(JUnitParamsRunner.class)
public class MetricsRequestEventListenerTest {
private MeterRegistry meterRegistry;
@ -89,11 +100,11 @@ public class MetricsRequestEventListenerTest {
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);
when(meterRegistry.counter(eq(MetricsRequestEventListener.REQUEST_COUNTER_NAME), any(Iterable.class))).thenReturn(counter);
listener.onEvent(event);
verify(meterRegistry).counter(eq(MetricsRequestEventListener.COUNTER_NAME), tagCaptor.capture());
verify(meterRegistry).counter(eq(MetricsRequestEventListener.REQUEST_COUNTER_NAME), tagCaptor.capture());
final Iterable<Tag> tagIterable = tagCaptor.getValue();
final Set<Tag> tags = new HashSet<>();
@ -147,7 +158,7 @@ public class MetricsRequestEventListenerTest {
when(request.getHeader("User-Agent")).thenReturn("Signal-Android 4.53.7 (Android 8.1)");
final ArgumentCaptor<Iterable<Tag>> tagCaptor = ArgumentCaptor.forClass(Iterable.class);
when(meterRegistry.counter(eq(MetricsRequestEventListener.COUNTER_NAME), any(Iterable.class))).thenReturn(counter);
when(meterRegistry.counter(eq(MetricsRequestEventListener.REQUEST_COUNTER_NAME), any(Iterable.class))).thenReturn(counter);
provider.onWebSocketConnect(session);
@ -162,7 +173,7 @@ public class MetricsRequestEventListenerTest {
assertThat(response.getStatus()).isEqualTo(200);
verify(meterRegistry).counter(eq(MetricsRequestEventListener.COUNTER_NAME), tagCaptor.capture());
verify(meterRegistry).counter(eq(MetricsRequestEventListener.REQUEST_COUNTER_NAME), tagCaptor.capture());
final Iterable<Tag> tagIterable = tagCaptor.getValue();
final Set<Tag> tags = new HashSet<>();
@ -203,7 +214,7 @@ public class MetricsRequestEventListenerTest {
when(session.getRemote()).thenReturn(remoteEndpoint);
final ArgumentCaptor<Iterable<Tag>> tagCaptor = ArgumentCaptor.forClass(Iterable.class);
when(meterRegistry.counter(eq(MetricsRequestEventListener.COUNTER_NAME), any(Iterable.class))).thenReturn(counter);
when(meterRegistry.counter(eq(MetricsRequestEventListener.REQUEST_COUNTER_NAME), any(Iterable.class))).thenReturn(counter);
provider.onWebSocketConnect(session);
@ -218,7 +229,7 @@ public class MetricsRequestEventListenerTest {
assertThat(response.getStatus()).isEqualTo(200);
verify(meterRegistry).counter(eq(MetricsRequestEventListener.COUNTER_NAME), tagCaptor.capture());
verify(meterRegistry).counter(eq(MetricsRequestEventListener.REQUEST_COUNTER_NAME), tagCaptor.capture());
final Iterable<Tag> tagIterable = tagCaptor.getValue();
final Set<Tag> tags = new HashSet<>();
@ -234,6 +245,63 @@ public class MetricsRequestEventListenerTest {
assertTrue(tags.containsAll(UserAgentTagUtil.UNRECOGNIZED_TAGS));
}
@Test
@Parameters(method = "argumentsForTestRecordDesktopOperatingSystem")
public void testRecordDesktopOperatingSystem(final UserAgent userAgent, final String expectedOperatingSystem) {
when(meterRegistry.counter(eq(MetricsRequestEventListener.DESKTOP_REQUEST_COUNTER_NAME), (String)any())).thenReturn(counter);
listener.recordDesktopOperatingSystem(userAgent);
if (expectedOperatingSystem != null) {
final ArgumentCaptor<String> tagCaptor = ArgumentCaptor.forClass(String.class);
verify(meterRegistry).counter(eq(MetricsRequestEventListener.DESKTOP_REQUEST_COUNTER_NAME), tagCaptor.capture());
assertEquals(List.of(MetricsRequestEventListener.OS_TAG, expectedOperatingSystem), tagCaptor.getAllValues());
} else {
verify(meterRegistry, never()).counter(eq(MetricsRequestEventListener.DESKTOP_REQUEST_COUNTER_NAME));
verify(meterRegistry, never()).counter(eq(MetricsRequestEventListener.DESKTOP_REQUEST_COUNTER_NAME), (String)any());
}
}
private static Object argumentsForTestRecordDesktopOperatingSystem() {
return new Object[] {
new Object[] { new UserAgent(ClientPlatform.DESKTOP, new Semver("1.2.3"), "Linux"), "linux" },
new Object[] { new UserAgent(ClientPlatform.DESKTOP, new Semver("1.2.3"), "macOS"), "macos" },
new Object[] { new UserAgent(ClientPlatform.DESKTOP, new Semver("1.2.3"), "Windows"), "windows" },
new Object[] { new UserAgent(ClientPlatform.DESKTOP, new Semver("1.2.3")), null },
new Object[] { new UserAgent(ClientPlatform.ANDROID, new Semver("4.68.3"), "Android/25"), null },
new Object[] { new UserAgent(ClientPlatform.IOS, new Semver("3.9.0"), "(iPhone; iOS 12.2; Scale/3.00)"), null },
};
}
@Test
@Parameters(method = "argumentsForTestRecordAndroidSdkVersion")
public void testRecordAndroidSdkVersion(final UserAgent userAgent, final String expectedSdkVersion) {
when(meterRegistry.counter(eq(MetricsRequestEventListener.ANDROID_REQUEST_COUNTER_NAME), (String)any())).thenReturn(counter);
listener.recordAndroidSdkVersion(userAgent);
if (expectedSdkVersion != null) {
final ArgumentCaptor<String> tagCaptor = ArgumentCaptor.forClass(String.class);
verify(meterRegistry).counter(eq(MetricsRequestEventListener.ANDROID_REQUEST_COUNTER_NAME), tagCaptor.capture());
assertEquals(List.of(MetricsRequestEventListener.SDK_TAG, expectedSdkVersion), tagCaptor.getAllValues());
} else {
verify(meterRegistry, never()).counter(eq(MetricsRequestEventListener.ANDROID_REQUEST_COUNTER_NAME));
verify(meterRegistry, never()).counter(eq(MetricsRequestEventListener.ANDROID_REQUEST_COUNTER_NAME), (String)any());
}
}
private static Object argumentsForTestRecordAndroidSdkVersion() {
return new Object[] {
new Object[] { new UserAgent(ClientPlatform.ANDROID, new Semver("4.68.3"), "Android/1"), null },
new Object[] { new UserAgent(ClientPlatform.ANDROID, new Semver("4.68.3"), "Android/25"), "25" },
new Object[] { new UserAgent(ClientPlatform.ANDROID, new Semver("4.68.3"), "Android/700000"), null },
new Object[] { new UserAgent(ClientPlatform.ANDROID, new Semver("4.68.3"), "Android/"), null },
new Object[] { new UserAgent(ClientPlatform.ANDROID, new Semver("4.68.3"), null), null },
new Object[] { new UserAgent(ClientPlatform.DESKTOP, new Semver("1.2.3"), "Linux"), null },
new Object[] { new UserAgent(ClientPlatform.IOS, new Semver("3.9.0"), "(iPhone; iOS 12.2; Scale/3.00)"), null }
};
}
private static SubProtocol.WebSocketResponseMessage getResponse(ArgumentCaptor<ByteBuffer> responseCaptor) throws InvalidProtocolBufferException {
return SubProtocol.WebSocketMessage.parseFrom(responseCaptor.getValue().array()).getResponse();
}