Record OS versions for desktop and SDK versions for Android.
This commit is contained in:
parent
0de3a400eb
commit
34a11c2338
|
@ -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();
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue