From 9fdf976fa768a24332d0232390741a7db1bdce8f Mon Sep 17 00:00:00 2001 From: njshah301 Date: Tue, 13 Jan 2026 08:28:20 +0000 Subject: [PATCH 1/7] Add MosApiMetrics exporter with status code mapping Introduces the metrics exporter for the MoSAPI system. - Implements `MosApiMetrics` to export TLD and service states to Cloud Monitoring. - Maps ICANN status codes to numeric gauges: 1 (UP), 0 (DOWN), and 2 (DISABLED/INCONCLUSIVE). - Sets `MAX_TIMESERIES_PER_REQUEST` to 195 to respect Cloud Monitoring API limits --- .../registry/config/RegistryConfig.java | 6 + .../config/RegistryConfigSettings.java | 2 + .../registry/config/files/default-config.yaml | 6 + .../google/registry/mosapi/MosApiMetrics.java | 188 +++++++++++++++++- .../registry/mosapi/module/MosApiModule.java | 16 ++ .../registry/mosapi/MosApiMetricsTest.java | 148 ++++++++++++++ 6 files changed, 363 insertions(+), 3 deletions(-) create mode 100644 core/src/test/java/google/registry/mosapi/MosApiMetricsTest.java diff --git a/core/src/main/java/google/registry/config/RegistryConfig.java b/core/src/main/java/google/registry/config/RegistryConfig.java index 7d1da470e05..4f07ecdc741 100644 --- a/core/src/main/java/google/registry/config/RegistryConfig.java +++ b/core/src/main/java/google/registry/config/RegistryConfig.java @@ -1468,6 +1468,12 @@ public static int provideMosapiTldThreads(RegistryConfigSettings config) { return config.mosapi.tldThreadCnt; } + @Provides + @Config("mosapiMetricsThreadCnt") + public static int provideMosapiMetricsThreads(RegistryConfigSettings config) { + return config.mosapi.metricsThreadCnt; + } + private static String formatComments(String text) { return Splitter.on('\n').omitEmptyStrings().trimResults().splitToList(text).stream() .map(s -> "# " + s) diff --git a/core/src/main/java/google/registry/config/RegistryConfigSettings.java b/core/src/main/java/google/registry/config/RegistryConfigSettings.java index 7cdb42ee45b..4d44af4b4e4 100644 --- a/core/src/main/java/google/registry/config/RegistryConfigSettings.java +++ b/core/src/main/java/google/registry/config/RegistryConfigSettings.java @@ -273,5 +273,7 @@ public static class MosApi { public List tlds; public List services; public int tldThreadCnt; + + public int metricsThreadCnt; } } diff --git a/core/src/main/java/google/registry/config/files/default-config.yaml b/core/src/main/java/google/registry/config/files/default-config.yaml index 99f532df1cd..1254972af78 100644 --- a/core/src/main/java/google/registry/config/files/default-config.yaml +++ b/core/src/main/java/google/registry/config/files/default-config.yaml @@ -647,3 +647,9 @@ mosapi: # ICANN MoSAPI Specification, Section 12.3 tldThreadCnt: 4 + # Metrics Reporting Thread Count + # Set to 1. Given the current TLD volume and the 5-minute reporting interval, + # a single thread provides sufficient throughput to process all metrics + # sequentially without backlog. + metricsThreadCnt: 1 + diff --git a/core/src/main/java/google/registry/mosapi/MosApiMetrics.java b/core/src/main/java/google/registry/mosapi/MosApiMetrics.java index c2756d46135..2465aa64971 100644 --- a/core/src/main/java/google/registry/mosapi/MosApiMetrics.java +++ b/core/src/main/java/google/registry/mosapi/MosApiMetrics.java @@ -14,21 +14,203 @@ package google.registry.mosapi; +import com.google.api.client.util.DateTime; +import com.google.api.services.monitoring.v3.Monitoring; +import com.google.api.services.monitoring.v3.model.CreateTimeSeriesRequest; +import com.google.api.services.monitoring.v3.model.Metric; +import com.google.api.services.monitoring.v3.model.MonitoredResource; +import com.google.api.services.monitoring.v3.model.Point; +import com.google.api.services.monitoring.v3.model.TimeInterval; +import com.google.api.services.monitoring.v3.model.TimeSeries; +import com.google.api.services.monitoring.v3.model.TypedValue; +import com.google.common.base.Ascii; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.Lists; import com.google.common.flogger.FluentLogger; +import google.registry.config.RegistryConfig.Config; +import google.registry.mosapi.MosApiModels.ServiceStatus; import google.registry.mosapi.MosApiModels.TldServiceState; import jakarta.inject.Inject; +import jakarta.inject.Named; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; import java.util.List; +import java.util.Map; +import java.util.concurrent.ExecutorService; /** Metrics Exporter for MoSAPI. */ public class MosApiMetrics { private static final FluentLogger logger = FluentLogger.forEnclosingClass(); + // Google Cloud Monitoring Limit: Max 200 TimeSeries per request + private static final int MAX_TIMESERIES_PER_REQUEST = 195; + + // Magic String Constants + private static final String METRIC_DOMAIN = "custom.googleapis.com/mosapi/"; + private static final String PROJECT_RESOURCE_PREFIX = "projects/"; + private static final String RESOURCE_TYPE_GLOBAL = "global"; + private static final String LABEL_PROJECT_ID = "project_id"; + private static final String LABEL_TLD = "tld"; + private static final String LABEL_SERVICE_TYPE = "service_type"; + + // Metric Names + private static final String METRIC_TLD_STATUS = "tld_status"; + private static final String METRIC_SERVICE_STATUS = "service_status"; + private static final String METRIC_EMERGENCY_USAGE = "emergency_usage"; + + // MoSAPI Status Constants + private static final String STATUS_UP_INCONCLUSIVE = "UP-INCONCLUSIVE"; + private static final String STATUS_DOWN = "DOWN"; + private static final String STATUS_DISABLED = "DISABLED"; + + private final Monitoring monitoringClient; + private final String projectId; + private final ExecutorService executor; + @Inject - public MosApiMetrics() {} + public MosApiMetrics( + Monitoring monitoringClient, + @Config("projectId") String projectId, + @Named("mosapiMetricsExecutor") ExecutorService executor) { + this.monitoringClient = monitoringClient; + this.projectId = projectId; + this.executor = executor; + } + /** Accepts a list of states and processes them in a single async batch task. */ public void recordStates(List states) { - // b/467541269: Logic to push status to Cloud Monitoring goes here - logger.atInfo().log("MoSAPI record metrics logic will be implemented from here"); + executor.execute( + () -> { + try { + pushBatchMetrics(states); + } catch (Throwable t) { + logger.atWarning().withCause(t).log("Async batch metric push failed."); + } + }); + } + + private void pushBatchMetrics(List states) throws IOException { + List allTimeSeries = new ArrayList<>(); + TimeInterval interval = + new TimeInterval().setEndTime(new DateTime(System.currentTimeMillis()).toString()); + + for (TldServiceState state : states) { + // 1. TLD Status Metric + allTimeSeries.add(createTldStatusTimeSeries(state, interval)); + + // 2. Service-level Metrics + Map services = state.serviceStatuses(); + if (services != null) { + for (Map.Entry entry : services.entrySet()) { + addServiceMetrics(allTimeSeries, state.tld(), entry.getKey(), entry.getValue(), interval); + } + } + } + + for (List chunk : Lists.partition(allTimeSeries, MAX_TIMESERIES_PER_REQUEST)) { + CreateTimeSeriesRequest request = new CreateTimeSeriesRequest().setTimeSeries(chunk); + monitoringClient + .projects() + .timeSeries() + .create(PROJECT_RESOURCE_PREFIX + projectId, request) + .execute(); + logger.atInfo().log( + "Successfully pushed batch of %d time series to Cloud Monitoring.", chunk.size()); + } + } + + private void addServiceMetrics( + List list, + String tld, + String serviceType, + ServiceStatus statusObj, + TimeInterval interval) { + ImmutableMap labels = + ImmutableMap.of(LABEL_TLD, tld, LABEL_SERVICE_TYPE, serviceType); + + list.add( + createTimeSeries( + METRIC_SERVICE_STATUS, labels, parseServiceStatus(statusObj.status()), interval)); + + list.add( + createTimeSeries(METRIC_EMERGENCY_USAGE, labels, statusObj.emergencyThreshold(), interval)); + } + + private TimeSeries createTldStatusTimeSeries(TldServiceState state, TimeInterval interval) { + return createTimeSeries( + METRIC_TLD_STATUS, + ImmutableMap.of(LABEL_TLD, state.tld()), + parseTldStatus(state.status()), + interval); + } + + private TimeSeries createTimeSeries( + String suffix, Map labels, Number val, TimeInterval interval) { + Metric metric = new Metric().setType(METRIC_DOMAIN + suffix).setLabels(labels); + MonitoredResource resource = + new MonitoredResource() + .setType(RESOURCE_TYPE_GLOBAL) + .setLabels(Collections.singletonMap(LABEL_PROJECT_ID, projectId)); + + TypedValue tv = new TypedValue(); + if (val instanceof Double) { + tv.setDoubleValue((Double) val); + } else { + tv.setInt64Value(val.longValue()); + } + + return new TimeSeries() + .setMetric(metric) + .setResource(resource) + .setPoints(ImmutableList.of(new Point().setInterval(interval).setValue(tv))); + } + + /** + * Translates MoSAPI status to a numeric metric. + * + *

Mappings: 1 (UP) = Healthy; 0 (DOWN) = Critical failure; 2 (UP-INCONCLUSIVE) = Disabled/Not + * Monitored/In Maintenance. + * + *

