com.yahoo.container.jdisc.state.StateHandler Maven / Gradle / Ivy
// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
package com.yahoo.container.jdisc.state;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.google.inject.Inject;
import com.yahoo.collections.Tuple2;
import com.yahoo.component.Vtag;
import com.yahoo.component.provider.ComponentRegistry;
import com.yahoo.container.core.ApplicationMetadataConfig;
import com.yahoo.jdisc.Request;
import com.yahoo.jdisc.Response;
import com.yahoo.jdisc.Timer;
import com.yahoo.jdisc.handler.AbstractRequestHandler;
import com.yahoo.jdisc.handler.ContentChannel;
import com.yahoo.jdisc.handler.ResponseDispatch;
import com.yahoo.jdisc.handler.ResponseHandler;
import com.yahoo.jdisc.http.HttpHeaders;
import java.io.ByteArrayOutputStream;
import java.io.PrintStream;
import java.net.URI;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import static com.yahoo.container.jdisc.state.JsonUtil.sanitizeDouble;
/**
* A handler which returns state (health) information from this container instance: Status, metrics and vespa version.
*
* @author Simon Thoresen Hult
*/
public class StateHandler extends AbstractRequestHandler {
private static final ObjectMapper jsonMapper = new ObjectMapper();
public static final String STATE_API_ROOT = "/state/v1";
private static final String METRICS_PATH = "metrics";
private static final String HISTOGRAMS_PATH = "metrics/histograms";
private static final String CONFIG_GENERATION_PATH = "config";
private static final String HEALTH_PATH = "health";
private static final String VERSION_PATH = "version";
private final static MetricDimensions NULL_DIMENSIONS = StateMetricContext.newInstance(null);
private final StateMonitor monitor;
private final Timer timer;
private final byte[] config;
private final SnapshotProvider snapshotProvider;
@Inject
public StateHandler(StateMonitor monitor, Timer timer, ApplicationMetadataConfig config,
ComponentRegistry snapshotProviders) {
this.monitor = monitor;
this.timer = timer;
this.config = buildConfigOutput(config);
snapshotProvider = getSnapshotProviderOrThrow(snapshotProviders);
}
static SnapshotProvider getSnapshotProviderOrThrow(ComponentRegistry preprocessors) {
List allPreprocessors = preprocessors.allComponents();
if (allPreprocessors.size() > 0) {
return allPreprocessors.get(0);
} else {
throw new IllegalArgumentException("At least one snapshot provider is required.");
}
}
@Override
public ContentChannel handleRequest(Request request, ResponseHandler handler) {
new ResponseDispatch() {
@Override
protected Response newResponse() {
Response response = new Response(Response.Status.OK);
response.headers().add(HttpHeaders.Names.CONTENT_TYPE, resolveContentType(request.getUri()));
return response;
}
@Override
protected Iterable responseContent() {
return Collections.singleton(buildContent(request.getUri()));
}
}.dispatch(handler);
return null;
}
private String resolveContentType(URI requestUri) {
if (resolvePath(requestUri).equals(HISTOGRAMS_PATH)) {
return "text/plain; charset=utf-8";
} else {
return "application/json";
}
}
private ByteBuffer buildContent(URI requestUri) {
String suffix = resolvePath(requestUri);
switch (suffix) {
case "":
return ByteBuffer.wrap(apiLinks(requestUri));
case CONFIG_GENERATION_PATH:
return ByteBuffer.wrap(config);
case HISTOGRAMS_PATH:
return ByteBuffer.wrap(buildHistogramsOutput());
case HEALTH_PATH:
case METRICS_PATH:
return ByteBuffer.wrap(buildMetricOutput(suffix));
case VERSION_PATH:
return ByteBuffer.wrap(buildVersionOutput());
default:
// XXX should possibly do something else here
return ByteBuffer.wrap(buildMetricOutput(suffix));
}
}
private byte[] apiLinks(URI requestUri) {
try {
int port = requestUri.getPort();
String host = requestUri.getHost();
StringBuilder base = new StringBuilder("http://");
base.append(host);
if (port != -1) {
base.append(":").append(port);
}
base.append(STATE_API_ROOT);
String uriBase = base.toString();
ArrayNode linkList = jsonMapper.createArrayNode();
for (String api : new String[] {METRICS_PATH, CONFIG_GENERATION_PATH, HEALTH_PATH, VERSION_PATH}) {
ObjectNode resource = jsonMapper.createObjectNode();
resource.put("url", uriBase + "/" + api);
linkList.add(resource);
}
JsonNode resources = jsonMapper.createObjectNode().set("resources", linkList);
return toPrettyString(resources);
} catch (JsonProcessingException e) {
throw new RuntimeException("Bad JSON construction", e);
}
}
private static String resolvePath(URI uri) {
String path = uri.getPath();
if (path.endsWith("/")) {
path = path.substring(0, path.length() - 1);
}
if (path.startsWith(STATE_API_ROOT)) {
path = path.substring(STATE_API_ROOT.length());
}
if (path.startsWith("/")) {
path = path.substring(1);
}
return path;
}
private static byte[] buildConfigOutput(ApplicationMetadataConfig config) {
try {
return toPrettyString(
jsonMapper.createObjectNode()
.set(CONFIG_GENERATION_PATH, jsonMapper.createObjectNode()
.put("generation", config.generation())
.set("container", jsonMapper.createObjectNode()
.put("generation", config.generation()))));
} catch (JsonProcessingException e) {
throw new RuntimeException("Bad JSON construction.", e);
}
}
private static byte[] buildVersionOutput() {
try {
return toPrettyString(
jsonMapper.createObjectNode()
.put("version", Vtag.currentVersion.toString()));
} catch (JsonProcessingException e) {
throw new RuntimeException("Bad JSON construction.", e);
}
}
private byte[] buildMetricOutput(String consumer) {
try {
return toPrettyString(buildJsonForConsumer(consumer));
} catch (JsonProcessingException e) {
throw new RuntimeException("Bad JSON construction.", e);
}
}
private byte[] buildHistogramsOutput() {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
if (snapshotProvider != null) {
snapshotProvider.histogram(new PrintStream(baos));
}
return baos.toByteArray();
}
private ObjectNode buildJsonForConsumer(String consumer) {
ObjectNode ret = jsonMapper.createObjectNode();
ret.put("time", timer.currentTimeMillis());
ret.set("status", jsonMapper.createObjectNode().put("code", getStatus().name()));
ret.set(METRICS_PATH, buildJsonForSnapshot(consumer, getSnapshot()));
return ret;
}
private MetricSnapshot getSnapshot() {
return snapshotProvider.latestSnapshot();
}
private StateMonitor.Status getStatus() {
return monitor.status();
}
private ObjectNode buildJsonForSnapshot(String consumer, MetricSnapshot metricSnapshot) {
if (metricSnapshot == null) {
return jsonMapper.createObjectNode();
}
ObjectNode jsonMetric = jsonMapper.createObjectNode();
jsonMetric.set("snapshot", jsonMapper.createObjectNode()
.put("from", sanitizeDouble(metricSnapshot.getFromTime(TimeUnit.MILLISECONDS) / 1000.0))
.put("to", sanitizeDouble(metricSnapshot.getToTime(TimeUnit.MILLISECONDS) / 1000.0)));
boolean includeDimensions = !consumer.equals(HEALTH_PATH);
long periodInMillis = metricSnapshot.getToTime(TimeUnit.MILLISECONDS) -
metricSnapshot.getFromTime(TimeUnit.MILLISECONDS);
for (Tuple tuple : collapseMetrics(metricSnapshot, consumer)) {
ObjectNode jsonTuple = jsonMapper.createObjectNode();
jsonTuple.put("name", tuple.key);
if (tuple.val instanceof CountMetric) {
CountMetric count = (CountMetric)tuple.val;
jsonTuple.set("values", jsonMapper.createObjectNode()
.put("count", count.getCount())
.put("rate", sanitizeDouble(count.getCount() * 1000.0) / periodInMillis));
} else if (tuple.val instanceof GaugeMetric) {
GaugeMetric gauge = (GaugeMetric) tuple.val;
ObjectNode valueFields = jsonMapper.createObjectNode();
valueFields.put("average", sanitizeDouble(gauge.getAverage()))
.put("sum", sanitizeDouble(gauge.getSum()))
.put("count", gauge.getCount())
.put("last", sanitizeDouble(gauge.getLast()))
.put("max", sanitizeDouble(gauge.getMax()))
.put("min", sanitizeDouble(gauge.getMin()))
.put("rate", sanitizeDouble((gauge.getCount() * 1000.0) / periodInMillis));
if (gauge.getPercentiles().isPresent()) {
for (Tuple2 prefixAndValue : gauge.getPercentiles().get()) {
valueFields.put(prefixAndValue.first + "percentile", sanitizeDouble(prefixAndValue.second));
}
}
jsonTuple.set("values", valueFields);
} else {
throw new UnsupportedOperationException(tuple.val.getClass().getName());
}
if (tuple.dim != null) {
Iterator> it = tuple.dim.iterator();
if (it.hasNext() && includeDimensions) {
ObjectNode jsonDim = jsonMapper.createObjectNode();
while (it.hasNext()) {
Map.Entry entry = it.next();
jsonDim.put(entry.getKey(), entry.getValue());
}
jsonTuple.set("dimensions", jsonDim);
}
}
ArrayNode values = (ArrayNode) jsonMetric.get("values");
if (values == null) {
values = jsonMapper.createArrayNode();
jsonMetric.set("values", values);
}
values.add(jsonTuple);
}
return jsonMetric;
}
private static List collapseMetrics(MetricSnapshot snapshot, String consumer) {
switch (consumer) {
case HEALTH_PATH:
return collapseHealthMetrics(snapshot);
case "all": // deprecated name
case METRICS_PATH:
return flattenAllMetrics(snapshot);
default:
throw new IllegalArgumentException("Unknown consumer '" + consumer + "'.");
}
}
private static List collapseHealthMetrics(MetricSnapshot snapshot) {
Tuple requestsPerSecond = new Tuple(NULL_DIMENSIONS, "requestsPerSecond", null);
Tuple latencySeconds = new Tuple(NULL_DIMENSIONS, "latencySeconds", null);
for (Map.Entry entry : snapshot) {
MetricSet metricSet = entry.getValue();
MetricValue val = metricSet.get("serverTotalSuccessfulResponseLatency");
if (val instanceof GaugeMetric) {
GaugeMetric gauge = (GaugeMetric)val;
latencySeconds.add(GaugeMetric.newInstance(gauge.getLast() / 1000,
gauge.getMax() / 1000,
gauge.getMin() / 1000,
gauge.getSum() / 1000,
gauge.getCount()));
}
requestsPerSecond.add(metricSet.get("serverNumSuccessfulResponses"));
}
List lst = new ArrayList<>();
if (requestsPerSecond.val != null) {
lst.add(requestsPerSecond);
}
if (latencySeconds.val != null) {
lst.add(latencySeconds);
}
return lst;
}
/** Produces a flat list of metric entries from a snapshot (which organizes metrics by dimensions) */
static List flattenAllMetrics(MetricSnapshot snapshot) {
List metrics = new ArrayList<>();
for (Map.Entry snapshotEntry : snapshot) {
for (Map.Entry metricSetEntry : snapshotEntry.getValue()) {
metrics.add(new Tuple(snapshotEntry.getKey(), metricSetEntry.getKey(), metricSetEntry.getValue()));
}
}
return metrics;
}
private static byte[] toPrettyString(JsonNode resources) throws JsonProcessingException {
return jsonMapper.writerWithDefaultPrettyPrinter()
.writeValueAsString(resources)
.getBytes();
}
static class Tuple {
final MetricDimensions dim;
final String key;
MetricValue val;
Tuple(MetricDimensions dim, String key, MetricValue val) {
this.dim = dim;
this.key = key;
this.val = val;
}
void add(MetricValue val) {
if (val == null) {
return;
}
if (this.val == null) {
this.val = val;
} else {
this.val.add(val);
}
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy