Skip to content

Commit 44605b9

Browse files
committed
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
1 parent d6e0a7b commit 44605b9

6 files changed

Lines changed: 363 additions & 3 deletions

File tree

core/src/main/java/google/registry/config/RegistryConfig.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1468,6 +1468,12 @@ public static int provideMosapiTldThreads(RegistryConfigSettings config) {
14681468
return config.mosapi.tldThreadCnt;
14691469
}
14701470

1471+
@Provides
1472+
@Config("mosapiMetricsThreadCnt")
1473+
public static int provideMosapiMetricsThreads(RegistryConfigSettings config) {
1474+
return config.mosapi.metricsThreadCnt;
1475+
}
1476+
14711477
private static String formatComments(String text) {
14721478
return Splitter.on('\n').omitEmptyStrings().trimResults().splitToList(text).stream()
14731479
.map(s -> "# " + s)

core/src/main/java/google/registry/config/RegistryConfigSettings.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -273,5 +273,7 @@ public static class MosApi {
273273
public List<String> tlds;
274274
public List<String> services;
275275
public int tldThreadCnt;
276+
277+
public int metricsThreadCnt;
276278
}
277279
}

core/src/main/java/google/registry/config/files/default-config.yaml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -647,3 +647,9 @@ mosapi:
647647
# ICANN MoSAPI Specification, Section 12.3</a>
648648
tldThreadCnt: 4
649649

650+
# Metrics Reporting Thread Count
651+
# Set to 1. Given the current TLD volume and the 3-minute reporting interval,
652+
# a single thread provides sufficient throughput to process all metrics
653+
# sequentially without backlog.
654+
metricsThreadCnt: 1
655+

core/src/main/java/google/registry/mosapi/MosApiMetrics.java

Lines changed: 185 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,21 +14,203 @@
1414

1515
package google.registry.mosapi;
1616

17+
import com.google.api.client.util.DateTime;
18+
import com.google.api.services.monitoring.v3.Monitoring;
19+
import com.google.api.services.monitoring.v3.model.CreateTimeSeriesRequest;
20+
import com.google.api.services.monitoring.v3.model.Metric;
21+
import com.google.api.services.monitoring.v3.model.MonitoredResource;
22+
import com.google.api.services.monitoring.v3.model.Point;
23+
import com.google.api.services.monitoring.v3.model.TimeInterval;
24+
import com.google.api.services.monitoring.v3.model.TimeSeries;
25+
import com.google.api.services.monitoring.v3.model.TypedValue;
26+
import com.google.common.base.Ascii;
27+
import com.google.common.collect.ImmutableList;
28+
import com.google.common.collect.ImmutableMap;
29+
import com.google.common.collect.Lists;
1730
import com.google.common.flogger.FluentLogger;
31+
import google.registry.config.RegistryConfig.Config;
32+
import google.registry.mosapi.MosApiModels.ServiceStatus;
1833
import google.registry.mosapi.MosApiModels.TldServiceState;
1934
import jakarta.inject.Inject;
35+
import jakarta.inject.Named;
36+
import java.io.IOException;
37+
import java.util.ArrayList;
38+
import java.util.Collections;
2039
import java.util.List;
40+
import java.util.Map;
41+
import java.util.concurrent.ExecutorService;
2142

2243
/** Metrics Exporter for MoSAPI. */
2344
public class MosApiMetrics {
2445

2546
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
2647

48+
// Google Cloud Monitoring Limit: Max 200 TimeSeries per request
49+
private static final int MAX_TIMESERIES_PER_REQUEST = 195;
50+
51+
// Magic String Constants
52+
private static final String METRIC_DOMAIN = "custom.googleapis.com/mosapi/";
53+
private static final String PROJECT_RESOURCE_PREFIX = "projects/";
54+
private static final String RESOURCE_TYPE_GLOBAL = "global";
55+
private static final String LABEL_PROJECT_ID = "project_id";
56+
private static final String LABEL_TLD = "tld";
57+
private static final String LABEL_SERVICE_TYPE = "service_type";
58+
59+
// Metric Names
60+
private static final String METRIC_TLD_STATUS = "tld_status";
61+
private static final String METRIC_SERVICE_STATUS = "service_status";
62+
private static final String METRIC_EMERGENCY_USAGE = "emergency_usage";
63+
64+
// MoSAPI Status Constants
65+
private static final String STATUS_UP_INCONCLUSIVE = "UP-INCONCLUSIVE";
66+
private static final String STATUS_DOWN = "DOWN";
67+
private static final String STATUS_DISABLED = "DISABLED";
68+
69+
private final Monitoring monitoringClient;
70+
private final String projectId;
71+
private final ExecutorService executor;
72+
2773
@Inject
28-
public MosApiMetrics() {}
74+
public MosApiMetrics(
75+
Monitoring monitoringClient,
76+
@Config("projectId") String projectId,
77+
@Named("mosapiMetricsExecutor") ExecutorService executor) {
78+
this.monitoringClient = monitoringClient;
79+
this.projectId = projectId;
80+
this.executor = executor;
81+
}
2982

83+
/** Accepts a list of states and processes them in a single async batch task. */
3084
public void recordStates(List<TldServiceState> states) {
31-
// b/467541269: Logic to push status to Cloud Monitoring goes here
32-
logger.atInfo().log("MoSAPI record metrics logic will be implemented from here");
85+
executor.execute(
86+
() -> {
87+
try {
88+
pushBatchMetrics(states);
89+
} catch (Throwable t) {
90+
logger.atWarning().withCause(t).log("Async batch metric push failed.");
91+
}
92+
});
93+
}
94+
95+
private void pushBatchMetrics(List<TldServiceState> states) throws IOException {
96+
List<TimeSeries> allTimeSeries = new ArrayList<>();
97+
TimeInterval interval =
98+
new TimeInterval().setEndTime(new DateTime(System.currentTimeMillis()).toString());
99+
100+
for (TldServiceState state : states) {
101+
// 1. TLD Status Metric
102+
allTimeSeries.add(createTldStatusTimeSeries(state, interval));
103+
104+
// 2. Service-level Metrics
105+
Map<String, ServiceStatus> services = state.serviceStatuses();
106+
if (services != null) {
107+
for (Map.Entry<String, ServiceStatus> entry : services.entrySet()) {
108+
addServiceMetrics(allTimeSeries, state.tld(), entry.getKey(), entry.getValue(), interval);
109+
}
110+
}
111+
}
112+
113+
for (List<TimeSeries> chunk : Lists.partition(allTimeSeries, MAX_TIMESERIES_PER_REQUEST)) {
114+
CreateTimeSeriesRequest request = new CreateTimeSeriesRequest().setTimeSeries(chunk);
115+
monitoringClient
116+
.projects()
117+
.timeSeries()
118+
.create(PROJECT_RESOURCE_PREFIX + projectId, request)
119+
.execute();
120+
logger.atInfo().log(
121+
"Successfully pushed batch of %d time series to Cloud Monitoring.", chunk.size());
122+
}
123+
}
124+
125+
private void addServiceMetrics(
126+
List<TimeSeries> list,
127+
String tld,
128+
String serviceType,
129+
ServiceStatus statusObj,
130+
TimeInterval interval) {
131+
ImmutableMap<String, String> labels =
132+
ImmutableMap.of(LABEL_TLD, tld, LABEL_SERVICE_TYPE, serviceType);
133+
134+
list.add(
135+
createTimeSeries(
136+
METRIC_SERVICE_STATUS, labels, parseServiceStatus(statusObj.status()), interval));
137+
138+
list.add(
139+
createTimeSeries(METRIC_EMERGENCY_USAGE, labels, statusObj.emergencyThreshold(), interval));
140+
}
141+
142+
private TimeSeries createTldStatusTimeSeries(TldServiceState state, TimeInterval interval) {
143+
return createTimeSeries(
144+
METRIC_TLD_STATUS,
145+
ImmutableMap.of(LABEL_TLD, state.tld()),
146+
parseTldStatus(state.status()),
147+
interval);
148+
}
149+
150+
private TimeSeries createTimeSeries(
151+
String suffix, Map<String, String> labels, Number val, TimeInterval interval) {
152+
Metric metric = new Metric().setType(METRIC_DOMAIN + suffix).setLabels(labels);
153+
MonitoredResource resource =
154+
new MonitoredResource()
155+
.setType(RESOURCE_TYPE_GLOBAL)
156+
.setLabels(Collections.singletonMap(LABEL_PROJECT_ID, projectId));
157+
158+
TypedValue tv = new TypedValue();
159+
if (val instanceof Double) {
160+
tv.setDoubleValue((Double) val);
161+
} else {
162+
tv.setInt64Value(val.longValue());
163+
}
164+
165+
return new TimeSeries()
166+
.setMetric(metric)
167+
.setResource(resource)
168+
.setPoints(ImmutableList.of(new Point().setInterval(interval).setValue(tv)));
169+
}
170+
171+
/**
172+
* Translates MoSAPI status to a numeric metric.
173+
*
174+
* <p>Mappings: 1 (UP) = Healthy; 0 (DOWN) = Critical failure; 2 (UP-INCONCLUSIVE) = Disabled/Not
175+
* Monitored/In Maintenance.
176+
*
177+
* <p>A status of 2 indicates the SLA monitoring system is under maintenance. The TLD is
178+
* considered "UP" by default, but individual service checks are disabled. This distinguishes
179+
* maintenance windows from actual availability or outages.
180+
*
181+
* @see <a href="https://www.icann.org/mosapi-specification.pdf">ICANN MoSAPI Spec Sec 5.1</a>
182+
*/
183+
private long parseTldStatus(String status) {
184+
if (status == null) {
185+
return 1;
186+
}
187+
return switch (Ascii.toUpperCase(status)) {
188+
case STATUS_DOWN -> 0;
189+
case STATUS_UP_INCONCLUSIVE -> 2;
190+
default -> 1; // status is up
191+
};
192+
}
193+
194+
/**
195+
* Translates MoSAPI service status to a numeric metric.
196+
*
197+
* <p>Mappings: 1 (UP) = Healthy; 0 (DOWN) = Critical failure; 2 (DISABLED/UP-INCONCLUSIVE*) =
198+
* Disabled/Not Monitored/In Maintenance.
199+
*
200+
* @see <a href="https://www.icann.org/mosapi-specification.pdf">ICANN MoSAPI Spec Sec 5.1</a>
201+
*/
202+
private long parseServiceStatus(String status) {
203+
if (status == null) {
204+
return 1;
205+
}
206+
String serviceStatus = Ascii.toUpperCase(status);
207+
if (serviceStatus.startsWith(STATUS_UP_INCONCLUSIVE)) {
208+
return 2;
209+
}
210+
return switch (serviceStatus) {
211+
case STATUS_DOWN -> 0;
212+
case STATUS_DISABLED -> 2;
213+
default -> 1; // status is Up
214+
};
33215
}
34216
}

core/src/main/java/google/registry/mosapi/module/MosApiModule.java

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -203,4 +203,20 @@ static ExecutorService provideMosapiTldExecutor(
203203
@Config("mosapiTldThreadCnt") int threadPoolSize) {
204204
return Executors.newFixedThreadPool(threadPoolSize);
205205
}
206+
207+
/**
208+
* Provides a single-threaded executor for sequential metrics reporting.
209+
*
210+
* <p>Bound to 1 thread because the Google Cloud Monitoring exporter processes batches
211+
* sequentially to respect API quotas and rate limits.
212+
*
213+
* @see <a href="https://cloud.google.com/monitoring/quotas">Google Cloud Monitoring Quotas</a>
214+
*/
215+
@Provides
216+
@Singleton
217+
@Named("mosapiMetricsExecutor")
218+
static ExecutorService provideMosapiMetricsExecutor(
219+
@Config("mosapiMetricsThreadCnt") int threadPoolSize) {
220+
return Executors.newFixedThreadPool(threadPoolSize);
221+
}
206222
}

0 commit comments

Comments
 (0)