aboutsummaryrefslogtreecommitdiffstats
path: root/container-core
diff options
context:
space:
mode:
authorgjoranv <gv@verizonmedia.com>2020-01-23 12:38:55 +0100
committergjoranv <gv@verizonmedia.com>2020-01-24 03:32:11 +0100
commit8f7d794b2da0803e7d7d917b8b10eca7b1e376e5 (patch)
treecf6687c3ac9bdb65ee86da5ba842b83e50a32dc9 /container-core
parentf484a3a339992f07ee54be7d3d128b48ffbb6a98 (diff)
Add MetricsV2Handler to application containers.
Diffstat (limited to 'container-core')
-rw-r--r--container-core/pom.xml15
-rw-r--r--container-core/src/main/java/com/yahoo/container/handler/metrics/MetricsV2Handler.java77
-rw-r--r--container-core/src/main/resources/configdefinitions/metrics-proxy-api.def6
-rw-r--r--container-core/src/test/java/com/yahoo/container/handler/metrics/MetricsV2HandlerTest.java143
-rw-r--r--container-core/src/test/resources/application-metrics.json92
5 files changed, 333 insertions, 0 deletions
diff --git a/container-core/pom.xml b/container-core/pom.xml
index f3861c92129..e51d99c3b78 100644
--- a/container-core/pom.xml
+++ b/container-core/pom.xml
@@ -16,6 +16,16 @@
<packaging>container-plugin</packaging>
<dependencies>
<dependency>
+ <groupId>com.yahoo.vespa</groupId>
+ <artifactId>http-utils</artifactId>
+ <version>${project.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.httpcomponents</groupId>
+ <artifactId>httpclient</artifactId>
+ </dependency>
+
+ <dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<scope>provided</scope>
@@ -227,6 +237,11 @@
<version>${project.version}</version>
<scope>test</scope>
</dependency>
+ <dependency>
+ <groupId>com.github.tomakehurst</groupId>
+ <artifactId>wiremock-standalone</artifactId>
+ <scope>test</scope>
+ </dependency>
</dependencies>
<build>
<plugins>
diff --git a/container-core/src/main/java/com/yahoo/container/handler/metrics/MetricsV2Handler.java b/container-core/src/main/java/com/yahoo/container/handler/metrics/MetricsV2Handler.java
new file mode 100644
index 00000000000..bfa825f0404
--- /dev/null
+++ b/container-core/src/main/java/com/yahoo/container/handler/metrics/MetricsV2Handler.java
@@ -0,0 +1,77 @@
+// Copyright 2019 Oath Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.container.handler.metrics;
+
+import ai.vespa.util.http.VespaHttpClientBuilder;
+import com.google.inject.Inject;
+import com.yahoo.container.jdisc.HttpResponse;
+import com.yahoo.restapi.Path;
+import com.yahoo.yolean.Exceptions;
+import org.apache.http.client.HttpClient;
+import org.apache.http.client.config.RequestConfig;
+import org.apache.http.client.methods.HttpGet;
+import org.apache.http.impl.client.BasicResponseHandler;
+import org.apache.http.impl.client.CloseableHttpClient;
+import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
+
+import java.io.IOException;
+import java.net.URI;
+import java.util.List;
+import java.util.Optional;
+import java.util.concurrent.Executor;
+
+import static com.yahoo.jdisc.Response.Status.INTERNAL_SERVER_ERROR;
+import static com.yahoo.jdisc.Response.Status.OK;
+
+/**
+ * @author gjoranv
+ */
+public class MetricsV2Handler extends HttpHandlerBase {
+
+ public static final String V2_PATH = "/metrics/v2";
+ static final String VALUES_PATH = V2_PATH + "/values";
+
+ private static final int HTTP_CONNECT_TIMEOUT = 5000;
+ private static final int HTTP_SOCKET_TIMEOUT = 30000;
+
+ private final String metricsProxyUri;
+ private final HttpClient httpClient = createHttpClient();
+
+ @Inject
+ public MetricsV2Handler(Executor executor,
+ MetricsProxyApiConfig config) {
+ super(executor);
+ metricsProxyUri = "http://localhost:" + config.metricsPort() + config.metricsApiPath();
+ }
+
+ @Override
+ protected Optional<HttpResponse> doHandle(URI requestUri, Path apiPath, String consumer) {
+ if (apiPath.matches(V2_PATH)) return Optional.of(resourceListResponse(requestUri, List.of(VALUES_PATH)));
+ if (apiPath.matches(VALUES_PATH)) return Optional.of(valuesResponse(consumer));
+ return Optional.empty();
+ }
+
+ private JsonResponse valuesResponse(String consumer) {
+ try {
+ String uri = metricsProxyUri + consumerQuery(consumer);
+ String metricsJson = httpClient.execute(new HttpGet(uri), new BasicResponseHandler());
+ return new JsonResponse(OK, metricsJson);
+ } catch (IOException e) {
+ log.warning("Unable to retrieve metrics from " + metricsProxyUri + ": " + Exceptions.toMessageString(e));
+ return new ErrorResponse(INTERNAL_SERVER_ERROR, e.getMessage());
+ }
+ }
+
+ private static CloseableHttpClient createHttpClient() {
+ return VespaHttpClientBuilder.create(PoolingHttpClientConnectionManager::new)
+ .setUserAgent("application-metrics-retriever")
+ .setDefaultRequestConfig(RequestConfig.custom()
+ .setConnectTimeout(HTTP_CONNECT_TIMEOUT)
+ .setSocketTimeout(HTTP_SOCKET_TIMEOUT)
+ .build())
+ .build();
+ }
+
+ static String consumerQuery(String consumer) {
+ return (consumer == null || consumer.isEmpty()) ? "" : "?consumer=" + consumer;
+ }
+}
diff --git a/container-core/src/main/resources/configdefinitions/metrics-proxy-api.def b/container-core/src/main/resources/configdefinitions/metrics-proxy-api.def
new file mode 100644
index 00000000000..3e5b973e3f3
--- /dev/null
+++ b/container-core/src/main/resources/configdefinitions/metrics-proxy-api.def
@@ -0,0 +1,6 @@
+# Copyright 2019 Oath Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+
+namespace=container.handler.metrics
+
+metricsPort int
+metricsApiPath string
diff --git a/container-core/src/test/java/com/yahoo/container/handler/metrics/MetricsV2HandlerTest.java b/container-core/src/test/java/com/yahoo/container/handler/metrics/MetricsV2HandlerTest.java
new file mode 100644
index 00000000000..b57814e50aa
--- /dev/null
+++ b/container-core/src/test/java/com/yahoo/container/handler/metrics/MetricsV2HandlerTest.java
@@ -0,0 +1,143 @@
+// Copyright 2019 Oath Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.container.handler.metrics;
+
+import com.github.tomakehurst.wiremock.junit.WireMockRule;
+import com.yahoo.container.jdisc.RequestHandlerTestDriver;
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.junit.Before;
+import org.junit.Ignore;
+import org.junit.Rule;
+import org.junit.Test;
+
+import java.io.BufferedReader;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.util.concurrent.Executors;
+import java.util.stream.Collectors;
+
+import static com.github.tomakehurst.wiremock.client.WireMock.aResponse;
+import static com.github.tomakehurst.wiremock.client.WireMock.equalTo;
+import static com.github.tomakehurst.wiremock.client.WireMock.get;
+import static com.github.tomakehurst.wiremock.client.WireMock.urlPathEqualTo;
+import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.options;
+import static com.yahoo.container.handler.metrics.MetricsV2Handler.V2_PATH;
+import static com.yahoo.container.handler.metrics.MetricsV2Handler.VALUES_PATH;
+import static com.yahoo.container.handler.metrics.MetricsV2Handler.consumerQuery;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+/**
+ * @author gjoranv
+ */
+public class MetricsV2HandlerTest {
+
+ private static final String URI_BASE = "http://localhost";
+
+ private static final String V2_URI = URI_BASE + V2_PATH;
+ private static final String VALUES_URI = URI_BASE + VALUES_PATH;
+
+ // Mock applicationmetrics api
+ private static final String MOCK_METRICS_PATH = "/node0";
+
+ private static final String TEST_FILE = "application-metrics.json";
+ private static final String RESPONSE = getFileContents(TEST_FILE);
+ private static final String CPU_METRIC = "cpu.util";
+ private static final String REPLACED_CPU_METRIC = "replaced_cpu_util";
+ private static final String CUSTOM_CONSUMER = "custom-consumer";
+
+ private static RequestHandlerTestDriver testDriver;
+
+ @Rule
+ public WireMockRule wireMockRule = new WireMockRule(options().dynamicPort());
+
+ @Before
+ public void setup() {
+ setupWireMock();
+ var handler = new MetricsV2Handler(Executors.newSingleThreadExecutor(),
+ new MetricsProxyApiConfig.Builder()
+ .metricsPort(wireMockRule.port())
+ .metricsApiPath(MOCK_METRICS_PATH)
+ .build());
+ testDriver = new RequestHandlerTestDriver(handler);
+ }
+
+ private void setupWireMock() {
+ wireMockRule.stubFor(get(urlPathEqualTo(MOCK_METRICS_PATH))
+ .willReturn(aResponse().withBody(RESPONSE)));
+
+ // Add a slightly different response for a custom consumer.
+ String myConsumerResponse = RESPONSE.replaceAll(CPU_METRIC, REPLACED_CPU_METRIC);
+ wireMockRule.stubFor(get(urlPathEqualTo(MOCK_METRICS_PATH))
+ .withQueryParam("consumer", equalTo(CUSTOM_CONSUMER))
+ .willReturn(aResponse().withBody(myConsumerResponse)));
+ }
+
+ @Test
+ public void v2_response_contains_values_uri() throws Exception {
+ String response = testDriver.sendRequest(V2_URI).readAll();
+ JSONObject root = new JSONObject(response);
+ assertTrue(root.has("resources"));
+
+ JSONArray resources = root.getJSONArray("resources");
+ assertEquals(1, resources.length());
+
+ JSONObject valuesUri = resources.getJSONObject(0);
+ assertEquals(VALUES_URI, valuesUri.getString("url"));
+ }
+
+ @Ignore
+ @Test
+ public void visually_inspect_values_response() throws Exception {
+ JSONObject responseJson = getResponseAsJson(null);
+ System.out.println(responseJson.toString(4));
+ }
+
+ @Test
+ public void invalid_path_yields_error_response() throws Exception {
+ String response = testDriver.sendRequest(V2_URI + "/invalid").readAll();
+ JSONObject root = new JSONObject(response);
+ assertTrue(root.has("error"));
+ assertTrue(root.getString("error" ).startsWith("No content"));
+ }
+
+ @Test
+ public void values_response_is_equal_to_test_file() {
+ String response = testDriver.sendRequest(VALUES_URI).readAll();
+ assertEquals(RESPONSE, response);
+ }
+
+ @Test
+ public void consumer_is_propagated_to_metrics_proxy_api() throws JSONException {
+ JSONObject responseJson = getResponseAsJson(CUSTOM_CONSUMER);
+
+ JSONObject firstNodeMetricsValues =
+ responseJson.getJSONArray("nodes").getJSONObject(0)
+ .getJSONObject("node")
+ .getJSONArray("metrics").getJSONObject(0)
+ .getJSONObject("values");
+
+ assertTrue(firstNodeMetricsValues.has(REPLACED_CPU_METRIC));
+ }
+
+ private JSONObject getResponseAsJson(String consumer) {
+ String response = testDriver.sendRequest(VALUES_URI + consumerQuery(consumer)).readAll();
+ try {
+ return new JSONObject(response);
+ } catch (JSONException e) {
+ fail("Failed to create json object: " + e.getMessage());
+ throw new RuntimeException(e);
+ }
+ }
+
+ private static String getFileContents(String filename) {
+ InputStream in = MetricsV2HandlerTest.class.getClassLoader().getResourceAsStream(filename);
+ if (in == null) {
+ throw new RuntimeException("File not found: " + filename);
+ }
+ return new BufferedReader(new InputStreamReader(in)).lines().collect(Collectors.joining("\n"));
+ }
+
+}
diff --git a/container-core/src/test/resources/application-metrics.json b/container-core/src/test/resources/application-metrics.json
new file mode 100644
index 00000000000..52cbb721bb1
--- /dev/null
+++ b/container-core/src/test/resources/application-metrics.json
@@ -0,0 +1,92 @@
+{
+ "nodes": [
+ {
+ "hostname": "node0",
+ "role": "role0",
+ "node": {
+ "timestamp": 1234,
+ "metrics": [
+ {
+ "values": {
+ "cpu.util": 16.222
+ },
+ "dimensions": {
+ "state": "active"
+ }
+ }
+ ]
+ },
+ "services": [
+ {
+ "name": "searchnode",
+ "timestamp": 1234,
+ "status": {
+ "code": "up"
+ },
+ "metrics": [
+ {
+ "values": {
+ "queries.count": 4
+ },
+ "dimensions": {
+ "documentType": "music"
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "hostname": "node1",
+ "role": "role1",
+ "node": {
+ "timestamp": 1234,
+ "metrics": [
+ {
+ "values": {
+ "cpu.util": 32.444
+ },
+ "dimensions": {
+ "state": "active"
+ }
+ }
+ ]
+ },
+ "services": [
+ {
+ "name": "searchnode",
+ "timestamp": 1234,
+ "status": {
+ "code": "up"
+ },
+ "metrics": [
+ {
+ "values": {
+ "queries.count": 8
+ },
+ "dimensions": {
+ "documentType": "music"
+ }
+ }
+ ]
+ },
+ {
+ "name": "slobrok",
+ "timestamp": 1234,
+ "status": {
+ "code": "unknown",
+ "description": "Unable to fetch metrics from service 'slobrok'"
+ },
+ "metrics": [
+ {
+ "values": {},
+ "dimensions": {
+ "instance": "slobrok0"
+ }
+ }
+ ]
+ }
+ ]
+ }
+ ]
+}