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