diff options
author | gjoranv <gv@verizonmedia.com> | 2019-07-08 10:21:43 +0200 |
---|---|---|
committer | gjoranv <gv@verizonmedia.com> | 2019-07-09 11:22:56 +0200 |
commit | 447357bd91208cb3d672850412e11d7bfc8ba009 (patch) | |
tree | e305a638516606de1b0e501c98546391314681ba /metrics-proxy/src/main | |
parent | 262be1a425e21e4f5c149f1d3162f4fdaed372e7 (diff) |
Implement the /metrics/v1 rest api.
- Json errors are now handled in ErrorResponse instead of
JsonRenderingException
Diffstat (limited to 'metrics-proxy/src/main')
6 files changed, 231 insertions, 129 deletions
diff --git a/metrics-proxy/src/main/java/ai/vespa/metricsproxy/http/ErrorResponse.java b/metrics-proxy/src/main/java/ai/vespa/metricsproxy/http/ErrorResponse.java new file mode 100644 index 00000000000..679bae84f8e --- /dev/null +++ b/metrics-proxy/src/main/java/ai/vespa/metricsproxy/http/ErrorResponse.java @@ -0,0 +1,33 @@ +/* + * Copyright 2019 Oath Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. + */ + +package ai.vespa.metricsproxy.http; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; + +import java.util.Map; +import java.util.logging.Logger; + +import static java.util.logging.Level.WARNING; + +/** + * @author gjoranv + */ +class ErrorResponse extends JsonResponse { + private static Logger log = Logger.getLogger(ErrorResponse.class.getName()); + + ErrorResponse(int code, String message) { + super(code, asErrorJson(message)); + } + + static String asErrorJson(String message) { + try { + return new ObjectMapper().writeValueAsString(Map.of("error", message)); + } catch (JsonProcessingException e) { + log.log(WARNING, "Could not encode error message to json:", e); + return "Could not encode error message to json, check the log for details."; + } + } +} diff --git a/metrics-proxy/src/main/java/ai/vespa/metricsproxy/http/GenericMetricsHandler.java b/metrics-proxy/src/main/java/ai/vespa/metricsproxy/http/GenericMetricsHandler.java deleted file mode 100644 index f61a96917a9..00000000000 --- a/metrics-proxy/src/main/java/ai/vespa/metricsproxy/http/GenericMetricsHandler.java +++ /dev/null @@ -1,100 +0,0 @@ -/* - * Copyright 2019 Oath Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. - */ - -package ai.vespa.metricsproxy.http; - -import ai.vespa.metricsproxy.core.MetricsConsumers; -import ai.vespa.metricsproxy.core.MetricsManager; -import ai.vespa.metricsproxy.metric.model.ConsumerId; -import ai.vespa.metricsproxy.metric.model.MetricsPacket; -import ai.vespa.metricsproxy.metric.model.json.JsonRenderingException; -import ai.vespa.metricsproxy.service.VespaServices; -import com.google.inject.Inject; -import com.yahoo.container.jdisc.HttpRequest; -import com.yahoo.container.jdisc.HttpResponse; -import com.yahoo.container.jdisc.ThreadedHttpRequestHandler; - -import java.io.IOException; -import java.io.OutputStream; -import java.nio.charset.Charset; -import java.time.Instant; -import java.util.List; -import java.util.concurrent.Executor; -import java.util.logging.Logger; -import java.util.stream.Collectors; - -import static ai.vespa.metricsproxy.metric.model.ConsumerId.toConsumerId; -import static ai.vespa.metricsproxy.metric.model.json.GenericJsonUtil.toGenericJsonModel; - -/** - * Http handler that exposes the generic metrics format. - * - * @author gjoranv - */ -public class GenericMetricsHandler extends ThreadedHttpRequestHandler { - private static final Logger log = Logger.getLogger(GenericMetricsHandler.class.getName()); - - public static final ConsumerId DEFAULT_PUBLIC_CONSUMER_ID = toConsumerId("default"); - - private final MetricsConsumers metricsConsumers; - private final MetricsManager metricsManager; - private final VespaServices vespaServices; - - @Inject - public GenericMetricsHandler(Executor executor, - MetricsManager metricsManager, - VespaServices vespaServices, - MetricsConsumers metricsConsumers) { - super(executor); - this.metricsConsumers = metricsConsumers; - this.metricsManager = metricsManager; - this.vespaServices = vespaServices; - } - - @Override - public HttpResponse handle(HttpRequest request) { - try { - ConsumerId consumer = getConsumerOrDefault(request.getProperty("consumer")); - - List<MetricsPacket> metrics = metricsManager.getMetrics(vespaServices.getVespaServices(), Instant.now()) - .stream() - .filter(metricsPacket -> metricsPacket.consumers().contains(consumer)) - .collect(Collectors.toList()); - return new Response(200, toGenericJsonModel(metrics).serialize()); - } catch (JsonRenderingException e) { - return new Response(500, e.getMessageAsJson()); - } - } - - private ConsumerId getConsumerOrDefault(String consumer) { - if (consumer == null) return DEFAULT_PUBLIC_CONSUMER_ID; - - ConsumerId consumerId = toConsumerId(consumer); - if (! metricsConsumers.getAllConsumers().contains(consumerId)) { - log.info("No consumer with id '" + consumer + "' - using the default consumer instead."); - return DEFAULT_PUBLIC_CONSUMER_ID; - } - return consumerId; - } - - private static class Response extends HttpResponse { - private final byte[] data; - - Response(int code, String data) { - super(code); - this.data = data.getBytes(Charset.forName(DEFAULT_CHARACTER_ENCODING)); - } - - @Override - public String getContentType() { - return "application/json"; - } - - @Override - public void render(OutputStream outputStream) throws IOException { - outputStream.write(data); - } - } - -} diff --git a/metrics-proxy/src/main/java/ai/vespa/metricsproxy/http/JsonResponse.java b/metrics-proxy/src/main/java/ai/vespa/metricsproxy/http/JsonResponse.java new file mode 100644 index 00000000000..758a043a823 --- /dev/null +++ b/metrics-proxy/src/main/java/ai/vespa/metricsproxy/http/JsonResponse.java @@ -0,0 +1,33 @@ +/* + * Copyright 2019 Oath Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. + */ + +package ai.vespa.metricsproxy.http; + +import com.yahoo.container.jdisc.HttpResponse; + +import java.io.IOException; +import java.io.OutputStream; +import java.nio.charset.Charset; + +/** + * @author gjoranv + */ +class JsonResponse extends HttpResponse { + private final byte[] data; + + JsonResponse(int code, String data) { + super(code); + this.data = data.getBytes(Charset.forName(DEFAULT_CHARACTER_ENCODING)); + } + + @Override + public String getContentType() { + return "application/json"; + } + + @Override + public void render(OutputStream outputStream) throws IOException { + outputStream.write(data); + } +} diff --git a/metrics-proxy/src/main/java/ai/vespa/metricsproxy/http/MetricsHandler.java b/metrics-proxy/src/main/java/ai/vespa/metricsproxy/http/MetricsHandler.java new file mode 100644 index 00000000000..1d7206f177d --- /dev/null +++ b/metrics-proxy/src/main/java/ai/vespa/metricsproxy/http/MetricsHandler.java @@ -0,0 +1,99 @@ +/* + * Copyright 2019 Oath Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. + */ + +package ai.vespa.metricsproxy.http; + +import ai.vespa.metricsproxy.core.MetricsConsumers; +import ai.vespa.metricsproxy.core.MetricsManager; +import ai.vespa.metricsproxy.metric.model.json.JsonRenderingException; +import ai.vespa.metricsproxy.service.VespaServices; +import com.google.inject.Inject; +import com.yahoo.container.jdisc.HttpRequest; +import com.yahoo.container.jdisc.HttpResponse; +import com.yahoo.container.jdisc.ThreadedHttpRequestHandler; +import com.yahoo.restapi.Path; +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import java.net.URI; +import java.util.concurrent.Executor; + +import static com.yahoo.jdisc.Response.Status.INTERNAL_SERVER_ERROR; +import static com.yahoo.jdisc.Response.Status.METHOD_NOT_ALLOWED; +import static com.yahoo.jdisc.Response.Status.NOT_FOUND; +import static com.yahoo.jdisc.Response.Status.OK; +import static com.yahoo.jdisc.http.HttpRequest.Method.GET; + +/** + * Http handler for the metrics/v1 rest api. + * + * @author gjoranv + */ +public class MetricsHandler extends ThreadedHttpRequestHandler { + + static final String V1_PATH = "/metrics/v1"; + static final String VALUES_PATH = V1_PATH + "/values"; + + private final ValuesFetcher valuesFetcher; + + @Inject + public MetricsHandler(Executor executor, + MetricsManager metricsManager, + VespaServices vespaServices, + MetricsConsumers metricsConsumers) { + super(executor); + valuesFetcher = new ValuesFetcher(metricsManager, vespaServices, metricsConsumers); + } + + @Override + public HttpResponse handle(HttpRequest request) { + if (request.getMethod() != GET) return new JsonResponse(METHOD_NOT_ALLOWED, "Only GET is supported"); + + Path path = new Path(request.getUri()); + + if (path.matches(V1_PATH)) return v1Response(request.getUri()); + if (path.matches(VALUES_PATH)) return valuesResponse(request); + + return new ErrorResponse(NOT_FOUND, "No content at given path"); + } + + private JsonResponse v1Response(URI requestUri) { + try { + return new JsonResponse(OK, v1Content(requestUri)); + } catch (JSONException e) { + log.warning("Bad JSON construction in " + V1_PATH + " response: " + e.getMessage()); + return new ErrorResponse(INTERNAL_SERVER_ERROR, + "An error occurred, please try path '" + VALUES_PATH + "'"); + } + } + + private JsonResponse valuesResponse(HttpRequest request) { + try { + return new JsonResponse(OK, valuesFetcher.fetch(request.getProperty("consumer"))); + } catch (JsonRenderingException e) { + return new ErrorResponse(INTERNAL_SERVER_ERROR, e.getMessage()); + } + } + + // TODO: Use jackson with a "Resources" class instead of JSONObject + private String v1Content(URI requestUri) throws JSONException { + int port = requestUri.getPort(); + String host = requestUri.getHost(); + StringBuilder base = new StringBuilder("http://"); + base.append(host); + if (port >= 0) { + base.append(":").append(port); + } + String uriBase = base.toString(); + JSONArray linkList = new JSONArray(); + for (String api : new String[] {VALUES_PATH}) { + JSONObject resource = new JSONObject(); + resource.put("url", uriBase + api); + linkList.put(resource); + } + return new JSONObject().put("resources", linkList).toString(4); + } + +} diff --git a/metrics-proxy/src/main/java/ai/vespa/metricsproxy/http/ValuesFetcher.java b/metrics-proxy/src/main/java/ai/vespa/metricsproxy/http/ValuesFetcher.java new file mode 100644 index 00000000000..4686d9c1751 --- /dev/null +++ b/metrics-proxy/src/main/java/ai/vespa/metricsproxy/http/ValuesFetcher.java @@ -0,0 +1,65 @@ +/* + * Copyright 2019 Oath Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. + */ + +package ai.vespa.metricsproxy.http; + +import ai.vespa.metricsproxy.core.MetricsConsumers; +import ai.vespa.metricsproxy.core.MetricsManager; +import ai.vespa.metricsproxy.metric.model.ConsumerId; +import ai.vespa.metricsproxy.metric.model.MetricsPacket; +import ai.vespa.metricsproxy.metric.model.json.JsonRenderingException; +import ai.vespa.metricsproxy.service.VespaServices; + +import java.time.Instant; +import java.util.List; +import java.util.logging.Logger; +import java.util.stream.Collectors; + +import static ai.vespa.metricsproxy.metric.model.ConsumerId.toConsumerId; +import static ai.vespa.metricsproxy.metric.model.json.GenericJsonUtil.toGenericJsonModel; + +/** + * Generates metrics values in json format for the metrics/v1 rest api. + * + * @author gjoranv + */ +public class ValuesFetcher { + private static final Logger log = Logger.getLogger(ValuesFetcher.class.getName()); + + public static final ConsumerId DEFAULT_PUBLIC_CONSUMER_ID = toConsumerId("default"); + + private final MetricsManager metricsManager; + private final VespaServices vespaServices; + private final MetricsConsumers metricsConsumers; + + ValuesFetcher(MetricsManager metricsManager, + VespaServices vespaServices, + MetricsConsumers metricsConsumers) { + this.metricsManager = metricsManager; + this.vespaServices = vespaServices; + this.metricsConsumers = metricsConsumers; + } + + public String fetch(String requestedConsumer) throws JsonRenderingException { + ConsumerId consumer = getConsumerOrDefault(requestedConsumer); + + List<MetricsPacket> metrics = metricsManager.getMetrics(vespaServices.getVespaServices(), Instant.now()) + .stream() + .filter(metricsPacket -> metricsPacket.consumers().contains(consumer)) + .collect(Collectors.toList()); + return toGenericJsonModel(metrics).serialize(); + } + + private ConsumerId getConsumerOrDefault(String consumer) { + if (consumer == null) return DEFAULT_PUBLIC_CONSUMER_ID; + + ConsumerId consumerId = toConsumerId(consumer); + if (! metricsConsumers.getAllConsumers().contains(consumerId)) { + log.info("No consumer with id '" + consumer + "' - using the default consumer instead."); + return DEFAULT_PUBLIC_CONSUMER_ID; + } + return consumerId; + } + +} diff --git a/metrics-proxy/src/main/java/ai/vespa/metricsproxy/metric/model/json/JsonRenderingException.java b/metrics-proxy/src/main/java/ai/vespa/metricsproxy/metric/model/json/JsonRenderingException.java index 4c1d42d75ee..02292cea164 100644 --- a/metrics-proxy/src/main/java/ai/vespa/metricsproxy/metric/model/json/JsonRenderingException.java +++ b/metrics-proxy/src/main/java/ai/vespa/metricsproxy/metric/model/json/JsonRenderingException.java @@ -4,43 +4,15 @@ package ai.vespa.metricsproxy.metric.model.json; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.ObjectMapper; - -import java.util.Map; -import java.util.logging.Logger; - -import static java.util.logging.Level.WARNING; - /** - * An exception to be thrown upon errors in json rendering, and that can deliver its message wrapped - * in an "error" json object. + * An unchecked exception to be thrown upon errors in json rendering. * * @author gjoranv */ public class JsonRenderingException extends RuntimeException { - private static Logger log = Logger.getLogger(JsonRenderingException.class.getName()); - JsonRenderingException(String message) { super(message); } - /** - * Returns the message wrapped in an "error" json object. In the unlikely case that json rendering of the - * error message fails, a plain text string will be returned instead. - */ - public String getMessageAsJson() { - return wrap(getMessage()); - } - - private static String wrap(String message) { - try { - return new ObjectMapper().writeValueAsString(Map.of("error", message)); - } catch (JsonProcessingException e) { - log.log(WARNING, "Could not encode error message to json:", e); - return "Could not encode error message to json, check the log for details."; - } - } - } |