diff options
13 files changed, 406 insertions, 171 deletions
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/admin/metricsproxy/MetricsProxyContainer.java b/config-model/src/main/java/com/yahoo/vespa/model/admin/metricsproxy/MetricsProxyContainer.java index fd924eb2a0f..2b1a763b148 100644 --- a/config-model/src/main/java/com/yahoo/vespa/model/admin/metricsproxy/MetricsProxyContainer.java +++ b/config-model/src/main/java/com/yahoo/vespa/model/admin/metricsproxy/MetricsProxyContainer.java @@ -2,6 +2,7 @@ package com.yahoo.vespa.model.admin.metricsproxy; +import ai.vespa.metricsproxy.http.metrics.NodeInfoConfig; import ai.vespa.metricsproxy.metric.dimensions.NodeDimensions; import ai.vespa.metricsproxy.metric.dimensions.NodeDimensionsConfig; import ai.vespa.metricsproxy.metric.dimensions.PublicDimensions; @@ -28,6 +29,7 @@ import static com.yahoo.vespa.model.admin.metricsproxy.MetricsProxyContainerClus */ public class MetricsProxyContainer extends Container implements NodeDimensionsConfig.Producer, + NodeInfoConfig.Producer, RpcConnectorConfig.Producer, VespaServicesConfig.Producer { @@ -121,7 +123,21 @@ public class MetricsProxyContainer extends Container implements } } - private void addMetricsProxyComponent(Class<?> componentClass) { + @Override + public void getConfig(NodeInfoConfig.Builder builder) { + builder.role(getNodeRole()) + .hostname(getHostName()); + } + + private String getNodeRole() { + String hostConfigId = getHost().getConfigId(); + if (! isHostedVespa) return hostConfigId; + return getHostResource().spec().membership() + .map(ClusterMembership::stringValue) + .orElse(hostConfigId); + } + + private void addMetricsProxyComponent(Class<?> componentClass) { addSimpleComponent(componentClass.getName(), null, METRICS_PROXY_BUNDLE_NAME); } diff --git a/config-model/src/test/java/com/yahoo/vespa/model/admin/metricsproxy/MetricsProxyContainerTest.java b/config-model/src/test/java/com/yahoo/vespa/model/admin/metricsproxy/MetricsProxyContainerTest.java index 621cebd6246..30616b2c322 100644 --- a/config-model/src/test/java/com/yahoo/vespa/model/admin/metricsproxy/MetricsProxyContainerTest.java +++ b/config-model/src/test/java/com/yahoo/vespa/model/admin/metricsproxy/MetricsProxyContainerTest.java @@ -1,6 +1,7 @@ // Copyright 2020 Oath Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. package com.yahoo.vespa.model.admin.metricsproxy; +import ai.vespa.metricsproxy.http.metrics.NodeInfoConfig; import ai.vespa.metricsproxy.metric.dimensions.NodeDimensionsConfig; import ai.vespa.metricsproxy.metric.dimensions.PublicDimensions; import ai.vespa.metricsproxy.rpc.RpcConnectorConfig; @@ -10,10 +11,10 @@ import com.yahoo.vespa.model.test.VespaModelTester; import org.junit.Test; import static com.yahoo.config.model.api.container.ContainerServiceType.METRICS_PROXY_CONTAINER; -import static com.yahoo.vespa.model.admin.metricsproxy.MetricsProxyModelTester.CLUSTER_CONFIG_ID; import static com.yahoo.vespa.model.admin.metricsproxy.MetricsProxyModelTester.CONTAINER_CONFIG_ID; import static com.yahoo.vespa.model.admin.metricsproxy.MetricsProxyModelTester.TestMode.hosted; import static com.yahoo.vespa.model.admin.metricsproxy.MetricsProxyModelTester.TestMode.self_hosted; +import static com.yahoo.vespa.model.admin.metricsproxy.MetricsProxyModelTester.containerConfigId; import static com.yahoo.vespa.model.admin.metricsproxy.MetricsProxyModelTester.getModel; import static com.yahoo.vespa.model.admin.metricsproxy.MetricsProxyModelTester.getNodeDimensionsConfig; @@ -90,13 +91,23 @@ public class MetricsProxyContainerTest { String services = servicesWithContent(); VespaModel hostedModel = getModel(services, hosted); assertEquals(1, hostedModel.getHosts().size()); - String configId = CLUSTER_CONFIG_ID + "/" + hostedModel.getHosts().iterator().next().getHostname(); + String configId = containerConfigId(hostedModel, hosted); NodeDimensionsConfig config = getNodeDimensionsConfig(hostedModel, configId); assertEquals("content", config.dimensions(PublicDimensions.INTERNAL_CLUSTER_TYPE)); assertEquals("my-content", config.dimensions(PublicDimensions.INTERNAL_CLUSTER_ID)); } + @Test + public void node_info_config_is_generated() { + String services = servicesWithContent(); + VespaModel hostedModel = getModel(services, hosted); + String configId = containerConfigId(hostedModel, hosted); + + NodeInfoConfig config = hostedModel.getConfig(NodeInfoConfig.class, configId); + assertTrue(config.role().startsWith("content/my-content/0/")); + assertTrue(config.hostname().startsWith("node-1-3-9-")); + } @Test public void vespa_services_config_has_all_services() { diff --git a/config-model/src/test/java/com/yahoo/vespa/model/admin/metricsproxy/MetricsProxyModelTester.java b/config-model/src/test/java/com/yahoo/vespa/model/admin/metricsproxy/MetricsProxyModelTester.java index f3140aafdaf..7cbc9db5eb2 100644 --- a/config-model/src/test/java/com/yahoo/vespa/model/admin/metricsproxy/MetricsProxyModelTester.java +++ b/config-model/src/test/java/com/yahoo/vespa/model/admin/metricsproxy/MetricsProxyModelTester.java @@ -49,7 +49,7 @@ class MetricsProxyModelTester { return tester.createModel(servicesXml, true); } - static String configId(VespaModel model, MetricsProxyModelTester.TestMode mode) { + static String containerConfigId(VespaModel model, MetricsProxyModelTester.TestMode mode) { return (mode == hosted) ? CLUSTER_CONFIG_ID + "/" + model.getHosts().iterator().next().getHostname() : CONTAINER_CONFIG_ID; 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 768c1beebef..ae0ef2fa57a 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 @@ -46,6 +46,16 @@ public class ValuesFetcher { .collect(Collectors.toList()); } + public List<MetricsPacket.Builder> fetchMetricsAsBuilders(String requestedConsumer) throws JsonRenderingException { + ConsumerId consumer = getConsumerOrDefault(requestedConsumer, metricsConsumers); + + return metricsManager.getMetricsAsBuilders(vespaServices.getVespaServices(), Instant.now()) + .stream() + .filter(builder -> builder.hasConsumer(consumer)) + .collect(Collectors.toList()); + } + + public List<MetricsPacket> fetchAllMetrics() throws JsonRenderingException { return metricsManager.getMetrics(vespaServices.getVespaServices(), Instant.now()); } diff --git a/metrics-proxy/src/main/java/ai/vespa/metricsproxy/http/application/Node.java b/metrics-proxy/src/main/java/ai/vespa/metricsproxy/http/application/Node.java index c8a8e65be5d..c439a037774 100644 --- a/metrics-proxy/src/main/java/ai/vespa/metricsproxy/http/application/Node.java +++ b/metrics-proxy/src/main/java/ai/vespa/metricsproxy/http/application/Node.java @@ -25,7 +25,7 @@ public class Node { } public Node(String role, String hostname, int port, String path) { - Objects.requireNonNull(role, "Null configId is not allowed"); + Objects.requireNonNull(role, "Null role is not allowed"); Objects.requireNonNull(hostname, "Null hostname is not allowed"); Objects.requireNonNull(path, "Null path is not allowed"); diff --git a/metrics-proxy/src/main/java/ai/vespa/metricsproxy/http/application/PublicDimensionsProcessor.java b/metrics-proxy/src/main/java/ai/vespa/metricsproxy/http/application/PublicDimensionsProcessor.java index 395ec0bea4f..4d1d57644b5 100644 --- a/metrics-proxy/src/main/java/ai/vespa/metricsproxy/http/application/PublicDimensionsProcessor.java +++ b/metrics-proxy/src/main/java/ai/vespa/metricsproxy/http/application/PublicDimensionsProcessor.java @@ -21,7 +21,7 @@ public class PublicDimensionsProcessor implements MetricsProcessor { private final int maxDimensions; private Set<DimensionId> publicDimensions = getPublicDimensions(); - PublicDimensionsProcessor(int maxDimensions) { + public PublicDimensionsProcessor(int maxDimensions) { int numCommonDimensions = PublicDimensions.commonDimensions.size(); if (numCommonDimensions > maxDimensions) { throw new IllegalArgumentException(String.format( diff --git a/metrics-proxy/src/main/java/ai/vespa/metricsproxy/http/metrics/MetricsV2Handler.java b/metrics-proxy/src/main/java/ai/vespa/metricsproxy/http/metrics/MetricsV2Handler.java new file mode 100644 index 00000000000..71d7857e48a --- /dev/null +++ b/metrics-proxy/src/main/java/ai/vespa/metricsproxy/http/metrics/MetricsV2Handler.java @@ -0,0 +1,92 @@ +// Copyright 2020 Oath Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package ai.vespa.metricsproxy.http.metrics; + +import ai.vespa.metricsproxy.core.MetricsConsumers; +import ai.vespa.metricsproxy.core.MetricsManager; +import ai.vespa.metricsproxy.http.ValuesFetcher; +import ai.vespa.metricsproxy.http.application.ClusterIdDimensionProcessor; +import ai.vespa.metricsproxy.http.application.Node; +import ai.vespa.metricsproxy.http.application.PublicDimensionsProcessor; +import ai.vespa.metricsproxy.http.application.ServiceIdDimensionProcessor; +import ai.vespa.metricsproxy.metric.model.MetricsPacket; +import ai.vespa.metricsproxy.metric.model.processing.MetricsProcessor; +import ai.vespa.metricsproxy.service.VespaServices; +import com.google.inject.Inject; +import com.yahoo.container.handler.metrics.ErrorResponse; +import com.yahoo.container.handler.metrics.HttpHandlerBase; +import com.yahoo.container.handler.metrics.JsonResponse; +import com.yahoo.container.jdisc.HttpResponse; +import com.yahoo.restapi.Path; + +import java.net.URI; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.Executor; +import java.util.logging.Level; + +import static ai.vespa.metricsproxy.metric.model.json.GenericJsonUtil.toGenericApplicationModel; +import static ai.vespa.metricsproxy.metric.model.processing.MetricsProcessor.applyProcessors; +import static com.yahoo.jdisc.Response.Status.INTERNAL_SERVER_ERROR; +import static com.yahoo.jdisc.Response.Status.OK; +import static java.util.Collections.singletonMap; +import static java.util.stream.Collectors.toList; + +/** + * Http handler for the metrics/v2 rest api. + * + * @author gjoranv + */ +public class MetricsV2Handler extends HttpHandlerBase { + + public static final String V2_PATH = "/metrics/v2"; + public static final String VALUES_PATH = V2_PATH + "/values"; + private static final int MAX_DIMENSIONS = 10; + + private final ValuesFetcher valuesFetcher; + private final NodeInfoConfig nodeInfoConfig; + + @Inject + public MetricsV2Handler(Executor executor, + MetricsManager metricsManager, + VespaServices vespaServices, + MetricsConsumers metricsConsumers, + NodeInfoConfig nodeInfoConfig) { + super(executor); + this.nodeInfoConfig = nodeInfoConfig; + valuesFetcher = new ValuesFetcher(metricsManager, vespaServices, metricsConsumers); + } + + @Override + public 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 { + List<MetricsPacket.Builder> builders = valuesFetcher.fetchMetricsAsBuilders(consumer); + List<MetricsPacket> metrics = processAndBuild(builders, + new ServiceIdDimensionProcessor(), + new ClusterIdDimensionProcessor(), + new PublicDimensionsProcessor(MAX_DIMENSIONS)); + + Node localNode = new Node(nodeInfoConfig.role(), nodeInfoConfig.hostname(), 0, ""); + Map<Node, List<MetricsPacket>> metricsByNode = singletonMap(localNode, metrics); + return new JsonResponse(OK, toGenericApplicationModel(metricsByNode).serialize()); + } catch (Exception e) { + log.log(Level.WARNING, "Got exception when rendering metrics:", e); + return new ErrorResponse(INTERNAL_SERVER_ERROR, e.getMessage()); + } + } + + private static List<MetricsPacket> processAndBuild(List<MetricsPacket.Builder> builders, + MetricsProcessor... processors) { + return builders.stream() + .map(builder -> applyProcessors(builder, processors)) + .map(MetricsPacket.Builder::build) + .collect(toList()); + } + +} diff --git a/metrics-proxy/src/main/resources/configdefinitions/node-info.def b/metrics-proxy/src/main/resources/configdefinitions/node-info.def new file mode 100644 index 00000000000..e66433a96d0 --- /dev/null +++ b/metrics-proxy/src/main/resources/configdefinitions/node-info.def @@ -0,0 +1,5 @@ +# Copyright 2020 Oath Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package=ai.vespa.metricsproxy.http.metrics + +role string +hostname string 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 index 77c3a719cd9..d776368687d 100644 --- a/metrics-proxy/src/test/java/ai/vespa/metricsproxy/http/HttpHandlerTestBase.java +++ b/metrics-proxy/src/test/java/ai/vespa/metricsproxy/http/HttpHandlerTestBase.java @@ -24,6 +24,7 @@ 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.metric.dimensions.PublicDimensions.REASON; import static ai.vespa.metricsproxy.service.DummyService.METRIC_1; /** @@ -61,11 +62,12 @@ public class HttpHandlerTestBase { } protected static MetricsConsumers getMetricsConsumers() { + // Must use a whitelisted dimension to avoid it being removed for the MetricsV2Handler var defaultConsumerDimension = new ConsumersConfig.Consumer.Metric.Dimension.Builder() - .key("consumer-dim").value("default-val"); + .key(REASON).value("default-val"); var customConsumerDimension = new ConsumersConfig.Consumer.Metric.Dimension.Builder() - .key("consumer-dim").value("custom-val"); + .key(REASON).value("custom-val"); return new MetricsConsumers(new ConsumersConfig.Builder() .consumer(new ConsumersConfig.Consumer.Builder() diff --git a/metrics-proxy/src/test/java/ai/vespa/metricsproxy/http/metrics/MetricsHandlerTestBase.java b/metrics-proxy/src/test/java/ai/vespa/metricsproxy/http/metrics/MetricsHandlerTestBase.java new file mode 100644 index 00000000000..1c5ce695155 --- /dev/null +++ b/metrics-proxy/src/test/java/ai/vespa/metricsproxy/http/metrics/MetricsHandlerTestBase.java @@ -0,0 +1,196 @@ +// Copyright 2020 Oath Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package ai.vespa.metricsproxy.http.metrics; + +import ai.vespa.metricsproxy.http.HttpHandlerTestBase; +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 com.fasterxml.jackson.databind.ObjectMapper; +import org.json.JSONArray; +import org.json.JSONObject; +import org.junit.Ignore; +import org.junit.Test; + +import java.io.IOException; + +import static ai.vespa.metricsproxy.metric.dimensions.PublicDimensions.INTERNAL_SERVICE_ID; +import static ai.vespa.metricsproxy.metric.dimensions.PublicDimensions.REASON; +import static ai.vespa.metricsproxy.metric.dimensions.PublicDimensions.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; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +/** + * @author gjoranv + */ +public abstract class MetricsHandlerTestBase<MODEL> extends HttpHandlerTestBase { + + static String rootUri; + static String valuesUri; + + Class<MODEL> modelClass; + + abstract GenericJsonModel getGenericJsonModel(MODEL model); + + private MODEL getResponseAsJsonModel(String consumer) { + String response = testDriver.sendRequest(valuesUri + "?consumer=" + consumer).readAll(); + try { + return createObjectMapper().readValue(response, modelClass); + } catch (IOException e) { + fail("Failed to create json model: " + e.getMessage()); + throw new RuntimeException(e); + } + } + + private GenericJsonModel getResponseAsGenericJsonModel(String consumer) { + return getGenericJsonModel(getResponseAsJsonModel(consumer)); + } + + @Test + public void invalid_path_yields_error_response() throws Exception { + String response = testDriver.sendRequest(rootUri + "/invalid").readAll(); + JSONObject root = new JSONObject(response); + assertTrue(root.has("error")); + } + + @Test + public void root_response_contains_values_uri() throws Exception { + String response = testDriver.sendRequest(rootUri).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(valuesUri, valuesUrl.getString("url")); + } + + @Ignore + @Test + public void visually_inspect_values_response() throws Exception { + String response = testDriver.sendRequest(valuesUri).readAll(); + ObjectMapper mapper = createObjectMapper(); + var jsonModel = mapper.readValue(response, modelClass); + System.out.println(mapper.writerWithDefaultPrettyPrinter().writeValueAsString(jsonModel)); + } + + @Test + public void no_explicit_consumer_gives_the_default_consumer() { + String responseDefaultConsumer = testDriver.sendRequest(valuesUri + "?consumer=default").readAll(); + String responseNoConsumer = testDriver.sendRequest(valuesUri).readAll(); + assertEqualsExceptTimestamps(responseDefaultConsumer, responseNoConsumer); + } + + @Test + public void unknown_consumer_gives_the_default_consumer() { + String response = testDriver.sendRequest(valuesUri).readAll(); + String responseUnknownConsumer = testDriver.sendRequest(valuesUri + "?consumer=not_defined").readAll(); + assertEqualsExceptTimestamps(response, responseUnknownConsumer); + } + + private void assertEqualsExceptTimestamps(String s1, String s2) { + assertEquals(replaceTimestamps(s1), replaceTimestamps(s2)); + } + + private String replaceTimestamps(String s) { + return s.replaceAll("timestamp\":\\d+,", "timestamp\":1,"); + } + + @Test + public void response_contains_node_metrics() { + GenericJsonModel jsonModel = getResponseAsGenericJsonModel(DEFAULT_CONSUMER); + + assertNotNull(jsonModel.node); + assertEquals(1, jsonModel.node.metrics.size()); + assertEquals(12.345, jsonModel.node.metrics.get(0).values.get(CPU_METRIC), 0.0001d); + } + + @Test + public void response_contains_service_metrics() { + GenericJsonModel jsonModel = getResponseAsGenericJsonModel(DEFAULT_CONSUMER); + + assertEquals(2, jsonModel.services.size()); + GenericService dummyService = jsonModel.services.get(0); + assertEquals(2, dummyService.metrics.size()); + + GenericMetrics dummy0Metrics = getMetricsForService("dummy0", dummyService); + assertEquals(1L, dummy0Metrics.values.get(METRIC_1).longValue()); + assertEquals("default-val", dummy0Metrics.dimensions.get(REASON)); + + GenericMetrics dummy1Metrics = getMetricsForService("dummy1", dummyService); + assertEquals(6L, dummy1Metrics.values.get(METRIC_1).longValue()); + assertEquals("default-val", dummy1Metrics.dimensions.get(REASON)); + } + + @Test + public void custom_consumer_gets_only_its_whitelisted_metrics() { + GenericJsonModel jsonModel = getResponseAsGenericJsonModel(CUSTOM_CONSUMER); + + assertNotNull(jsonModel.node); + // TODO: see comment in ExternalMetrics.setExtraMetrics + // assertEquals(0, jsonModel.node.metrics.size()); + + assertEquals(2, jsonModel.services.size()); + GenericService dummyService = jsonModel.services.get(0); + assertEquals(2, dummyService.metrics.size()); + + GenericMetrics dummy0Metrics = getMetricsForService("dummy0", dummyService); + assertEquals("custom-val", dummy0Metrics.dimensions.get(REASON)); + + GenericMetrics dummy1Metrics = getMetricsForService("dummy1", dummyService); + assertEquals("custom-val", dummy1Metrics.dimensions.get(REASON)); + } + + private static GenericMetrics getMetricsForService(String serviceInstance, GenericService service) { + for (var metrics : service.metrics) { + if (getServiceIdDimension(metrics).equals(serviceInstance)) + return metrics; + } + fail("Could not find metrics for service instance " + serviceInstance); + throw new RuntimeException(); + } + + @Test + public void all_timestamps_are_equal_and_non_zero() { + GenericJsonModel jsonModel = getResponseAsGenericJsonModel(DEFAULT_CONSUMER); + + Long nodeTimestamp = jsonModel.node.timestamp; + assertNotEquals(0L, (long) nodeTimestamp); + for (var service : jsonModel.services) + assertEquals(nodeTimestamp, service.timestamp); + } + + @Test + public void all_consumers_get_health_from_service_that_is_down() { + assertDownServiceHealth(DEFAULT_CONSUMER); + assertDownServiceHealth(CUSTOM_CONSUMER); + } + + private void assertDownServiceHealth(String consumer) { + GenericJsonModel jsonModel = getResponseAsGenericJsonModel(consumer); + + GenericService downService = jsonModel.services.get(1); + assertEquals(DOWN.status, downService.status.code); + assertEquals("No response", downService.status.description); + + // Service should output metric dimensions, even without metrics, because they contain important info about the service. + assertEquals(1, downService.metrics.size()); + assertEquals(0, downService.metrics.get(0).values.size()); + assertFalse(downService.metrics.get(0).dimensions.isEmpty()); + assertEquals(DownService.NAME, getServiceIdDimension(downService.metrics.get(0))); + } + + private static String getServiceIdDimension(GenericMetrics metrics) { + var instanceDimension = metrics.dimensions.get(INTERNAL_SERVICE_ID); + return instanceDimension != null ? instanceDimension : metrics.dimensions.get(SERVICE_ID); + } + +} diff --git a/metrics-proxy/src/test/java/ai/vespa/metricsproxy/http/metrics/MetricsV1HandlerTest.java b/metrics-proxy/src/test/java/ai/vespa/metricsproxy/http/metrics/MetricsV1HandlerTest.java index 22f61114622..fe823466f7b 100644 --- a/metrics-proxy/src/test/java/ai/vespa/metricsproxy/http/metrics/MetricsV1HandlerTest.java +++ b/metrics-proxy/src/test/java/ai/vespa/metricsproxy/http/metrics/MetricsV1HandlerTest.java @@ -1,46 +1,30 @@ // Copyright 2020 Oath Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. package ai.vespa.metricsproxy.http.metrics; -import ai.vespa.metricsproxy.http.HttpHandlerTestBase; 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 com.fasterxml.jackson.databind.ObjectMapper; import com.yahoo.container.jdisc.RequestHandlerTestDriver; -import org.json.JSONArray; -import org.json.JSONObject; +import org.junit.Before; import org.junit.BeforeClass; -import org.junit.Ignore; -import org.junit.Test; -import java.io.IOException; import java.util.concurrent.Executors; -import static ai.vespa.metricsproxy.core.VespaMetrics.INSTANCE_DIMENSION_ID; import static ai.vespa.metricsproxy.http.metrics.MetricsV1Handler.V1_PATH; import static ai.vespa.metricsproxy.http.metrics.MetricsV1Handler.VALUES_PATH; -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; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertNotEquals; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertTrue; -import static org.junit.Assert.fail; /** * @author gjoranv */ @SuppressWarnings("UnstableApiUsage") -public class MetricsV1HandlerTest extends HttpHandlerTestBase { +public class MetricsV1HandlerTest extends MetricsHandlerTestBase<GenericJsonModel> { private static final String V1_URI = URI_BASE + V1_PATH; private static final String VALUES_URI = URI_BASE + VALUES_PATH; + @BeforeClass public static void setup() { + rootUri = V1_URI; + valuesUri = VALUES_URI; var handler = new MetricsV1Handler(Executors.newSingleThreadExecutor(), getMetricsManager(), vespaServices, @@ -48,149 +32,14 @@ public class MetricsV1HandlerTest extends HttpHandlerTestBase { testDriver = new RequestHandlerTestDriver(handler); } - private GenericJsonModel getResponseAsJsonModel(String consumer) { - String response = testDriver.sendRequest(VALUES_URI + "?consumer=" + consumer).readAll(); - try { - return createObjectMapper().readValue(response, GenericJsonModel.class); - } catch (IOException e) { - fail("Failed to create json model: " + e.getMessage()); - throw new RuntimeException(e); - } - } - - @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() throws Exception { - String response = testDriver.sendRequest(VALUES_URI).readAll(); - ObjectMapper mapper = createObjectMapper(); - var jsonModel = mapper.readValue(response, GenericJsonModel.class); - System.out.println(mapper.writerWithDefaultPrettyPrinter().writeValueAsString(jsonModel)); - } - - @Test - public void no_explicit_consumer_gives_the_default_consumer() { - String responseDefaultConsumer = testDriver.sendRequest(VALUES_URI + "?consumer=default").readAll(); - String responseNoConsumer = testDriver.sendRequest(VALUES_URI).readAll(); - assertEqualsExceptTimestamps(responseDefaultConsumer, responseNoConsumer); - } - - @Test - public void unknown_consumer_gives_the_default_consumer() { - String response = testDriver.sendRequest(VALUES_URI).readAll(); - String responseUnknownConsumer = testDriver.sendRequest(VALUES_URI + "?consumer=not_defined").readAll(); - assertEqualsExceptTimestamps(response, responseUnknownConsumer); - } - - private void assertEqualsExceptTimestamps(String s1, String s2) { - assertEquals(replaceTimestamps(s1), replaceTimestamps(s2)); - } - - @Test - public void response_contains_node_metrics() { - GenericJsonModel jsonModel = getResponseAsJsonModel(DEFAULT_CONSUMER); - - assertNotNull(jsonModel.node); - assertEquals(1, jsonModel.node.metrics.size()); - assertEquals(12.345, jsonModel.node.metrics.get(0).values.get(CPU_METRIC), 0.0001d); - } - - @Test - public void response_contains_service_metrics() { - GenericJsonModel jsonModel = getResponseAsJsonModel(DEFAULT_CONSUMER); - - assertEquals(2, jsonModel.services.size()); - GenericService dummyService = jsonModel.services.get(0); - assertEquals(2, dummyService.metrics.size()); - - GenericMetrics dummy0Metrics = getMetricsForInstance("dummy0", dummyService); - assertEquals(1L, dummy0Metrics.values.get(METRIC_1).longValue()); - assertEquals("default-val", dummy0Metrics.dimensions.get("consumer-dim")); - - GenericMetrics dummy1Metrics = getMetricsForInstance("dummy1", dummyService); - assertEquals(6L, dummy1Metrics.values.get(METRIC_1).longValue()); - assertEquals("default-val", dummy1Metrics.dimensions.get("consumer-dim")); - } - - @Test - public void all_consumers_get_health_from_service_that_is_down() { - assertDownServiceHealth(DEFAULT_CONSUMER); - assertDownServiceHealth(CUSTOM_CONSUMER); - } - - @Test - public void all_timestamps_are_equal_and_non_zero() { - GenericJsonModel jsonModel = getResponseAsJsonModel(DEFAULT_CONSUMER); - - Long nodeTimestamp = jsonModel.node.timestamp; - assertNotEquals(0L, (long) nodeTimestamp); - for (var service : jsonModel.services) - assertEquals(nodeTimestamp, service.timestamp); - } - - @Test - public void custom_consumer_gets_only_its_whitelisted_metrics() { - GenericJsonModel jsonModel = getResponseAsJsonModel(CUSTOM_CONSUMER); - - assertNotNull(jsonModel.node); - // TODO: see comment in ExternalMetrics.setExtraMetrics - // assertEquals(0, jsonModel.node.metrics.size()); - - assertEquals(2, jsonModel.services.size()); - GenericService dummyService = jsonModel.services.get(0); - assertEquals(2, dummyService.metrics.size()); - - GenericMetrics dummy0Metrics = getMetricsForInstance("dummy0", dummyService); - assertEquals("custom-val", dummy0Metrics.dimensions.get("consumer-dim")); - - GenericMetrics dummy1Metrics = getMetricsForInstance("dummy1", dummyService); - 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); - - GenericService downService = jsonModel.services.get(1); - assertEquals(DOWN.status, downService.status.code); - assertEquals("No response", downService.status.description); - - // Service should output metric dimensions, even without metrics, because they contain important info about the service. - assertEquals(1, downService.metrics.size()); - assertEquals(0, downService.metrics.get(0).values.size()); - assertFalse(downService.metrics.get(0).dimensions.isEmpty()); - assertEquals(DownService.NAME, downService.metrics.get(0).dimensions.get(INSTANCE_DIMENSION_ID.id)); - } - - private String replaceTimestamps(String s) { - return s.replaceAll("timestamp\":\\d+,", "timestamp\":1,"); + @Before + public void initModelClass() { + modelClass = GenericJsonModel.class; } - private static GenericMetrics getMetricsForInstance(String instance, GenericService service) { - for (var metrics : service.metrics) { - if (metrics.dimensions.get(INSTANCE_DIMENSION_ID.id).equals(instance)) - return metrics; - } - fail("Could not find metrics for service instance " + instance); - throw new RuntimeException(); + @Override + GenericJsonModel getGenericJsonModel(GenericJsonModel genericJsonModel) { + return genericJsonModel; } } diff --git a/metrics-proxy/src/test/java/ai/vespa/metricsproxy/http/metrics/MetricsV2HandlerTest.java b/metrics-proxy/src/test/java/ai/vespa/metricsproxy/http/metrics/MetricsV2HandlerTest.java new file mode 100644 index 00000000000..27ee6be4be3 --- /dev/null +++ b/metrics-proxy/src/test/java/ai/vespa/metricsproxy/http/metrics/MetricsV2HandlerTest.java @@ -0,0 +1,53 @@ +// Copyright 2020 Oath Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package ai.vespa.metricsproxy.http.metrics; + +import ai.vespa.metricsproxy.metric.model.json.GenericApplicationModel; +import ai.vespa.metricsproxy.metric.model.json.GenericJsonModel; +import com.yahoo.container.jdisc.RequestHandlerTestDriver; +import org.junit.Before; +import org.junit.BeforeClass; + +import java.util.concurrent.Executors; + +import static ai.vespa.metricsproxy.http.metrics.MetricsV2Handler.V2_PATH; +import static ai.vespa.metricsproxy.http.metrics.MetricsV2Handler.VALUES_PATH; + +/** + * @author gjoranv + */ +@SuppressWarnings("UnstableApiUsage") +public class MetricsV2HandlerTest extends MetricsHandlerTestBase<GenericApplicationModel> { + + private static final String V2_URI = URI_BASE + V2_PATH; + private static final String VALUES_URI = URI_BASE + VALUES_PATH; + + + @BeforeClass + public static void setup() { + rootUri = V2_URI; + valuesUri = VALUES_URI; + var handler = new MetricsV2Handler(Executors.newSingleThreadExecutor(), + getMetricsManager(), + vespaServices, + getMetricsConsumers(), + nodeInfoConfig()); + testDriver = new RequestHandlerTestDriver(handler); + } + + @Before + public void initModelClass() { + modelClass = GenericApplicationModel.class; + } + + @Override + GenericJsonModel getGenericJsonModel(GenericApplicationModel genericApplicationModel) { + return genericApplicationModel.nodes.get(0); + } + + private static NodeInfoConfig nodeInfoConfig() { + return new NodeInfoConfig.Builder() + .role("my-role") + .hostname("my-hostname") + .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 index a85f0425b4b..a224c4090b3 100644 --- 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 @@ -12,6 +12,7 @@ import org.junit.Test; import java.util.concurrent.Executors; +import static ai.vespa.metricsproxy.metric.dimensions.PublicDimensions.REASON; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; @@ -83,7 +84,7 @@ public class PrometheusHandlerTest extends HttpHandlerTestBase { @Test public void service_metrics_have_configured_dimensions() { String dummy0 = getLine(valuesResponse, DummyService.NAME + "0"); - assertTrue(dummy0.contains("consumer_dim=\"default-val\"")); + assertTrue(dummy0.contains(REASON + "=\"default-val\"")); } @Test |