Skip to content

Commit

Permalink
Health report (#407)
Browse files Browse the repository at this point in the history
* Add health report endpoint. Add HealthReporterService.

* Add status page.

* Fix CrossOrigin settigns for health report endpoint. Fix status page.

* Remove excessive cache operations.
  • Loading branch information
ekharkunov authored Aug 11, 2024
1 parent 80f7938 commit b4b352e
Show file tree
Hide file tree
Showing 5 changed files with 241 additions and 8 deletions.
140 changes: 140 additions & 0 deletions server/html_data/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
<!DOCTYPE html>
<html>
<head>
<script src="https://code.jquery.com/jquery-1.11.1.min.js"></script>
<link href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.0/css/bootstrap.min.css" rel="stylesheet" id="bootstrap-css">
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.0/js/bootstrap.min.js"></script>
</head>
<link href="https://netdna.bootstrapcdn.com/font-awesome/4.0.3/css/font-awesome.min.css" rel="stylesheet" type="text/css" />
<script src="https://code.jquery.com/jquery.min.js"></script>

<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.2/js/bootstrap.min.js"></script>
<div class="container">
<div class="row">
<div class="col-md-12">
<h1>Extender Status Page</h1>
</div>
</div>
<div class="row clearfix">
<div id="stage" class="col-md-12 column">
<h2 class="panel-title">Stage server</h2>
<div id="statusPanel" class="panel">
<div class="panel-heading">
<h3 id="statusTitle" class="panel-title">
<!-- Not All Systems Operational -->
</h3>
</div>
</div>
<div class="row clearfix">
<div class="col-md-6 column">
<div id="list-1" class="list-group"></div>
</div>
<div class="col-md-6 column">
<div id="list-2" class="list-group"></div>
</div>
</div>
</div>

<div id="prod" class="col-md-12 column">
<h2 class="panel-title">Production server</h2>
<div id="statusPanel" class="panel">
<div class="panel-heading">
<h3 id="statusTitle" class="panel-title">
<!-- Not All Systems Operational -->
</h3>
</div>
</div>
<div class="row clearfix">
<div class="col-md-6 column">
<div id="list-1" class="list-group"></div>
</div>
<div class="col-md-6 column">
<div id="list-2" class="list-group"></div>
</div>
</div>
</div>
</div>
</div>
</div>
<script type="text/javascript">
const OperationalStatus = "Operational";
const UnreachableStatus = "Unreachable";
const NotFullyOperationalStatus = "NotFullyOperational";
function addBlock(id, status, parent_block) {
var top_block = $("<div/>");
top_block.addClass("list-group-item");

var heading = $("<h4/>");
heading.addClass("list-group-item-heading");
heading.text(id);
heading.appendTo(top_block);

var status_block = $("<p/>");
status_block.addClass("list-group-item-text");

var inner_span = $("<span/>");
inner_span.addClass("label");
if (status == OperationalStatus) {
inner_span.addClass("label-success");
} else if (status == UnreachableStatus) {
inner_span.addClass("label-danger");
} else {
inner_span.addClass("label-warning");
}
inner_span.text(status);

inner_span.appendTo(top_block);

top_block.appendTo(parent_block);
}
function handleResponse(response, rootId) {
var totalFullyOperatedPlatforms, totalUnreachablePlatforms = 0;
var parent_block_1 = $(rootId).find("#list-1");
var parent_block_2 = $(rootId).find("#list-2");
var idx = 0;
for (var key in response) {
addBlock(key, response[key], idx % 2 == 0 ? parent_block_1 : parent_block_2);
if (response[key] == OperationalStatus) {
totalFullyOperatedPlatforms++;
} else if (response[key] == UnreachableStatus) {
totalUnreachablePlatforms++;
}
idx++;
}
if (totalUnreachablePlatforms == idx) {
$(rootId).find("#statusPanel").addClass("panel-danger");
$(rootId).find("#statusTitle").text(UnreachableStatus);
} else if (totalFullyOperatedPlatforms == idx) {
$(rootId).find("#statusPanel").addClass("panel-success");
$(rootId).find("#statusTitle").text(OperationalStatus);
} else {
$(rootId).find("#statusPanel").addClass("panel-warning");
$(rootId).find("#statusTitle").text(NotFullyOperationalStatus);
}
}
function handleError(err, rootId) {
$(rootId).find("#statusPanel").addClass("panel-danger");
$(rootId).find("#statusTitle").text(UnreachableStatus);
}

var r = new XMLHttpRequest();
r.open("GET", "https://build-stage.defold.com/health_report");
r.onload = function(response) {
handleResponse(JSON.parse(response.target.responseText), "#stage");
}
r.onerror = function(err) {
handleError(err, "#stage");
}
r.send(null);

var r = new XMLHttpRequest();
r.open("GET", "https://build.defold.com/health_report");
r.onload = function(response) {
handleResponse(JSON.parse(response.target.responseText), "#prod");
}
r.onerror = function(err) {
handleError(err, "#prod");
}
r.send(null);
</script>
</html>
4 changes: 0 additions & 4 deletions server/src/main/java/com/defold/extender/AsyncBuilder.java
Original file line number Diff line number Diff line change
Expand Up @@ -139,9 +139,6 @@ public void asyncBuildEngine(MetricsWriter metricsWriter, String platform, Strin
LOGGER.error(String.format("Exception while building or sending response - SDK: %s", sdkVersion), e);
isSuccefull = false;
} finally {
// Regardless of success/fail status, we want to cache the uploaded files
long totalUploadSize = dataCacheService.cacheFiles(uploadDirectory);
metricsWriter.measureCacheUpload(totalUploadSize);
metricsWriter.measureCounterBuild(platform, sdkVersion, "async", isSuccefull);

// Delete temporary upload directory
Expand All @@ -154,7 +151,6 @@ public void asyncBuildEngine(MetricsWriter metricsWriter, String platform, Strin
else {
LOGGER.info("Keeping job directory due to debug flags");
}

LOGGER.info("Job done");
}
}
Expand Down
15 changes: 14 additions & 1 deletion server/src/main/java/com/defold/extender/ExtenderController.java
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,10 @@
import com.defold.extender.services.DefoldSdkService;
import com.defold.extender.services.DataCacheService;
import com.defold.extender.services.GradleService;
import com.defold.extender.services.HealthReporterService;
import com.defold.extender.services.CocoaPodsService;
import com.defold.extender.services.UserUpdateService;

import org.apache.commons.io.FilenameUtils;
import org.apache.commons.fileupload2.core.FileUploadException;
import org.apache.commons.fileupload2.jakarta.JakartaServletFileUpload;
Expand Down Expand Up @@ -43,6 +45,7 @@
import java.util.regex.Matcher;
import java.util.regex.Pattern;


@RestController
public class ExtenderController {
private static final Logger LOGGER = LoggerFactory.getLogger(ExtenderController.class);
Expand All @@ -59,6 +62,7 @@ public class ExtenderController {
private final MeterRegistry meterRegistry;
private final UserUpdateService userUpdateService;
private final AsyncBuilder asyncBuilder;
private final HealthReporterService healthReporter;

private final RemoteEngineBuilder remoteEngineBuilder;
private Map<String, String> remoteBuilderPlatformMappings;
Expand Down Expand Up @@ -98,6 +102,7 @@ public ExtenderController(DefoldSdkService defoldSdkService,
AsyncBuilder asyncBuilder,
RemoteEngineBuilder remoteEngineBuilder,
RemoteHostConfiguration remoteHostConfiguration,
HealthReporterService healthReporter,
@Value("${extender.remote-builder.enabled}") boolean remoteBuilderEnabled,
@Value("${spring.servlet.multipart.max-request-size}") String maxPackageSize,
@Value("${extender.job-result.location}") String jobResultLocation) {
Expand All @@ -107,6 +112,7 @@ public ExtenderController(DefoldSdkService defoldSdkService,
this.cocoaPodsService = cocoaPodsService;
this.meterRegistry = meterRegistry;
this.userUpdateService = userUpdateService;
this.healthReporter = healthReporter;

this.remoteEngineBuilder = remoteEngineBuilder;
this.remoteBuilderEnabled = remoteBuilderEnabled;
Expand Down Expand Up @@ -339,7 +345,7 @@ public void buildEngineAsync(HttpServletRequest _request,
if (remoteBuilderEnabled && isRemotePlatform(buildEnvDescription[0], buildEnvDescription[1])) {
LOGGER.info("Building engine on remote builder");
String remoteUrl = getRemoteBuilderUrl(buildEnvDescription[0], buildEnvDescription[1]);
this.remoteEngineBuilder.buildAsync(remoteUrl, uploadDirectory, platform, sdkVersion, jobDirectory, uploadDirectory, buildDirectory, metricsWriter);
this.remoteEngineBuilder.buildAsync(remoteUrl, uploadDirectory, platform, sdkVersion, jobDirectory, buildDirectory, metricsWriter);
} else {
asyncBuilder.asyncBuildEngine(metricsWriter, platform, sdkVersion, jobDirectory, uploadDirectory, buildDirectory);
}
Expand Down Expand Up @@ -439,6 +445,13 @@ public Integer getBuildStatus(@RequestParam String jobId) throws IOException {
return null;
}

@GetMapping(path= "/health_report", produces="application/json")
@ResponseBody
@CrossOrigin
public String getHealthReport() {
return healthReporter.collectHealthReport(remoteBuilderEnabled, remoteBuilderPlatformMappings);
}

static private boolean isRelativePath(File parent, File file) throws IOException {
String parentPath = parent.getCanonicalPath();
String filePath = file.getCanonicalPath();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,6 @@
import com.defold.extender.metrics.MetricsWriter;
import com.defold.extender.Timer;

import io.micrometer.core.instrument.MeterRegistry;

import org.apache.commons.io.FileUtils;
import org.apache.commons.io.IOUtils;
import org.apache.http.HttpEntity;
Expand Down Expand Up @@ -108,7 +106,7 @@ public void buildAsync(final String remoteBuilderUrl,
final File projectDirectory,
final String platform,
final String sdkVersion,
File jobDirectory, File uploadDirectory, File buildDirectory, MetricsWriter metricsWriter) throws FileNotFoundException, IOException {
File jobDirectory, File buildDirectory, MetricsWriter metricsWriter) throws FileNotFoundException, IOException {

LOGGER.info("Building engine remotely at {}", remoteBuilderUrl);
String jobName = jobDirectory.getName();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
package com.defold.extender.services;

import java.util.Collections;
import java.util.HashMap;
import java.util.Map;

import org.apache.http.HttpResponse;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.util.EntityUtils;
import org.json.simple.JSONObject;
import org.json.simple.parser.JSONParser;
import org.springframework.stereotype.Service;

@Service
public class HealthReporterService {
public enum OperationalStatus {
Unreachable,
NotFullyOperational,
Operational
}

public HealthReporterService() {
}

@SuppressWarnings("unchecked")
public String collectHealthReport(boolean isRemoteBuildEnabled, Map<String, String> remoteBuilderPlatformMappings) {
if (isRemoteBuildEnabled) {
// we collect information by platform. If one of the builder is unreachable - set
Map<String, OperationalStatus> platformOperationalStatus = new HashMap<>();
JSONObject result = new JSONObject();
JSONParser parser = new JSONParser();
final HttpClient client = HttpClientBuilder.create().build();
for (Map.Entry<String, String> entry : remoteBuilderPlatformMappings.entrySet()) {
final String healthUrl = String.format("%s/health_report", entry.getValue());
final HttpGet request = new HttpGet(healthUrl);
String platform = getPlatform(entry.getKey());
try {
HttpResponse response = client.execute(request);
if (response.getStatusLine().getStatusCode() == org.apache.http.HttpStatus.SC_OK) {
JSONObject responseBody = (JSONObject)parser.parse(EntityUtils.toString(response.getEntity()));
if (responseBody.containsKey("status")
&& OperationalStatus.valueOf((String)responseBody.get("status")) == OperationalStatus.Operational) {
updateOperationalStatus(platformOperationalStatus, platform, true);
} else {
updateOperationalStatus(platformOperationalStatus, platform, false);
}
} else {
updateOperationalStatus(platformOperationalStatus, platform, false);
}
} catch(Exception exc) {
updateOperationalStatus(platformOperationalStatus, platform, false);
}
}

for (Map.Entry<String, OperationalStatus> entry : platformOperationalStatus.entrySet()) {
result.put(entry.getKey(), entry.getValue().toString());
}
return result.toJSONString();
} else {
return JSONObject.toJSONString(Collections.singletonMap("status", OperationalStatus.Operational.toString()));
}
}

private String getPlatform(String hostId) {
return hostId.split("-")[0];
}

private void updateOperationalStatus(Map<String, OperationalStatus> statuses, String platform, boolean isBuilderUp) {
if (statuses.containsKey(platform)) {
OperationalStatus prevStatus = (OperationalStatus)statuses.get(platform);
if (isBuilderUp) {
if (prevStatus == OperationalStatus.Unreachable) {
statuses.put(platform, OperationalStatus.NotFullyOperational);
}
} else {
if (prevStatus == OperationalStatus.Operational) {
statuses.put(platform, OperationalStatus.NotFullyOperational);
}
}
} else {
statuses.put(platform, isBuilderUp ? OperationalStatus.Operational : OperationalStatus.Unreachable);
}
}
}

0 comments on commit b4b352e

Please sign in to comment.