Skip to content

Commit 338d5ba

Browse files
committed
Add GetServiceState action for MoSAPI service monitoring
Implements the `/api/mosapi/getServiceState` endpoint to retrieve service health summaries for TLDs from the MoSAPI system. - Introduces `GetServiceStateAction` to fetch TLD service status. - Implements `MosApiStateService` to transform raw MoSAPI responses into a curated `ServiceStateSummary`. - Uses concurrent processing with a fixed thread pool to fetch states for all configured TLDs efficiently while respecting MoSAPI rate limits. junit test added
1 parent fcdac3e commit 338d5ba

25 files changed

+1486
-0
lines changed

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1462,6 +1462,12 @@ public static ImmutableSet<String> provideMosapiServices(RegistryConfigSettings
14621462
return ImmutableSet.copyOf(config.mosapi.services);
14631463
}
14641464

1465+
@Provides
1466+
@Config("mosapiTldThreadCnt")
1467+
public static Integer provideMosapiTldThreads(RegistryConfigSettings config) {
1468+
return config.mosapi.tldThreadCnt;
1469+
}
1470+
14651471
private static String formatComments(String text) {
14661472
return Splitter.on('\n').omitEmptyStrings().trimResults().splitToList(text).stream()
14671473
.map(s -> "# " + s)

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -272,5 +272,6 @@ public static class MosApi {
272272
public String entityType;
273273
public List<String> tlds;
274274
public List<String> services;
275+
public Integer tldThreadCnt;
275276
}
276277
}

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -642,4 +642,8 @@ mosapi:
642642
- "epp"
643643
- "dnssec"
644644

645+
#Provides a fixed thread pool for parallel TLD processing.
646+
# @see <a href="https://www.icann.org/mosapi-specification.pdf">
647+
# ICANN MoSAPI Specification, Section 12.3</a>
648+
tldThreadCnt: 4
645649