A status of 2 indicates the SLA monitoring system is under maintenance. The TLD is + * considered "UP" by default, but individual service checks are disabled. This distinguishes + * maintenance windows from actual availability or outages. + * + * @see ICANN MoSAPI Spec Sec 5.1 + */ + private long parseTldStatus(String status) { + if (status == null) { + return 1; + } + return switch (Ascii.toUpperCase(status)) { + case STATUS_DOWN -> 0; + case STATUS_UP_INCONCLUSIVE -> 2; + default -> 1; // status is up + }; + } + + /** + * Translates MoSAPI service status to a numeric metric. + * + *

Mappings: 1 (UP) = Healthy; 0 (DOWN) = Critical failure; 2 (DISABLED/UP-INCONCLUSIVE*) = + * Disabled/Not Monitored/In Maintenance. + * + * @see ICANN MoSAPI Spec Sec 5.1 + */ + private long parseServiceStatus(String status) { + if (status == null) { + return 1; + } + String serviceStatus = Ascii.toUpperCase(status); + if (serviceStatus.startsWith(STATUS_UP_INCONCLUSIVE)) { + return 2; + } + return switch (serviceStatus) { + case STATUS_DOWN -> 0; + case STATUS_DISABLED -> 2; + default -> 1; // status is Up + }; } } diff --git a/core/src/main/java/google/registry/mosapi/module/MosApiModule.java b/core/src/main/java/google/registry/mosapi/module/MosApiModule.java index 31ef9dd4a62..5e337652381 100644 --- a/core/src/main/java/google/registry/mosapi/module/MosApiModule.java +++ b/core/src/main/java/google/registry/mosapi/module/MosApiModule.java @@ -203,4 +203,20 @@ static ExecutorService provideMosapiTldExecutor( @Config("mosapiTldThreadCnt") int threadPoolSize) { return Executors.newFixedThreadPool(threadPoolSize); } + + /** + * Provides a single-threaded executor for sequential metrics reporting. + * + *

Bound to 1 thread because the Google Cloud Monitoring exporter processes batches + * sequentially to respect API quotas and rate limits. + * + * @see Google Cloud Monitoring Quotas + */ + @Provides + @Singleton + @Named("mosapiMetricsExecutor") + static ExecutorService provideMosapiMetricsExecutor( + @Config("mosapiMetricsThreadCnt") int threadPoolSize) { + return Executors.newFixedThreadPool(threadPoolSize); + } } diff --git a/core/src/test/java/google/registry/mosapi/MosApiMetricsTest.java b/core/src/test/java/google/registry/mosapi/MosApiMetricsTest.java new file mode 100644 index 00000000000..01f6433e395 --- /dev/null +++ b/core/src/test/java/google/registry/mosapi/MosApiMetricsTest.java @@ -0,0 +1,148 @@ +// Copyright 2026 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.mosapi; + +import static com.google.common.truth.Truth.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import com.google.api.services.monitoring.v3.Monitoring; +import com.google.api.services.monitoring.v3.model.CreateTimeSeriesRequest; +import com.google.api.services.monitoring.v3.model.TimeSeries; // This is the model data class +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.common.util.concurrent.MoreExecutors; +import google.registry.mosapi.MosApiModels.ServiceStatus; +import google.registry.mosapi.MosApiModels.TldServiceState; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; + +public class MosApiMetricsTest { + private static final String PROJECT_ID = "domain-registry-test"; + + private final Monitoring monitoringClient = mock(Monitoring.class); + private final Monitoring.Projects projects = mock(Monitoring.Projects.class); + private final Monitoring.Projects.TimeSeries timeSeriesResource = + mock(Monitoring.Projects.TimeSeries.class); + private final Monitoring.Projects.TimeSeries.Create createRequest = + mock(Monitoring.Projects.TimeSeries.Create.class); + + private MosApiMetrics mosApiMetrics; + + @BeforeEach + void setUp() throws IOException { + when(monitoringClient.projects()).thenReturn(projects); + when(projects.timeSeries()).thenReturn(timeSeriesResource); + when(timeSeriesResource.create(anyString(), any(CreateTimeSeriesRequest.class))) + .thenReturn(createRequest); + + mosApiMetrics = + new MosApiMetrics(monitoringClient, PROJECT_ID, MoreExecutors.newDirectExecutorService()); + } + + @Test + void testRecordStates_mapsStatusesToCorrectValues() throws IOException { + + TldServiceState stateUp = createTldState("tld-up", "UP", "UP"); + TldServiceState stateDown = createTldState("tld-down", "DOWN", "DOWN"); + TldServiceState stateMaint = createTldState("tld-maint", "UP-INCONCLUSIVE", "DISABLED"); + + mosApiMetrics.recordStates(ImmutableList.of(stateUp, stateDown, stateMaint)); + + ArgumentCaptor captor = + ArgumentCaptor.forClass(CreateTimeSeriesRequest.class); + verify(timeSeriesResource).create(eq("projects/" + PROJECT_ID), captor.capture()); + + List pushedSeries = captor.getValue().getTimeSeries(); + + // Verify TLD Status Mappings: 1 (UP), 0 (DOWN), 2 (UP-INCONCLUSIVE) + assertThat(getValueFor(pushedSeries, "tld-up", "tld_status")).isEqualTo(1); + assertThat(getValueFor(pushedSeries, "tld-down", "tld_status")).isEqualTo(0); + assertThat(getValueFor(pushedSeries, "tld-maint", "tld_status")).isEqualTo(2); + + // Verify Service Status Mappings: UP -> 1, DOWN -> 0, DISABLED -> 2 + assertThat(getValueFor(pushedSeries, "tld-up", "service_status")).isEqualTo(1); + assertThat(getValueFor(pushedSeries, "tld-down", "service_status")).isEqualTo(0); + assertThat(getValueFor(pushedSeries, "tld-maint", "service_status")).isEqualTo(2); + } + + @Test + void testRecordStates_partitionsTimeSeries_atLimit() throws IOException { + + List largeBatch = new ArrayList<>(); + for (int i = 0; i < 70; i++) { + largeBatch.add(createTldState("tld-" + i, "UP", "UP")); + } + + mosApiMetrics.recordStates(largeBatch); + + verify(timeSeriesResource, times(2)) + .create(eq("projects/" + PROJECT_ID), any(CreateTimeSeriesRequest.class)); + } + + @Test + void testMetricStructure_containsExpectedLabelsAndResource() throws IOException { + TldServiceState state = createTldState("example.tld", "UP", "UP"); + mosApiMetrics.recordStates(ImmutableList.of(state)); + + ArgumentCaptor captor = + ArgumentCaptor.forClass(CreateTimeSeriesRequest.class); + verify(timeSeriesResource).create(anyString(), captor.capture()); + + TimeSeries ts = captor.getValue().getTimeSeries().get(0); + + assertThat(ts.getMetric().getType()).startsWith("custom.googleapis.com/mosapi/"); + assertThat(ts.getMetric().getLabels()).containsEntry("tld", "example.tld"); + + assertThat(ts.getResource().getType()).isEqualTo("global"); + assertThat(ts.getResource().getLabels()).containsEntry("project_id", PROJECT_ID); + } + + /** Extracts the numeric value for a specific TLD and metric type from a list of TimeSeries. */ + private long getValueFor(List seriesList, String tld, String metricSuffix) { + String fullMetric = "custom.googleapis.com/mosapi/" + metricSuffix; + return seriesList.stream() + .filter(ts -> tld.equals(ts.getMetric().getLabels().get("tld"))) + .filter(ts -> ts.getMetric().getType().equals(fullMetric)) + .findFirst() + .map(ts -> ts.getPoints().get(0).getValue().getInt64Value()) + .orElseThrow( + () -> + new AssertionError( + "Metric not found for TLD: " + tld + ", Suffix: " + metricSuffix)); + } + + /** Mocks a TldServiceState with a single service status. */ + private TldServiceState createTldState(String tld, String tldStatus, String serviceStatus) { + ServiceStatus sStatus = mock(ServiceStatus.class); + when(sStatus.status()).thenReturn(serviceStatus); + when(sStatus.emergencyThreshold()).thenReturn(50.0); + + TldServiceState state = mock(TldServiceState.class); + when(state.tld()).thenReturn(tld); + when(state.status()).thenReturn(tldStatus); + when(state.serviceStatuses()).thenReturn(ImmutableMap.of("dns", sStatus)); + + return state; + } +} From f2dbfc4062756bea74f9e878b567877b7534fa13 Mon Sep 17 00:00:00 2001 From: njshah301 Date: Wed, 14 Jan 2026 15:22:03 +0000 Subject: [PATCH 2/7] Automate metric descriptor creation on startup in Cloud Monitoring --- .../google/registry/mosapi/MosApiMetrics.java | 101 ++++++++++++++++++ .../registry/mosapi/MosApiMetricsTest.java | 50 +++++++++ 2 files changed, 151 insertions(+) diff --git a/core/src/main/java/google/registry/mosapi/MosApiMetrics.java b/core/src/main/java/google/registry/mosapi/MosApiMetrics.java index 2465aa64971..d3c06d19e8d 100644 --- a/core/src/main/java/google/registry/mosapi/MosApiMetrics.java +++ b/core/src/main/java/google/registry/mosapi/MosApiMetrics.java @@ -17,7 +17,9 @@ import com.google.api.client.util.DateTime; import com.google.api.services.monitoring.v3.Monitoring; import com.google.api.services.monitoring.v3.model.CreateTimeSeriesRequest; +import com.google.api.services.monitoring.v3.model.LabelDescriptor; import com.google.api.services.monitoring.v3.model.Metric; +import com.google.api.services.monitoring.v3.model.MetricDescriptor; import com.google.api.services.monitoring.v3.model.MonitoredResource; import com.google.api.services.monitoring.v3.model.Point; import com.google.api.services.monitoring.v3.model.TimeInterval; @@ -33,6 +35,7 @@ import google.registry.mosapi.MosApiModels.TldServiceState; import jakarta.inject.Inject; import jakarta.inject.Named; +import jakarta.inject.Singleton; import java.io.IOException; import java.util.ArrayList; import java.util.Collections; @@ -41,6 +44,7 @@ import java.util.concurrent.ExecutorService; /** Metrics Exporter for MoSAPI. */ +@Singleton public class MosApiMetrics { private static final FluentLogger logger = FluentLogger.forEnclosingClass(); @@ -61,6 +65,22 @@ public class MosApiMetrics { private static final String METRIC_SERVICE_STATUS = "service_status"; private static final String METRIC_EMERGENCY_USAGE = "emergency_usage"; + // Metric Display Names & Descriptions + private static final String DISPLAY_NAME_TLD_STATUS = + "Health of TLDs. 1 = UP, 0 = DOWN, 2= DISABLED/NOT_MONITORED"; + private static final String DESC_TLD_STATUS = "Overall Health of TLDs reported from ICANN"; + + private static final String DISPLAY_NAME_SERVICE_STATUS = + "Health of Services. 1 = UP, 0 = DOWN, 2= DISABLED/NOT_MONITORED"; + private static final String DESC_SERVICE_STATUS = + "Overall Health of Services reported from ICANN"; + + private static final String DISPLAY_NAME_EMERGENCY_USAGE = + "Percentage of Emergency Threshold Consumed"; + private static final String DESC_EMERGENCY_USAGE = + "Downtime threshold that if reached by any of the monitored Services may cause the TLDs" + + " Services emergency transition to an interim Registry Operator"; + // MoSAPI Status Constants private static final String STATUS_UP_INCONCLUSIVE = "UP-INCONCLUSIVE"; private static final String STATUS_DOWN = "DOWN"; @@ -78,6 +98,87 @@ public MosApiMetrics( this.monitoringClient = monitoringClient; this.projectId = projectId; this.executor = executor; + + // Initialize Metric Descriptors once on startup + ensureMetricDescriptors(); + } + + // Defines the custom metrics in Cloud Monitoring + private void ensureMetricDescriptors() { + executor.execute( + () -> { + try { + String projectName = PROJECT_RESOURCE_PREFIX + projectId; + + // 1. TLD Status Descriptor + createMetricDescriptor( + projectName, + METRIC_TLD_STATUS, + DISPLAY_NAME_TLD_STATUS, + DESC_TLD_STATUS, + "GAUGE", + "INT64", + ImmutableList.of(LABEL_TLD)); + + // 2. Service Status Descriptor + createMetricDescriptor( + projectName, + METRIC_SERVICE_STATUS, + DISPLAY_NAME_SERVICE_STATUS, + DESC_SERVICE_STATUS, + "GAUGE", + "INT64", + ImmutableList.of(LABEL_TLD, LABEL_SERVICE_TYPE)); + + // 3. Emergency Usage Descriptor + createMetricDescriptor( + projectName, + METRIC_EMERGENCY_USAGE, + DISPLAY_NAME_EMERGENCY_USAGE, + DESC_EMERGENCY_USAGE, + "GAUGE", + "DOUBLE", + ImmutableList.of(LABEL_TLD, LABEL_SERVICE_TYPE)); + + logger.atInfo().log("Metric descriptors ensured for project %s", projectId); + } catch (Exception e) { + logger.atWarning().withCause(e).log( + "Failed to create metric descriptors (they may already exist)."); + } + }); + } + + private void createMetricDescriptor( + String projectName, + String metricTypeSuffix, + String displayName, + String description, + String metricKind, + String valueType, + List labelKeys) + throws IOException { + + List labelDescriptors = new ArrayList<>(); + for (String key : labelKeys) { + LabelDescriptor ld = + new LabelDescriptor() + .setKey(key) + .setValueType("STRING") + .setDescription( + key.equals(LABEL_TLD) ? "The TLD being monitored" : "The type of service"); + labelDescriptors.add(ld); + } + + MetricDescriptor descriptor = + new MetricDescriptor() + .setType(METRIC_DOMAIN + metricTypeSuffix) + .setMetricKind(metricKind) + .setValueType(valueType) + .setDisplayName(displayName) + .setDescription(description) + .setLabels(labelDescriptors); + + monitoringClient.projects().metricDescriptors().create(projectName, descriptor).execute(); } /** Accepts a list of states and processes them in a single async batch task. */ diff --git a/core/src/test/java/google/registry/mosapi/MosApiMetricsTest.java b/core/src/test/java/google/registry/mosapi/MosApiMetricsTest.java index 01f6433e395..e9737ecc648 100644 --- a/core/src/test/java/google/registry/mosapi/MosApiMetricsTest.java +++ b/core/src/test/java/google/registry/mosapi/MosApiMetricsTest.java @@ -24,6 +24,7 @@ import com.google.api.services.monitoring.v3.Monitoring; import com.google.api.services.monitoring.v3.model.CreateTimeSeriesRequest; +import com.google.api.services.monitoring.v3.model.MetricDescriptor; import com.google.api.services.monitoring.v3.model.TimeSeries; // This is the model data class import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; @@ -47,6 +48,12 @@ public class MosApiMetricsTest { private final Monitoring.Projects.TimeSeries.Create createRequest = mock(Monitoring.Projects.TimeSeries.Create.class); + // Mocks for Metric Descriptors + private final Monitoring.Projects.MetricDescriptors metricDescriptorsResource = + mock(Monitoring.Projects.MetricDescriptors.class); + private final Monitoring.Projects.MetricDescriptors.Create createDescriptorRequest = + mock(Monitoring.Projects.MetricDescriptors.Create.class); + private MosApiMetrics mosApiMetrics; @BeforeEach @@ -56,10 +63,53 @@ void setUp() throws IOException { when(timeSeriesResource.create(anyString(), any(CreateTimeSeriesRequest.class))) .thenReturn(createRequest); + // Setup for Metric Descriptors + when(projects.metricDescriptors()).thenReturn(metricDescriptorsResource); + when(metricDescriptorsResource.create(anyString(), any(MetricDescriptor.class))) + .thenReturn(createDescriptorRequest); + mosApiMetrics = new MosApiMetrics(monitoringClient, PROJECT_ID, MoreExecutors.newDirectExecutorService()); } + @Test + void testConstructor_initializesMetricDescriptors() throws IOException { + ArgumentCaptor captor = ArgumentCaptor.forClass(MetricDescriptor.class); + + // Verify that create was called 3 times (once for each metric) + verify(metricDescriptorsResource, times(3)) + .create(eq("projects/" + PROJECT_ID), captor.capture()); + + List descriptors = captor.getAllValues(); + + // Verify TLD Status Descriptor + MetricDescriptor tldStatus = + descriptors.stream() + .filter(d -> d.getType().endsWith("tld_status")) + .findFirst() + .orElseThrow(); + assertThat(tldStatus.getMetricKind()).isEqualTo("GAUGE"); + assertThat(tldStatus.getValueType()).isEqualTo("INT64"); + + // Verify Service Status Descriptor + MetricDescriptor serviceStatus = + descriptors.stream() + .filter(d -> d.getType().endsWith("service_status")) + .findFirst() + .orElseThrow(); + assertThat(serviceStatus.getMetricKind()).isEqualTo("GAUGE"); + assertThat(serviceStatus.getValueType()).isEqualTo("INT64"); + + // Verify Emergency Usage Descriptor + MetricDescriptor emergencyUsage = + descriptors.stream() + .filter(d -> d.getType().endsWith("emergency_usage")) + .findFirst() + .orElseThrow(); + assertThat(emergencyUsage.getMetricKind()).isEqualTo("GAUGE"); + assertThat(emergencyUsage.getValueType()).isEqualTo("DOUBLE"); + } + @Test void testRecordStates_mapsStatusesToCorrectValues() throws IOException { From 88a2fbc9fba4d35b6eed74b04721b31f1688e837 Mon Sep 17 00:00:00 2001 From: njshah301 Date: Thu, 22 Jan 2026 14:55:48 +0000 Subject: [PATCH 3/7] Refactor MoSAPI metrics for resilience and standards --- .../registry/config/RegistryConfig.java | 4 +- .../config/RegistryConfigSettings.java | 3 +- .../registry/config/files/default-config.yaml | 8 +- .../google/registry/mosapi/MosApiMetrics.java | 256 ++++++++++-------- .../registry/mosapi/MosApiStateService.java | 17 +- .../registry/mosapi/module/MosApiModule.java | 9 +- .../registry/mosapi/MosApiMetricsTest.java | 30 +- .../mosapi/MosApiStateServiceTest.java | 19 ++ 8 files changed, 203 insertions(+), 143 deletions(-) diff --git a/core/src/main/java/google/registry/config/RegistryConfig.java b/core/src/main/java/google/registry/config/RegistryConfig.java index 4f07ecdc741..ac304358ecc 100644 --- a/core/src/main/java/google/registry/config/RegistryConfig.java +++ b/core/src/main/java/google/registry/config/RegistryConfig.java @@ -1469,9 +1469,9 @@ public static int provideMosapiTldThreads(RegistryConfigSettings config) { } @Provides - @Config("mosapiMetricsThreadCnt") + @Config("mosapiMetricsThreadCount") public static int provideMosapiMetricsThreads(RegistryConfigSettings config) { - return config.mosapi.metricsThreadCnt; + return config.mosapi.metricsThreadCount; } private static String formatComments(String text) { diff --git a/core/src/main/java/google/registry/config/RegistryConfigSettings.java b/core/src/main/java/google/registry/config/RegistryConfigSettings.java index 4d44af4b4e4..c5074ea19c4 100644 --- a/core/src/main/java/google/registry/config/RegistryConfigSettings.java +++ b/core/src/main/java/google/registry/config/RegistryConfigSettings.java @@ -273,7 +273,6 @@ public static class MosApi { public List tlds; public List services; public int tldThreadCnt; - - public int metricsThreadCnt; + public int metricsThreadCount; } } diff --git a/core/src/main/java/google/registry/config/files/default-config.yaml b/core/src/main/java/google/registry/config/files/default-config.yaml index 1254972af78..4df6ef0a63e 100644 --- a/core/src/main/java/google/registry/config/files/default-config.yaml +++ b/core/src/main/java/google/registry/config/files/default-config.yaml @@ -648,8 +648,8 @@ mosapi: tldThreadCnt: 4 # Metrics Reporting Thread Count - # Set to 1. Given the current TLD volume and the 5-minute reporting interval, - # a single thread provides sufficient throughput to process all metrics - # sequentially without backlog. - metricsThreadCnt: 1 + # Defaults to 1. This field determines the number of threads used to report + # metrics to Cloud Monitoring. For most deployments, a single thread is + # sufficient to process all metrics sequentially within the reporting interval + metricsThreadCount: 1 diff --git a/core/src/main/java/google/registry/mosapi/MosApiMetrics.java b/core/src/main/java/google/registry/mosapi/MosApiMetrics.java index d3c06d19e8d..2b9794e8e2b 100644 --- a/core/src/main/java/google/registry/mosapi/MosApiMetrics.java +++ b/core/src/main/java/google/registry/mosapi/MosApiMetrics.java @@ -14,7 +14,9 @@ package google.registry.mosapi; -import com.google.api.client.util.DateTime; +import static com.google.common.collect.ImmutableList.toImmutableList; + +import com.google.api.client.googleapis.json.GoogleJsonResponseException; import com.google.api.services.monitoring.v3.Monitoring; import com.google.api.services.monitoring.v3.model.CreateTimeSeriesRequest; import com.google.api.services.monitoring.v3.model.LabelDescriptor; @@ -33,15 +35,16 @@ import google.registry.config.RegistryConfig.Config; import google.registry.mosapi.MosApiModels.ServiceStatus; import google.registry.mosapi.MosApiModels.TldServiceState; +import google.registry.util.Clock; import jakarta.inject.Inject; import jakarta.inject.Named; import jakarta.inject.Singleton; import java.io.IOException; -import java.util.ArrayList; -import java.util.Collections; +import java.time.Instant; import java.util.List; -import java.util.Map; import java.util.concurrent.ExecutorService; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.stream.Stream; /** Metrics Exporter for MoSAPI. */ @Singleton @@ -52,6 +55,8 @@ public class MosApiMetrics { // Google Cloud Monitoring Limit: Max 200 TimeSeries per request private static final int MAX_TIMESERIES_PER_REQUEST = 195; + private static final int METRICS_ALREADY_EXIST = 409; + // Magic String Constants private static final String METRIC_DOMAIN = "custom.googleapis.com/mosapi/"; private static final String PROJECT_RESOURCE_PREFIX = "projects/"; @@ -88,63 +93,66 @@ public class MosApiMetrics { private final Monitoring monitoringClient; private final String projectId; + private final Clock clock; + private final MonitoredResource monitoredResource; private final ExecutorService executor; + // Flag to ensure we only create descriptors once, lazily + private final AtomicBoolean isDescriptorInitialized = new AtomicBoolean(false); @Inject public MosApiMetrics( Monitoring monitoringClient, @Config("projectId") String projectId, + Clock clock, @Named("mosapiMetricsExecutor") ExecutorService executor) { this.monitoringClient = monitoringClient; this.projectId = projectId; + this.clock = clock; this.executor = executor; - // Initialize Metric Descriptors once on startup - ensureMetricDescriptors(); + this.monitoredResource = + new MonitoredResource() + .setType(RESOURCE_TYPE_GLOBAL) + .setLabels(ImmutableMap.of(LABEL_PROJECT_ID, projectId)); } // Defines the custom metrics in Cloud Monitoring private void ensureMetricDescriptors() { executor.execute( () -> { - try { - String projectName = PROJECT_RESOURCE_PREFIX + projectId; - - // 1. TLD Status Descriptor - createMetricDescriptor( - projectName, - METRIC_TLD_STATUS, - DISPLAY_NAME_TLD_STATUS, - DESC_TLD_STATUS, - "GAUGE", - "INT64", - ImmutableList.of(LABEL_TLD)); - - // 2. Service Status Descriptor - createMetricDescriptor( - projectName, - METRIC_SERVICE_STATUS, - DISPLAY_NAME_SERVICE_STATUS, - DESC_SERVICE_STATUS, - "GAUGE", - "INT64", - ImmutableList.of(LABEL_TLD, LABEL_SERVICE_TYPE)); - - // 3. Emergency Usage Descriptor - createMetricDescriptor( - projectName, - METRIC_EMERGENCY_USAGE, - DISPLAY_NAME_EMERGENCY_USAGE, - DESC_EMERGENCY_USAGE, - "GAUGE", - "DOUBLE", - ImmutableList.of(LABEL_TLD, LABEL_SERVICE_TYPE)); - - logger.atInfo().log("Metric descriptors ensured for project %s", projectId); - } catch (Exception e) { - logger.atWarning().withCause(e).log( - "Failed to create metric descriptors (they may already exist)."); - } + String projectName = PROJECT_RESOURCE_PREFIX + projectId; + + // 1. TLD Status Descriptor + createMetricDescriptor( + projectName, + METRIC_TLD_STATUS, + DISPLAY_NAME_TLD_STATUS, + DESC_TLD_STATUS, + "GAUGE", + "INT64", + ImmutableList.of(LABEL_TLD)); + + // 2. Service Status Descriptor + createMetricDescriptor( + projectName, + METRIC_SERVICE_STATUS, + DISPLAY_NAME_SERVICE_STATUS, + DESC_SERVICE_STATUS, + "GAUGE", + "INT64", + ImmutableList.of(LABEL_TLD, LABEL_SERVICE_TYPE)); + + // 3. Emergency Usage Descriptor + createMetricDescriptor( + projectName, + METRIC_EMERGENCY_USAGE, + DISPLAY_NAME_EMERGENCY_USAGE, + DESC_EMERGENCY_USAGE, + "GAUGE", + "DOUBLE", + ImmutableList.of(LABEL_TLD, LABEL_SERVICE_TYPE)); + + logger.atInfo().log("Metric descriptors ensured for project %s", projectId); }); } @@ -155,88 +163,112 @@ private void createMetricDescriptor( String description, String metricKind, String valueType, - List labelKeys) - throws IOException { - - List labelDescriptors = new ArrayList<>(); - for (String key : labelKeys) { - LabelDescriptor ld = - new LabelDescriptor() - .setKey(key) - .setValueType("STRING") - .setDescription( - key.equals(LABEL_TLD) ? "The TLD being monitored" : "The type of service"); - labelDescriptors.add(ld); + ImmutableList labelKeys) { + try { + ImmutableList labelDescriptors = + labelKeys.stream() + .map( + key -> + new LabelDescriptor() + .setKey(key) + .setValueType("STRING") + .setDescription( + key.equals(LABEL_TLD) + ? "The TLD being monitored" + : "The type of service")) + .collect(toImmutableList()); + + MetricDescriptor descriptor = + new MetricDescriptor() + .setType(METRIC_DOMAIN + metricTypeSuffix) + .setMetricKind(metricKind) + .setValueType(valueType) + .setDisplayName(displayName) + .setDescription(description) + .setLabels(labelDescriptors); + + monitoringClient.projects().metricDescriptors().create(projectName, descriptor).execute(); + } catch (GoogleJsonResponseException e) { + if (e.getStatusCode() == METRICS_ALREADY_EXIST) { + // the metric already exists. This is expected. + logger.atFine().log("Metric descriptor %s already exists.", metricTypeSuffix); + } else { + logger.atWarning().withCause(e).log( + "Failed to create metric descriptor %s. Status: %d", + metricTypeSuffix, e.getStatusCode()); + } + } catch (Exception e) { + logger.atWarning().withCause(e).log( + "Unexpected error creating metric descriptor %s.", metricTypeSuffix); } - - MetricDescriptor descriptor = - new MetricDescriptor() - .setType(METRIC_DOMAIN + metricTypeSuffix) - .setMetricKind(metricKind) - .setValueType(valueType) - .setDisplayName(displayName) - .setDescription(description) - .setLabels(labelDescriptors); - - monitoringClient.projects().metricDescriptors().create(projectName, descriptor).execute(); } /** Accepts a list of states and processes them in a single async batch task. */ - public void recordStates(List states) { + public void recordStates(ImmutableList states) { + // If this is the first time we are recording, ensure descriptors exist. + if (isDescriptorInitialized.compareAndSet(false, true)) { + ensureMetricDescriptors(); + } executor.execute( () -> { try { pushBatchMetrics(states); - } catch (Throwable t) { - logger.atWarning().withCause(t).log("Async batch metric push failed."); + } catch (Exception e) { + logger.atWarning().withCause(e).log("Async batch metric push failed."); } }); } - private void pushBatchMetrics(List states) throws IOException { - List allTimeSeries = new ArrayList<>(); - TimeInterval interval = - new TimeInterval().setEndTime(new DateTime(System.currentTimeMillis()).toString()); - - for (TldServiceState state : states) { - // 1. TLD Status Metric - allTimeSeries.add(createTldStatusTimeSeries(state, interval)); - - // 2. Service-level Metrics - Map services = state.serviceStatuses(); - if (services != null) { - for (Map.Entry entry : services.entrySet()) { - addServiceMetrics(allTimeSeries, state.tld(), entry.getKey(), entry.getValue(), interval); - } - } - } + private void pushBatchMetrics(ImmutableList states) { + Instant now = Instant.ofEpochMilli(clock.nowUtc().getMillis()); + TimeInterval interval = new TimeInterval().setEndTime(now.toString()); + ImmutableList allTimeSeries = + states.stream() + .flatMap(state -> createMetricsForState(state, interval)) + .collect(toImmutableList()); for (List chunk : Lists.partition(allTimeSeries, MAX_TIMESERIES_PER_REQUEST)) { - CreateTimeSeriesRequest request = new CreateTimeSeriesRequest().setTimeSeries(chunk); - monitoringClient - .projects() - .timeSeries() - .create(PROJECT_RESOURCE_PREFIX + projectId, request) - .execute(); - logger.atInfo().log( - "Successfully pushed batch of %d time series to Cloud Monitoring.", chunk.size()); + try { + CreateTimeSeriesRequest request = new CreateTimeSeriesRequest().setTimeSeries(chunk); + monitoringClient + .projects() + .timeSeries() + .create(PROJECT_RESOURCE_PREFIX + projectId, request) + .execute(); + logger.atInfo().log( + "Successfully pushed batch of %d time series to Cloud Monitoring.", chunk.size()); + } catch (IOException e) { + logger.atWarning().withCause(e).log( + "Failed to push batch of %d time series to Cloud Monitoring. Proceeding to next batch.", + chunk.size()); + } } } - private void addServiceMetrics( - List list, - String tld, - String serviceType, - ServiceStatus statusObj, - TimeInterval interval) { + /** Generates all TimeSeries (TLD + Services) for a single state object. */ + private Stream createMetricsForState(TldServiceState state, TimeInterval interval) { + // 1. TLD Status + Stream tldStream = Stream.of(createTldStatusTimeSeries(state, interval)); + + // 2. Service Metrics (if any) + Stream serviceStream = + state.serviceStatuses().entrySet().stream() + .flatMap( + entry -> + createServiceMetricsStream( + state.tld(), entry.getKey(), entry.getValue(), interval)); + + return Stream.concat(tldStream, serviceStream); + } + + private Stream createServiceMetricsStream( + String tld, String serviceType, ServiceStatus statusObj, TimeInterval interval) { ImmutableMap labels = ImmutableMap.of(LABEL_TLD, tld, LABEL_SERVICE_TYPE, serviceType); - list.add( + return Stream.of( createTimeSeries( - METRIC_SERVICE_STATUS, labels, parseServiceStatus(statusObj.status()), interval)); - - list.add( + METRIC_SERVICE_STATUS, labels, parseServiceStatus(statusObj.status()), interval), createTimeSeries(METRIC_EMERGENCY_USAGE, labels, statusObj.emergencyThreshold(), interval)); } @@ -249,12 +281,8 @@ private TimeSeries createTldStatusTimeSeries(TldServiceState state, TimeInterval } private TimeSeries createTimeSeries( - String suffix, Map labels, Number val, TimeInterval interval) { + String suffix, ImmutableMap labels, Number val, TimeInterval interval) { Metric metric = new Metric().setType(METRIC_DOMAIN + suffix).setLabels(labels); - MonitoredResource resource = - new MonitoredResource() - .setType(RESOURCE_TYPE_GLOBAL) - .setLabels(Collections.singletonMap(LABEL_PROJECT_ID, projectId)); TypedValue tv = new TypedValue(); if (val instanceof Double) { @@ -265,7 +293,7 @@ private TimeSeries createTimeSeries( return new TimeSeries() .setMetric(metric) - .setResource(resource) + .setResource(this.monitoredResource) .setPoints(ImmutableList.of(new Point().setInterval(interval).setValue(tv))); } @@ -282,9 +310,6 @@ private TimeSeries createTimeSeries( * @see ICANN MoSAPI Spec Sec 5.1 */ private long parseTldStatus(String status) { - if (status == null) { - return 1; - } return switch (Ascii.toUpperCase(status)) { case STATUS_DOWN -> 0; case STATUS_UP_INCONCLUSIVE -> 2; @@ -301,9 +326,6 @@ private long parseTldStatus(String status) { * @see ICANN MoSAPI Spec Sec 5.1 */ private long parseServiceStatus(String status) { - if (status == null) { - return 1; - } String serviceStatus = Ascii.toUpperCase(status); if (serviceStatus.startsWith(STATUS_UP_INCONCLUSIVE)) { return 2; diff --git a/core/src/main/java/google/registry/mosapi/MosApiStateService.java b/core/src/main/java/google/registry/mosapi/MosApiStateService.java index 0cd242b1105..b737a9e22ad 100644 --- a/core/src/main/java/google/registry/mosapi/MosApiStateService.java +++ b/core/src/main/java/google/registry/mosapi/MosApiStateService.java @@ -26,11 +26,9 @@ import google.registry.mosapi.MosApiModels.TldServiceState; import jakarta.inject.Inject; import jakarta.inject.Named; -import java.util.List; import java.util.Objects; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutorService; -import java.util.stream.Collectors; /** A service that provides business logic for interacting with MoSAPI Service State. */ public class MosApiStateService { @@ -135,11 +133,12 @@ public void triggerMetricsForAllServiceStateSummaries() { tldExecutor)) .collect(toImmutableList()); - List allStates = + ImmutableList allStates = futures.stream() .map(CompletableFuture::join) .filter(Objects::nonNull) - .collect(Collectors.toList()); + .filter(this::isValidForMetrics) + .collect(toImmutableList()); if (!allStates.isEmpty()) { try { @@ -152,4 +151,14 @@ public void triggerMetricsForAllServiceStateSummaries() { logger.atWarning().log("No successful TLD states fetched; skipping metrics push."); } } + + private boolean isValidForMetrics(TldServiceState state) { + if (state.tld() == null || state.status() == null) { + logger.atSevere().log( + "Contract Violation: Received invalid state (TLD=%s, Status=%s). Skipping.", + state.tld(), state.status()); + return false; + } + return true; + } } diff --git a/core/src/main/java/google/registry/mosapi/module/MosApiModule.java b/core/src/main/java/google/registry/mosapi/module/MosApiModule.java index 5e337652381..5e984e692f1 100644 --- a/core/src/main/java/google/registry/mosapi/module/MosApiModule.java +++ b/core/src/main/java/google/registry/mosapi/module/MosApiModule.java @@ -205,10 +205,11 @@ static ExecutorService provideMosapiTldExecutor( } /** - * Provides a single-threaded executor for sequential metrics reporting. + * Provides executor for metrics reporting. * - *

Bound to 1 thread because the Google Cloud Monitoring exporter processes batches - * sequentially to respect API quotas and rate limits. + *

We should use a single thread to ensure metric batches are sent sequentially. This acts as a + * client-side rate limiter to avoid exceeding Google Cloud Monitoring API quotas (requests per + * second). * * @see Google Cloud Monitoring Quotas */ @@ -216,7 +217,7 @@ static ExecutorService provideMosapiTldExecutor( @Singleton @Named("mosapiMetricsExecutor") static ExecutorService provideMosapiMetricsExecutor( - @Config("mosapiMetricsThreadCnt") int threadPoolSize) { + @Config("mosapiMetricsThreadCount") int threadPoolSize) { return Executors.newFixedThreadPool(threadPoolSize); } } diff --git a/core/src/test/java/google/registry/mosapi/MosApiMetricsTest.java b/core/src/test/java/google/registry/mosapi/MosApiMetricsTest.java index e9737ecc648..fc08a8c6e4a 100644 --- a/core/src/test/java/google/registry/mosapi/MosApiMetricsTest.java +++ b/core/src/test/java/google/registry/mosapi/MosApiMetricsTest.java @@ -25,19 +25,21 @@ import com.google.api.services.monitoring.v3.Monitoring; import com.google.api.services.monitoring.v3.model.CreateTimeSeriesRequest; import com.google.api.services.monitoring.v3.model.MetricDescriptor; -import com.google.api.services.monitoring.v3.model.TimeSeries; // This is the model data class +import com.google.api.services.monitoring.v3.model.TimeSeries; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.util.concurrent.MoreExecutors; import google.registry.mosapi.MosApiModels.ServiceStatus; import google.registry.mosapi.MosApiModels.TldServiceState; +import google.registry.testing.FakeClock; import java.io.IOException; -import java.util.ArrayList; import java.util.List; +import org.joda.time.DateTime; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.mockito.ArgumentCaptor; +/** Unit tests for {@link MosApiMetrics}. */ public class MosApiMetricsTest { private static final String PROJECT_ID = "domain-registry-test"; @@ -54,6 +56,8 @@ public class MosApiMetricsTest { private final Monitoring.Projects.MetricDescriptors.Create createDescriptorRequest = mock(Monitoring.Projects.MetricDescriptors.Create.class); + // Fixed Clock for deterministic testing + private final FakeClock clock = new FakeClock(DateTime.parse("2026-01-01T12:00:00Z")); private MosApiMetrics mosApiMetrics; @BeforeEach @@ -69,14 +73,18 @@ void setUp() throws IOException { .thenReturn(createDescriptorRequest); mosApiMetrics = - new MosApiMetrics(monitoringClient, PROJECT_ID, MoreExecutors.newDirectExecutorService()); + new MosApiMetrics( + monitoringClient, PROJECT_ID, clock, MoreExecutors.newDirectExecutorService()); } @Test - void testConstructor_initializesMetricDescriptors() throws IOException { + void testRecordStates_lazilyInitializesMetricDescriptors() throws IOException { + TldServiceState state = createTldState("test.tld", "UP", "UP"); + + mosApiMetrics.recordStates(ImmutableList.of(state)); + ArgumentCaptor captor = ArgumentCaptor.forClass(MetricDescriptor.class); - // Verify that create was called 3 times (once for each metric) verify(metricDescriptorsResource, times(3)) .create(eq("projects/" + PROJECT_ID), captor.capture()); @@ -139,11 +147,10 @@ void testRecordStates_mapsStatusesToCorrectValues() throws IOException { @Test void testRecordStates_partitionsTimeSeries_atLimit() throws IOException { - List largeBatch = new ArrayList<>(); - for (int i = 0; i < 70; i++) { - largeBatch.add(createTldState("tld-" + i, "UP", "UP")); - } - + ImmutableList largeBatch = + java.util.stream.IntStream.range(0, 70) + .mapToObj(i -> createTldState("tld-" + i, "UP", "UP")) + .collect(ImmutableList.toImmutableList()); mosApiMetrics.recordStates(largeBatch); verify(timeSeriesResource, times(2)) @@ -166,6 +173,9 @@ void testMetricStructure_containsExpectedLabelsAndResource() throws IOException assertThat(ts.getResource().getType()).isEqualTo("global"); assertThat(ts.getResource().getLabels()).containsEntry("project_id", PROJECT_ID); + + // Verify that the interval matches our fixed clock + assertThat(ts.getPoints().get(0).getInterval().getEndTime()).isEqualTo("2026-01-01T12:00:00Z"); } /** Extracts the numeric value for a specific TLD and metric type from a list of TimeSeries. */ diff --git a/core/src/test/java/google/registry/mosapi/MosApiStateServiceTest.java b/core/src/test/java/google/registry/mosapi/MosApiStateServiceTest.java index 4e67c9f1769..2457cfdca25 100644 --- a/core/src/test/java/google/registry/mosapi/MosApiStateServiceTest.java +++ b/core/src/test/java/google/registry/mosapi/MosApiStateServiceTest.java @@ -169,4 +169,23 @@ void testTriggerMetricsForAllServiceStateSummaries_partialFailure_recordsErrorMe && states.stream() .anyMatch(s -> s.tld().equals("tld1") && s.status().equals("Up")))); } + + @Test + void testTriggerMetrics_filtersOutInvalidContractStates() throws Exception { + // 1. Valid State + TldServiceState validState = new TldServiceState("tld1", 1L, "Up", ImmutableMap.of()); + + // 2. Invalid State (Status is NULL) + // We instantiate it directly to simulate a bad response object. + TldServiceState invalidState = new TldServiceState("tld2", 2L, null, ImmutableMap.of()); + + when(client.getTldServiceState("tld1")).thenReturn(validState); + when(client.getTldServiceState("tld2")).thenReturn(invalidState); + + service.triggerMetricsForAllServiceStateSummaries(); + + // Verify: Only the valid state (tld1) is passed to recordStates + verify(metrics) + .recordStates(argThat(states -> states.size() == 1 && states.get(0).tld().equals("tld1"))); + } } From c82000bb8ece8421a5eb7d1339ef3fb7d1b183e9 Mon Sep 17 00:00:00 2001 From: njshah301 Date: Fri, 23 Jan 2026 08:17:29 +0000 Subject: [PATCH 4/7] Refactor and nits - Kept projectName as part constant instead of inside method signature - Added Summary logs for metrics execution - Metric Executor defaults to Single Threaded --- .../registry/config/RegistryConfig.java | 10 +- .../config/RegistryConfigSettings.java | 3 +- .../registry/config/files/default-config.yaml | 8 +- .../google/registry/mosapi/MosApiMetrics.java | 139 ++++++++++-------- .../registry/mosapi/module/MosApiModule.java | 11 +- .../registry/mosapi/MosApiMetricsTest.java | 20 ++- 6 files changed, 105 insertions(+), 86 deletions(-) diff --git a/core/src/main/java/google/registry/config/RegistryConfig.java b/core/src/main/java/google/registry/config/RegistryConfig.java index ac304358ecc..1e00ecc4ed5 100644 --- a/core/src/main/java/google/registry/config/RegistryConfig.java +++ b/core/src/main/java/google/registry/config/RegistryConfig.java @@ -1463,15 +1463,9 @@ public static ImmutableSet provideMosapiServices(RegistryConfigSettings } @Provides - @Config("mosapiTldThreadCnt") + @Config("mosapiTldThreadCount") public static int provideMosapiTldThreads(RegistryConfigSettings config) { - return config.mosapi.tldThreadCnt; - } - - @Provides - @Config("mosapiMetricsThreadCount") - public static int provideMosapiMetricsThreads(RegistryConfigSettings config) { - return config.mosapi.metricsThreadCount; + return config.mosapi.tldThreadCount; } private static String formatComments(String text) { diff --git a/core/src/main/java/google/registry/config/RegistryConfigSettings.java b/core/src/main/java/google/registry/config/RegistryConfigSettings.java index c5074ea19c4..ed89723a428 100644 --- a/core/src/main/java/google/registry/config/RegistryConfigSettings.java +++ b/core/src/main/java/google/registry/config/RegistryConfigSettings.java @@ -272,7 +272,6 @@ public static class MosApi { public String entityType; public List tlds; public List services; - public int tldThreadCnt; - public int metricsThreadCount; + public int tldThreadCount; } } diff --git a/core/src/main/java/google/registry/config/files/default-config.yaml b/core/src/main/java/google/registry/config/files/default-config.yaml index 4df6ef0a63e..f8e5e82e8dd 100644 --- a/core/src/main/java/google/registry/config/files/default-config.yaml +++ b/core/src/main/java/google/registry/config/files/default-config.yaml @@ -645,11 +645,5 @@ mosapi: # Provides a fixed thread pool for parallel TLD processing. # @see # ICANN MoSAPI Specification, Section 12.3 - tldThreadCnt: 4 - - # Metrics Reporting Thread Count - # Defaults to 1. This field determines the number of threads used to report - # metrics to Cloud Monitoring. For most deployments, a single thread is - # sufficient to process all metrics sequentially within the reporting interval - metricsThreadCount: 1 + tldThreadCount: 4 diff --git a/core/src/main/java/google/registry/mosapi/MosApiMetrics.java b/core/src/main/java/google/registry/mosapi/MosApiMetrics.java index 2b9794e8e2b..8f7c8981883 100644 --- a/core/src/main/java/google/registry/mosapi/MosApiMetrics.java +++ b/core/src/main/java/google/registry/mosapi/MosApiMetrics.java @@ -30,7 +30,7 @@ import com.google.common.base.Ascii; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; -import com.google.common.collect.Lists; +import com.google.common.collect.Iterators; import com.google.common.flogger.FluentLogger; import google.registry.config.RegistryConfig.Config; import google.registry.mosapi.MosApiModels.ServiceStatus; @@ -41,6 +41,7 @@ import jakarta.inject.Singleton; import java.io.IOException; import java.time.Instant; +import java.util.Iterator; import java.util.List; import java.util.concurrent.ExecutorService; import java.util.concurrent.atomic.AtomicBoolean; @@ -93,6 +94,7 @@ public class MosApiMetrics { private final Monitoring monitoringClient; private final String projectId; + private final String projectName; private final Clock clock; private final MonitoredResource monitoredResource; private final ExecutorService executor; @@ -110,21 +112,36 @@ public MosApiMetrics( this.clock = clock; this.executor = executor; + this.projectName = PROJECT_RESOURCE_PREFIX + projectId; + this.monitoredResource = new MonitoredResource() .setType(RESOURCE_TYPE_GLOBAL) .setLabels(ImmutableMap.of(LABEL_PROJECT_ID, projectId)); } - // Defines the custom metrics in Cloud Monitoring - private void ensureMetricDescriptors() { + /** Accepts a list of states and processes them in a single async batch task. */ + public void recordStates(ImmutableList states) { + // If this is the first time we are recording, ensure descriptors exist. + if (isDescriptorInitialized.compareAndSet(false, true)) { + createCustomMetricDescriptors(); + } executor.execute( () -> { - String projectName = PROJECT_RESOURCE_PREFIX + projectId; + try { + pushBatchMetrics(states); + } catch (Exception e) { + logger.atSevere().withCause(e).log("Async batch metric push failed."); + } + }); + } + // Defines the custom metrics in Cloud Monitoring + private void createCustomMetricDescriptors() { + executor.execute( + () -> { // 1. TLD Status Descriptor createMetricDescriptor( - projectName, METRIC_TLD_STATUS, DISPLAY_NAME_TLD_STATUS, DESC_TLD_STATUS, @@ -134,7 +151,6 @@ private void ensureMetricDescriptors() { // 2. Service Status Descriptor createMetricDescriptor( - projectName, METRIC_SERVICE_STATUS, DISPLAY_NAME_SERVICE_STATUS, DESC_SERVICE_STATUS, @@ -144,7 +160,6 @@ private void ensureMetricDescriptors() { // 3. Emergency Usage Descriptor createMetricDescriptor( - projectName, METRIC_EMERGENCY_USAGE, DISPLAY_NAME_EMERGENCY_USAGE, DESC_EMERGENCY_USAGE, @@ -157,37 +172,40 @@ private void ensureMetricDescriptors() { } private void createMetricDescriptor( - String projectName, String metricTypeSuffix, String displayName, String description, String metricKind, String valueType, ImmutableList labelKeys) { + + ImmutableList labelDescriptors = + labelKeys.stream() + .map( + key -> + new LabelDescriptor() + .setKey(key) + .setValueType("STRING") + .setDescription( + key.equals(LABEL_TLD) + ? "The TLD being monitored" + : "The type of service")) + .collect(toImmutableList()); + + MetricDescriptor descriptor = + new MetricDescriptor() + .setType(METRIC_DOMAIN + metricTypeSuffix) + .setMetricKind(metricKind) + .setValueType(valueType) + .setDisplayName(displayName) + .setDescription(description) + .setLabels(labelDescriptors); try { - ImmutableList labelDescriptors = - labelKeys.stream() - .map( - key -> - new LabelDescriptor() - .setKey(key) - .setValueType("STRING") - .setDescription( - key.equals(LABEL_TLD) - ? "The TLD being monitored" - : "The type of service")) - .collect(toImmutableList()); - - MetricDescriptor descriptor = - new MetricDescriptor() - .setType(METRIC_DOMAIN + metricTypeSuffix) - .setMetricKind(metricKind) - .setValueType(valueType) - .setDisplayName(displayName) - .setDescription(description) - .setLabels(labelDescriptors); - - monitoringClient.projects().metricDescriptors().create(projectName, descriptor).execute(); + monitoringClient + .projects() + .metricDescriptors() + .create(this.projectName, descriptor) + .execute(); } catch (GoogleJsonResponseException e) { if (e.getStatusCode() == METRICS_ALREADY_EXIST) { // the metric already exists. This is expected. @@ -203,46 +221,43 @@ private void createMetricDescriptor( } } - /** Accepts a list of states and processes them in a single async batch task. */ - public void recordStates(ImmutableList states) { - // If this is the first time we are recording, ensure descriptors exist. - if (isDescriptorInitialized.compareAndSet(false, true)) { - ensureMetricDescriptors(); - } - executor.execute( - () -> { - try { - pushBatchMetrics(states); - } catch (Exception e) { - logger.atWarning().withCause(e).log("Async batch metric push failed."); - } - }); - } - private void pushBatchMetrics(ImmutableList states) { Instant now = Instant.ofEpochMilli(clock.nowUtc().getMillis()); TimeInterval interval = new TimeInterval().setEndTime(now.toString()); - ImmutableList allTimeSeries = - states.stream() - .flatMap(state -> createMetricsForState(state, interval)) - .collect(toImmutableList()); + Stream allTimeSeriesStream = + states.stream().flatMap(state -> createMetricsForState(state, interval)); + + Iterator> batchIterator = + Iterators.partition(allTimeSeriesStream.iterator(), MAX_TIMESERIES_PER_REQUEST); + + int successCount = 0; + int failureCount = 0; - for (List chunk : Lists.partition(allTimeSeries, MAX_TIMESERIES_PER_REQUEST)) { + // Iterate and count + while (batchIterator.hasNext()) { + List batch = batchIterator.next(); try { - CreateTimeSeriesRequest request = new CreateTimeSeriesRequest().setTimeSeries(chunk); - monitoringClient - .projects() - .timeSeries() - .create(PROJECT_RESOURCE_PREFIX + projectId, request) - .execute(); - logger.atInfo().log( - "Successfully pushed batch of %d time series to Cloud Monitoring.", chunk.size()); + CreateTimeSeriesRequest request = new CreateTimeSeriesRequest().setTimeSeries(batch); + monitoringClient.projects().timeSeries().create(this.projectName, request).execute(); + + successCount++; + } catch (IOException e) { + failureCount++; + // Log individual batch failures, so we have the stack trace for debugging logger.atWarning().withCause(e).log( - "Failed to push batch of %d time series to Cloud Monitoring. Proceeding to next batch.", - chunk.size()); + "Failed to push batch of %d time series.", batch.size()); } } + + // 4. Log the final summary + if (failureCount > 0) { + logger.atWarning().log( + "Metric push finished with errors. Batches Succeeded: %d, Failed: %d", + successCount, failureCount); + } else { + logger.atInfo().log("Metric push finished successfully. Batches Succeeded: %d", successCount); + } } /** Generates all TimeSeries (TLD + Services) for a single state object. */ diff --git a/core/src/main/java/google/registry/mosapi/module/MosApiModule.java b/core/src/main/java/google/registry/mosapi/module/MosApiModule.java index 5e984e692f1..879e5e1d7c9 100644 --- a/core/src/main/java/google/registry/mosapi/module/MosApiModule.java +++ b/core/src/main/java/google/registry/mosapi/module/MosApiModule.java @@ -14,6 +14,7 @@ package google.registry.mosapi.module; +import com.google.common.util.concurrent.ThreadFactoryBuilder; import dagger.Module; import dagger.Provides; import google.registry.config.RegistryConfig.Config; @@ -200,14 +201,14 @@ static OkHttpClient provideMosapiHttpClient( @Singleton @Named("mosapiTldExecutor") static ExecutorService provideMosapiTldExecutor( - @Config("mosapiTldThreadCnt") int threadPoolSize) { + @Config("mosapiTldThreadCount") int threadPoolSize) { return Executors.newFixedThreadPool(threadPoolSize); } /** * Provides executor for metrics reporting. * - *

We should use a single thread to ensure metric batches are sent sequentially. This acts as a + *

We are using a single thread to ensure metric batches are sent sequentially. This acts as a * client-side rate limiter to avoid exceeding Google Cloud Monitoring API quotas (requests per * second). * @@ -216,8 +217,8 @@ static ExecutorService provideMosapiTldExecutor( @Provides @Singleton @Named("mosapiMetricsExecutor") - static ExecutorService provideMosapiMetricsExecutor( - @Config("mosapiMetricsThreadCount") int threadPoolSize) { - return Executors.newFixedThreadPool(threadPoolSize); + static ExecutorService provideMosapiMetricsExecutor() { + return Executors.newSingleThreadExecutor( + new ThreadFactoryBuilder().setNameFormat("mosapi-metrics-%d").build()); } } diff --git a/core/src/test/java/google/registry/mosapi/MosApiMetricsTest.java b/core/src/test/java/google/registry/mosapi/MosApiMetricsTest.java index fc08a8c6e4a..ab69fd37975 100644 --- a/core/src/test/java/google/registry/mosapi/MosApiMetricsTest.java +++ b/core/src/test/java/google/registry/mosapi/MosApiMetricsTest.java @@ -142,6 +142,14 @@ void testRecordStates_mapsStatusesToCorrectValues() throws IOException { assertThat(getValueFor(pushedSeries, "tld-up", "service_status")).isEqualTo(1); assertThat(getValueFor(pushedSeries, "tld-down", "service_status")).isEqualTo(0); assertThat(getValueFor(pushedSeries, "tld-maint", "service_status")).isEqualTo(2); + + // 3. Verify Emergency Usage (DOUBLE) + assertThat(getValueFor(pushedSeries, "tld-up", "emergency_usage").doubleValue()) + .isEqualTo(50.0); + assertThat(getValueFor(pushedSeries, "tld-down", "emergency_usage").doubleValue()) + .isEqualTo(50.0); + assertThat(getValueFor(pushedSeries, "tld-maint", "emergency_usage").doubleValue()) + .isEqualTo(50.0); } @Test @@ -179,13 +187,21 @@ void testMetricStructure_containsExpectedLabelsAndResource() throws IOException } /** Extracts the numeric value for a specific TLD and metric type from a list of TimeSeries. */ - private long getValueFor(List seriesList, String tld, String metricSuffix) { + private Number getValueFor(List seriesList, String tld, String metricSuffix) { String fullMetric = "custom.googleapis.com/mosapi/" + metricSuffix; return seriesList.stream() .filter(ts -> tld.equals(ts.getMetric().getLabels().get("tld"))) .filter(ts -> ts.getMetric().getType().equals(fullMetric)) .findFirst() - .map(ts -> ts.getPoints().get(0).getValue().getInt64Value()) + .map( + ts -> { + Double dVal = ts.getPoints().get(0).getValue().getDoubleValue(); + if (dVal != null) { + return (Number) dVal; + } + // Fallback to Int64. + return (Number) ts.getPoints().get(0).getValue().getInt64Value(); + }) .orElseThrow( () -> new AssertionError( From 20c00d4217d70d39d7b3495ec2ac341787816517 Mon Sep 17 00:00:00 2001 From: njshah301 Date: Mon, 26 Jan 2026 20:41:53 +0000 Subject: [PATCH 5/7] junit test refactoring --- .../java/google/registry/mosapi/MosApiMetricsTest.java | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/core/src/test/java/google/registry/mosapi/MosApiMetricsTest.java b/core/src/test/java/google/registry/mosapi/MosApiMetricsTest.java index ab69fd37975..4c3e92bbca6 100644 --- a/core/src/test/java/google/registry/mosapi/MosApiMetricsTest.java +++ b/core/src/test/java/google/registry/mosapi/MosApiMetricsTest.java @@ -120,7 +120,6 @@ void testRecordStates_lazilyInitializesMetricDescriptors() throws IOException { @Test void testRecordStates_mapsStatusesToCorrectValues() throws IOException { - TldServiceState stateUp = createTldState("tld-up", "UP", "UP"); TldServiceState stateDown = createTldState("tld-down", "DOWN", "DOWN"); TldServiceState stateMaint = createTldState("tld-maint", "UP-INCONCLUSIVE", "DISABLED"); @@ -154,7 +153,6 @@ void testRecordStates_mapsStatusesToCorrectValues() throws IOException { @Test void testRecordStates_partitionsTimeSeries_atLimit() throws IOException { - ImmutableList largeBatch = java.util.stream.IntStream.range(0, 70) .mapToObj(i -> createTldState("tld-" + i, "UP", "UP")) @@ -202,10 +200,7 @@ private Number getValueFor(List seriesList, String tld, String metri // Fallback to Int64. return (Number) ts.getPoints().get(0).getValue().getInt64Value(); }) - .orElseThrow( - () -> - new AssertionError( - "Metric not found for TLD: " + tld + ", Suffix: " + metricSuffix)); + .get(); } /** Mocks a TldServiceState with a single service status. */ From 10cd37f0d2ff0e4ce3f1f87dc80df3d478a15d80 Mon Sep 17 00:00:00 2001 From: njshah301 Date: Tue, 27 Jan 2026 05:25:11 +0000 Subject: [PATCH 6/7] Fix Metric kind to GAUGE for all metrics --- .../main/java/google/registry/mosapi/MosApiMetrics.java | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/core/src/main/java/google/registry/mosapi/MosApiMetrics.java b/core/src/main/java/google/registry/mosapi/MosApiMetrics.java index 8f7c8981883..1ce1eb19fea 100644 --- a/core/src/main/java/google/registry/mosapi/MosApiMetrics.java +++ b/core/src/main/java/google/registry/mosapi/MosApiMetrics.java @@ -70,6 +70,7 @@ public class MosApiMetrics { private static final String METRIC_TLD_STATUS = "tld_status"; private static final String METRIC_SERVICE_STATUS = "service_status"; private static final String METRIC_EMERGENCY_USAGE = "emergency_usage"; + private static final String GAUGE_METRIC_KIND = "GAUGE"; // Metric Display Names & Descriptions private static final String DISPLAY_NAME_TLD_STATUS = @@ -145,7 +146,6 @@ private void createCustomMetricDescriptors() { METRIC_TLD_STATUS, DISPLAY_NAME_TLD_STATUS, DESC_TLD_STATUS, - "GAUGE", "INT64", ImmutableList.of(LABEL_TLD)); @@ -154,7 +154,6 @@ private void createCustomMetricDescriptors() { METRIC_SERVICE_STATUS, DISPLAY_NAME_SERVICE_STATUS, DESC_SERVICE_STATUS, - "GAUGE", "INT64", ImmutableList.of(LABEL_TLD, LABEL_SERVICE_TYPE)); @@ -163,7 +162,6 @@ private void createCustomMetricDescriptors() { METRIC_EMERGENCY_USAGE, DISPLAY_NAME_EMERGENCY_USAGE, DESC_EMERGENCY_USAGE, - "GAUGE", "DOUBLE", ImmutableList.of(LABEL_TLD, LABEL_SERVICE_TYPE)); @@ -175,7 +173,6 @@ private void createMetricDescriptor( String metricTypeSuffix, String displayName, String description, - String metricKind, String valueType, ImmutableList labelKeys) { @@ -195,7 +192,7 @@ private void createMetricDescriptor( MetricDescriptor descriptor = new MetricDescriptor() .setType(METRIC_DOMAIN + metricTypeSuffix) - .setMetricKind(metricKind) + .setMetricKind(GAUGE_METRIC_KIND) .setValueType(valueType) .setDisplayName(displayName) .setDescription(description) From d5922b9c23d050fc9458855eb376a045a9a9cbda Mon Sep 17 00:00:00 2001 From: njshah301 Date: Tue, 27 Jan 2026 18:20:34 +0000 Subject: [PATCH 7/7] Refactor MosApiMetrics to remove async ExecutorService --- .../google/registry/mosapi/MosApiMetrics.java | 77 ++++++++----------- .../registry/mosapi/module/MosApiModule.java | 18 ----- .../registry/mosapi/MosApiMetricsTest.java | 3 +- 3 files changed, 34 insertions(+), 64 deletions(-) diff --git a/core/src/main/java/google/registry/mosapi/MosApiMetrics.java b/core/src/main/java/google/registry/mosapi/MosApiMetrics.java index 1ce1eb19fea..62833b384b0 100644 --- a/core/src/main/java/google/registry/mosapi/MosApiMetrics.java +++ b/core/src/main/java/google/registry/mosapi/MosApiMetrics.java @@ -37,13 +37,11 @@ import google.registry.mosapi.MosApiModels.TldServiceState; import google.registry.util.Clock; import jakarta.inject.Inject; -import jakarta.inject.Named; import jakarta.inject.Singleton; import java.io.IOException; import java.time.Instant; import java.util.Iterator; import java.util.List; -import java.util.concurrent.ExecutorService; import java.util.concurrent.atomic.AtomicBoolean; import java.util.stream.Stream; @@ -98,20 +96,15 @@ public class MosApiMetrics { private final String projectName; private final Clock clock; private final MonitoredResource monitoredResource; - private final ExecutorService executor; // Flag to ensure we only create descriptors once, lazily private final AtomicBoolean isDescriptorInitialized = new AtomicBoolean(false); @Inject public MosApiMetrics( - Monitoring monitoringClient, - @Config("projectId") String projectId, - Clock clock, - @Named("mosapiMetricsExecutor") ExecutorService executor) { + Monitoring monitoringClient, @Config("projectId") String projectId, Clock clock) { this.monitoringClient = monitoringClient; this.projectId = projectId; this.clock = clock; - this.executor = executor; this.projectName = PROJECT_RESOURCE_PREFIX + projectId; @@ -127,46 +120,42 @@ public void recordStates(ImmutableList states) { if (isDescriptorInitialized.compareAndSet(false, true)) { createCustomMetricDescriptors(); } - executor.execute( - () -> { - try { - pushBatchMetrics(states); - } catch (Exception e) { - logger.atSevere().withCause(e).log("Async batch metric push failed."); - } - }); + + try { + pushBatchMetrics(states); + } catch (Exception e) { + logger.atSevere().withCause(e).log("MosApi Batch metric push failed."); + throw new RuntimeException("Batch metric push failed", e); + } } // Defines the custom metrics in Cloud Monitoring private void createCustomMetricDescriptors() { - executor.execute( - () -> { - // 1. TLD Status Descriptor - createMetricDescriptor( - METRIC_TLD_STATUS, - DISPLAY_NAME_TLD_STATUS, - DESC_TLD_STATUS, - "INT64", - ImmutableList.of(LABEL_TLD)); - - // 2. Service Status Descriptor - createMetricDescriptor( - METRIC_SERVICE_STATUS, - DISPLAY_NAME_SERVICE_STATUS, - DESC_SERVICE_STATUS, - "INT64", - ImmutableList.of(LABEL_TLD, LABEL_SERVICE_TYPE)); - - // 3. Emergency Usage Descriptor - createMetricDescriptor( - METRIC_EMERGENCY_USAGE, - DISPLAY_NAME_EMERGENCY_USAGE, - DESC_EMERGENCY_USAGE, - "DOUBLE", - ImmutableList.of(LABEL_TLD, LABEL_SERVICE_TYPE)); - - logger.atInfo().log("Metric descriptors ensured for project %s", projectId); - }); + // 1. TLD Status Descriptor + createMetricDescriptor( + METRIC_TLD_STATUS, + DISPLAY_NAME_TLD_STATUS, + DESC_TLD_STATUS, + "INT64", + ImmutableList.of(LABEL_TLD)); + + // 2. Service Status Descriptor + createMetricDescriptor( + METRIC_SERVICE_STATUS, + DISPLAY_NAME_SERVICE_STATUS, + DESC_SERVICE_STATUS, + "INT64", + ImmutableList.of(LABEL_TLD, LABEL_SERVICE_TYPE)); + + // 3. Emergency Usage Descriptor + createMetricDescriptor( + METRIC_EMERGENCY_USAGE, + DISPLAY_NAME_EMERGENCY_USAGE, + DESC_EMERGENCY_USAGE, + "DOUBLE", + ImmutableList.of(LABEL_TLD, LABEL_SERVICE_TYPE)); + + logger.atInfo().log("Metric descriptors ensured for project %s", projectId); } private void createMetricDescriptor( diff --git a/core/src/main/java/google/registry/mosapi/module/MosApiModule.java b/core/src/main/java/google/registry/mosapi/module/MosApiModule.java index 879e5e1d7c9..73ff6621fef 100644 --- a/core/src/main/java/google/registry/mosapi/module/MosApiModule.java +++ b/core/src/main/java/google/registry/mosapi/module/MosApiModule.java @@ -14,7 +14,6 @@ package google.registry.mosapi.module; -import com.google.common.util.concurrent.ThreadFactoryBuilder; import dagger.Module; import dagger.Provides; import google.registry.config.RegistryConfig.Config; @@ -204,21 +203,4 @@ static ExecutorService provideMosapiTldExecutor( @Config("mosapiTldThreadCount") int threadPoolSize) { return Executors.newFixedThreadPool(threadPoolSize); } - - /** - * Provides executor for metrics reporting. - * - *

We are using a single thread to ensure metric batches are sent sequentially. This acts as a - * client-side rate limiter to avoid exceeding Google Cloud Monitoring API quotas (requests per - * second). - * - * @see Google Cloud Monitoring Quotas - */ - @Provides - @Singleton - @Named("mosapiMetricsExecutor") - static ExecutorService provideMosapiMetricsExecutor() { - return Executors.newSingleThreadExecutor( - new ThreadFactoryBuilder().setNameFormat("mosapi-metrics-%d").build()); - } } diff --git a/core/src/test/java/google/registry/mosapi/MosApiMetricsTest.java b/core/src/test/java/google/registry/mosapi/MosApiMetricsTest.java index 4c3e92bbca6..fe4fda265ea 100644 --- a/core/src/test/java/google/registry/mosapi/MosApiMetricsTest.java +++ b/core/src/test/java/google/registry/mosapi/MosApiMetricsTest.java @@ -28,7 +28,6 @@ import com.google.api.services.monitoring.v3.model.TimeSeries; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; -import com.google.common.util.concurrent.MoreExecutors; import google.registry.mosapi.MosApiModels.ServiceStatus; import google.registry.mosapi.MosApiModels.TldServiceState; import google.registry.testing.FakeClock; @@ -74,7 +73,7 @@ void setUp() throws IOException { mosApiMetrics = new MosApiMetrics( - monitoringClient, PROJECT_ID, clock, MoreExecutors.newDirectExecutorService()); + monitoringClient, PROJECT_ID, clock); } @Test