diff --git a/java/google/registry/model/BUILD b/java/google/registry/model/BUILD index 108e825e9..34d2c83f8 100644 --- a/java/google/registry/model/BUILD +++ b/java/google/registry/model/BUILD @@ -14,6 +14,7 @@ java_library( visibility = ["//visibility:public"], deps = [ "//java/google/registry/config", + "//java/google/registry/monitoring/metrics", "//java/google/registry/util", "//java/google/registry/xml", "//third_party/java/objectify:objectify-v4_1", diff --git a/java/google/registry/model/registry/label/DomainLabelMetrics.java b/java/google/registry/model/registry/label/DomainLabelMetrics.java new file mode 100644 index 000000000..c4b6262cb --- /dev/null +++ b/java/google/registry/model/registry/label/DomainLabelMetrics.java @@ -0,0 +1,127 @@ +// Copyright 2017 The Nomulus Authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package google.registry.model.registry.label; + +import com.google.auto.value.AutoValue; +import com.google.common.annotations.VisibleForTesting; +import com.google.common.collect.ImmutableSet; +import google.registry.monitoring.metrics.EventMetric; +import google.registry.monitoring.metrics.IncrementableMetric; +import google.registry.monitoring.metrics.LabelDescriptor; +import google.registry.monitoring.metrics.MetricRegistryImpl; + +/** Instrumentation for reserved lists. */ +class DomainLabelMetrics { + + @AutoValue + abstract static class MetricsReservedListMatch { + static MetricsReservedListMatch create( + String reservedListName, ReservationType reservationType) { + return new AutoValue_DomainLabelMetrics_MetricsReservedListMatch( + reservedListName, reservationType); + } + + abstract String reservedListName(); + abstract ReservationType reservationType(); + } + + /** + * Labels attached to {@link #reservedListChecks} and {@link #reservedListProcessingTimes} + * metrics. + * + *

A domain name can be matched by multiple reserved lists. To keep the metrics useful by + * emitting only one metric result for each check, while avoiding potential combinatorial + * explosion if all the matching lists and reservation types were to be displayed, we store as + * labels only the number of matching lists, along with the most severe match found. Note that + * "most severe" may not be meaningful, and this should only be treated as "one of the matches + * that we found". But we might as well make it as useful as possible. + */ + private static final ImmutableSet RESERVED_LIST_LABEL_DESCRIPTORS = + ImmutableSet.of( + LabelDescriptor.create("tld", "TLD"), + LabelDescriptor.create("reserved_list_count", "Number of matching reserved lists."), + LabelDescriptor.create("most_severe_reserved_list", "Reserved list name, if any."), + LabelDescriptor.create("most_severe_reservation_type", "Type of reservation found.")); + + /** Labels attached to {@link #reservedListHits} metric. */ + private static final ImmutableSet RESERVED_LIST_HIT_LABEL_DESCRIPTORS = + ImmutableSet.of( + LabelDescriptor.create("tld", "TLD"), + LabelDescriptor.create("reserved_list", "Reserved list name."), + LabelDescriptor.create("reservation_type", "Type of reservation found.")); + + /** Metric counting the number of times a label was checked against all reserved lists. */ + @VisibleForTesting + static final IncrementableMetric reservedListChecks = + MetricRegistryImpl.getDefault() + .newIncrementableMetric( + "/domain_label/reserved/checks", + "Count of reserved list checks", + "count", + RESERVED_LIST_LABEL_DESCRIPTORS); + + /** Metric recording the amount of time required to check a label against all reserved lists. */ + @VisibleForTesting + static final EventMetric reservedListProcessingTimes = + MetricRegistryImpl.getDefault() + .newEventMetric( + "/domain_label/reserved/processing_time", + "Reserved list check processing time", + "milliseconds", + RESERVED_LIST_LABEL_DESCRIPTORS, + EventMetric.DEFAULT_FITTER); + + /** + * Metric recording the number of times a label was found in a reserved list. + * + *

Each time a label is checked, and a list associated with the TLD contains that label, that + * count is incremented. A label can be found in more than one list, which would result in a + * single increment of {@link #reservedListChecks}, but multiple increments of {@link + * #reservedListHits}. It can of course also match zero lists, which would still result in a + * single increment of {@link #reservedListChecks}, but no increments of {@link + * #reservedListHits}. + */ + @VisibleForTesting + static final IncrementableMetric reservedListHits = + MetricRegistryImpl.getDefault() + .newIncrementableMetric( + "/domain_label/reserved/hits", + "Count of reserved list hits", + "count", + RESERVED_LIST_HIT_LABEL_DESCRIPTORS); + + /** Update all three reserved list metrics. */ + static void recordReservedListCheckOutcome( + String tld, ImmutableSet matches, double elapsedMillis) { + MetricsReservedListMatch mostSevereMatch = null; + for (MetricsReservedListMatch match : matches) { + reservedListHits.increment(tld, match.reservedListName(), match.reservationType().toString()); + if ((mostSevereMatch == null) + || (match.reservationType().compareTo(mostSevereMatch.reservationType()) > 0)) { + mostSevereMatch = match; + } + } + String matchCount = String.valueOf(matches.size()); + String mostSevereReservedList = + matches.isEmpty() ? "(none)" : mostSevereMatch.reservedListName(); + String mostSevereReservationType = + (matches.isEmpty() ? ReservationType.UNRESERVED : mostSevereMatch.reservationType()) + .toString(); + reservedListChecks.increment( + tld, matchCount, mostSevereReservedList, mostSevereReservationType); + reservedListProcessingTimes.record( + elapsedMillis, tld, matchCount, mostSevereReservedList, mostSevereReservationType); + } +} diff --git a/java/google/registry/model/registry/label/ReservedList.java b/java/google/registry/model/registry/label/ReservedList.java index 15bbc70f7..6ae68df9d 100644 --- a/java/google/registry/model/registry/label/ReservedList.java +++ b/java/google/registry/model/registry/label/ReservedList.java @@ -27,6 +27,7 @@ import static google.registry.model.registry.label.ReservationType.RESERVED_FOR_ import static google.registry.model.registry.label.ReservationType.UNRESERVED; import static google.registry.util.CollectionUtils.nullToEmpty; import static java.util.concurrent.TimeUnit.MILLISECONDS; +import static org.joda.time.DateTimeZone.UTC; import com.google.common.base.Function; import com.google.common.base.Optional; @@ -46,12 +47,14 @@ import com.googlecode.objectify.annotation.Entity; import com.googlecode.objectify.annotation.Mapify; import com.googlecode.objectify.mapper.Mapper; import google.registry.model.registry.Registry; +import google.registry.model.registry.label.DomainLabelMetrics.MetricsReservedListMatch; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.ExecutionException; import javax.annotation.Nullable; +import org.joda.time.DateTime; /** * A reserved list entity, persisted to Datastore, that is used to check domain label reservations. @@ -211,19 +214,27 @@ public final class ReservedList * no such entry exists. */ private static ImmutableSet getReservedListEntries(String label, String tld) { + DateTime startTime = DateTime.now(UTC); Registry registry = Registry.get(checkNotNull(tld, "tld")); ImmutableSet> reservedLists = registry.getReservedLists(); ImmutableSet lists = loadReservedLists(reservedLists); - ImmutableSet.Builder entries = new ImmutableSet.Builder<>(); + ImmutableSet.Builder entriesBuilder = new ImmutableSet.Builder<>(); + ImmutableSet.Builder metricMatchesBuilder = + new ImmutableSet.Builder<>(); // Loop through all reservation lists and add each of them. for (ReservedList rl : lists) { if (rl.getReservedListEntries().containsKey(label)) { - entries.add(rl.getReservedListEntries().get(label)); + ReservedListEntry entry = rl.getReservedListEntries().get(label); + entriesBuilder.add(entry); + metricMatchesBuilder.add( + MetricsReservedListMatch.create(rl.getName(), entry.reservationType)); } } - - return entries.build(); + ImmutableSet entries = entriesBuilder.build(); + DomainLabelMetrics.recordReservedListCheckOutcome( + tld, metricMatchesBuilder.build(), DateTime.now(UTC).getMillis() - startTime.getMillis()); + return entries; } private static ImmutableSet loadReservedLists( diff --git a/javatests/google/registry/model/BUILD b/javatests/google/registry/model/BUILD index 15538408c..c576b70bf 100644 --- a/javatests/google/registry/model/BUILD +++ b/javatests/google/registry/model/BUILD @@ -22,6 +22,7 @@ java_library( "//java/google/registry/config", "//java/google/registry/flows", "//java/google/registry/model", + "//java/google/registry/monitoring/metrics/contrib", "//java/google/registry/util", "//java/google/registry/xml", "//javatests/google/registry/testing", diff --git a/javatests/google/registry/model/registry/label/ReservedListTest.java b/javatests/google/registry/model/registry/label/ReservedListTest.java index ff7f8b189..01dc1ee3c 100644 --- a/javatests/google/registry/model/registry/label/ReservedListTest.java +++ b/javatests/google/registry/model/registry/label/ReservedListTest.java @@ -16,13 +16,19 @@ package google.registry.model.registry.label; import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth.assertWithMessage; +import static google.registry.model.registry.label.DomainLabelMetrics.reservedListChecks; +import static google.registry.model.registry.label.DomainLabelMetrics.reservedListHits; +import static google.registry.model.registry.label.DomainLabelMetrics.reservedListProcessingTimes; import static google.registry.model.registry.label.ReservationType.ALLOWED_IN_SUNRISE; import static google.registry.model.registry.label.ReservationType.FULLY_BLOCKED; +import static google.registry.model.registry.label.ReservationType.MISTAKEN_PREMIUM; import static google.registry.model.registry.label.ReservationType.NAME_COLLISION; import static google.registry.model.registry.label.ReservationType.RESERVED_FOR_ANCHOR_TENANT; import static google.registry.model.registry.label.ReservationType.UNRESERVED; import static google.registry.model.registry.label.ReservedList.getReservationTypes; import static google.registry.model.registry.label.ReservedList.matchesAnchorTenantReservation; +import static google.registry.monitoring.metrics.contrib.EventMetricSubject.assertThat; +import static google.registry.monitoring.metrics.contrib.IncrementableMetricSubject.assertThat; import static google.registry.testing.DatastoreHelper.createTld; import static google.registry.testing.DatastoreHelper.persistReservedList; import static google.registry.testing.DatastoreHelper.persistResource; @@ -67,6 +73,21 @@ public class ReservedListTest { public void before() throws Exception { inject.setStaticField(Ofy.class, "clock", clock); createTld("tld"); + reservedListChecks.reset(); + reservedListProcessingTimes.reset(); + reservedListHits.reset(); + } + + private static void verifyUnreservedCheckCount(int unreservedCount) { + assertThat(reservedListChecks) + .hasValueForLabels(unreservedCount, "tld", "0", "(none)", UNRESERVED.toString()) + .and() + .hasNoOtherValues(); + assertThat(reservedListProcessingTimes) + .hasAnyValueForLabels("tld", "0", "(none)", UNRESERVED.toString()) + .and() + .hasNoOtherValues(); + assertThat(reservedListHits).hasNoOtherValues(); } @Test @@ -75,11 +96,13 @@ public class ReservedListTest { assertThat(getReservationTypes("doodle", "tld")).containsExactly(UNRESERVED); assertThat(getReservationTypes("access", "tld")).containsExactly(UNRESERVED); assertThat(getReservationTypes("rich", "tld")).containsExactly(UNRESERVED); + verifyUnreservedCheckCount(3); } @Test public void testZeroReservedLists_doesNotCauseError() throws Exception { assertThat(getReservationTypes("doodle", "tld")).containsExactly(UNRESERVED); + verifyUnreservedCheckCount(1); } @Test @@ -87,6 +110,7 @@ public class ReservedListTest { for (String sld : ImmutableList.of("aa", "az", "zz", "91", "1n", "j5")) { assertThat(getReservationTypes(sld, "tld")).containsExactly(UNRESERVED); } + verifyUnreservedCheckCount(6); } @Test @@ -95,6 +119,7 @@ public class ReservedListTest { for (char c = 'a'; c <= 'z'; c++) { assertThat(getReservationTypes("" + c, "tld")).containsExactly(UNRESERVED); } + verifyUnreservedCheckCount(26); } @Test @@ -120,6 +145,22 @@ public class ReservedListTest { .isFalse(); assertThat(matchesAnchorTenantReservation(InternetDomainName.from("random.tld"), "abcdefg")) .isFalse(); + assertThat(reservedListChecks) + .hasValueForLabels(1, "tld", "0", "(none)", UNRESERVED.toString()) + .and() + .hasValueForLabels(6, "tld", "1", "reserved1", RESERVED_FOR_ANCHOR_TENANT.toString()) + .and() + .hasNoOtherValues(); + assertThat(reservedListProcessingTimes) + .hasAnyValueForLabels("tld", "0", "(none)", UNRESERVED.toString()) + .and() + .hasAnyValueForLabels("tld", "1", "reserved1", RESERVED_FOR_ANCHOR_TENANT.toString()) + .and() + .hasNoOtherValues(); + assertThat(reservedListHits) + .hasValueForLabels(6, "tld", "reserved1", RESERVED_FOR_ANCHOR_TENANT.toString()) + .and() + .hasNoOtherValues(); } @Test @@ -138,6 +179,40 @@ public class ReservedListTest { assertThat(matchesAnchorTenantReservation(InternetDomainName.from("lol3.tld"), "")).isFalse(); assertThat(matchesAnchorTenantReservation(InternetDomainName.from("lol4.tld"), "")).isFalse(); assertThat(matchesAnchorTenantReservation(InternetDomainName.from("lol5.tld"), "")).isFalse(); + assertThat(reservedListChecks) + .hasValueForLabels(1, "tld", "1", "reserved2", FULLY_BLOCKED.toString()) + .and() + .hasValueForLabels(1, "tld", "1", "reserved2", NAME_COLLISION.toString()) + .and() + .hasValueForLabels(1, "tld", "1", "reserved2", MISTAKEN_PREMIUM.toString()) + .and() + .hasValueForLabels(1, "tld", "1", "reserved2", ALLOWED_IN_SUNRISE.toString()) + .and() + .hasValueForLabels(1, "tld", "0", "(none)", UNRESERVED.toString()) + .and() + .hasNoOtherValues(); + assertThat(reservedListProcessingTimes) + .hasAnyValueForLabels("tld", "1", "reserved2", FULLY_BLOCKED.toString()) + .and() + .hasAnyValueForLabels("tld", "1", "reserved2", NAME_COLLISION.toString()) + .and() + .hasAnyValueForLabels("tld", "1", "reserved2", MISTAKEN_PREMIUM.toString()) + .and() + .hasAnyValueForLabels("tld", "1", "reserved2", ALLOWED_IN_SUNRISE.toString()) + .and() + .hasAnyValueForLabels("tld", "0", "(none)", UNRESERVED.toString()) + .and() + .hasNoOtherValues(); + assertThat(reservedListHits) + .hasValueForLabels(1, "tld", "reserved2", FULLY_BLOCKED.toString()) + .and() + .hasValueForLabels(1, "tld", "reserved2", NAME_COLLISION.toString()) + .and() + .hasValueForLabels(1, "tld", "reserved2", MISTAKEN_PREMIUM.toString()) + .and() + .hasValueForLabels(1, "tld", "reserved2", ALLOWED_IN_SUNRISE.toString()) + .and() + .hasNoOtherValues(); } @Test @@ -173,6 +248,28 @@ public class ReservedListTest { assertThat(getReservationTypes("roflcopter", "tld")).containsExactly(FULLY_BLOCKED); assertThat(getReservationTypes("snowcrash", "tld")).containsExactly(FULLY_BLOCKED); assertThat(getReservationTypes("doge", "tld")).containsExactly(UNRESERVED); + assertThat(reservedListChecks) + .hasValueForLabels(1, "tld", "0", "(none)", UNRESERVED.toString()) + .and() + .hasValueForLabels(2, "tld", "1", "reserved1", FULLY_BLOCKED.toString()) + .and() + .hasValueForLabels(2, "tld", "1", "reserved2", FULLY_BLOCKED.toString()) + .and() + .hasNoOtherValues(); + assertThat(reservedListProcessingTimes) + .hasAnyValueForLabels("tld", "0", "(none)", UNRESERVED.toString()) + .and() + .hasAnyValueForLabels("tld", "1", "reserved1", FULLY_BLOCKED.toString()) + .and() + .hasAnyValueForLabels("tld", "1", "reserved2", FULLY_BLOCKED.toString()) + .and() + .hasNoOtherValues(); + assertThat(reservedListHits) + .hasValueForLabels(2, "tld", "reserved1", FULLY_BLOCKED.toString()) + .and() + .hasValueForLabels(2, "tld", "reserved2", FULLY_BLOCKED.toString()) + .and() + .hasNoOtherValues(); } @Test @@ -207,6 +304,22 @@ public class ReservedListTest { + " after unsetting the registry's second reserved list") .that(getReservationTypes("roflcopter", "tld")) .containsExactly(UNRESERVED); + assertThat(reservedListChecks) + .hasValueForLabels(1, "tld", "1", "reserved2", FULLY_BLOCKED.toString()) + .and() + .hasValueForLabels(1, "tld", "0", "(none)", UNRESERVED.toString()) + .and() + .hasNoOtherValues(); + assertThat(reservedListProcessingTimes) + .hasAnyValueForLabels("tld", "1", "reserved2", FULLY_BLOCKED.toString()) + .and() + .hasAnyValueForLabels("tld", "0", "(none)", UNRESERVED.toString()) + .and() + .hasNoOtherValues(); + assertThat(reservedListHits) + .hasValueForLabels(1, "tld", "reserved2", FULLY_BLOCKED.toString()) + .and() + .hasNoOtherValues(); } @Test @@ -218,6 +331,26 @@ public class ReservedListTest { persistResource(Registry.get("tld").asBuilder().setReservedLists(rl1, rl2).build()); assertThat(getReservationTypes("lol", "tld")).containsExactly(FULLY_BLOCKED, NAME_COLLISION); assertThat(getReservationTypes("roflcopter", "tld")).containsExactly(ALLOWED_IN_SUNRISE); + assertThat(reservedListChecks) + .hasValueForLabels(1, "tld", "1", "reserved1", ALLOWED_IN_SUNRISE.toString()) + .and() + .hasValueForLabels(1, "tld", "2", "reserved2", FULLY_BLOCKED.toString()) + .and() + .hasNoOtherValues(); + assertThat(reservedListProcessingTimes) + .hasAnyValueForLabels("tld", "1", "reserved1", ALLOWED_IN_SUNRISE.toString()) + .and() + .hasAnyValueForLabels("tld", "2", "reserved2", FULLY_BLOCKED.toString()) + .and() + .hasNoOtherValues(); + assertThat(reservedListHits) + .hasValueForLabels(1, "tld", "reserved1", NAME_COLLISION.toString()) + .and() + .hasValueForLabels(1, "tld", "reserved1", ALLOWED_IN_SUNRISE.toString()) + .and() + .hasValueForLabels(1, "tld", "reserved2", FULLY_BLOCKED.toString()) + .and() + .hasNoOtherValues(); } @Test