/**
* 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 org.apache.aurora.scheduler.http;
import java.io.StringWriter;
import java.util.Locale;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Objects;
import javax.annotation.Nullable;
import javax.inject.Inject;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.Response.Status;
import com.google.common.base.Function;
import com.google.common.collect.FluentIterable;
import org.apache.aurora.common.base.MorePreconditions;
import org.apache.aurora.common.util.templating.StringTemplateHelper;
import org.apache.aurora.common.util.templating.StringTemplateHelper.TemplateException;
import org.apache.aurora.scheduler.base.Query;
import org.apache.aurora.scheduler.resources.ResourceType;
import org.apache.aurora.scheduler.stats.ResourceCounter;
import org.apache.aurora.scheduler.stats.ResourceCounter.Metric;
import org.apache.aurora.scheduler.stats.ResourceCounter.MetricType;
import org.apache.aurora.scheduler.storage.entities.IServerInfo;
import org.apache.aurora.scheduler.storage.entities.ITaskConfig;
/**
* A servlet to give an aggregate view of cluster resources consumed, grouped by category.
*/
@Path("/utilization")
public class Utilization {
private final String clusterName;
private final ResourceCounter counter;
private final StringTemplateHelper templateHelper;
@Inject
Utilization(ResourceCounter counter, IServerInfo serverInfo) {
templateHelper = new StringTemplateHelper(getClass(), "utilization", true);
this.counter = Objects.requireNonNull(counter);
this.clusterName = MorePreconditions.checkNotBlank(serverInfo.getClusterName());
}
private String fillTemplate(Map<Display, Metric> metrics) {
Function<Entry<Display, Metric>, DisplayMetric> transform =
entry -> new DisplayMetric(entry.getKey(), entry.getValue());
return fillTemplate(FluentIterable.from(metrics.entrySet()).transform(transform).toList());
}
private String fillTemplate(final Iterable<DisplayMetric> metrics) {
StringWriter output = new StringWriter();
try {
templateHelper.writeTemplate(output, template -> {
template.setAttribute("cluster_name", clusterName);
template.setAttribute("metrics", metrics);
});
} catch (TemplateException e) {
throw new WebApplicationException(e);
}
return output.toString();
}
private static class Display {
private final String title;
@Nullable
private final String link;
Display(String title, @Nullable String link) {
this.title = title;
this.link = link;
}
@Override
public int hashCode() {
return Objects.hash(title, link);
}
@Override
public boolean equals(Object o) {
if (!(o instanceof Display)) {
return false;
}
Display other = (Display) o;
return Objects.equals(title, other.title) && Objects.equals(link, other.link);
}
}
private static class DisplayMetric extends Metric {
private final Display display;
DisplayMetric(Display display, Metric wrapped) {
super(wrapped);
this.display = display;
}
public String getTitle() {
return display.title;
}
@Nullable
public String getLink() {
return display.link;
}
public long getCpu() {
return valueOf(ResourceType.CPUS);
}
public long getRam() {
return valueOf(ResourceType.RAM_MB);
}
public long getDisk() {
return valueOf(ResourceType.DISK_MB);
}
private long valueOf(ResourceType type) {
return getBag().valueOf(type).longValue();
}
@Override
public boolean equals(Object o) {
if (!(o instanceof DisplayMetric)) {
return false;
}
DisplayMetric other = (DisplayMetric) o;
return super.equals(o)
&& display.equals(other.display);
}
@Override
public int hashCode() {
return Objects.hash(super.hashCode(), display);
}
}
private static final Function<Metric, DisplayMetric> TO_DISPLAY =
count -> new DisplayMetric(
new Display(
count.type.name().replace('_', ' ').toLowerCase(),
count.type.name().toLowerCase()),
count);
/**
* Displays the aggregate utilization for the entire cluster.
*
* @return HTML-formatted cluster utilization.
*/
@GET
@Produces(MediaType.TEXT_HTML)
public Response aggregateCluster() {
Iterable<DisplayMetric> metrics =
FluentIterable.from(counter.computeConsumptionTotals()).transform(TO_DISPLAY).toList();
return Response.ok(fillTemplate(metrics)).build();
}
private MetricType getTypeByName(String name) throws WebApplicationException {
MetricType type = MetricType.valueOf(name.toUpperCase(Locale.ENGLISH));
if (type == null) {
throw new WebApplicationException(
Response.status(Status.BAD_REQUEST).entity("Invalid metric type.").build());
}
return type;
}
/**
* Displays the aggregate utilization for roles within a metric type.
*
* @param metric Metric id.
* @return HTML-formatted utilization within the metric type.
*/
@GET
@Path("/{metric}")
@Produces(MediaType.TEXT_HTML)
public Response aggregateRoles(@PathParam("metric") final String metric) {
final MetricType type = getTypeByName(metric);
Function<ITaskConfig, Display> toKey = task -> {
String role = task.getJob().getRole();
return new Display(role, metric + "/" + role);
};
Map<Display, Metric> byRole =
counter.computeAggregates(Query.unscoped().active(), type.filter, toKey);
return Response.ok(fillTemplate(byRole)).build();
}
/**
* Displays the aggregate utilization for jobs within a role.
*
* @param metric Metric id.
* @param role Role for jobs to aggregate.
* @return HTML-formatted utilization within the metric/role.
*/
@GET
@Path("/{metric}/{role}")
@Produces(MediaType.TEXT_HTML)
public Response aggregateJobs(
@PathParam("metric") String metric,
@PathParam("role") String role) {
MetricType type = getTypeByName(metric);
Function<ITaskConfig, Display> toKey = task -> new Display(task.getJob().getName(), null);
Map<Display, Metric> byJob =
counter.computeAggregates(Query.roleScoped(role).active(), type.filter, toKey);
return Response.ok(fillTemplate(byJob)).build();
}
}