core/src/main/java/google/registry/module/RequestComponent.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,8 @@
6161
import google.registry.module.ReadinessProbeAction.ReadinessProbeActionPubApi;
6262
import google.registry.module.ReadinessProbeAction.ReadinessProbeConsoleAction;
6363
import google.registry.monitoring.whitebox.WhiteboxModule;
64+
import google.registry.mosapi.GetServiceStateAction;
65+
import google.registry.mosapi.module.MosApiRequestModule;
6466
import google.registry.rdap.RdapAutnumAction;
6567
import google.registry.rdap.RdapDomainAction;
6668
import google.registry.rdap.RdapDomainSearchAction;
@@ -150,6 +152,7 @@
150152
EppToolModule.class,
151153
IcannReportingModule.class,
152154
LoadTestModule.class,
155+
MosApiRequestModule.class,
153156
RdapModule.class,
154157
RdeModule.class,
155158
ReportingModule.class,
@@ -229,6 +232,8 @@ interface RequestComponent {
229232

230233
GenerateZoneFilesAction generateZoneFilesAction();
231234

235+
GetServiceStateAction getServiceStateAction();
236+
232237
IcannReportingStagingAction icannReportingStagingAction();
233238

234239
IcannReportingUploadAction icannReportingUploadAction();
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
// Copyright 2025 The Nomulus Authors. All Rights Reserved.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package google.registry.mosapi;
16+
17+
import com.google.common.flogger.FluentLogger;
18+
import com.google.common.net.MediaType;
19+
import com.google.gson.Gson;
20+
import google.registry.request.Action;
21+
import google.registry.request.HttpException.ServiceUnavailableException;
22+
import google.registry.request.Parameter;
23+
import google.registry.request.Response;
24+
import google.registry.request.auth.Auth;
25+
import jakarta.inject.Inject;
26+
import java.util.Optional;
27+
28+
/** An action that returns the current MoSAPI service state for a given TLD or all TLDs. */
29+
@Action(
30+
service = Action.Service.BACKEND,
31+
path = GetServiceStateAction.PATH,
32+
method = Action.Method.GET,
33+
auth = Auth.AUTH_ADMIN)
34+
public class GetServiceStateAction implements Runnable {
35+
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
36+
37+
public static final String PATH = "/_dr/mosapi/getServiceState";
38+
public static final String TLD_PARAM = "tld";
39+
40+
private final MosApiStateService stateService;
41+
private final Response response;
42+
private final Gson gson;
43+
private final Optional<String> tld;
44+
45+
@Inject
46+
public GetServiceStateAction(
47+
MosApiStateService stateService,
48+
Response response,
49+
Gson gson,
50+
@Parameter(TLD_PARAM) Optional<String> tld) {
51+
this.stateService = stateService;
52+
this.response = response;
53+
this.gson = gson;
54+
this.tld = tld;
55+
}
56+
57+
@Override
58+
public void run() {
59+
response.setContentType(MediaType.JSON_UTF_8);
60+
try {
61+
if (tld.isPresent()) {
62+
response.setPayload(gson.toJson(stateService.getServiceStateSummary(tld.get())));
63+
} else {
64+
response.setPayload(gson.toJson(stateService.getAllServiceStateSummaries()));
65+
}
66+
} catch (MosApiException e) {
67+
logger.atWarning().withCause(e).log(
68+
"MoSAPI client failed to get Service state for %s TLD", tld.orElse("all"));
69+
throw new ServiceUnavailableException("Error fetching MoSAPI service state.");
70+
}
71+
}
72+
}
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
// Copyright 2025 The Nomulus Authors. All Rights Reserved.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package google.registry.mosapi;
16+
17+
import static com.google.common.collect.ImmutableList.toImmutableList;
18+
19+
import com.google.common.collect.ImmutableList;
20+
import com.google.common.collect.ImmutableSet;
21+
import com.google.common.flogger.FluentLogger;
22+
import google.registry.config.RegistryConfig.Config;
23+
import google.registry.mosapi.model.AllServicesStateResponse;
24+
import google.registry.mosapi.model.ServiceStateSummary;
25+
import google.registry.mosapi.model.ServiceStatus;
26+
import google.registry.mosapi.model.TldServiceState;
27+
import jakarta.inject.Inject;
28+
import jakarta.inject.Named;
29+
import java.util.List;
30+
import java.util.concurrent.CompletableFuture;
31+
import java.util.concurrent.ExecutorService;
32+
33+
/** A service that provides business logic for interacting with MoSAPI Service State. */
34+
public class MosApiStateService {
35+
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
36+
private final ServiceMonitoringClient serviceMonitoringClient;
37+
private final ExecutorService tldExecutor;
38+
39+
private final ImmutableSet<String> tlds;
40+
41+
private static final String DOWN_STATUS = "Down";
42+
private static final String FETCH_ERROR_STATUS = "ERROR";
43+
44+
@Inject
45+
public MosApiStateService(
46+
ServiceMonitoringClient serviceMonitoringClient,
47+
@Config("mosapiTlds") ImmutableSet<String> tlds,
48+
@Named("mosapiTldExecutor") ExecutorService tldExecutor) {
49+
this.serviceMonitoringClient = serviceMonitoringClient;
50+
this.tlds = tlds;
51+
this.tldExecutor = tldExecutor;
52+
}
53+
54+
/** Fetches and transforms the service state for a given TLD into a summary. */
55+
public ServiceStateSummary getServiceStateSummary(String tld) throws MosApiException {
56+
TldServiceState rawState = serviceMonitoringClient.getTldServiceState(tld);
57+
// TODO(b/467541269): Expose MosApi Service Monitoring response to Cloud monitoring
58+
return transformToSummary(rawState);
59+
}
60+
61+
/** Fetches and transforms the service state for all configured TLDs. */
62+
public AllServicesStateResponse getAllServiceStateSummaries() {
63+
ImmutableList<CompletableFuture<ServiceStateSummary>> futures =
64+
tlds.stream()
65+
.map(
66+
tld ->
67+
CompletableFuture.supplyAsync(
68+
() -> {
69+
try {
70+
return getServiceStateSummary(tld);
71+
} catch (MosApiException e) {
72+
logger.atWarning().withCause(e).log(
73+
"Failed to get service state for TLD %s.", tld);
74+
// we don't want to throw exception if fetch failed
75+
return new ServiceStateSummary(tld, FETCH_ERROR_STATUS, null);
76+
}
77+
},
78+
tldExecutor))
79+
.collect(ImmutableList.toImmutableList());
80+
81+
ImmutableList<ServiceStateSummary> summaries =
82+
futures.stream()
83+
.map(CompletableFuture::join) // Waits for all tasks to complete
84+
.collect(toImmutableList());
85+
86+
return new AllServicesStateResponse(summaries);
87+
}
88+
89+
private ServiceStateSummary transformToSummary(TldServiceState rawState) {
90+
List<ServiceStatus> activeIncidents = null;
91+
if (DOWN_STATUS.equalsIgnoreCase(rawState.getStatus())) {
92+
activeIncidents =
93+
rawState.getServiceStatuses().entrySet().stream()
94+
.filter(
95+
entry -> {
96+
ServiceStatus serviceStatus = entry.getValue();
97+
return serviceStatus.getIncidents() != null
98+
&& !serviceStatus.getIncidents().isEmpty();
99+
})
100+
.map(
101+
entry ->
102+
new ServiceStatus(
103+
// key is the service name
104+
entry.getKey(),
105+
entry.getValue().getEmergencyThreshold(),
106+
entry.getValue().getIncidents()))
107+
.collect(toImmutableList());
108+
}
109+
return new ServiceStateSummary(rawState.getTld(), rawState.getStatus(), activeIncidents);
110+
}
111+
}
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
// Copyright 2025 The Nomulus Authors. All Rights Reserved.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package google.registry.mosapi;
16+
17+
import com.google.gson.Gson;
18+
import com.google.gson.JsonIOException;
19+
import com.google.gson.JsonSyntaxException;
20+
import google.registry.mosapi.model.MosApiErrorResponse;
21+
import google.registry.mosapi.model.TldServiceState;
22+
import jakarta.inject.Inject;
23+
import java.util.Collections;
24+
import okhttp3.Response;
25+
26+
/** Facade for MoSAPI's service monitoring endpoints. */
27+
public class ServiceMonitoringClient {
28+
private final MosApiClient mosApiClient;
29+
private final Gson gson;
30+
31+
@Inject
32+
public ServiceMonitoringClient(MosApiClient mosApiClient, Gson gson) {
33+
this.mosApiClient = mosApiClient;
34+
this.gson = gson;
35+
}
36+
37+
/**
38+
* Fetches the current state of all monitored services for a given TLD.
39+
*
40+
* @see <a href="https://www.icann.org/mosapi-specification.pdf">ICANN MoSAPI Specification,
41+
* Section 5.1</a>
42+
*/
43+
public TldServiceState getTldServiceState(String tld) throws MosApiException {
44+
String endpoint = "v2/monitoring/state";
45+
try (Response response =
46+
mosApiClient.sendGetRequest(
47+
tld, endpoint, Collections.emptyMap(), Collections.emptyMap())) {
48+
if (!response.isSuccessful()) {
49+
throw MosApiException.create(
50+
gson.fromJson(response.body().charStream(), MosApiErrorResponse.class));
51+
}
52+
return gson.fromJson(response.body().charStream(), TldServiceState.class);
53+
} catch (JsonIOException | JsonSyntaxException e) {
54+
// Catch Gson's runtime exceptions (parsing errors) and wrap them
55+
throw new MosApiException("Failed to parse TLD service state response", e);
56+
}
57+
}
58+
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
// Copyright 2025 The Nomulus Authors. All Rights Reserved.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
package google.registry.mosapi.model;
15+
16+
import com.google.gson.annotations.Expose;
17+
import com.google.gson.annotations.SerializedName;
18+
import java.util.List;
19+
20+
/**
21+
* A wrapper response containing the state summaries of all monitored services.
22+
*
23+
* <p>This corresponds to the collection of service statuses returned when monitoring the state of a
24+
* TLD
25+
*
26+
* @see <a href="https://www.icann.org/mosapi-specification.pdf">ICANN MoSAPI Specification, Section
27+
* 5.1</a>
28+
*/
29+
public final class AllServicesStateResponse {
30+
31+
// A list of state summaries for each monitored service (e.g. DNS, RDDS, etc.)
32+
@Expose
33+
@SerializedName("serviceStates")
34+
private final List<ServiceStateSummary> serviceStates;
35+
36+
public AllServicesStateResponse(List<ServiceStateSummary> serviceStates) {
37+
this.serviceStates = serviceStates;
38+
}
39+
40+
public List<ServiceStateSummary> getServiceStates() {
41+
return serviceStates;
42+
}
43+
}

0 commit comments

Comments
 (0)