summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorOla Aunrønning <olaa@verizonmedia.com>2019-08-30 11:00:10 +0200
committerOla Aunrønning <olaa@verizonmedia.com>2019-08-30 11:00:10 +0200
commit27de6084e585507dd0f253f4d20fcca9a9a325d4 (patch)
tree1ec629b57f4c9feea7924a9a606fd334e42cfbc2
parent6d1f6a5589ae52e1c58fd9c28927562dbda3f468 (diff)
parentc232a31df58fd153ff08b5a209556f4151861558 (diff)
Merge branch 'gjoranv/prometheus-handler' of github.com:vespa-engine/vespa into metrics-proxy-gather-node-metrics
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/admin/metricsproxy/MetricsProxyContainerCluster.java15
-rw-r--r--config-model/src/test/java/com/yahoo/vespa/model/admin/metricsproxy/MetricsProxyContainerClusterTest.java20
-rw-r--r--metrics-proxy/pom.xml8
-rw-r--r--metrics-proxy/src/main/java/ai/vespa/metricsproxy/http/ErrorResponse.java4
-rw-r--r--metrics-proxy/src/main/java/ai/vespa/metricsproxy/http/JsonResponse.java4
-rw-r--r--metrics-proxy/src/main/java/ai/vespa/metricsproxy/http/MetricsHandler.java45
-rw-r--r--metrics-proxy/src/main/java/ai/vespa/metricsproxy/http/RestApiUtil.java55
-rw-r--r--metrics-proxy/src/main/java/ai/vespa/metricsproxy/http/TextResponse.java31
-rw-r--r--metrics-proxy/src/main/java/ai/vespa/metricsproxy/http/ValuesFetcher.java7
-rw-r--r--metrics-proxy/src/main/java/ai/vespa/metricsproxy/http/prometheus/PrometheusHandler.java73
-rw-r--r--metrics-proxy/src/main/java/ai/vespa/metricsproxy/metric/model/prometheus/PrometheusModel.java52
-rw-r--r--metrics-proxy/src/main/java/ai/vespa/metricsproxy/metric/model/prometheus/PrometheusRenderingException.java16
-rw-r--r--metrics-proxy/src/main/java/ai/vespa/metricsproxy/metric/model/prometheus/PrometheusUtil.java78
-rw-r--r--metrics-proxy/src/test/java/ai/vespa/metricsproxy/http/HttpHandlerTestBase.java100
-rw-r--r--metrics-proxy/src/test/java/ai/vespa/metricsproxy/http/MetricsHandlerTest.java91
-rw-r--r--metrics-proxy/src/test/java/ai/vespa/metricsproxy/http/prometheus/PrometheusHandlerTest.java106
-rw-r--r--metrics-proxy/src/test/java/ai/vespa/metricsproxy/service/DummyService.java2
-rw-r--r--parent/pom.xml11
18 files changed, 586 insertions, 132 deletions
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/admin/metricsproxy/MetricsProxyContainerCluster.java b/config-model/src/main/java/com/yahoo/vespa/model/admin/metricsproxy/MetricsProxyContainerCluster.java
index 42c49a7a25c..94ae5c07d8b 100644
--- a/config-model/src/main/java/com/yahoo/vespa/model/admin/metricsproxy/MetricsProxyContainerCluster.java
+++ b/config-model/src/main/java/com/yahoo/vespa/model/admin/metricsproxy/MetricsProxyContainerCluster.java
@@ -10,6 +10,7 @@ import ai.vespa.metricsproxy.core.MetricsManager;
import ai.vespa.metricsproxy.core.MonitoringConfig;
import ai.vespa.metricsproxy.core.VespaMetrics;
import ai.vespa.metricsproxy.http.MetricsHandler;
+import ai.vespa.metricsproxy.http.prometheus.PrometheusHandler;
import ai.vespa.metricsproxy.metric.ExternalMetrics;
import ai.vespa.metricsproxy.metric.dimensions.ApplicationDimensions;
import ai.vespa.metricsproxy.metric.dimensions.ApplicationDimensionsConfig;
@@ -23,6 +24,7 @@ import com.yahoo.config.model.producer.AbstractConfigProducerRoot;
import com.yahoo.config.provision.ApplicationId;
import com.yahoo.config.provision.Zone;
import com.yahoo.container.handler.ThreadpoolConfig;
+import com.yahoo.container.jdisc.ThreadedHttpRequestHandler;
import com.yahoo.osgi.provider.model.ComponentModel;
import com.yahoo.search.config.QrStartConfig;
import com.yahoo.vespa.model.VespaModel;
@@ -74,8 +76,6 @@ public class MetricsProxyContainerCluster extends ContainerCluster<MetricsProxyC
static final Path METRICS_PROXY_BUNDLE_FILE = absoluteBundlePath((Paths.get(METRICS_PROXY_NAME + JAR_WITH_DEPS.suffix)));
static final String METRICS_PROXY_BUNDLE_NAME = "com.yahoo.vespa." + METRICS_PROXY_NAME;
- private static final String METRICS_HANDLER_BINDING = "/metrics/v1";
-
static final class AppDimensionNames {
static final String ZONE = "zone";
static final String APPLICATION_ID = "applicationId"; // tenant.app.instance
@@ -99,7 +99,6 @@ public class MetricsProxyContainerCluster extends ContainerCluster<MetricsProxyC
addPlatformBundle(METRICS_PROXY_BUNDLE_FILE);
addClusterComponents();
- addGenericMetricsHandler();
}
private void addClusterComponents() {
@@ -111,13 +110,15 @@ public class MetricsProxyContainerCluster extends ContainerCluster<MetricsProxyC
addMetricsProxyComponent(RpcServer.class);
addMetricsProxyComponent(SystemPollerProvider.class);
addMetricsProxyComponent(VespaMetrics.class);
+ addHttpHandler(MetricsHandler.class, MetricsHandler.V1_PATH);
+ addHttpHandler(PrometheusHandler.class, PrometheusHandler.V1_PATH);
}
- private void addGenericMetricsHandler() {
+ private void addHttpHandler(Class<? extends ThreadedHttpRequestHandler> clazz, String bindingPath) {
Handler<AbstractConfigProducer<?>> metricsHandler = new Handler<>(
- new ComponentModel(MetricsHandler.class.getName(), null, METRICS_PROXY_BUNDLE_NAME, null));
- metricsHandler.addServerBindings("http://*" + METRICS_HANDLER_BINDING,
- "http://*" + METRICS_HANDLER_BINDING + "/*");
+ new ComponentModel(clazz.getName(), null, METRICS_PROXY_BUNDLE_NAME, null));
+ metricsHandler.addServerBindings("http://*" + bindingPath,
+ "http://*" + bindingPath + "/*");
addComponent(metricsHandler);
}
diff --git a/config-model/src/test/java/com/yahoo/vespa/model/admin/metricsproxy/MetricsProxyContainerClusterTest.java b/config-model/src/test/java/com/yahoo/vespa/model/admin/metricsproxy/MetricsProxyContainerClusterTest.java
index 48c3e9fdda9..db29dbba306 100644
--- a/config-model/src/test/java/com/yahoo/vespa/model/admin/metricsproxy/MetricsProxyContainerClusterTest.java
+++ b/config-model/src/test/java/com/yahoo/vespa/model/admin/metricsproxy/MetricsProxyContainerClusterTest.java
@@ -5,7 +5,10 @@
package com.yahoo.vespa.model.admin.metricsproxy;
import ai.vespa.metricsproxy.core.ConsumersConfig;
+import ai.vespa.metricsproxy.http.MetricsHandler;
+import ai.vespa.metricsproxy.http.prometheus.PrometheusHandler;
import ai.vespa.metricsproxy.metric.dimensions.ApplicationDimensionsConfig;
+import com.yahoo.component.ComponentSpecification;
import com.yahoo.config.model.test.MockApplicationPackage;
import com.yahoo.config.provision.Zone;
import com.yahoo.container.BundlesConfig;
@@ -15,10 +18,14 @@ import com.yahoo.vespa.model.VespaModel;
import com.yahoo.vespa.model.admin.metricsproxy.MetricsProxyContainerCluster.AppDimensionNames;
import com.yahoo.vespa.model.admin.monitoring.Metric;
import com.yahoo.vespa.model.admin.monitoring.MetricSet;
+import com.yahoo.vespa.model.container.component.Component;
+import com.yahoo.vespa.model.container.component.Handler;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
+import java.util.Collection;
+
import static com.yahoo.vespa.model.admin.metricsproxy.MetricsProxyContainerCluster.METRICS_PROXY_BUNDLE_FILE;
import static com.yahoo.vespa.model.admin.metricsproxy.MetricsProxyContainerCluster.zoneString;
import static com.yahoo.vespa.model.admin.metricsproxy.MetricsProxyModelTester.CLUSTER_CONFIG_ID;
@@ -36,13 +43,15 @@ import static com.yahoo.vespa.model.admin.metricsproxy.MetricsProxyModelTester.g
import static com.yahoo.vespa.model.admin.metricsproxy.MetricsProxyModelTester.getQrStartConfig;
import static com.yahoo.vespa.model.admin.monitoring.DefaultPublicConsumer.DEFAULT_PUBLIC_CONSUMER_ID;
import static com.yahoo.vespa.model.admin.monitoring.DefaultPublicMetrics.defaultPublicMetricSet;
-import static com.yahoo.vespa.model.admin.monitoring.VespaMetricsConsumer.VESPA_CONSUMER_ID;
import static com.yahoo.vespa.model.admin.monitoring.DefaultVespaMetrics.defaultVespaMetricSet;
import static com.yahoo.vespa.model.admin.monitoring.NetworkMetrics.networkMetricSet;
import static com.yahoo.vespa.model.admin.monitoring.SystemMetrics.systemMetricSet;
import static com.yahoo.vespa.model.admin.monitoring.VespaMetricSet.vespaMetricSet;
+import static com.yahoo.vespa.model.admin.monitoring.VespaMetricsConsumer.VESPA_CONSUMER_ID;
import static java.util.Collections.singleton;
+import static java.util.stream.Collectors.toList;
import static org.hamcrest.CoreMatchers.endsWith;
+import static org.hamcrest.CoreMatchers.hasItem;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertThat;
@@ -90,6 +99,15 @@ public class MetricsProxyContainerClusterTest {
assertFalse(config.jvm().verbosegc());
}
+ @Test
+ public void http_handlers_are_set_up() {
+ VespaModel model = getModel(servicesWithAdminOnly(), self_hosted);
+ Collection<Handler<?>> handlers = model.getAdmin().getMetricsProxyCluster().getHandlers();
+ Collection<ComponentSpecification> handlerClasses = handlers.stream().map(Component::getClassId).collect(toList());
+
+ assertThat(handlerClasses, hasItem(ComponentSpecification.fromString(MetricsHandler.class.getName())));
+ assertThat(handlerClasses, hasItem(ComponentSpecification.fromString(PrometheusHandler.class.getName())));
+ }
@Test
public void default_public_consumer_is_set_up_for_self_hosted() {
diff --git a/metrics-proxy/pom.xml b/metrics-proxy/pom.xml
index 29ab5bc3f91..ff314147090 100644
--- a/metrics-proxy/pom.xml
+++ b/metrics-proxy/pom.xml
@@ -116,6 +116,14 @@
<version>${project.version}</version>
</dependency>
<dependency>
+ <groupId>io.prometheus</groupId>
+ <artifactId>simpleclient</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>io.prometheus</groupId>
+ <artifactId>simpleclient_common</artifactId>
+ </dependency>
+ <dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
</dependency>
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
index daa69191506..d3296331a6a 100644
--- a/metrics-proxy/src/main/java/ai/vespa/metricsproxy/http/ErrorResponse.java
+++ b/metrics-proxy/src/main/java/ai/vespa/metricsproxy/http/ErrorResponse.java
@@ -15,12 +15,12 @@ import static java.util.logging.Level.WARNING;
/**
* @author gjoranv
*/
-class ErrorResponse extends JsonResponse {
+public class ErrorResponse extends JsonResponse {
private static Logger log = Logger.getLogger(ErrorResponse.class.getName());
private static ObjectMapper objectMapper = new ObjectMapper();
- ErrorResponse(int code, String message) {
+ public ErrorResponse(int code, String message) {
super(code, asErrorJson(message));
}
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
index 758a043a823..9f31e9356e8 100644
--- a/metrics-proxy/src/main/java/ai/vespa/metricsproxy/http/JsonResponse.java
+++ b/metrics-proxy/src/main/java/ai/vespa/metricsproxy/http/JsonResponse.java
@@ -13,10 +13,10 @@ import java.nio.charset.Charset;
/**
* @author gjoranv
*/
-class JsonResponse extends HttpResponse {
+public class JsonResponse extends HttpResponse {
private final byte[] data;
- JsonResponse(int code, String data) {
+ public JsonResponse(int code, String data) {
super(code);
this.data = data.getBytes(Charset.forName(DEFAULT_CHARACTER_ENCODING));
}
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
index e56c54e39b3..1eb3429f6d4 100644
--- a/metrics-proxy/src/main/java/ai/vespa/metricsproxy/http/MetricsHandler.java
+++ b/metrics-proxy/src/main/java/ai/vespa/metricsproxy/http/MetricsHandler.java
@@ -6,6 +6,7 @@ package ai.vespa.metricsproxy.http;
import ai.vespa.metricsproxy.core.MetricsConsumers;
import ai.vespa.metricsproxy.core.MetricsManager;
+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;
@@ -13,20 +14,17 @@ 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.List;
import java.util.concurrent.Executor;
-import java.util.logging.Level;
+import static ai.vespa.metricsproxy.http.RestApiUtil.resourceListResponse;
+import static ai.vespa.metricsproxy.metric.model.json.GenericJsonUtil.toGenericJsonModel;
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;
-import static java.util.logging.Level.WARNING;
/**
* Http handler for the metrics/v1 rest api.
@@ -35,7 +33,7 @@ import static java.util.logging.Level.WARNING;
*/
public class MetricsHandler extends ThreadedHttpRequestHandler {
- static final String V1_PATH = "/metrics/v1";
+ public static final String V1_PATH = "/metrics/v1";
static final String VALUES_PATH = V1_PATH + "/values";
private final ValuesFetcher valuesFetcher;
@@ -56,46 +54,19 @@ public class MetricsHandler extends ThreadedHttpRequestHandler {
Path path = new Path(request.getUri());
- if (path.matches(V1_PATH)) return v1Response(request.getUri());
+ if (path.matches(V1_PATH)) return resourceListResponse(request.getUri(), List.of(VALUES_PATH));
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.log(WARNING, "Bad JSON construction in " + V1_PATH + " response", e);
- 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")));
+ List<MetricsPacket> metrics = valuesFetcher.fetch(request.getProperty("consumer"));
+ return new JsonResponse(OK, toGenericJsonModel(metrics).serialize());
} 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/RestApiUtil.java b/metrics-proxy/src/main/java/ai/vespa/metricsproxy/http/RestApiUtil.java
new file mode 100644
index 00000000000..9d9256e17e8
--- /dev/null
+++ b/metrics-proxy/src/main/java/ai/vespa/metricsproxy/http/RestApiUtil.java
@@ -0,0 +1,55 @@
+/*
+ * 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 org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.net.URI;
+import java.util.List;
+import java.util.logging.Logger;
+
+import static com.yahoo.jdisc.Response.Status.INTERNAL_SERVER_ERROR;
+import static com.yahoo.jdisc.Response.Status.OK;
+import static java.util.logging.Level.WARNING;
+
+/**
+ * @author gjoranv
+ */
+public class RestApiUtil {
+ private static Logger log = Logger.getLogger(RestApiUtil.class.getName());
+
+
+ public static JsonResponse resourceListResponse(URI requestUri, List<String> resources) {
+ try {
+ return new JsonResponse(OK, RestApiUtil.resourceList(requestUri, resources));
+ } catch (JSONException e) {
+ log.log(WARNING, "Bad JSON construction in generated resource list for " + requestUri.getPath(), e);
+ return new ErrorResponse(INTERNAL_SERVER_ERROR,
+ "An error occurred when generating the list of api resources.");
+ }
+ }
+
+ // TODO: Use jackson with a "Resources" class instead of JSONObject
+ private static String resourceList(URI requestUri, List<String> resources) 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 : resources) {
+ 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/TextResponse.java b/metrics-proxy/src/main/java/ai/vespa/metricsproxy/http/TextResponse.java
new file mode 100644
index 00000000000..7dfc232d253
--- /dev/null
+++ b/metrics-proxy/src/main/java/ai/vespa/metricsproxy/http/TextResponse.java
@@ -0,0 +1,31 @@
+/*
+ * 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 yj-jtakagi
+ * @author gjoranv
+ */
+public class TextResponse extends HttpResponse {
+
+ private final byte[] data;
+
+ public TextResponse(int code, String data) {
+ super(code);
+ this.data = data.getBytes(Charset.forName(DEFAULT_CHARACTER_ENCODING));
+ }
+
+ @Override
+ public void render(OutputStream outputStream) throws IOException {
+ outputStream.write(data);
+ }
+
+}
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
index cd433378aee..34fedfcfa7a 100644
--- a/metrics-proxy/src/main/java/ai/vespa/metricsproxy/http/ValuesFetcher.java
+++ b/metrics-proxy/src/main/java/ai/vespa/metricsproxy/http/ValuesFetcher.java
@@ -34,7 +34,7 @@ public class ValuesFetcher {
private final VespaServices vespaServices;
private final MetricsConsumers metricsConsumers;
- ValuesFetcher(MetricsManager metricsManager,
+ public ValuesFetcher(MetricsManager metricsManager,
VespaServices vespaServices,
MetricsConsumers metricsConsumers) {
this.metricsManager = metricsManager;
@@ -42,14 +42,13 @@ public class ValuesFetcher {
this.metricsConsumers = metricsConsumers;
}
- public String fetch(String requestedConsumer) throws JsonRenderingException {
+ public List<MetricsPacket> fetch(String requestedConsumer) throws JsonRenderingException {
ConsumerId consumer = getConsumerOrDefault(requestedConsumer);
- List<MetricsPacket> metrics = metricsManager.getMetrics(vespaServices.getVespaServices(), Instant.now())
+ return 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) {
diff --git a/metrics-proxy/src/main/java/ai/vespa/metricsproxy/http/prometheus/PrometheusHandler.java b/metrics-proxy/src/main/java/ai/vespa/metricsproxy/http/prometheus/PrometheusHandler.java
new file mode 100644
index 00000000000..f2009280128
--- /dev/null
+++ b/metrics-proxy/src/main/java/ai/vespa/metricsproxy/http/prometheus/PrometheusHandler.java
@@ -0,0 +1,73 @@
+/*
+ * 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.prometheus;
+
+import ai.vespa.metricsproxy.core.MetricsConsumers;
+import ai.vespa.metricsproxy.core.MetricsManager;
+import ai.vespa.metricsproxy.http.ErrorResponse;
+import ai.vespa.metricsproxy.http.JsonResponse;
+import ai.vespa.metricsproxy.http.TextResponse;
+import ai.vespa.metricsproxy.http.ValuesFetcher;
+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 com.yahoo.restapi.Path;
+
+import java.util.List;
+import java.util.concurrent.Executor;
+
+import static ai.vespa.metricsproxy.http.RestApiUtil.resourceListResponse;
+import static ai.vespa.metricsproxy.metric.model.prometheus.PrometheusUtil.toPrometheusModel;
+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;
+
+/**
+ * @author gjoranv
+ */
+public class PrometheusHandler extends ThreadedHttpRequestHandler {
+
+ public static final String V1_PATH = "/prometheus/v1";
+ static final String VALUES_PATH = V1_PATH + "/values";
+
+ private final ValuesFetcher valuesFetcher;
+
+ @Inject
+ public PrometheusHandler(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 resourceListResponse(request.getUri(), List.of(VALUES_PATH));
+ if (path.matches(VALUES_PATH)) return valuesResponse(request);
+
+ return new ErrorResponse(NOT_FOUND, "No content at given path");
+ }
+
+ private TextResponse valuesResponse(HttpRequest request) {
+ try {
+ List<MetricsPacket> metrics = valuesFetcher.fetch(request.getProperty("consumer"));
+ return new TextResponse(OK, toPrometheusModel(metrics).serialize());
+ } catch (JsonRenderingException e) {
+ return new TextResponse(INTERNAL_SERVER_ERROR, e.getMessage());
+ }
+ }
+
+}
diff --git a/metrics-proxy/src/main/java/ai/vespa/metricsproxy/metric/model/prometheus/PrometheusModel.java b/metrics-proxy/src/main/java/ai/vespa/metricsproxy/metric/model/prometheus/PrometheusModel.java
new file mode 100644
index 00000000000..83119a552f1
--- /dev/null
+++ b/metrics-proxy/src/main/java/ai/vespa/metricsproxy/metric/model/prometheus/PrometheusModel.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright 2019 Oath Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+ */
+
+package ai.vespa.metricsproxy.metric.model.prometheus;
+
+import io.prometheus.client.Collector;
+import io.prometheus.client.exporter.common.TextFormat;
+
+import java.io.IOException;
+import java.io.StringWriter;
+import java.util.Enumeration;
+import java.util.Iterator;
+import java.util.List;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+/**
+ * @author yj-jtakagi
+ * @author gjoranv
+ */
+public class PrometheusModel implements Enumeration<Collector.MetricFamilySamples> {
+ private static Logger log = Logger.getLogger(PrometheusModel.class.getName());
+
+ private final Iterator<Collector.MetricFamilySamples> metricFamilySamplesIterator;
+
+ PrometheusModel(List<Collector.MetricFamilySamples> metricFamilySamples) {
+ this.metricFamilySamplesIterator = metricFamilySamples.iterator();
+ }
+
+ @Override
+ public boolean hasMoreElements() {
+ return metricFamilySamplesIterator.hasNext();
+ }
+
+ @Override
+ public Collector.MetricFamilySamples nextElement() {
+ return metricFamilySamplesIterator.next();
+ }
+
+ public String serialize() {
+ var writer = new StringWriter();
+ try {
+ TextFormat.write004(writer, this);
+ } catch (IOException e) {
+ log.log(Level.WARNING, "Got exception when rendering metrics:", e);
+ throw new PrometheusRenderingException("Could not render metrics. Check the log for details.");
+ }
+ return writer.toString();
+ }
+
+}
diff --git a/metrics-proxy/src/main/java/ai/vespa/metricsproxy/metric/model/prometheus/PrometheusRenderingException.java b/metrics-proxy/src/main/java/ai/vespa/metricsproxy/metric/model/prometheus/PrometheusRenderingException.java
new file mode 100644
index 00000000000..68db7e64bfd
--- /dev/null
+++ b/metrics-proxy/src/main/java/ai/vespa/metricsproxy/metric/model/prometheus/PrometheusRenderingException.java
@@ -0,0 +1,16 @@
+/*
+ * Copyright 2019 Oath Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+ */
+
+package ai.vespa.metricsproxy.metric.model.prometheus;
+
+/**
+ * @author gjoranv
+ */
+public class PrometheusRenderingException extends RuntimeException {
+
+ PrometheusRenderingException(String message) {
+ super(message);
+ }
+
+}
diff --git a/metrics-proxy/src/main/java/ai/vespa/metricsproxy/metric/model/prometheus/PrometheusUtil.java b/metrics-proxy/src/main/java/ai/vespa/metricsproxy/metric/model/prometheus/PrometheusUtil.java
new file mode 100644
index 00000000000..d43baa9c9c9
--- /dev/null
+++ b/metrics-proxy/src/main/java/ai/vespa/metricsproxy/metric/model/prometheus/PrometheusUtil.java
@@ -0,0 +1,78 @@
+/*
+ * Copyright 2019 Oath Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+ */
+
+package ai.vespa.metricsproxy.metric.model.prometheus;
+
+import ai.vespa.metricsproxy.metric.model.MetricsPacket;
+import ai.vespa.metricsproxy.metric.model.ServiceId;
+import io.prometheus.client.Collector;
+import io.prometheus.client.Collector.MetricFamilySamples;
+import io.prometheus.client.Collector.MetricFamilySamples.Sample;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.regex.Pattern;
+import java.util.stream.Collectors;
+
+import static java.util.Collections.emptyList;
+import static java.util.Collections.singletonList;
+
+/**
+ * @author yj-jtakagi
+ * @author gjoranv
+ */
+public class PrometheusUtil {
+
+ public static PrometheusModel toPrometheusModel(List<MetricsPacket> metricsPackets) {
+ Map<ServiceId, List<MetricsPacket>> packetsByService = metricsPackets.stream()
+ .collect(Collectors.groupingBy(packet -> packet.service));
+
+ List<MetricFamilySamples> metricFamilySamples = new ArrayList<>(packetsByService.size());
+
+ packetsByService.forEach(((serviceId, packets) -> {
+ Map<String, List<Sample>> samples = new HashMap<>();
+
+ var serviceName = Collector.sanitizeMetricName(serviceId.id);
+ for (var packet : packets) {
+ var dimensions = packet.dimensions();
+ List<String> labels = new ArrayList<>(dimensions.size());
+ List<String> labelValues = new ArrayList<>(dimensions.size());
+ for (var entry : dimensions.entrySet()) {
+ var labelName = Collector.sanitizeMetricName(entry.getKey().id);
+ labels.add(labelName);
+ labelValues.add(entry.getValue());
+ }
+ labels.add("vespa_service");
+ labelValues.add(serviceName);
+
+ for (var metric : packet.metrics().entrySet()) {
+ var metricName = Collector.sanitizeMetricName(metric.getKey().id);
+ List<Sample> sampleList;
+ if (samples.containsKey(metricName)) {
+ sampleList = samples.get(metricName);
+ } else {
+ sampleList = new ArrayList<>();
+ samples.put(metricName, sampleList);
+ metricFamilySamples.add(new MetricFamilySamples(metricName, Collector.Type.UNTYPED, "", sampleList));
+ }
+ sampleList.add(new Sample(metricName, labels, labelValues, metric.getValue().doubleValue(), packet.timestamp * 1000));
+ }
+ }
+ // convert status message to 0,1 metric
+ var firstPacket = packets.get(0);
+ String statusMetricName = serviceName + "_status";
+ // MetricsPacket status 0 means OK, but it's the opposite in Prometheus.
+ double statusMetricValue = (firstPacket.statusCode == 0) ? 1.0 : 0.0;
+ List<Sample> sampleList = singletonList(new Sample(statusMetricName, emptyList(), emptyList(),
+ statusMetricValue, firstPacket.timestamp * 1000));
+ metricFamilySamples.add(new MetricFamilySamples(statusMetricName, Collector.Type.UNTYPED, "status of service", sampleList));
+ }));
+
+ return new PrometheusModel(metricFamilySamples);
+ }
+
+}
diff --git a/metrics-proxy/src/test/java/ai/vespa/metricsproxy/http/HttpHandlerTestBase.java b/metrics-proxy/src/test/java/ai/vespa/metricsproxy/http/HttpHandlerTestBase.java
new file mode 100644
index 00000000000..bd6fba84e35
--- /dev/null
+++ b/metrics-proxy/src/test/java/ai/vespa/metricsproxy/http/HttpHandlerTestBase.java
@@ -0,0 +1,100 @@
+/*
+ * 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.TestUtil;
+import ai.vespa.metricsproxy.core.ConsumersConfig;
+import ai.vespa.metricsproxy.core.MetricsConsumers;
+import ai.vespa.metricsproxy.core.MetricsManager;
+import ai.vespa.metricsproxy.metric.HealthMetric;
+import ai.vespa.metricsproxy.metric.Metric;
+import ai.vespa.metricsproxy.metric.dimensions.ApplicationDimensions;
+import ai.vespa.metricsproxy.metric.dimensions.ApplicationDimensionsConfig;
+import ai.vespa.metricsproxy.metric.dimensions.NodeDimensions;
+import ai.vespa.metricsproxy.metric.dimensions.NodeDimensionsConfig;
+import ai.vespa.metricsproxy.metric.model.MetricsPacket;
+import ai.vespa.metricsproxy.service.DownService;
+import ai.vespa.metricsproxy.service.DummyService;
+import ai.vespa.metricsproxy.service.VespaService;
+import ai.vespa.metricsproxy.service.VespaServices;
+import com.google.common.collect.ImmutableList;
+import com.yahoo.container.jdisc.RequestHandlerTestDriver;
+
+import java.time.Instant;
+import java.util.List;
+
+import static ai.vespa.metricsproxy.http.ValuesFetcher.DEFAULT_PUBLIC_CONSUMER_ID;
+import static ai.vespa.metricsproxy.metric.ExternalMetrics.VESPA_NODE_SERVICE_ID;
+import static ai.vespa.metricsproxy.service.DummyService.METRIC_1;
+
+/**
+ * Base class for http handler tests.
+ *
+ * @author gjoranv
+ */
+@SuppressWarnings("UnstableApiUsage")
+public class HttpHandlerTestBase {
+
+ protected static final List<VespaService> testServices = ImmutableList.of(
+ new DummyService(0, ""),
+ new DummyService(1, ""),
+ new DownService(HealthMetric.getDown("No response")));
+
+ protected static final VespaServices vespaServices = new VespaServices(testServices);
+
+ protected static final String DEFAULT_CONSUMER = "default";
+ protected static final String CUSTOM_CONSUMER = "custom-consumer";
+
+ protected static final String CPU_METRIC = "cpu";
+
+ protected static final String URI_BASE = "http://localhost";
+
+ protected static RequestHandlerTestDriver testDriver;
+
+
+ protected static MetricsManager getMetricsManager() {
+ MetricsManager metricsManager = TestUtil.createMetricsManager(vespaServices, getMetricsConsumers(), getApplicationDimensions(), getNodeDimensions());
+ metricsManager.setExtraMetrics(ImmutableList.of(
+ new MetricsPacket.Builder(VESPA_NODE_SERVICE_ID)
+ .timestamp(Instant.now().getEpochSecond())
+ .putMetrics(ImmutableList.of(new Metric(CPU_METRIC, 12.345)))));
+ return metricsManager;
+ }
+
+ protected static MetricsConsumers getMetricsConsumers() {
+ var defaultConsumerDimension = new ConsumersConfig.Consumer.Metric.Dimension.Builder()
+ .key("consumer-dim").value("default-val");
+
+ var customConsumerDimension = new ConsumersConfig.Consumer.Metric.Dimension.Builder()
+ .key("consumer-dim").value("custom-val");
+
+ return new MetricsConsumers(new ConsumersConfig.Builder()
+ .consumer(new ConsumersConfig.Consumer.Builder()
+ .name(DEFAULT_PUBLIC_CONSUMER_ID.id)
+ .metric(new ConsumersConfig.Consumer.Metric.Builder()
+ .name(CPU_METRIC)
+ .outputname(CPU_METRIC))
+ .metric(new ConsumersConfig.Consumer.Metric.Builder()
+ .name(METRIC_1)
+ .outputname(METRIC_1)
+ .dimension(defaultConsumerDimension)))
+ .consumer(new ConsumersConfig.Consumer.Builder()
+ .name(CUSTOM_CONSUMER)
+ .metric(new ConsumersConfig.Consumer.Metric.Builder()
+ .name(METRIC_1)
+ .outputname(METRIC_1)
+ .dimension(customConsumerDimension)))
+ .build());
+ }
+
+ protected static ApplicationDimensions getApplicationDimensions() {
+ return new ApplicationDimensions(new ApplicationDimensionsConfig.Builder().build());
+ }
+
+ protected static NodeDimensions getNodeDimensions() {
+ return new NodeDimensions(new NodeDimensionsConfig.Builder().build());
+ }
+
+}
diff --git a/metrics-proxy/src/test/java/ai/vespa/metricsproxy/http/MetricsHandlerTest.java b/metrics-proxy/src/test/java/ai/vespa/metricsproxy/http/MetricsHandlerTest.java
index 66220464e3e..83cd97bbeda 100644
--- a/metrics-proxy/src/test/java/ai/vespa/metricsproxy/http/MetricsHandlerTest.java
+++ b/metrics-proxy/src/test/java/ai/vespa/metricsproxy/http/MetricsHandlerTest.java
@@ -4,27 +4,11 @@
package ai.vespa.metricsproxy.http;
-import ai.vespa.metricsproxy.TestUtil;
-import ai.vespa.metricsproxy.core.ConsumersConfig;
-import ai.vespa.metricsproxy.core.ConsumersConfig.Consumer;
-import ai.vespa.metricsproxy.core.MetricsConsumers;
-import ai.vespa.metricsproxy.core.MetricsManager;
-import ai.vespa.metricsproxy.metric.HealthMetric;
-import ai.vespa.metricsproxy.metric.Metric;
-import ai.vespa.metricsproxy.metric.dimensions.ApplicationDimensions;
-import ai.vespa.metricsproxy.metric.dimensions.ApplicationDimensionsConfig;
-import ai.vespa.metricsproxy.metric.dimensions.NodeDimensions;
-import ai.vespa.metricsproxy.metric.dimensions.NodeDimensionsConfig;
-import ai.vespa.metricsproxy.metric.model.MetricsPacket;
import ai.vespa.metricsproxy.metric.model.json.GenericJsonModel;
import ai.vespa.metricsproxy.metric.model.json.GenericMetrics;
import ai.vespa.metricsproxy.metric.model.json.GenericService;
import ai.vespa.metricsproxy.service.DownService;
-import ai.vespa.metricsproxy.service.DummyService;
-import ai.vespa.metricsproxy.service.VespaService;
-import ai.vespa.metricsproxy.service.VespaServices;
import com.fasterxml.jackson.databind.ObjectMapper;
-import com.google.common.collect.ImmutableList;
import com.yahoo.container.jdisc.RequestHandlerTestDriver;
import org.json.JSONArray;
import org.json.JSONObject;
@@ -33,15 +17,11 @@ import org.junit.Ignore;
import org.junit.Test;
import java.io.IOException;
-import java.time.Instant;
-import java.util.List;
import java.util.concurrent.Executors;
import static ai.vespa.metricsproxy.core.VespaMetrics.INSTANCE_DIMENSION_ID;
import static ai.vespa.metricsproxy.http.MetricsHandler.V1_PATH;
import static ai.vespa.metricsproxy.http.MetricsHandler.VALUES_PATH;
-import static ai.vespa.metricsproxy.http.ValuesFetcher.DEFAULT_PUBLIC_CONSUMER_ID;
-import static ai.vespa.metricsproxy.metric.ExternalMetrics.VESPA_NODE_SERVICE_ID;
import static ai.vespa.metricsproxy.metric.model.StatusCode.DOWN;
import static ai.vespa.metricsproxy.metric.model.json.JacksonUtil.createObjectMapper;
import static ai.vespa.metricsproxy.service.DummyService.METRIC_1;
@@ -56,35 +36,17 @@ import static org.junit.Assert.fail;
* @author gjoranv
*/
@SuppressWarnings("UnstableApiUsage")
-public class MetricsHandlerTest {
+public class MetricsHandlerTest extends HttpHandlerTestBase {
- private static final List<VespaService> testServices = ImmutableList.of(
- new DummyService(0, ""),
- new DummyService(1, ""),
- new DownService(HealthMetric.getDown("No response")));
-
- private static final VespaServices vespaServices = new VespaServices(testServices);
-
- private static final String DEFAULT_CONSUMER = "default";
- private static final String CUSTOM_CONSUMER = "custom-consumer";
-
- private static final String CPU_METRIC = "cpu";
-
- private static final String URI_BASE = "http://localhost";
private static final String V1_URI = URI_BASE + V1_PATH;
private static final String VALUES_URI = URI_BASE + VALUES_PATH;
-
- private static RequestHandlerTestDriver testDriver;
-
@BeforeClass
public static void setup() {
- MetricsManager metricsManager = TestUtil.createMetricsManager(vespaServices, getMetricsConsumers(), getApplicationDimensions(), getNodeDimensions());
- metricsManager.setExtraMetrics(ImmutableList.of(
- new MetricsPacket.Builder(VESPA_NODE_SERVICE_ID)
- .timestamp(Instant.now().getEpochSecond())
- .putMetrics(ImmutableList.of(new Metric(CPU_METRIC, 12.345)))));
- MetricsHandler handler = new MetricsHandler(Executors.newSingleThreadExecutor(), metricsManager, vespaServices, getMetricsConsumers());
+ MetricsHandler handler = new MetricsHandler(Executors.newSingleThreadExecutor(),
+ getMetricsManager(),
+ vespaServices,
+ getMetricsConsumers());
testDriver = new RequestHandlerTestDriver(handler);
}
@@ -113,7 +75,7 @@ public class MetricsHandlerTest {
@Ignore
@Test
- public void visually_inspect_values_response() throws Exception{
+ public void visually_inspect_values_response() throws Exception {
String response = testDriver.sendRequest(VALUES_URI).readAll();
ObjectMapper mapper = createObjectMapper();
var jsonModel = mapper.readValue(response, GenericJsonModel.class);
@@ -199,6 +161,13 @@ public class MetricsHandlerTest {
assertEquals("custom-val", dummy1Metrics.dimensions.get("consumer-dim"));
}
+ @Test
+ public void invalid_path_yields_error_response() throws Exception {
+ String response = testDriver.sendRequest(V1_URI + "/invalid").readAll();
+ JSONObject root = new JSONObject(response);
+ assertTrue(root.has("error"));
+ }
+
private void assertDownServiceHealth(String consumer) {
GenericJsonModel jsonModel = getResponseAsJsonModel(consumer);
@@ -226,38 +195,4 @@ public class MetricsHandlerTest {
throw new RuntimeException();
}
- private static MetricsConsumers getMetricsConsumers() {
- var defaultConsumerDimension = new Consumer.Metric.Dimension.Builder()
- .key("consumer-dim").value("default-val");
-
- var customConsumerDimension = new Consumer.Metric.Dimension.Builder()
- .key("consumer-dim").value("custom-val");
-
- return new MetricsConsumers(new ConsumersConfig.Builder()
- .consumer(new Consumer.Builder()
- .name(DEFAULT_PUBLIC_CONSUMER_ID.id)
- .metric(new Consumer.Metric.Builder()
- .name(CPU_METRIC)
- .outputname(CPU_METRIC))
- .metric(new Consumer.Metric.Builder()
- .name(METRIC_1)
- .outputname(METRIC_1)
- .dimension(defaultConsumerDimension)))
- .consumer(new Consumer.Builder()
- .name(CUSTOM_CONSUMER)
- .metric(new Consumer.Metric.Builder()
- .name(METRIC_1)
- .outputname(METRIC_1)
- .dimension(customConsumerDimension)))
- .build());
- }
-
- private static ApplicationDimensions getApplicationDimensions() {
- return new ApplicationDimensions(new ApplicationDimensionsConfig.Builder().build());
- }
-
- private static NodeDimensions getNodeDimensions() {
- return new NodeDimensions(new NodeDimensionsConfig.Builder().build());
- }
-
}
diff --git a/metrics-proxy/src/test/java/ai/vespa/metricsproxy/http/prometheus/PrometheusHandlerTest.java b/metrics-proxy/src/test/java/ai/vespa/metricsproxy/http/prometheus/PrometheusHandlerTest.java
new file mode 100644
index 00000000000..69cdd9954e4
--- /dev/null
+++ b/metrics-proxy/src/test/java/ai/vespa/metricsproxy/http/prometheus/PrometheusHandlerTest.java
@@ -0,0 +1,106 @@
+/*
+ * 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.prometheus;
+
+import ai.vespa.metricsproxy.http.HttpHandlerTestBase;
+import ai.vespa.metricsproxy.service.DummyService;
+import com.yahoo.container.jdisc.RequestHandlerTestDriver;
+import org.json.JSONArray;
+import org.json.JSONObject;
+import org.junit.BeforeClass;
+import org.junit.Ignore;
+import org.junit.Test;
+
+import java.util.concurrent.Executors;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+/**
+ * @author gjoranv
+ */
+@SuppressWarnings("UnstableApiUsage")
+public class PrometheusHandlerTest extends HttpHandlerTestBase {
+
+ private static final String V1_URI = URI_BASE + PrometheusHandler.V1_PATH;
+ private static final String VALUES_URI = URI_BASE + PrometheusHandler.VALUES_PATH;
+
+ private static String valuesResponse;
+
+ @BeforeClass
+ public static void setup() {
+ PrometheusHandler handler = new PrometheusHandler(Executors.newSingleThreadExecutor(),
+ getMetricsManager(),
+ vespaServices,
+ getMetricsConsumers());
+ testDriver = new RequestHandlerTestDriver(handler);
+ valuesResponse = testDriver.sendRequest(VALUES_URI).readAll();
+ }
+
+ @Test
+ public void v1_response_contains_values_uri() throws Exception {
+ String response = testDriver.sendRequest(V1_URI).readAll();
+ JSONObject root = new JSONObject(response);
+ assertTrue(root.has("resources"));
+
+ JSONArray resources = root.getJSONArray("resources");
+ assertEquals(1, resources.length());
+
+ JSONObject valuesUrl = resources.getJSONObject(0);
+ assertEquals(VALUES_URI, valuesUrl.getString("url"));
+ }
+
+ @Ignore
+ @Test
+ public void visually_inspect_values_response() {
+ System.out.println(valuesResponse);
+ }
+
+ @Test
+ public void response_contains_node_status() {
+ assertTrue(valuesResponse.contains("vespa_node_status 1.0"));
+ }
+
+ @Test
+ public void response_contains_node_metrics() {
+ String cpu = getLine(valuesResponse, CPU_METRIC + "{");
+ assertTrue(cpu.contains("} 12.345")); // metric value
+ assertTrue(cpu.contains("{vespaVersion="));
+ }
+
+ @Test
+ public void response_contains_service_status() {
+ assertTrue(valuesResponse.contains("vespa_dummy_status 1.0"));
+ assertTrue(valuesResponse.contains("vespa_down_service_status 0.0"));
+ }
+
+ @Test
+ public void response_contains_service_metrics() {
+ String dummy0 = getLine(valuesResponse, DummyService.NAME + "0");
+ assertTrue(dummy0.contains("c_test")); // metric name
+ assertTrue(dummy0.contains("} 1.0")); // metric value
+ }
+
+ @Test
+ public void service_metrics_have_configured_dimensions() {
+ String dummy0 = getLine(valuesResponse, DummyService.NAME + "0");
+ assertTrue(dummy0.contains("consumer_dim=\"default-val\""));
+ }
+
+ @Test
+ public void service_metrics_have_vespa_service_dimension() {
+ String dummy0 = getLine(valuesResponse, DummyService.NAME + "0");
+ assertTrue(dummy0.contains("vespa_service=\"vespa_dummy\""));
+ }
+
+ // Find the first line that contains the given string
+ private String getLine(String raw, String searchString) {
+ for (var s : raw.split("\\n")) {
+ if (s.contains(searchString))
+ return s;
+ }
+ throw new IllegalArgumentException("No line containing string: " + searchString);
+ }
+}
diff --git a/metrics-proxy/src/test/java/ai/vespa/metricsproxy/service/DummyService.java b/metrics-proxy/src/test/java/ai/vespa/metricsproxy/service/DummyService.java
index 380a992aead..cf559628490 100644
--- a/metrics-proxy/src/test/java/ai/vespa/metricsproxy/service/DummyService.java
+++ b/metrics-proxy/src/test/java/ai/vespa/metricsproxy/service/DummyService.java
@@ -11,7 +11,7 @@ import ai.vespa.metricsproxy.metric.Metrics;
* @author Unknown
*/
public class DummyService extends VespaService {
- static final String NAME = "dummy";
+ public static final String NAME = "dummy";
public static final String METRIC_1 = "c.test";
public static final String METRIC_2 = "val";
diff --git a/parent/pom.xml b/parent/pom.xml
index a81f64d1725..1b22bcefb18 100644
--- a/parent/pom.xml
+++ b/parent/pom.xml
@@ -436,6 +436,16 @@
<version>0.7</version>
</dependency>
<dependency>
+ <groupId>io.prometheus</groupId>
+ <artifactId>simpleclient</artifactId>
+ <version>${prometheus.client.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>io.prometheus</groupId>
+ <artifactId>simpleclient_common</artifactId>
+ <version>${prometheus.client.version}</version>
+ </dependency>
+ <dependency>
<groupId>org.ow2.asm</groupId>
<artifactId>asm</artifactId>
<version>${asm.version}</version>
@@ -777,6 +787,7 @@
<doclint>all</doclint>
<surefire.version>2.22.0</surefire.version>
<junit.version>5.4.2</junit.version>
+ <prometheus.client.version>0.6.0</prometheus.client.version>
<protobuf.version>3.7.0</protobuf.version>
</properties>