diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/metrics/MetricsRequestEventListener.java b/service/src/main/java/org/whispersystems/textsecuregcm/metrics/MetricsRequestEventListener.java index 2fb5312a4..104560c04 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/metrics/MetricsRequestEventListener.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/metrics/MetricsRequestEventListener.java @@ -8,36 +8,21 @@ 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 COUNTER_NAME = MetricRegistry.name(MetricsRequestEventListener.class, "request"); + static final String PATH_TAG = "path"; + static final String STATUS_CODE_TAG = "status"; - 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 OVERFLOW_TAGS = List.of(Tag.of(PLATFORM_TAG, "overflow"), Tag.of(VERSION_TAG, "overflow")); - static final List 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> seenVersions = new HashSet<>(); + private final MeterRegistry meterRegistry; public MetricsRequestEventListener() { this(Metrics.globalRegistry); @@ -59,7 +44,7 @@ class MetricsRequestEventListener implements RequestEventListener { event.getContainerRequest().getRequestHeader("User-Agent") .stream() .findFirst() - .map(this::getUserAgentTags) + .map(UserAgentTagUtil::getUserAgentTags) .ifPresent(tags::addAll); meterRegistry.counter(COUNTER_NAME, tags).increment(); @@ -77,26 +62,4 @@ class MetricsRequestEventListener implements RequestEventListener { return pathBuilder.toString(); } - - @VisibleForTesting - List getUserAgentTags(final String userAgent) { - final Matcher matcher = USER_AGENT_PATTERN.matcher(userAgent); - final List tags; - - if (matcher.matches()) { - final Pair 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; - } } diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/metrics/UserAgentTagUtil.java b/service/src/main/java/org/whispersystems/textsecuregcm/metrics/UserAgentTagUtil.java new file mode 100644 index 000000000..31c479752 --- /dev/null +++ b/service/src/main/java/org/whispersystems/textsecuregcm/metrics/UserAgentTagUtil.java @@ -0,0 +1,52 @@ +package org.whispersystems.textsecuregcm.metrics; + +import io.micrometer.core.instrument.Tag; +import org.whispersystems.textsecuregcm.util.Pair; + +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Utility class for extracting platform/version metrics tags from User-Agent strings. + */ +public class UserAgentTagUtil { + + static final int MAX_VERSIONS = 10_000; + + public static final String PLATFORM_TAG = "platform"; + public static final String VERSION_TAG = "clientVersion"; + + static final List OVERFLOW_TAGS = List.of(Tag.of(PLATFORM_TAG, "overflow"), Tag.of(VERSION_TAG, "overflow")); + static final List 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 static final Set> SEEN_VERSIONS = new HashSet<>(); + + private UserAgentTagUtil() { + } + + public static List getUserAgentTags(final String userAgent) { + final Matcher matcher = USER_AGENT_PATTERN.matcher(userAgent); + final List tags; + + if (matcher.matches()) { + final Pair platformAndVersion = new Pair<>(matcher.group(1).toLowerCase(), matcher.group(2)); + + final boolean allowVersion; + + synchronized (SEEN_VERSIONS) { + allowVersion = SEEN_VERSIONS.contains(platformAndVersion) || (SEEN_VERSIONS.size() < MAX_VERSIONS && SEEN_VERSIONS.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; + } +} diff --git a/service/src/test/java/org/whispersystems/textsecuregcm/metrics/MetricsRequestEventListenerTest.java b/service/src/test/java/org/whispersystems/textsecuregcm/metrics/MetricsRequestEventListenerTest.java index eb593e40e..0e5ef1c0a 100644 --- a/service/src/test/java/org/whispersystems/textsecuregcm/metrics/MetricsRequestEventListenerTest.java +++ b/service/src/test/java/org/whispersystems/textsecuregcm/metrics/MetricsRequestEventListenerTest.java @@ -81,8 +81,8 @@ public class MetricsRequestEventListenerTest { 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"))); + assertTrue(tags.contains(Tag.of(UserAgentTagUtil.PLATFORM_TAG, "android"))); + assertTrue(tags.contains(Tag.of(UserAgentTagUtil.VERSION_TAG, "4.53.7"))); } @Test @@ -96,32 +96,4 @@ public class MetricsRequestEventListenerTest { 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 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 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"))); - } } diff --git a/service/src/test/java/org/whispersystems/textsecuregcm/metrics/UserAgentTagUtilTest.java b/service/src/test/java/org/whispersystems/textsecuregcm/metrics/UserAgentTagUtilTest.java new file mode 100644 index 000000000..e84b74194 --- /dev/null +++ b/service/src/test/java/org/whispersystems/textsecuregcm/metrics/UserAgentTagUtilTest.java @@ -0,0 +1,39 @@ +package org.whispersystems.textsecuregcm.metrics; + +import io.micrometer.core.instrument.Tag; +import org.junit.Test; + +import java.util.List; + +import static org.junit.Assert.*; + +public class UserAgentTagUtilTest { + + @Test + public void testGetUserAgentTags() { + assertEquals(UserAgentTagUtil.UNRECOGNIZED_TAGS, + UserAgentTagUtil.getUserAgentTags("This is obviously not a reasonable User-Agent string.")); + + final List tags = UserAgentTagUtil.getUserAgentTags("Signal-Android 4.53.7 (Android 8.1)"); + + assertEquals(2, tags.size()); + assertTrue(tags.contains(Tag.of(UserAgentTagUtil.PLATFORM_TAG, "android"))); + assertTrue(tags.contains(Tag.of(UserAgentTagUtil.VERSION_TAG, "4.53.7"))); + } + + @Test + public void testGetUserAgentTagsFlooded() { + for (int i = 0; i < UserAgentTagUtil.MAX_VERSIONS; i++) { + UserAgentTagUtil.getUserAgentTags(String.format("Signal-Android 1.0.%d (Android 8.1)", i)); + } + + assertEquals(UserAgentTagUtil.OVERFLOW_TAGS, + UserAgentTagUtil.getUserAgentTags("Signal-Android 2.0.0 (Android 8.1)")); + + final List tags = UserAgentTagUtil.getUserAgentTags("Signal-Android 1.0.0 (Android 8.1)"); + + assertEquals(2, tags.size()); + assertTrue(tags.contains(Tag.of(UserAgentTagUtil.PLATFORM_TAG, "android"))); + assertTrue(tags.contains(Tag.of(UserAgentTagUtil.VERSION_TAG, "1.0.0"))); + } +}