com.yahoo.container.jdisc.state.StateHandler Maven / Gradle / Ivy
// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
package com.yahoo.container.jdisc.state;
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 com.yahoo.metrics.MetricsPresentationConfig;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.net.URI;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.io.PrintStream;
import java.io.ByteArrayOutputStream;
/**
* 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 {
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 snapshotPreprocessor;
@Inject
public StateHandler(StateMonitor monitor, Timer timer, ApplicationMetadataConfig config,
ComponentRegistry preprocessors, MetricsPresentationConfig presentation) {
this.monitor = monitor;
this.timer = timer;
this.config = buildConfigOutput(config);
List allPreprocessors = preprocessors.allComponents();
if (presentation.slidingwindow() && allPreprocessors.size() > 0) {
snapshotPreprocessor = allPreprocessors.get(0);
} else {
snapshotPreprocessor = null;
}
}
@Override
public ContentChannel handleRequest(final 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();
JSONArray linkList = new JSONArray();
for (String api : new String[] {METRICS_PATH, CONFIG_GENERATION_PATH, HEALTH_PATH, VERSION_PATH}) {
JSONObject resource = new JSONObject();
resource.put("url", uriBase + "/" + api);
linkList.put(resource);
}
return new JSONObjectWithLegibleException()
.put("resources", linkList)
.toString(4).getBytes(StandardCharsets.UTF_8);
} catch (JSONException 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 new JSONObjectWithLegibleException()
.put(CONFIG_GENERATION_PATH, new JSONObjectWithLegibleException()
.put("generation", config.generation())
.put("container", new JSONObjectWithLegibleException()
.put("generation", config.generation())))
.toString(4).getBytes(StandardCharsets.UTF_8);
} catch (JSONException e) {
throw new RuntimeException("Bad JSON construction.", e);
}
}
private static byte[] buildVersionOutput() {
try {
return new JSONObjectWithLegibleException()
.put("version", Vtag.currentVersion)
.toString(4).getBytes(StandardCharsets.UTF_8);
} catch (JSONException e) {
throw new RuntimeException("Bad JSON construction.", e);
}
}
private byte[] buildMetricOutput(String consumer) {
try {
return buildJsonForConsumer(consumer).toString(4).getBytes(StandardCharsets.UTF_8);
} catch (JSONException e) {
throw new RuntimeException("Bad JSON construction.", e);
}
}
private byte[] buildHistogramsOutput() {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
if (snapshotPreprocessor != null) {
snapshotPreprocessor.histogram(new PrintStream(baos));
}
return baos.toByteArray();
}
private JSONObjectWithLegibleException buildJsonForConsumer(String consumer) throws JSONException {
JSONObjectWithLegibleException ret = new JSONObjectWithLegibleException();
ret.put("time", timer.currentTimeMillis());
ret.put("status", new JSONObjectWithLegibleException().put("code", "up"));
ret.put(METRICS_PATH, buildJsonForSnapshot(consumer, getSnapshot()));
return ret;
}
private MetricSnapshot getSnapshot() {
if (snapshotPreprocessor == null) {
return monitor.snapshot();
} else {
return snapshotPreprocessor.latestSnapshot();
}
}
private JSONObjectWithLegibleException buildJsonForSnapshot(String consumer, MetricSnapshot metricSnapshot) throws JSONException {
if (metricSnapshot == null) {
return new JSONObjectWithLegibleException();
}
JSONObjectWithLegibleException jsonMetric = new JSONObjectWithLegibleException();
jsonMetric.put("snapshot", new JSONObjectWithLegibleException()
.put("from", metricSnapshot.getFromTime(TimeUnit.MILLISECONDS) / 1000.0)
.put("to", 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)) {
JSONObjectWithLegibleException jsonTuple = new JSONObjectWithLegibleException();
jsonTuple.put("name", tuple.key);
if (tuple.val instanceof CountMetric) {
CountMetric count = (CountMetric)tuple.val;
jsonTuple.put("values", new JSONObjectWithLegibleException()
.put("count", count.getCount())
.put("rate", (count.getCount() * 1000.0) / periodInMillis));
} else if (tuple.val instanceof GaugeMetric) {
GaugeMetric gauge = (GaugeMetric) tuple.val;
JSONObjectWithLegibleException valueFields = new JSONObjectWithLegibleException();
valueFields.put("average", gauge.getAverage())
.put("count", gauge.getCount())
.put("last", gauge.getLast())
.put("max", gauge.getMax())
.put("min", gauge.getMin())
.put("rate", (gauge.getCount() * 1000.0) / periodInMillis);
if (gauge.getPercentiles().isPresent()) {
for (Tuple2 prefixAndValue : gauge.getPercentiles().get()) {
valueFields.put(prefixAndValue.first + "percentile", prefixAndValue.second.doubleValue());
}
}
jsonTuple.put("values", valueFields);
} else {
throw new UnsupportedOperationException(tuple.val.getClass().getName());
}
Iterator> it = tuple.dim.iterator();
if (it.hasNext() && includeDimensions) {
JSONObjectWithLegibleException jsonDim = new JSONObjectWithLegibleException();
while (it.hasNext()) {
Map.Entry entry = it.next();
jsonDim.put(entry.getKey(), entry.getValue());
}
jsonTuple.put("dimensions", jsonDim);
}
jsonMetric.append("values", 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) */
private 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 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);
}
}
}
static class JSONObjectWithLegibleException extends JSONObject {
@Override
public JSONObject put(String s, boolean b) {
try {
return super.put(s, b);
} catch (JSONException e) {
throw new RuntimeException(getErrorMessage(s, b, e), e);
}
}
@Override
public JSONObject put(String s, double v) {
try {
Double guardedVal = (((Double)v).isNaN() || ((Double)v).isInfinite()) ?
0.0 : v;
return super.put(s, guardedVal);
} catch (JSONException e) {
throw new RuntimeException(getErrorMessage(s, v, e), e);
}
}
@Override
public JSONObject put(String s, int i) {
try {
return super.put(s, i);
} catch (JSONException e) {
throw new RuntimeException(getErrorMessage(s, i, e), e);
}
}
@Override
public JSONObject put(String s, long l) {
try {
return super.put(s, l);
} catch (JSONException e) {
throw new RuntimeException(getErrorMessage(s, l, e), e);
}
}
@Override
public JSONObject put(String s, Collection collection) {
try {
return super.put(s, collection);
} catch (JSONException e) {
throw new RuntimeException(getErrorMessage(s, collection, e), e);
}
}
@Override
public JSONObject put(String s, Map map) {
try {
return super.put(s, map);
} catch (JSONException e) {
throw new RuntimeException(getErrorMessage(s, map, e), e);
}
}
@Override
public JSONObject put(String s, Object o) {
try {
return super.put(s, o);
} catch (JSONException e) {
throw new RuntimeException(getErrorMessage(s, o, e), e);
}
}
private String getErrorMessage(String key, Object value, JSONException e) {
return "Trying to add invalid JSON object with key '" + key +
"' and value '" + value +
"' - " + e.getMessage();
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy