diff options
Diffstat (limited to 'metrics-proxy/src/test')
28 files changed, 2208 insertions, 0 deletions
diff --git a/metrics-proxy/src/test/java/ai/vespa/metricsproxy/TestUtil.java b/metrics-proxy/src/test/java/ai/vespa/metricsproxy/TestUtil.java new file mode 100644 index 00000000000..5997f85a46f --- /dev/null +++ b/metrics-proxy/src/test/java/ai/vespa/metricsproxy/TestUtil.java @@ -0,0 +1,27 @@ +/* + * Copyright 2019 Oath Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. + */ + +package ai.vespa.metricsproxy; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.util.stream.Collectors; + +/** + * @author gjoranv + */ +public class TestUtil { + + public static String getContents(String filename) { + InputStream in = TestUtil.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/metrics-proxy/src/test/java/ai/vespa/metricsproxy/core/MetricsManagerTest.java b/metrics-proxy/src/test/java/ai/vespa/metricsproxy/core/MetricsManagerTest.java new file mode 100644 index 00000000000..81634323269 --- /dev/null +++ b/metrics-proxy/src/test/java/ai/vespa/metricsproxy/core/MetricsManagerTest.java @@ -0,0 +1,244 @@ +/* + * Copyright 2019 Oath Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. + */ + +package ai.vespa.metricsproxy.core; + +import ai.vespa.metricsproxy.core.ConsumersConfig.Consumer; +import ai.vespa.metricsproxy.metric.ExternalMetrics; +import ai.vespa.metricsproxy.metric.Metric; +import ai.vespa.metricsproxy.metric.Metrics; +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.DimensionId; +import ai.vespa.metricsproxy.metric.model.MetricsPacket; +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 org.junit.Before; +import org.junit.Test; + +import java.time.Instant; +import java.util.List; +import java.util.Map; + +import static ai.vespa.metricsproxy.core.MetricsManager.VESPA_VERSION; +import static ai.vespa.metricsproxy.core.VespaMetrics.METRIC_TYPE_DIMENSION_ID; +import static ai.vespa.metricsproxy.core.VespaMetrics.VESPA_CONSUMER_ID; +import static ai.vespa.metricsproxy.metric.ExternalMetrics.ROLE_DIMENSION; +import static ai.vespa.metricsproxy.metric.model.DimensionId.toDimensionId; +import static ai.vespa.metricsproxy.metric.model.MetricId.toMetricId; +import static ai.vespa.metricsproxy.metric.model.ServiceId.toServiceId; +import static org.hamcrest.CoreMatchers.containsString; +import static org.hamcrest.CoreMatchers.is; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.assertTrue; + +/** + * @author gjoranv + */ +public class MetricsManagerTest { + + private MetricsManager metricsManager; + + private static final String SERVICE_0_ID = "dummy/id/0"; + private static final String SERVICE_1_ID = "dummy/id/1"; + + private static final List<VespaService> testServices = ImmutableList.of( + new DummyService(0, SERVICE_0_ID), + new DummyService(1, SERVICE_1_ID)); + + private static final String WHITELISTED_METRIC_ID = "whitelisted"; + + @Before + public void setupMetricsManager() { + metricsManager = getMetricsManager(); + } + + @Test + public void each_service_gets_separate_metrics_packets() { + List<MetricsPacket> packets = metricsManager.getMetrics(testServices, Instant.EPOCH); + assertThat(packets.size(), is(2)); + + assertThat(packets.get(0).dimensions().get(toDimensionId("instance")), is("dummy0")); + assertThat(packets.get(0).metrics().get(toMetricId("c.test")), is(1.0)); + assertThat(packets.get(0).metrics().get(toMetricId("val")), is(1.05)); + + assertThat(packets.get(1).dimensions().get(toDimensionId("instance")), is("dummy1")); + assertThat(packets.get(1).metrics().get(toMetricId("c.test")), is(6.0)); + assertThat(packets.get(1).metrics().get(toMetricId("val")), is(2.35)); + } + + @Test + public void verify_expected_output_from_getMetricsById() { + String dummy0Metrics = metricsManager.getMetricsByConfigId("dummy/id/0"); + assertThat(dummy0Metrics, containsString("'dummy.id.0'.val=1.050")); + assertThat(dummy0Metrics, containsString("'dummy.id.0'.c_test=1")); + + String dummy1Metrics = metricsManager.getMetricsByConfigId("dummy/id/1"); + assertThat(dummy1Metrics, containsString("'dummy.id.1'.val=2.350")); + assertThat(dummy1Metrics, containsString("'dummy.id.1'.c_test=6")); + } + + @Test + public void getServices_returns_service_types() { + assertThat(metricsManager.getAllVespaServices(), is("dummy")); + } + + @Test + public void global_dimensions_are_added_but_do_not_override_metric_dimensions() { + List<MetricsPacket> packets = metricsManager.getMetrics(testServices, Instant.EPOCH); + assertEquals(2, packets.size()); + assertGlobalDimensions(packets.get(0).dimensions()); + assertGlobalDimensions(packets.get(1).dimensions()); + } + + private void assertGlobalDimensions(Map<DimensionId, String> dimensions) { + assertTrue(dimensions.containsKey(VESPA_VERSION)); + assertEquals("value", dimensions.get(toDimensionId("global"))); + assertEquals("metric-dim", dimensions.get(toDimensionId("dim0"))); + } + + + @Test + public void system_metrics_are_added() { + VespaService service0 = testServices.get(0); + Metrics oldSystemMetrics = service0.getSystemMetrics(); + + service0.getSystemMetrics().add(new Metric("cpu", 1)); + + List<MetricsPacket> packets = metricsManager.getMetrics(testServices, Instant.EPOCH); + assertEquals(3, packets.size()); + + MetricsPacket systemPacket = packets.get(0); // system metrics are added before other metrics + assertThat(systemPacket.metrics().get(toMetricId("cpu")), is(1.0)); + assertThat(systemPacket.dimensions().get(toDimensionId("metrictype")), is("system")); + + service0.setSystemMetrics(oldSystemMetrics); + } + + @Test + public void extra_metrics_packets_containing_whitelisted_metrics_are_added() { + metricsManager.setExtraMetrics(ImmutableList.of( + new MetricsPacket.Builder(toServiceId("foo")) + .putMetrics(ImmutableList.of(new Metric(WHITELISTED_METRIC_ID, 0))))); + + List<MetricsPacket> packets = metricsManager.getMetrics(testServices, Instant.EPOCH); + assertThat(packets.size(), is(3)); + } + + @Test + public void extra_metrics_packets_without_whitelisted_metrics_are_not_added() { + metricsManager.setExtraMetrics(ImmutableList.of( + new MetricsPacket.Builder(toServiceId("foo")) + .putMetrics(ImmutableList.of(new Metric("not-whitelisted", 0))))); + + List<MetricsPacket> packets = metricsManager.getMetrics(testServices, Instant.EPOCH); + assertThat(packets.size(), is(2)); + } + + @Test + public void extra_dimensions_are_added_to_metrics_packets_that_do_not_have_those_dimensions() { + metricsManager.setExtraMetrics(ImmutableList.of( + new MetricsPacket.Builder(toServiceId("foo")) + .putMetrics(ImmutableList.of(new Metric(WHITELISTED_METRIC_ID, 0))) + .putDimension(ROLE_DIMENSION, "role from extraMetrics"))); + + List<MetricsPacket> packets = metricsManager.getMetrics(testServices, Instant.EPOCH); + for (MetricsPacket packet : packets) { + assertThat(packet.dimensions().get(ROLE_DIMENSION), is("role from extraMetrics")); + } + } + + @Test + public void extra_dimensions_do_not_overwrite_existing_dimension_values() { + metricsManager.setExtraMetrics(ImmutableList.of( + new MetricsPacket.Builder(toServiceId("foo")) + .putMetrics(ImmutableList.of(new Metric(WHITELISTED_METRIC_ID, 0))) + .putDimension(METRIC_TYPE_DIMENSION_ID, "from extraMetrics"))); + + List<MetricsPacket> packets = metricsManager.getMetrics(testServices, Instant.EPOCH); + assertThat(packets.get(0).dimensions().get(METRIC_TYPE_DIMENSION_ID), is("standard")); + assertThat(packets.get(1).dimensions().get(METRIC_TYPE_DIMENSION_ID), is("standard")); + assertThat(packets.get(2).dimensions().get(METRIC_TYPE_DIMENSION_ID), is("from extraMetrics")); + } + + @Test + public void timestamp_is_adjusted_when_metric_is_less_than_one_minute_younger_than_start_time() { + Instant START_TIME = Instant.ofEpochSecond(0); + Instant METRIC_TIME = Instant.ofEpochSecond(59); + assertEquals(START_TIME, getAdjustedTimestamp(START_TIME, METRIC_TIME)); + } + + @Test + public void timestamp_is_adjusted_when_metric_is_less_than_one_minute_older_than_start_time() { + Instant START_TIME = Instant.ofEpochSecond(59); + Instant METRIC_TIME = Instant.ofEpochSecond(0); + assertEquals(START_TIME, getAdjustedTimestamp(START_TIME, METRIC_TIME)); + } + + @Test + public void timestamp_is_not_adjusted_when_metric_is_at_least_one_minute_younger_than_start_time() { + Instant START_TIME = Instant.ofEpochSecond(0); + Instant METRIC_TIME = Instant.ofEpochSecond(60); + assertEquals(METRIC_TIME, getAdjustedTimestamp(START_TIME, METRIC_TIME)); + } + + @Test + public void timestamp_is_not_adjusted_when_metric_is_at_least_one_minute_older_than_start_time() { + Instant START_TIME = Instant.ofEpochSecond(60); + Instant METRIC_TIME = Instant.ofEpochSecond(0); + assertEquals(METRIC_TIME, getAdjustedTimestamp(START_TIME, METRIC_TIME)); + } + + private Instant getAdjustedTimestamp(Instant startTime, Instant metricTime) { + MetricsPacket.Builder builder = new MetricsPacket.Builder(toServiceId("foo")) + .timestamp(metricTime.getEpochSecond()); + return MetricsManager.adjustTimestamp(builder, startTime).getTimestamp(); + } + + private MetricsManager getMetricsManager() { + VespaServices vespaServices = new VespaServices(testServices); + MetricsConsumers consumers = getMetricsConsumers(); + VespaMetrics metrics = new VespaMetrics(consumers, vespaServices); + + return new MetricsManager(vespaServices, metrics, new ExternalMetrics(consumers), + getApplicationDimensions(),getNodeDimensions()); + } + + private static MetricsConsumers getMetricsConsumers() { + Consumer.Metric.Dimension.Builder metricDimension = new Consumer.Metric.Dimension.Builder() + .key("dim0").value("metric-dim"); + + return new MetricsConsumers(new ConsumersConfig.Builder() + .consumer(new Consumer.Builder() + .name(VESPA_CONSUMER_ID.id) + .metric(new Consumer.Metric.Builder() + .name(WHITELISTED_METRIC_ID) + .outputname(WHITELISTED_METRIC_ID)) + .metric(new Consumer.Metric.Builder() + .name(DummyService.METRIC_1) + .outputname(DummyService.METRIC_1) + .dimension(metricDimension)) + .metric(new Consumer.Metric.Builder() + .name(DummyService.METRIC_2) + .outputname(DummyService.METRIC_2) + .dimension(metricDimension))) + .build()); + } + + private ApplicationDimensions getApplicationDimensions() { + return new ApplicationDimensions(new ApplicationDimensionsConfig.Builder() + .dimensions("global", "value").build()); + } + + private NodeDimensions getNodeDimensions() { + return new NodeDimensions(new NodeDimensionsConfig.Builder() + .dimensions("dim0", "should not override metric dim").build()); + } + +} diff --git a/metrics-proxy/src/test/java/ai/vespa/metricsproxy/metric/ExternalMetricsTest.java b/metrics-proxy/src/test/java/ai/vespa/metricsproxy/metric/ExternalMetricsTest.java new file mode 100644 index 00000000000..11c271d46e4 --- /dev/null +++ b/metrics-proxy/src/test/java/ai/vespa/metricsproxy/metric/ExternalMetricsTest.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.metric; + +import ai.vespa.metricsproxy.core.ConsumersConfig; +import ai.vespa.metricsproxy.core.MetricsConsumers; +import ai.vespa.metricsproxy.metric.model.ConsumerId; +import ai.vespa.metricsproxy.metric.model.MetricsPacket; +import com.google.common.collect.ImmutableList; +import org.junit.Test; + +import java.util.List; + +import static ai.vespa.metricsproxy.metric.ExternalMetrics.VESPA_NODE_SERVICE_ID; +import static ai.vespa.metricsproxy.metric.model.ConsumerId.toConsumerId; +import static ai.vespa.metricsproxy.metric.model.ServiceId.toServiceId; +import static org.junit.Assert.assertEquals; + +/** + * @author gjoranv + */ +public class ExternalMetricsTest { + private static final ConsumerId CUSTOM_CONSUMER_1 = toConsumerId("consumer-1"); + private static final ConsumerId CUSTOM_CONSUMER_2 = toConsumerId("consumer-2"); + + @Test + public void extra_metrics_are_added() { + MetricsConsumers noConsumers = new MetricsConsumers(new ConsumersConfig.Builder().build()); + ExternalMetrics externalMetrics = new ExternalMetrics(noConsumers); + + externalMetrics.setExtraMetrics(ImmutableList.of( + new MetricsPacket.Builder(toServiceId("foo")))); + + List<MetricsPacket.Builder> packets = externalMetrics.getMetrics(); + assertEquals(1, packets.size()); + } + + @Test + public void service_id_is_set_to_vespa_node_id() { + MetricsConsumers noConsumers = new MetricsConsumers(new ConsumersConfig.Builder().build()); + ExternalMetrics externalMetrics = new ExternalMetrics(noConsumers); + externalMetrics.setExtraMetrics(ImmutableList.of( + new MetricsPacket.Builder(toServiceId("replace_with_vespa_node_id")))); + + List<MetricsPacket.Builder> packets = externalMetrics.getMetrics(); + assertEquals(1, packets.size()); + assertEquals(VESPA_NODE_SERVICE_ID, packets.get(0).build().service); + } + + @Test + public void custom_consumers_are_added() { + ConsumersConfig consumersConfig = new ConsumersConfig.Builder() + .consumer(new ConsumersConfig.Consumer.Builder().name(CUSTOM_CONSUMER_1.id)) + .consumer(new ConsumersConfig.Consumer.Builder().name(CUSTOM_CONSUMER_2.id)) + .build(); + MetricsConsumers consumers = new MetricsConsumers(consumersConfig); + ExternalMetrics externalMetrics = new ExternalMetrics(consumers); + + externalMetrics.setExtraMetrics(ImmutableList.of( + new MetricsPacket.Builder(toServiceId("foo")))); + + List<MetricsPacket.Builder> packets = externalMetrics.getMetrics(); + assertEquals(1, packets.size()); + + List<ConsumerId> consumerIds = packets.get(0).build().consumers(); + assertEquals(2, consumerIds.size()); + assertEquals(CUSTOM_CONSUMER_1, consumerIds.get(0)); + assertEquals(CUSTOM_CONSUMER_2, consumerIds.get(1)); + } + +} diff --git a/metrics-proxy/src/test/java/ai/vespa/metricsproxy/metric/MetricsTest.java b/metrics-proxy/src/test/java/ai/vespa/metricsproxy/metric/MetricsTest.java new file mode 100644 index 00000000000..b9e6377c27b --- /dev/null +++ b/metrics-proxy/src/test/java/ai/vespa/metricsproxy/metric/MetricsTest.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.metric; + +import ai.vespa.metricsproxy.service.DummyService; +import ai.vespa.metricsproxy.service.VespaService; +import org.junit.Test; + +import java.util.HashMap; +import java.util.Map; + +import static org.hamcrest.CoreMatchers.is; +import static org.junit.Assert.assertThat; + +/** + * @author Unknowm + */ +public class MetricsTest { + + @Test + public void testIterator() { + Metrics m = new Metrics(); + long now = System.currentTimeMillis() / 1000; + m.add(new Metric("a", 1, now)); + m.add(new Metric("b", 2.5, now)); + + //should expire after 0 seconds + m.add(new Metric("c", 2, now)); + + Map<String, Number> map = new HashMap<>(); + + for (Metric metric: m.getMetrics()) { + String k = metric.getName(); + + assertThat(map.containsKey(k), is(false)); + map.put(k, metric.getValue()); + } + + assertThat(map.get("a").intValue(), is(1)); + assertThat(map.get("b").doubleValue(), is(2.5)); + } + + @Test + public void testBasicMetric() { + Metrics m = new Metrics(); + m.add(new Metric("count", 1, System.currentTimeMillis() / 1000)); + assertThat(m.get("count").intValue(), is(1)); + } + + @Test + public void testHealthMetric() { + HealthMetric m = HealthMetric.get(null, null); + assertThat(m.isOk(), is(false)); + m = HealthMetric.get("up", "test message"); + assertThat(m.isOk(), is(true)); + assertThat(m.getMessage(), is("test message")); + m = HealthMetric.get("ok", "test message"); + assertThat(m.isOk(), is(true)); + assertThat(m.getMessage(), is("test message")); + + m = HealthMetric.get("bad", "test message"); + assertThat(m.isOk(), is(false)); + assertThat(m.getStatus(), is("bad")); + } + + @Test + public void testMetricFormatter() { + MetricsFormatter formatter = new MetricsFormatter(false, false); + VespaService service = new DummyService(0, "config.id"); + String data = formatter.format(service, "key", 1); + assertThat(data, is("'config.id'.key=1")); + + formatter = new MetricsFormatter(true, false); + data = formatter.format(service, "key", 1); + assertThat(data, is("dummy.'config.id'.key=1")); + + + formatter = new MetricsFormatter(true, true); + data = formatter.format(service, "key", 1); + assertThat(data, is("dummy.config.'id'.key=1")); + + formatter = new MetricsFormatter(false, true); + data = formatter.format(service, "key", 1); + assertThat(data, is("config.'id'.key=1")); + } + + @Test + public void testTimeAdjustment() { + assertThat(Metric.adjustTime(0L, 0L), is(0L)); + assertThat(Metric.adjustTime(59L, 59L), is(59L)); + assertThat(Metric.adjustTime(60L, 60L), is(60L)); + assertThat(Metric.adjustTime(59L, 60L), is(60L)); + assertThat(Metric.adjustTime(60L, 59L), is(60L)); + assertThat(Metric.adjustTime(59L, 61L), is(59L)); + } + +} diff --git a/metrics-proxy/src/test/java/ai/vespa/metricsproxy/metric/model/MetricsPacketTest.java b/metrics-proxy/src/test/java/ai/vespa/metricsproxy/metric/model/MetricsPacketTest.java new file mode 100644 index 00000000000..d522a56a9ac --- /dev/null +++ b/metrics-proxy/src/test/java/ai/vespa/metricsproxy/metric/model/MetricsPacketTest.java @@ -0,0 +1,111 @@ +/* + * 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; + +import ai.vespa.metricsproxy.metric.Metric; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; +import org.junit.Test; + +import java.util.Collections; +import java.util.List; +import java.util.Map; + +import static ai.vespa.metricsproxy.metric.model.ConsumerId.toConsumerId; +import static ai.vespa.metricsproxy.metric.model.MetricId.toMetricId; +import static ai.vespa.metricsproxy.metric.model.ServiceId.toServiceId; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +/** + * @author gjoranv + */ +public class MetricsPacketTest { + + @Test + public void service_cannot_be_null() { + try { + MetricsPacket packet = new MetricsPacket.Builder(null) + .statusCode(0) + .statusMessage("") + .timestamp(0L) + .build(); + fail("Expected exception due to null service."); + } catch (Exception e) { + assertEquals("Service cannot be null.", e.getMessage()); + } + } + + @Test + public void consumers_are_always_distinct() { + ConsumerId DUPLICATE_CONSUMER = toConsumerId("duplicateConsumer"); + + MetricsPacket packet = new MetricsPacket.Builder(toServiceId("foo")) + .statusCode(0) + .statusMessage("") + .addConsumers(Collections.singleton(DUPLICATE_CONSUMER)) + .addConsumers(Collections.singleton(DUPLICATE_CONSUMER)) + .build(); + assertEquals(1, packet.consumers().size()); + } + + @Test + public void builder_can_retain_subset_of_metrics() { + MetricsPacket packet = new MetricsPacket.Builder(toServiceId("foo")) + .putMetrics(ImmutableList.of( + new Metric("remove", 1), + new Metric("keep", 2))) + .retainMetrics(ImmutableSet.of(toMetricId("keep"), toMetricId("non-existent"))) + .build(); + + assertFalse("should not contain 'remove'", packet.metrics().containsKey(toMetricId("remove"))); + assertTrue("should contain 'keep'", packet.metrics().containsKey(toMetricId("keep"))); + assertFalse("should not contain 'non-existent'", packet.metrics().containsKey(toMetricId("non-existent"))); + } + + @Test + public void builder_applies_output_names() { + String ONE = "one"; + String TWO = "two"; + String THREE = "three"; + String NON_EXISTENT = "non-existent"; + MetricId ONE_ID = toMetricId(ONE); + MetricId TWO_ID = toMetricId(TWO); + MetricId THREE_ID = toMetricId(THREE); + MetricId NON_EXISTENT_ID = toMetricId(NON_EXISTENT); + + Map<MetricId, List<String>> outputNamesById = ImmutableMap.of( + toMetricId(ONE), ImmutableList.of(ONE), + toMetricId(TWO), ImmutableList.of(TWO, "dos"), + toMetricId(THREE), ImmutableList.of("3"), + toMetricId(NON_EXISTENT), ImmutableList.of(NON_EXISTENT)); + + MetricsPacket packet = new MetricsPacket.Builder(toServiceId("foo")) + .putMetrics(ImmutableList.of( + new Metric(ONE, 1), + new Metric(TWO, 2), + new Metric(THREE, 3))) + .applyOutputNames(outputNamesById) + .build(); + + // Only original name + assertTrue(packet.metrics().containsKey(ONE_ID)); + + // Both names + assertTrue(packet.metrics().containsKey(TWO_ID)); + assertTrue(packet.metrics().containsKey(toMetricId("dos"))); + + // Only new name + assertFalse(packet.metrics().containsKey(THREE_ID)); + assertTrue(packet.metrics().containsKey(toMetricId("3"))); + + // Non-existent metric not added + assertFalse(packet.metrics().containsKey(NON_EXISTENT_ID)); + } + +} diff --git a/metrics-proxy/src/test/java/ai/vespa/metricsproxy/metric/model/json/JsonUtilTest.java b/metrics-proxy/src/test/java/ai/vespa/metricsproxy/metric/model/json/JsonUtilTest.java new file mode 100644 index 00000000000..28912293fdb --- /dev/null +++ b/metrics-proxy/src/test/java/ai/vespa/metricsproxy/metric/model/json/JsonUtilTest.java @@ -0,0 +1,63 @@ +/* + * 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.json; + +import ai.vespa.metricsproxy.metric.model.MetricsPacket; +import org.junit.Test; + +import java.util.List; + +import static ai.vespa.metricsproxy.metric.model.ServiceId.toServiceId; +import static ai.vespa.metricsproxy.metric.model.json.JsonUtil.toMetricsPackets; +import static java.util.Collections.singleton; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +/** + * @author gjoranv + */ +public class JsonUtilTest { + @Test + public void json_model_gets_null_status_by_default() { + MetricsPacket packet = new MetricsPacket.Builder(toServiceId("foo")) + .build(); + YamasJsonModel jsonModel = JsonUtil.toYamasArray(singleton(packet)).metrics.get(0); + assertNull(jsonModel.status_code); + assertNull(jsonModel.status_msg); + } + + @Test + public void status_is_included_in_json_model_when_explicitly_asked_for() { + MetricsPacket packet = new MetricsPacket.Builder(toServiceId("foo")) + .build(); + YamasJsonModel jsonModel = JsonUtil.toYamasArray(singleton(packet), true).metrics.get(0); + assertNotNull(jsonModel.status_code); + assertNotNull(jsonModel.status_msg); + } + + @Test + public void timestamp_0_in_packet_is_translated_to_null_in_json_model() { + MetricsPacket packet = new MetricsPacket.Builder(toServiceId("foo")) + .timestamp(0L) + .build(); + YamasJsonModel jsonModel = JsonUtil.toYamasArray(singleton(packet)).metrics.get(0); + assertNull(jsonModel.timestamp); + } + + @Test + public void empty_consumers_is_translated_to_null_routing_in_json_model() { + MetricsPacket packet = new MetricsPacket.Builder(toServiceId("foo")) + .build(); + YamasJsonModel jsonModel = JsonUtil.toYamasArray(singleton(packet)).metrics.get(0); + assertNull(jsonModel.routing); + } + + @Test + public void empty_json_string_yields_empty_packet_list() { + List<MetricsPacket.Builder> builders = toMetricsPackets(""); + assertTrue(builders.isEmpty()); + } +} diff --git a/metrics-proxy/src/test/java/ai/vespa/metricsproxy/metric/model/json/YamasJsonModelTest.java b/metrics-proxy/src/test/java/ai/vespa/metricsproxy/metric/model/json/YamasJsonModelTest.java new file mode 100644 index 00000000000..e91ff32e3b4 --- /dev/null +++ b/metrics-proxy/src/test/java/ai/vespa/metricsproxy/metric/model/json/YamasJsonModelTest.java @@ -0,0 +1,94 @@ +/* + * 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.json; + +import ai.vespa.metricsproxy.metric.model.MetricsPacket; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.Test; + +import java.io.BufferedReader; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.Collections; + +import static ai.vespa.metricsproxy.metric.model.ConsumerId.toConsumerId; +import static ai.vespa.metricsproxy.metric.model.MetricId.toMetricId; +import static ai.vespa.metricsproxy.metric.model.ServiceId.toServiceId; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; + +/** + * Tests for YamasJsonModel and YamasArrayJsonModel + * + * @author smorgrav + * @author gjoranv + */ +public class YamasJsonModelTest { + + private static final String EXPECTED_JSON = "{\"metrics\":[{\"status_code\":0,\"timestamp\":1400047900,\"application\":\"vespa.searchnode\",\"metrics\":{\"cpu\":55.5555555555555,\"memory_virt\":22222222222,\"memory_rss\":5555555555},\"dimensions\":{\"applicationName\":\"app\",\"tenantName\":\"tenant\",\"metrictype\":\"system\",\"instance\":\"searchnode\",\"applicationInstance\":\"default\",\"clustername\":\"cluster\"},\"routing\":{\"yamas\":{\"namespaces\":[\"Vespa\"]}},\"status_msg\":\"Data collected successfully\"}]}"; + + @Test + public void array_definition_creates_correct_json() throws IOException { + YamasJsonModel jsonModel = getYamasJsonModel("yamas-array.json"); + + YamasArrayJsonModel yamasData = new YamasArrayJsonModel(); + yamasData.add(jsonModel); + + assertEquals(EXPECTED_JSON, yamasData.serialize()); + } + + @Test + public void deserialize_serialize_roundtrip() throws IOException { + YamasJsonModel jsonModel = getYamasJsonModel("yamas-array.json"); + + // Do some sanity checking + assertEquals("vespa.searchnode", jsonModel.application); + assertEquals("Vespa", jsonModel.routing.get("yamas").namespaces.get(0)); + assertEquals(5.555555555E9, jsonModel.metrics.get("memory_rss"), 0.1d); //Not using custom double renderer + + // Serialize and verify + YamasArrayJsonModel yamasArray = new YamasArrayJsonModel(); + yamasArray.add(jsonModel); + String string = yamasArray.serialize(); + assertEquals(EXPECTED_JSON, string); + } + + @Test + public void deserialize_serialize_roundtrip_with_metrics_packet() throws IOException { + YamasJsonModel jsonModel = getYamasJsonModel("yamas-array.json"); + MetricsPacket metricsPacket = JsonUtil.toMetricsPacketBuilder(jsonModel).build(); + + // Do some sanity checking + assertEquals(toServiceId("vespa.searchnode"), metricsPacket.service); + assertEquals(toConsumerId("Vespa"), metricsPacket.consumers().get(0)); + assertEquals(5.555555555E9, metricsPacket.metrics().get(toMetricId("memory_rss")).doubleValue(), 0.1d); //Not using custom double rendrer + + // Serialize and verify + YamasArrayJsonModel yamasArray = JsonUtil.toYamasArray(Collections.singleton(metricsPacket), true); + String string = yamasArray.serialize(); + assertEquals(EXPECTED_JSON, string); + } + + @Test + public void missing_routing_object_makes_it_null() throws IOException { + // Read file that was taken from production (real -life example that is) + String filename = getClass().getClassLoader().getResource("yamas-array-no-routing.json").getFile(); + BufferedReader reader = Files.newBufferedReader(Paths.get(filename)); + ObjectMapper mapper = new ObjectMapper(); + YamasJsonModel jsonModel = mapper.readValue(reader, YamasJsonModel.class); + + // Do some sanity checking + assertNull(jsonModel.routing); + } + + private YamasJsonModel getYamasJsonModel(String testFile) throws IOException { + String filename = getClass().getClassLoader().getResource(testFile).getFile(); + BufferedReader reader = Files.newBufferedReader(Paths.get(filename)); + ObjectMapper mapper = new ObjectMapper(); + return mapper.readValue(reader, YamasJsonModel.class); + } + +} diff --git a/metrics-proxy/src/test/java/ai/vespa/metricsproxy/rpc/IntegrationTester.java b/metrics-proxy/src/test/java/ai/vespa/metricsproxy/rpc/IntegrationTester.java new file mode 100644 index 00000000000..6bbf4ae1ef5 --- /dev/null +++ b/metrics-proxy/src/test/java/ai/vespa/metricsproxy/rpc/IntegrationTester.java @@ -0,0 +1,144 @@ +/* + * Copyright 2019 Oath Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. + */ + +package ai.vespa.metricsproxy.rpc; + +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.core.MonitoringConfig; +import ai.vespa.metricsproxy.core.VespaMetrics; +import ai.vespa.metricsproxy.metric.ExternalMetrics; +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.ConsumerId; +import ai.vespa.metricsproxy.metric.model.ServiceId; +import ai.vespa.metricsproxy.service.HttpMetricFetcher; +import ai.vespa.metricsproxy.service.MockHttpServer; +import ai.vespa.metricsproxy.service.VespaServices; +import ai.vespa.metricsproxy.service.VespaServicesConfig; +import ai.vespa.metricsproxy.service.VespaServicesConfig.Service; + +import java.io.IOException; + +import static ai.vespa.metricsproxy.core.VespaMetrics.VESPA_CONSUMER_ID; +import static ai.vespa.metricsproxy.metric.model.ConsumerId.toConsumerId; +import static ai.vespa.metricsproxy.metric.model.ServiceId.toServiceId; +import static ai.vespa.metricsproxy.service.HttpMetricFetcher.STATE_PATH; + + +/** + * Setup and shutdown of config and servers for integration-style unit tests. + * + * @author hmusum + * @author gjoranv + */ +public class IntegrationTester implements AutoCloseable { + + static final String MONITORING_SYSTEM = "test-system"; + static final ConsumerId CUSTOM_CONSUMER_ID = toConsumerId("custom-consumer"); + static final String SERVICE_1_CONFIG_ID = "container/qrserver.0"; + static final String SERVICE_2_CONFIG_ID = "storage/cluster.storage/storage/0"; + + private final int httpPort; + private final int rpcPort; + private final RpcConnector connector; + private final MockHttpServer mockHttpServer; + private final VespaServices vespaServices; + + static { + HttpMetricFetcher.CONNECTION_TIMEOUT = 60000; // 60 secs in unit tests + } + + IntegrationTester(int httpPort, int rpcPort) { + if (httpPort == 0 || rpcPort == 0) { + throw new IllegalArgumentException("http port and rpc port must be defined"); + } + this.httpPort = httpPort; + this.rpcPort = rpcPort; + try { + mockHttpServer = new MockHttpServer(httpPort, null, STATE_PATH); + } catch (IOException e) { + throw new RuntimeException("Unable to start web server on port:" + httpPort); + } + + vespaServices = new VespaServices(servicesConfig(), monitoringConfig(), null); + MetricsConsumers consumers = new MetricsConsumers(consumersConfig()); + VespaMetrics vespaMetrics = new VespaMetrics(consumers, vespaServices); + ExternalMetrics externalMetrics = new ExternalMetrics(consumers); + ApplicationDimensions appDimensions = new ApplicationDimensions(applicationDimensionsConfig()); + NodeDimensions nodeDimensions = new NodeDimensions(nodeDimensionsConfig()); + + connector = new RpcConnector(rpcConnectorConfig()); + RpcServer server = new RpcServer(connector, vespaServices, new MetricsManager(vespaServices, vespaMetrics, externalMetrics, appDimensions, nodeDimensions)); + } + + MockHttpServer httpServer() { + return mockHttpServer; + } + + VespaServices vespaServices() { return vespaServices; } + + @Override + public void close() { + mockHttpServer.close(); + this.connector.stop(); + } + + private RpcConnectorConfig rpcConnectorConfig() { + return new RpcConnectorConfig.Builder() + .port(rpcPort) + .build(); + } + + private ConsumersConfig consumersConfig() { + return new ConsumersConfig.Builder() + .consumer(createConsumer(VESPA_CONSUMER_ID, "foo.count", "foo_count")) + .consumer(createConsumer(CUSTOM_CONSUMER_ID, "foo.count", "foo.count")) + .build(); + } + + private static Consumer.Builder createConsumer(ConsumerId consumerId, String metricName, String outputName) { + return new Consumer.Builder() + .name(consumerId.id) + .metric(new Consumer.Metric.Builder() + .dimension(new Consumer.Metric.Dimension.Builder().key("somekey").value("somevalue")) + .name(metricName) + .outputname(outputName)); + } + + private VespaServicesConfig servicesConfig() { + return new VespaServicesConfig.Builder() + .service(createService(toServiceId("qrserver"), SERVICE_1_CONFIG_ID, httpPort)) + .service(createService(toServiceId("storagenode"), SERVICE_2_CONFIG_ID, httpPort)) + .build(); + } + + private static Service.Builder createService(ServiceId serviceId, String configId, int port) { + return new Service.Builder() + .name(serviceId.id) + .configId(configId) + .port(port) + .healthport(port) + .dimension(new Service.Dimension.Builder().key("serviceDim").value("serviceDimValue")); + } + + private MonitoringConfig monitoringConfig() { + return new MonitoringConfig.Builder() + .systemName(MONITORING_SYSTEM) + .build(); + } + + private ApplicationDimensionsConfig applicationDimensionsConfig() { + return new ApplicationDimensionsConfig.Builder().build(); + } + + private NodeDimensionsConfig nodeDimensionsConfig() { + return new NodeDimensionsConfig.Builder().build(); + } + +} diff --git a/metrics-proxy/src/test/java/ai/vespa/metricsproxy/rpc/RpcHealthMetricsTest.java b/metrics-proxy/src/test/java/ai/vespa/metricsproxy/rpc/RpcHealthMetricsTest.java new file mode 100644 index 00000000000..20fb69e410e --- /dev/null +++ b/metrics-proxy/src/test/java/ai/vespa/metricsproxy/rpc/RpcHealthMetricsTest.java @@ -0,0 +1,97 @@ +/* + * Copyright 2019 Oath Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. + */ + +package ai.vespa.metricsproxy.rpc; + +import ai.vespa.metricsproxy.TestUtil; +import ai.vespa.metricsproxy.metric.HealthMetric; +import ai.vespa.metricsproxy.service.MockHttpServer; +import ai.vespa.metricsproxy.service.VespaService; +import com.yahoo.jrt.Request; +import com.yahoo.jrt.Spec; +import com.yahoo.jrt.StringValue; +import com.yahoo.jrt.Supervisor; +import com.yahoo.jrt.Target; +import com.yahoo.jrt.Transport; +import org.junit.Test; + +import java.util.List; + +import static ai.vespa.metricsproxy.rpc.IntegrationTester.SERVICE_1_CONFIG_ID; +import static org.hamcrest.CoreMatchers.is; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertThat; + +/** + * @author jobergum + * @author gjoranv + */ +public class RpcHealthMetricsTest { + + private static final String HEALTH_OK_RESPONSE = + TestUtil.getContents("health-check.response.json"); + private static final String HEALTH_FAILED_RESPONSE = + TestUtil.getContents("health-check-failed.response.json"); + private static final String WANTED_RPC_RESPONSE = + TestUtil.getContents("rpc-json-output-check.json").trim(); + + // see factory/doc/port-ranges.txt + private static final int httpPort = 18635; + private static final int rpcPort = 18636; + + @Test + public void expected_response_is_returned() { + try (IntegrationTester tester = new IntegrationTester(httpPort, rpcPort)) { + + MockHttpServer mockHttpServer = tester.httpServer(); + mockHttpServer.setResponse(HEALTH_OK_RESPONSE); + List<VespaService> services = tester.vespaServices().getInstancesById(SERVICE_1_CONFIG_ID); + + assertThat(services.size(), is(1)); + VespaService qrserver = services.get(0); + HealthMetric h = qrserver.getHealth(); + assertNotNull("Health metric should never be null", h); + assertThat("Status failed, reason = " + h.getMessage(), h.isOk(), is(true)); + assertThat(h.getMessage(), is("WORKING")); + + mockHttpServer.setResponse(HEALTH_FAILED_RESPONSE); + h = qrserver.getHealth(); + assertNotNull("Health metric should never be null", h); + assertThat("Status should be failed" + h.getMessage(), h.isOk(), is(false)); + assertThat(h.getMessage(), is("SOMETHING FAILED")); + + String jsonRPCMessage = getHealthMetrics(qrserver.getMonitoringName()); + assertThat(jsonRPCMessage, is(WANTED_RPC_RESPONSE)); + } + } + + @Test + public void non_existent_service_name_returns_an_error_message() { + try (IntegrationTester tester = new IntegrationTester(httpPort, rpcPort)) { + String jsonRPCMessage = getHealthMetrics("non-existing service"); + assertThat(jsonRPCMessage, is("105: No service with name 'non-existing service'")); + } + } + + private String getHealthMetrics(String service) { + Supervisor supervisor = new Supervisor(new Transport()); + Target target = supervisor.connect(new Spec("localhost", rpcPort)); + Request req = new Request("getHealthMetricsForYamas"); + req.parameters().add(new StringValue(service)); + String returnValue; + + target.invokeSync(req, 20.0); + if (req.checkReturnTypes("s")) { + returnValue = req.returnValues().get(0).asString(); + } else { + System.out.println("RpcServer invocation failed " + req.errorCode() + ": " + req.errorMessage()); + returnValue = req.errorCode() + ": " + req.errorMessage(); + } + target.close(); + supervisor.transport().shutdown().join(); + + return returnValue; + } + +} diff --git a/metrics-proxy/src/test/java/ai/vespa/metricsproxy/rpc/RpcMetricsTest.java b/metrics-proxy/src/test/java/ai/vespa/metricsproxy/rpc/RpcMetricsTest.java new file mode 100644 index 00000000000..f264fd13ddc --- /dev/null +++ b/metrics-proxy/src/test/java/ai/vespa/metricsproxy/rpc/RpcMetricsTest.java @@ -0,0 +1,208 @@ +/* + * Copyright 2019 Oath Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. + */ + +package ai.vespa.metricsproxy.rpc; + +import ai.vespa.metricsproxy.TestUtil; +import ai.vespa.metricsproxy.metric.Metric; +import ai.vespa.metricsproxy.metric.Metrics; +import ai.vespa.metricsproxy.metric.model.ConsumerId; +import ai.vespa.metricsproxy.service.VespaService; +import com.yahoo.jrt.Request; +import com.yahoo.jrt.Spec; +import com.yahoo.jrt.StringValue; +import com.yahoo.jrt.Supervisor; +import com.yahoo.jrt.Target; +import com.yahoo.jrt.Transport; +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; +import org.junit.Test; + +import java.util.List; + +import static ai.vespa.metricsproxy.core.VespaMetrics.VESPA_CONSUMER_ID; +import static ai.vespa.metricsproxy.metric.model.DimensionId.toDimensionId; +import static ai.vespa.metricsproxy.rpc.IntegrationTester.CUSTOM_CONSUMER_ID; +import static ai.vespa.metricsproxy.rpc.IntegrationTester.MONITORING_SYSTEM; +import static ai.vespa.metricsproxy.rpc.IntegrationTester.SERVICE_1_CONFIG_ID; +import static ai.vespa.metricsproxy.rpc.IntegrationTester.SERVICE_2_CONFIG_ID; +import static ai.vespa.metricsproxy.service.VespaServices.ALL_SERVICES; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.notNullValue; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertThat; + +/** + * @author jobergum + * @author gjoranv + */ +public class RpcMetricsTest { + + private static final String METRICS_RESPONSE_CCL = + TestUtil.getContents("metrics-storage-simple.json").trim(); + + // see factory/doc/port-ranges.txt + private static final int httpPort = 18633; + private static final int rpcPort = 18634; + + @Test + public void testGetMetrics() throws Exception { + try (IntegrationTester tester = new IntegrationTester(httpPort, rpcPort)) { + tester.httpServer().setResponse(METRICS_RESPONSE_CCL); + List<VespaService> services = tester.vespaServices().getInstancesById(SERVICE_1_CONFIG_ID); + + assertThat("#Services should be 1 for config id " + SERVICE_1_CONFIG_ID, services.size(), is(1)); + + VespaService qrserver = services.get(0); + assertThat(qrserver.getMonitoringName(), is(MONITORING_SYSTEM + VespaService.SEPARATOR + "qrserver")); + + Metrics metrics = qrserver.getMetrics(); + assertThat("Fetched number of metrics is not correct", metrics.size(), is(2)); + Metric m = metrics.getMetric("foo.count"); + assertNotNull("Did not find expected metric with name 'foo.count'", m); + Metric m2 = metrics.getMetric("bar.count"); + assertNotNull("Did not find expected metric with name 'bar.count'", m2); + + // Setup RPC client + Supervisor supervisor = new Supervisor(new Transport()); + Target target = supervisor.connect(new Spec("localhost", rpcPort)); + + verifyMetricsFromRpcRequest(qrserver, target); + + services = tester.vespaServices().getInstancesById(SERVICE_2_CONFIG_ID); + assertThat("#Services should be 1 for config id " + SERVICE_2_CONFIG_ID, services.size(), is(1)); + + VespaService storageService = services.get(0); + verfiyMetricsFromServiceObject(storageService); + + String metricsById = getMetricsById(storageService.getConfigId(), target); + assertThat(metricsById, is("'storage.cluster.storage.storage.0'.foo_count=1 ")); + + String jsonResponse = getMetricsForYamas("non-existing", target).trim(); + assertThat(jsonResponse, is("105: No service with name 'non-existing'")); + + verifyMetricsFromRpcRequestForAllServices(target); + + // Shutdown RPC + target.close(); + supervisor.transport().shutdown().join(); + } + } + + private static void verifyMetricsFromRpcRequest(VespaService service, Target target) throws JSONException { + String jsonResponse = getMetricsForYamas(service.getMonitoringName(), target).trim(); + JSONArray metrics = new JSONObject(jsonResponse).getJSONArray("metrics"); + assertThat("Expected 3 metric messages", metrics.length(), is(3)); + for (int i = 0; i < metrics.length() - 1; i++) { // The last "metric message" contains only status code/message + JSONObject jsonObject = metrics.getJSONObject(i); + assertFalse(jsonObject.has("status_code")); + assertFalse(jsonObject.has("status_msg")); + assertThat(jsonObject.getJSONObject("dimensions").getString("foo"), is("bar")); + assertThat(jsonObject.getJSONObject("dimensions").getString("bar"), is("foo")); + assertThat(jsonObject.getJSONObject("dimensions").getString("serviceDim"), is("serviceDimValue")); + assertThat(jsonObject.getJSONObject("routing").getJSONObject("yamas").getJSONArray("namespaces").length(), is(1)); + if (jsonObject.getJSONObject("metrics").has("foo_count")) { + assertThat(jsonObject.getJSONObject("metrics").getInt("foo_count"), is(1)); + assertThat(jsonObject.getJSONObject("routing").getJSONObject("yamas").getJSONArray("namespaces").get(0), is(VESPA_CONSUMER_ID.id)); + } else { + assertThat(jsonObject.getJSONObject("metrics").getInt("foo.count"), is(1)); + assertThat(jsonObject.getJSONObject("routing").getJSONObject("yamas").getJSONArray("namespaces").get(0), is(CUSTOM_CONSUMER_ID.id)); + } + } + + verifyStatusMessage(metrics.getJSONObject(metrics.length() - 1)); + } + + private void verfiyMetricsFromServiceObject(VespaService service) { + Metrics storageMetrics = service.getMetrics(); + assertThat(storageMetrics.size(), is(2)); + Metric foo = storageMetrics.getMetric("foo.count"); + assertNotNull("Did not find expected metric with name 'foo.count'", foo); + assertThat("Expected 2 dimensions for metric foo", foo.getDimensions().size(), is(2)); + assertThat("Metric foo did not contain correct dimension mapping for key = foo.count", foo.getDimensions().containsKey(toDimensionId("foo")), is(true)); + assertThat("Metric foo did not contain correct dimension", foo.getDimensions().get(toDimensionId("foo")), is("bar")); + assertThat("Metric foo did not contain correct dimension", foo.getDimensions().containsKey(toDimensionId("bar")), is(true)); + assertThat("Metric foo did not contain correct dimension for key = bar", foo.getDimensions().get(toDimensionId("bar")), is("foo")); + } + + private void verifyMetricsFromRpcRequestForAllServices(Target target) throws JSONException { + // Verify that metrics for all services can be retrieved in one request. + String allServicesResponse = getMetricsForYamas(ALL_SERVICES, target).trim(); + JSONArray allServicesMetrics = new JSONObject(allServicesResponse).getJSONArray("metrics"); + assertThat(allServicesMetrics.length(), is(5)); + } + + @Test + public void testGetAllMetricNames() { + try (IntegrationTester tester = new IntegrationTester(httpPort, rpcPort)) { + + tester.httpServer().setResponse(METRICS_RESPONSE_CCL); + List<VespaService> services = tester.vespaServices().getInstancesById(SERVICE_1_CONFIG_ID); + + assertThat(services.size(), is(1)); + Metrics metrics = services.get(0).getMetrics(); + assertThat("Fetched number of metrics is not correct", metrics.size(), is(2)); + Metric m = metrics.getMetric("foo.count"); + assertNotNull("Did not find expected metric with name 'foo.count'", m); + + + Metric m2 = metrics.getMetric("bar.count"); + assertNotNull("Did not find expected metric with name 'bar'", m2); + + // Setup RPC + Supervisor supervisor = new Supervisor(new Transport()); + Target target = supervisor.connect(new Spec("localhost", rpcPort)); + + String response = getAllMetricNamesForService(services.get(0).getMonitoringName(), VESPA_CONSUMER_ID, target); + assertThat(response, is("foo.count=ON;output-name=foo_count,bar.count=OFF,")); + + // Shutdown RPC + target.close(); + supervisor.transport().shutdown().join(); + } + } + + private static String getMetricsForYamas(String service, Target target) { + Request req = new Request("getMetricsForYamas"); + req.parameters().add(new StringValue(service)); + return invoke(req, target); + } + + private String getMetricsById(String service, Target target) { + Request req = new Request("getMetricsById"); + req.parameters().add(new StringValue(service)); + return invoke(req, target); + } + + private String getAllMetricNamesForService(String service, ConsumerId consumer, Target target) { + Request req = new Request("getAllMetricNamesForService"); + req.parameters().add(new StringValue(service)); + req.parameters().add(new StringValue(consumer.id)); + return invoke(req, target); + } + + private static String invoke(Request req, Target target) { + String returnValue; + target.invokeSync(req, 20.0); + if (req.checkReturnTypes("s")) { + returnValue = req.returnValues().get(0).asString(); + } else { + System.out.println(req.methodName() + " from rpcserver - Invocation failed " + + req.errorCode() + ": " + req.errorMessage()); + returnValue = req.errorCode() + ": " + req.errorMessage(); + } + return returnValue; + } + + private static void verifyStatusMessage(JSONObject jsonObject) throws JSONException { + assertThat(jsonObject.getInt("status_code"), is(0)); + assertThat(jsonObject.getString("status_msg"), notNullValue()); + assertThat(jsonObject.getString("application"), notNullValue()); + assertThat(jsonObject.getString("routing"), notNullValue()); + assertThat(jsonObject.length(), is(4)); + } + +} diff --git a/metrics-proxy/src/test/java/ai/vespa/metricsproxy/service/ConfigSentinelClientTest.java b/metrics-proxy/src/test/java/ai/vespa/metricsproxy/service/ConfigSentinelClientTest.java new file mode 100644 index 00000000000..bd61b8443aa --- /dev/null +++ b/metrics-proxy/src/test/java/ai/vespa/metricsproxy/service/ConfigSentinelClientTest.java @@ -0,0 +1,104 @@ +/* + * Copyright 2019 Oath Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. + */ + +package ai.vespa.metricsproxy.service; + +import org.junit.Test; + +import java.util.ArrayList; +import java.util.List; + +import static org.hamcrest.CoreMatchers.is; +import static org.junit.Assert.assertThat; + +/** + * @author Unknown + */ +public class ConfigSentinelClientTest { + + @Test + public void testConfigSentinelClient() { + ConfigSentinelDummy configsentinel = new ConfigSentinelDummy(); + List<VespaService> services = new ArrayList<>(); + VespaService docproc = new VespaService("docprocservice", "docproc/cluster.x.indexing/0"); + VespaService searchnode4 = new VespaService("searchnode4", "search/cluster.x/g0/c1/r1"); + VespaService qrserver = new VespaService("qrserver", "container/qrserver.0"); + + services.add(searchnode4); + services.add(qrserver); + services.add(docproc); + + MockConfigSentinelClient client = new MockConfigSentinelClient(configsentinel); + client.updateServiceStatuses(services); + + assertThat(qrserver.getPid(), is(6520)); + assertThat(qrserver.getState(), is("RUNNING")); + assertThat(qrserver.isAlive(), is(true)); + assertThat(searchnode4.getPid(), is(6534)); + assertThat(searchnode4.getState(), is("RUNNING")); + assertThat(searchnode4.isAlive(), is(true)); + + assertThat(docproc.getPid(), is(-1)); + assertThat(docproc.getState(), is("FINISHED")); + assertThat(docproc.isAlive(), is(false)); + + + configsentinel.reConfigure(); + + client.ping(docproc); + assertThat(docproc.getPid(), is(100)); + assertThat(docproc.getState(), is("RUNNING")); + assertThat(docproc.isAlive(), is(true)); + + //qrserver has yet not been checked + assertThat(qrserver.isAlive(), is(true)); + + client.updateServiceStatuses(services); + + assertThat(docproc.getPid(), is(100)); + assertThat(docproc.getState(), is("RUNNING")); + assertThat(docproc.isAlive(), is(true)); + //qrserver is no longer running on this node - so should be false + assertThat(qrserver.isAlive(), is(false)); + } + + @Test + public void testElastic() throws Exception { + String response = "container state=RUNNING mode=AUTO pid=14338 exitstatus=0 autostart=TRUE autorestart=TRUE id=\"get/container.0\"\n" + + "container-clustercontroller state=RUNNING mode=AUTO pid=25020 exitstatus=0 autostart=TRUE autorestart=TRUE id=\"admin/cluster-controllers/0\"\n" + + "distributor state=RUNNING mode=AUTO pid=25024 exitstatus=0 autostart=TRUE autorestart=TRUE id=\"search/distributor/0\"\n" + + "docprocservice state=RUNNING mode=AUTO pid=11973 exitstatus=0 autostart=TRUE autorestart=TRUE id=\"docproc/cluster.search.indexing/0\"\n" + + "logd state=RUNNING mode=AUTO pid=25016 exitstatus=0 autostart=TRUE autorestart=TRUE id=\"hosts/vespa19.dev.gq1.yahoo.com/logd\"\n" + + "logserver state=RUNNING mode=AUTO pid=25018 exitstatus=0 autostart=TRUE autorestart=TRUE id=\"admin/logserver\"\n" + + "metricsproxy state=RUNNING mode=AUTO pid=13107 exitstatus=0 autostart=TRUE autorestart=TRUE id=\"hosts/vespa19.dev.gq1.yahoo.com/metricsproxy\"\n" + + "searchnode state=RUNNING mode=AUTO pid=25023 exitstatus=0 autostart=TRUE autorestart=TRUE id=\"search/search/cluster.search/0\"\n" + + "slobrok state=RUNNING mode=AUTO pid=25019 exitstatus=0 autostart=TRUE autorestart=TRUE id=\"admin/slobrok.0\"\n" + + "topleveldispatch state=RUNNING mode=AUTO pid=25026 exitstatus=0 autostart=TRUE autorestart=TRUE id=\"search/search/cluster.search/tlds/tld.0\"\n" + + "\n"; + + ConfigSentinelDummy configsentinel = new ConfigSentinelDummy(response); + List<VespaService> services = new ArrayList<>(); + + VespaService container = VespaService.create("container", "get/container.0", -1); + + VespaService containerClusterController = + VespaService.create("container-clustercontroller", "get/container.0", -1); + + VespaService notPresent = VespaService.create("dummy","fake", -1); + + services.add(container); + services.add(containerClusterController); + services.add(notPresent); + + MockConfigSentinelClient client = new MockConfigSentinelClient(configsentinel); + client.updateServiceStatuses(services); + assertThat(container.isAlive(),is(true)); + assertThat(container.getPid(),is(14338)); + assertThat(container.getState(),is("RUNNING")); + + assertThat(containerClusterController.isAlive(),is(true)); + assertThat(containerClusterController.getPid(),is(25020)); + assertThat(containerClusterController.getState(),is("RUNNING")); + } +} diff --git a/metrics-proxy/src/test/java/ai/vespa/metricsproxy/service/ConfigSentinelDummy.java b/metrics-proxy/src/test/java/ai/vespa/metricsproxy/service/ConfigSentinelDummy.java new file mode 100644 index 00000000000..108f5c18e1d --- /dev/null +++ b/metrics-proxy/src/test/java/ai/vespa/metricsproxy/service/ConfigSentinelDummy.java @@ -0,0 +1,61 @@ +/* + * Copyright 2019 Oath Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. + */ +package ai.vespa.metricsproxy.service; + +/** + * @author Eirik Nygaard + */ +public class ConfigSentinelDummy { + private String serviceList = + "docprocservice state=FINISHED mode=MANUAL pid=6555 exitstatus=0 autostart=TRUE autorestart=TRUE id=\"docproc/cluster.x.indexing/0\"\n" + + "distributor state=RUNNING mode=AUTO pid=6548 exitstatus=0 autostart=TRUE autorestart=TRUE id=\"storage/cluster.storage/distributor/0\"\n" + + "fleetcontroller state=RUNNING mode=AUTO pid=6543 exitstatus=0 autostart=TRUE autorestart=TRUE id=\"storage/cluster.storage/fleetcontroller/0\"\n" + + "storagenode state=RUNNING mode=AUTO pid=6539 exitstatus=0 autostart=TRUE autorestart=TRUE id=\"storage/cluster.storage/storage/0\"\n" + + "searchnode4 state=RUNNING mode=AUTO pid=6534 exitstatus=0 autostart=TRUE autorestart=TRUE id=\"search/cluster.x/g0/c1/r1\"\n" + + "qrserver2 state=RUNNING mode=AUTO pid=6521 exitstatus=0 autostart=TRUE autorestart=TRUE id=\"container/qrserver.1\"\n" + + "logserver state=RUNNING mode=AUTO pid=6518 exitstatus=0 autostart=TRUE autorestart=TRUE id=\"admin/logserver\"\n" + + "logd state=RUNNING mode=AUTO pid=6517 exitstatus=0 autostart=TRUE autorestart=TRUE id=\"hosts/dell-bl5s7.trondheim.corp.yahoo.com/logd\"\n" + + "searchnode2 state=RUNNING mode=AUTO pid=6527 exitstatus=0 autostart=TRUE autorestart=TRUE id=\"search/cluster.x/g0/c0/r1\"\n" + + "topleveldispatch2 state=RUNNING mode=AUTO pid=6525 exitstatus=0 autostart=TRUE autorestart=TRUE id=\"search/cluster.x/tlds/tld.1\"\n" + + "topleveldispatch state=RUNNING mode=AUTO pid=6524 exitstatus=0 autostart=TRUE autorestart=TRUE id=\"search/cluster.x/tlds/tld.0\"\n" + + "clustercontroller2 state=RUNNING mode=AUTO pid=6523 exitstatus=0 autostart=TRUE autorestart=TRUE id=\"search/cluster.x/rtx/1\"\n" + + "clustercontroller state=RUNNING mode=AUTO pid=6522 exitstatus=0 autostart=TRUE autorestart=TRUE id=\"search/cluster.x/rtx/0\"\n" + + "slobrok state=RUNNING mode=AUTO pid=6519 exitstatus=0 autostart=TRUE autorestart=TRUE id=\"admin/slobrok.0\"\n" + + "searchnode3 state=RUNNING mode=AUTO pid=6529 exitstatus=0 autostart=TRUE autorestart=TRUE id=\"search/cluster.x/g0/c1/r0\"\n" + + "searchnode state=RUNNING mode=AUTO pid=6526 exitstatus=0 autostart=TRUE autorestart=TRUE id=\"search/cluster.x/g0/c0/r0\"\n" + + "qrserver state=RUNNING mode=AUTO pid=6520 exitstatus=0 autostart=TRUE autorestart=TRUE id=\"container/qrserver.0\"\n" + + "\n"; + + + public ConfigSentinelDummy() { + } + + public ConfigSentinelDummy(String response) { + serviceList = response; + } + + public void reConfigure() { + this.serviceList = "docprocservice state=RUNNING mode=AUTO pid=100 exitstatus=0 autostart=TRUE autorestart=TRUE id=\"docproc/cluster.x.indexing/0\"\n" + + "distributor state=RUNNING mode=AUTO pid=6548 exitstatus=0 autostart=TRUE autorestart=TRUE id=\"storage/cluster.storage/distributor/0\"\n" + + "fleetcontroller state=RUNNING mode=AUTO pid=6543 exitstatus=0 autostart=TRUE autorestart=TRUE id=\"storage/cluster.storage/fleetcontroller/0\"\n" + + "storagenode state=RUNNING mode=AUTO pid=6539 exitstatus=0 autostart=TRUE autorestart=TRUE id=\"storage/cluster.storage/storage/0\"\n" + + "searchnode4 state=RUNNING mode=AUTO pid=6534 exitstatus=0 autostart=TRUE autorestart=TRUE id=\"search/cluster.x/g0/c1/r1\"\n" + + "qrserver2 state=RUNNING mode=AUTO pid=6521 exitstatus=0 autostart=TRUE autorestart=TRUE id=\"container/qrserver.1\"\n" + + "logserver state=RUNNING mode=AUTO pid=6518 exitstatus=0 autostart=TRUE autorestart=TRUE id=\"admin/logserver\"\n" + + "logd state=RUNNING mode=AUTO pid=6517 exitstatus=0 autostart=TRUE autorestart=TRUE id=\"hosts/dell-bl5s7.trondheim.corp.yahoo.com/logd\"\n" + + "searchnode2 state=RUNNING mode=AUTO pid=6527 exitstatus=0 autostart=TRUE autorestart=TRUE id=\"search/cluster.x/g0/c0/r1\"\n" + + "topleveldispatch2 state=RUNNING mode=AUTO pid=6525 exitstatus=0 autostart=TRUE autorestart=TRUE id=\"search/cluster.x/tlds/tld.1\"\n" + + "topleveldispatch state=RUNNING mode=AUTO pid=6524 exitstatus=0 autostart=TRUE autorestart=TRUE id=\"search/cluster.x/tlds/tld.0\"\n" + + "clustercontroller2 state=RUNNING mode=AUTO pid=6523 exitstatus=0 autostart=TRUE autorestart=TRUE id=\"search/cluster.x/rtx/1\"\n" + + "clustercontroller state=RUNNING mode=AUTO pid=6522 exitstatus=0 autostart=TRUE autorestart=TRUE id=\"search/cluster.x/rtx/0\"\n" + + "slobrok state=RUNNING mode=AUTO pid=6519 exitstatus=0 autostart=TRUE autorestart=TRUE id=\"admin/slobrok.0\"\n" + + "searchnode3 state=RUNNING mode=AUTO pid=6529 exitstatus=0 autostart=TRUE autorestart=TRUE id=\"search/cluster.x/g0/c1/r0\"\n" + + "searchnode state=RUNNING mode=AUTO pid=6526 exitstatus=0 autostart=TRUE autorestart=TRUE id=\"search/cluster.x/g0/c0/r0\"\n" + + "\n"; + } + + public String getServiceList() { + return serviceList; + } +} diff --git a/metrics-proxy/src/test/java/ai/vespa/metricsproxy/service/ContainerServiceTest.java b/metrics-proxy/src/test/java/ai/vespa/metricsproxy/service/ContainerServiceTest.java new file mode 100644 index 00000000000..4174b18f3a7 --- /dev/null +++ b/metrics-proxy/src/test/java/ai/vespa/metricsproxy/service/ContainerServiceTest.java @@ -0,0 +1,67 @@ +/* + * Copyright 2019 Oath Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. + */ + +package ai.vespa.metricsproxy.service; + +import ai.vespa.metricsproxy.TestUtil; +import ai.vespa.metricsproxy.metric.Metric; +import org.json.JSONException; +import org.junit.After; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; + +import static ai.vespa.metricsproxy.metric.model.DimensionId.toDimensionId; +import static org.hamcrest.CoreMatchers.is; +import static org.junit.Assert.assertThat; + +/** + * @author Unknown + */ +public class ContainerServiceTest { + + private MockHttpServer service; + private int csPort; + + @BeforeClass + public static void init() { + HttpMetricFetcher.CONNECTION_TIMEOUT = 60000; // 60 secs in unit tests + } + + @Before + public void setupHTTPServer() { + csPort = 18637; // see factory/doc/port-ranges.txt + try { + String response = TestUtil.getContents("metrics-container-state-multi-chain.json"); + service = new MockHttpServer(csPort, response, HttpMetricFetcher.METRICS_PATH); + } catch (Exception e) { + e.printStackTrace(); + } + } + + @Test + public void testMultipleQueryDimensions() throws JSONException { + int count = 0; + VespaService service = VespaService.create("service1", "id", csPort); + for (Metric m : service.getMetrics().getMetrics()) { + if (m.getName().equals("queries.rate")) { + count++; + System.out.println("Name: " + m.getName() + " value: " + m.getValue()); + if (m.getDimensions().get(toDimensionId("chain")).equals("asvBlendingResult")) { + assertThat((Double)m.getValue(), is(26.4)); + } else if (m.getDimensions().get(toDimensionId("chain")).equals("blendingResult")) { + assertThat((Double)m.getValue(), is(0.36666666666666664)); + } else { + assertThat("Unknown unknown chain", false, is(true)); + } + } + } + assertThat(count, is(2)); + } + + @After + public void shutdown() { + this.service.close(); + } +} 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 new file mode 100644 index 00000000000..380a992aead --- /dev/null +++ b/metrics-proxy/src/test/java/ai/vespa/metricsproxy/service/DummyService.java @@ -0,0 +1,36 @@ +/* + * Copyright 2019 Oath Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. + */ + +package ai.vespa.metricsproxy.service; + +import ai.vespa.metricsproxy.metric.Metric; +import ai.vespa.metricsproxy.metric.Metrics; + +/** + * @author Unknown + */ +public class DummyService extends VespaService { + static final String NAME = "dummy"; + public static final String METRIC_1 = "c.test"; + public static final String METRIC_2 = "val"; + + private final int num; + + public DummyService(int num, String configid) { + super(NAME, NAME + num, configid); + this.num = num; + } + + @Override + public Metrics getMetrics() { + Metrics m = new Metrics(); + + long timestamp = System.currentTimeMillis() / 1000; + m.add(new Metric(METRIC_1, 5 * num + 1, timestamp)); + m.add(new Metric(METRIC_2, 1.3 * num + 1.05, timestamp)); + + return m; + } + +} diff --git a/metrics-proxy/src/test/java/ai/vespa/metricsproxy/service/MetricsFetcherTest.java b/metrics-proxy/src/test/java/ai/vespa/metricsproxy/service/MetricsFetcherTest.java new file mode 100644 index 00000000000..27e1bb97943 --- /dev/null +++ b/metrics-proxy/src/test/java/ai/vespa/metricsproxy/service/MetricsFetcherTest.java @@ -0,0 +1,90 @@ +/* + * Copyright 2019 Oath Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. + */ + +package ai.vespa.metricsproxy.service; + +import ai.vespa.metricsproxy.TestUtil; +import ai.vespa.metricsproxy.metric.Metrics; +import org.junit.Test; + +import java.io.File; + +import static org.hamcrest.CoreMatchers.is; +import static org.junit.Assert.assertThat; + +/** + * @author Unknowm + */ +public class MetricsFetcherTest { + private static int port = 9; //port number is not used in this test + + @Test + public void testStateFormatMetricsParse() { + String jsonData = TestUtil.getContents("metrics-state.json"); + RemoteMetricsFetcher fetcher = new RemoteMetricsFetcher(new DummyService(0, "dummy/id/0"), port); + Metrics metrics = fetcher.createMetrics(jsonData, 0); + assertThat("Wrong number of metrics", metrics.size(), is(10)); + assertThat("Wrong value for metric", metrics.get("query_hits.count").intValue(), is(28)); + assertThat("Wrong value for metric ", metrics.get("queries.rate").doubleValue(), is(0.4667)); + assertThat("Wrong timestamp", metrics.getTimeStamp(), is(1334134700L)); + } + + @Test + public void testEmptyJson() { + String jsonData = "{}"; + RemoteMetricsFetcher fetcher = new RemoteMetricsFetcher(new DummyService(0, "dummy/id/0"), port); + Metrics metrics = fetcher.createMetrics(jsonData, 0); + assertThat("Wrong number of metrics", metrics.size(), is(0)); + } + + @Test + public void testErrors() { + String jsonData; + Metrics metrics; + + RemoteMetricsFetcher fetcher = new RemoteMetricsFetcher(new DummyService(0, "dummy/id/0"), port); + + jsonData = ""; + metrics = fetcher.createMetrics(jsonData, 0); + assertThat("Wrong number of metrics", metrics.size(), is(0)); + + jsonData = "{\n" + + "\"status\" : {\n" + + " \"code\" : \"up\",\n" + + " \"message\" : \"Everything ok here\"\n" + + "}\n" + + "}"; + metrics = fetcher.createMetrics(jsonData, 0); + assertThat("Wrong number of metrics", metrics.size(), is(0)); + + jsonData = "{\n" + + "\"status\" : {\n" + + " \"code\" : \"up\",\n" + + " \"message\" : \"Everything ok here\"\n" + + "},\n" + + "\"metrics\" : {\n" + + " \"snapshot\" : {\n" + + " \"from\" : 1334134640.089,\n" + + " \"to\" : 1334134700.088\n" + + " },\n" + + " \"values\" : [\n" + + " {\n" + + " \"name\" : \"queries\",\n" + + " \"description\" : \"Number of queries executed during snapshot interval\",\n" + + " \"values\" : {\n" + + " \"count\" : null,\n" + + " \"rate\" : 0.4667\n" + + " },\n" + + " \"dimensions\" : {\n" + + " \"searcherid\" : \"x\"\n" + + " }\n" + + " }\n" + "" + + " ]\n" + + "}\n" + + "}"; + + metrics = fetcher.createMetrics(jsonData, 0); + assertThat("Wrong number of metrics", metrics.size(), is(0)); + } +} diff --git a/metrics-proxy/src/test/java/ai/vespa/metricsproxy/service/MockConfigSentinelClient.java b/metrics-proxy/src/test/java/ai/vespa/metricsproxy/service/MockConfigSentinelClient.java new file mode 100644 index 00000000000..917c529e63e --- /dev/null +++ b/metrics-proxy/src/test/java/ai/vespa/metricsproxy/service/MockConfigSentinelClient.java @@ -0,0 +1,50 @@ +/* + * Copyright 2019 Oath Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. + */ +package ai.vespa.metricsproxy.service; + +import com.yahoo.log.LogLevel; + +import java.util.ArrayList; +import java.util.List; +import java.util.logging.Logger; + +/** + * Mock config sentinel + * + * @author hmusum + */ +public class MockConfigSentinelClient extends ConfigSentinelClient { + private final ConfigSentinelDummy configSentinel; + private final static Logger log = Logger.getLogger(MockConfigSentinelClient.class.getPackage().getName()); + + public MockConfigSentinelClient(ConfigSentinelDummy configSentinel) { + super(); + this.configSentinel = configSentinel; + } + + @Override + protected synchronized void setStatus(List<VespaService> services) throws Exception { + List<VespaService> updatedServices = new ArrayList<>(); + String[] lines = configSentinel.getServiceList().split("\n"); + for (String line : lines) { + if (line.equals("")) { + break; + } + + VespaService s = parseServiceString(line, services); + if (s != null) { + updatedServices.add(s); + } + } + + //Check if there are services that were not found in + //from the sentinel + for (VespaService s : services) { + if (!updatedServices.contains(s)) { + log.log(LogLevel.DEBUG, "Service " + s + " is no longer found with sentinel - setting alive = false"); + s.setAlive(false); + } + } + } +} diff --git a/metrics-proxy/src/test/java/ai/vespa/metricsproxy/service/MockHttpServer.java b/metrics-proxy/src/test/java/ai/vespa/metricsproxy/service/MockHttpServer.java new file mode 100644 index 00000000000..fdf2fae3081 --- /dev/null +++ b/metrics-proxy/src/test/java/ai/vespa/metricsproxy/service/MockHttpServer.java @@ -0,0 +1,56 @@ +/* + * Copyright 2019 Oath Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. + */ + +package ai.vespa.metricsproxy.service; + +import com.sun.net.httpserver.HttpExchange; +import com.sun.net.httpserver.HttpHandler; +import com.sun.net.httpserver.HttpServer; + +import java.io.IOException; +import java.io.OutputStream; +import java.net.InetSocketAddress; + +/** + * @author jobergum + */ +public class MockHttpServer { + + private String response; + private HttpServer server; + + /** + * Mock http server that will return response as body + * + * @param port the port to listen to + * @param response the response to return along with 200 OK + * @param path the file path that the server will accept requests for. E.g /state/v1/metrics + */ + public MockHttpServer(int port, String response, String path) throws IOException { + this.response = response; + this.server = HttpServer.create(new InetSocketAddress(port), 10); + this.server.createContext(path, new MyHandler()); + this.server.setExecutor(null); // creates a default executor + this.server.start(); + System.out.println("Started web server on port " + port); + } + + public synchronized void setResponse(String r) { + this.response = r; + } + + public void close() { + this.server.stop(0); + } + + private class MyHandler implements HttpHandler { + public void handle(HttpExchange t) throws IOException { + t.sendResponseHeaders(200, response.length()); + OutputStream os = t.getResponseBody(); + os.write(response.getBytes()); + os.close(); + } + } + +} diff --git a/metrics-proxy/src/test/java/ai/vespa/metricsproxy/service/SystemPollerTest.java b/metrics-proxy/src/test/java/ai/vespa/metricsproxy/service/SystemPollerTest.java new file mode 100644 index 00000000000..a42d52b7ea6 --- /dev/null +++ b/metrics-proxy/src/test/java/ai/vespa/metricsproxy/service/SystemPollerTest.java @@ -0,0 +1,45 @@ +/* + * Copyright 2019 Oath Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. + */ + +package ai.vespa.metricsproxy.service; + +import org.junit.Test; + +import java.util.ArrayList; +import java.util.List; + +import static org.hamcrest.CoreMatchers.is; +import static org.junit.Assert.assertThat; + +/** + * @author Unknown + */ +public class SystemPollerTest { + + @Test + public void testSystemPoller() { + DummyService s = new DummyService(0, "id"); + List<VespaService> services = new ArrayList<>(); + services.add(s); + + SystemPoller poller = new SystemPoller(services, 60*5); + assertThat(s.isAlive(), is(false)); + services.remove(0); + poller.setServices(services); + long n = poller.getPidJiffies(s); + assertThat(n, is(0L)); + long[] memusage = poller.getMemoryUsage(s); + assertThat(memusage[0], is(0L)); + assertThat(memusage[1], is(0L)); + } + + @Test + public void testCPUJiffies() { + String line = "cpu1 102180864 789 56766899 12800020140 1654757 0 0"; + CpuJiffies n = new CpuJiffies(line); + assertThat(n.getCpuId(), is(1)); + assertThat(n.getTotalJiffies(), is(12960623449L)); + } + +} diff --git a/metrics-proxy/src/test/java/ai/vespa/metricsproxy/service/VespaServiceTest.java b/metrics-proxy/src/test/java/ai/vespa/metricsproxy/service/VespaServiceTest.java new file mode 100644 index 00000000000..13be98db23a --- /dev/null +++ b/metrics-proxy/src/test/java/ai/vespa/metricsproxy/service/VespaServiceTest.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 ai.vespa.metricsproxy.service; + +import ai.vespa.metricsproxy.TestUtil; +import ai.vespa.metricsproxy.metric.Metrics; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import java.io.File; + +import static org.hamcrest.CoreMatchers.is; +import static org.junit.Assert.assertThat; + +/** + * @author Unknown + */ +public class VespaServiceTest { + private MockHttpServer service; + private int csPort; + private static final String response; + + static { + response = TestUtil.getContents("metrics-state.json"); + HttpMetricFetcher.CONNECTION_TIMEOUT = 60000; // 60 secs in unit tests + } + + @Before + public void setupHTTPServer() { + csPort = 18632; // see factory/doc/port-ranges.txt + try { + service = new MockHttpServer(csPort, response, HttpMetricFetcher.METRICS_PATH); + } catch (Exception e) { + e.printStackTrace(); + } + } + + @Test + public void testService() { + VespaService service = new VespaService("qrserver", "container/qrserver.0"); + assertThat(service.getServiceName(), is("qrserver")); + assertThat(service.getInstanceName(), is("qrserver")); + assertThat(service.getPid(), is(-1)); + assertThat(service.getConfigId(), is("container/qrserver.0")); + + + service = VespaService.create("qrserver2", "container/qrserver.0", -1); + assertThat(service.getServiceName(), is("qrserver")); + assertThat(service.getInstanceName(), is("qrserver2")); + assertThat(service.getPid(), is(-1)); + assertThat(service.getConfigId(), is("container/qrserver.0")); + } + + @Test + // TODO: Make it possible to test this without running a HTTP server to create the response + public void testMetricsFetching() { + VespaService service = VespaService.create("service1", "id", csPort); + Metrics metrics = service.getMetrics(); + assertThat(metrics.getMetric("queries.count").getValue().intValue(), is(28)); + + // Shutdown server and check that no metrics are returned (should use empty metrics + // when unable to fetch new metrics) + shutdown(); + + metrics = service.getMetrics(); + assertThat(metrics.size(), is(0)); + } + + @After + public void shutdown() { + this.service.close(); + } + +} diff --git a/metrics-proxy/src/test/java/ai/vespa/metricsproxy/service/VespaServicesTest.java b/metrics-proxy/src/test/java/ai/vespa/metricsproxy/service/VespaServicesTest.java new file mode 100644 index 00000000000..bd0b670ca35 --- /dev/null +++ b/metrics-proxy/src/test/java/ai/vespa/metricsproxy/service/VespaServicesTest.java @@ -0,0 +1,42 @@ +/* + * Copyright 2019 Oath Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. + */ + +package ai.vespa.metricsproxy.service; + +import com.google.common.collect.ImmutableList; +import org.junit.Test; + +import java.util.List; + +import static ai.vespa.metricsproxy.service.VespaServices.ALL_SERVICES; +import static org.hamcrest.CoreMatchers.is; +import static org.junit.Assert.assertThat; + +/** + * TODO: add more tests + * + * @author gjoranv + */ +public class VespaServicesTest { + + @Test + public void services_can_be_retrieved_from_monitoring_name() { + List<VespaService> dummyServices = ImmutableList.of( + new DummyService(0, "dummy/id/0"), + new DummyService(1, "dummy/id/1")); + VespaServices services = new VespaServices(dummyServices); + + assertThat(services.getMonitoringServices("vespa.dummy").size(), is(2)); + } + + @Test + public void all_services_can_be_retrieved_by_using_special_name() { + List<VespaService> dummyServices = ImmutableList.of( + new DummyService(0, "dummy/id/0")); + VespaServices services = new VespaServices(dummyServices); + + assertThat(services.getMonitoringServices(ALL_SERVICES).size(), is(1)); + } + +} diff --git a/metrics-proxy/src/test/resources/health-check-failed.response.json b/metrics-proxy/src/test/resources/health-check-failed.response.json new file mode 100644 index 00000000000..e118f10ec5e --- /dev/null +++ b/metrics-proxy/src/test/resources/health-check-failed.response.json @@ -0,0 +1,7 @@ +{ + "status": { + "code": "down", + "message":"SOMETHING FAILED" + }, + "metrics": [] + } diff --git a/metrics-proxy/src/test/resources/health-check.response.json b/metrics-proxy/src/test/resources/health-check.response.json new file mode 100644 index 00000000000..8e3858ec5d8 --- /dev/null +++ b/metrics-proxy/src/test/resources/health-check.response.json @@ -0,0 +1,6 @@ +{ + "status": { + "code": "OK", + "message":"WORKING" + } + }
\ No newline at end of file diff --git a/metrics-proxy/src/test/resources/metrics-container-state-multi-chain.json b/metrics-proxy/src/test/resources/metrics-container-state-multi-chain.json new file mode 100644 index 00000000000..76d0be50cca --- /dev/null +++ b/metrics-proxy/src/test/resources/metrics-container-state-multi-chain.json @@ -0,0 +1,281 @@ +{ + "metrics": { + "snapshot": { + "from": 1.383132197389E9, + "to": 1.383132257389E9 + }, + "values": [ + { + "name": "search_connections", + "values": { + "average": 3.459204315576534, + "count": 1, + "last": 3.459204315576534, + "max": 3.459204315576534, + "min": 3.459204315576534, + "rate": 0.016666666666666666 + } + }, + { + "name": "active_queries", + "values": { + "average": 0, + "count": 1, + "last": 0, + "max": 0, + "min": 0, + "rate": 0.016666666666666666 + } + }, + { + "dimensions": { + "serverName": "qrs-server", + "serverPort": "4080" + }, + "name": "serverNumFailedResponseWrites", + "values": { + "count": 85, + "rate": 1.4166666666666667 + } + }, + { + "dimensions": { + "serverName": "qrs-server", + "serverPort": "4080" + }, + "name": "serverNumFailedResponses", + "values": { + "count": 8, + "rate": 0.13333333333333333 + } + }, + { + "dimensions": { + "serverName": "qrs-server", + "serverPort": "4080" + }, + "name": "serverNumConnections", + "values": { + "count": 1630, + "rate": 27.166666666666668 + } + }, + { + "dimensions": { + "serverName": "qrs-server", + "serverPort": "4080" + }, + "name": "serverNumSuccessfulResponses", + "values": { + "count": 1621, + "rate": 27.016666666666666 + } + }, + { + "dimensions": { + "serverName": "qrs-server", + "serverPort": "4080" + }, + "name": "serverNetworkLatency", + "values": { + "average": 0.11715958713775308, + "count": 20152, + "last": 0, + "max": 55, + "min": 0, + "rate": 335.8666666666667 + } + }, + { + "dimensions": { + "serverName": "qrs-server", + "serverPort": "4080" + }, + "name": "serverNumSuccessfulResponseWrites", + "values": { + "count": 20152, + "rate": 335.8666666666667 + } + }, + { + "dimensions": { + "serverName": "qrs-server", + "serverPort": "4080" + }, + "name": "serverTotalSuccessfulResponseLatency", + "values": { + "average": 90.88401253918495, + "count": 1595, + "last": 80, + "max": 233, + "min": 0, + "rate": 26.583333333333332 + } + }, + { + "dimensions": { + "serverName": "qrs-server", + "serverPort": "4080" + }, + "name": "serverNumRequests", + "values": { + "count": 1633, + "rate": 27.216666666666665 + } + }, + { + "dimensions": { + "serverName": "qrs-server", + "serverPort": "4080" + }, + "name": "serverTotalFailedResponseLatency", + "values": { + "average": 0.75, + "count": 8, + "last": 1, + "max": 1, + "min": 0, + "rate": 0.13333333333333333 + } + }, + { + "dimensions": {"chain": "asvBlendingResult"}, + "name": "query_latency", + "values": { + "average": 83.35949367088608, + "count": 1580, + "last": 61, + "max": 224, + "min": 12, + "rate": 26.333333333333332 + } + }, + { + "dimensions": {"chain": "asvBlendingResult"}, + "name": "max_query_latency", + "values": { + "average": 83.35949367088608, + "count": 1580, + "last": 61, + "max": 224, + "min": 12, + "rate": 26.333333333333332 + } + }, + { + "dimensions": {"chain": "asvBlendingResult"}, + "name": "peak_qps", + "values": { + "average": 25.87656434951563, + "count": 6, + "last": 23.681592039800993, + "max": 29.7659845295212, + "min": 23.681592039800993, + "rate": 0.1 + } + }, + { + "dimensions": {"chain": "asvBlendingResult"}, + "name": "queries", + "values": { + "count": 1584, + "rate": 26.4 + } + }, + { + "dimensions": {"chain": "asvBlendingResult"}, + "name": "mean_query_latency", + "values": { + "average": 83.35949367088608, + "count": 1580, + "last": 61, + "max": 224, + "min": 12, + "rate": 26.333333333333332 + } + }, + { + "dimensions": {"chain": "asvBlendingResult"}, + "name": "hits_per_query", + "values": { + "average": 173.70126582278482, + "count": 1580, + "last": 175, + "max": 175, + "min": 5, + "rate": 26.333333333333332 + } + }, + { + "dimensions": {"chain": "blendingResult"}, + "name": "query_latency", + "values": { + "average": 39.40909090909091, + "count": 22, + "last": 26, + "max": 174, + "min": 13, + "rate": 0.36666666666666664 + } + }, + { + "dimensions": {"chain": "blendingResult"}, + "name": "max_query_latency", + "values": { + "average": 39.40909090909091, + "count": 22, + "last": 26, + "max": 174, + "min": 13, + "rate": 0.36666666666666664 + } + }, + { + "dimensions": {"chain": "blendingResult"}, + "name": "peak_qps", + "values": { + "average": 0.5890415170417276, + "count": 3, + "last": 0.40488561981240295, + "max": 0.864528399757932, + "min": 0.40488561981240295, + "rate": 0.05 + } + }, + { + "dimensions": {"chain": "blendingResult"}, + "name": "queries", + "values": { + "count": 22, + "rate": 0.36666666666666664 + } + }, + { + "dimensions": {"chain": "blendingResult"}, + "name": "mean_query_latency", + "values": { + "average": 39.40909090909091, + "count": 22, + "last": 26, + "max": 174, + "min": 13, + "rate": 0.36666666666666664 + } + }, + { + "dimensions": {"chain": "blendingResult"}, + "name": "hits_per_query", + "values": { + "average": 47.5, + "count": 22, + "last": 28, + "max": 176, + "min": 5, + "rate": 0.36666666666666664 + } + } + ] + }, + "status": {"code": "up"}, + "time": 1383132269767 +}
\ No newline at end of file diff --git a/metrics-proxy/src/test/resources/metrics-state.json b/metrics-proxy/src/test/resources/metrics-state.json new file mode 100644 index 00000000000..b7773e5fb8b --- /dev/null +++ b/metrics-proxy/src/test/resources/metrics-state.json @@ -0,0 +1,42 @@ +{ +"status" : { + "code" : "up", + "message" : "Everything ok here" +}, +"metrics" : { + "snapshot" : { + "from" : 1334134640.089, + "to" : 1334134700.088 + }, + "values" : [ + { + "name" : "queries", + "description" : "Number of queries executed during snapshot interval", + "values" : { + "count" : 28, + "rate" : 0.4667 + }, + "dimensions" : { + "searcherid" : "x" + } + }, + { + "name" : "query_hits", + "description" : "Number of documents matched per query during snapshot interval", + "values" : { + "count" : 28, + "rate" : 0.4667, + "average" : 128.3, + "min" : 0, + "max" : 10000, + "sum" : 3584, + "median" : 124.0, + "std_deviation": 5.43 + }, + "dimensions" : { + "searcherid" : "x" + } + } + ] +} +} diff --git a/metrics-proxy/src/test/resources/metrics-storage-simple.json b/metrics-proxy/src/test/resources/metrics-storage-simple.json new file mode 100644 index 00000000000..00715b52046 --- /dev/null +++ b/metrics-proxy/src/test/resources/metrics-storage-simple.json @@ -0,0 +1,38 @@ +{ + "status" : { + "code" : "up", + "message": "All good" + }, + + "metrics" : { + "snapshot" : { + "from": 1335523285, + "to": 1335525685 + }, + "values": [ + + { + "name" : "foo", + "values" : { + "count" : 1 + }, + "dimensions" : { + "foo": "bar", + "bar" : "foo" + } + }, + + { + "name" : "bar", + "values" : { + "count" : 2 + }, + "dimensions" : { + "d0": "d0val", + "d1" : "d1val" + } + } + ] + + } +} diff --git a/metrics-proxy/src/test/resources/rpc-json-output-check.json b/metrics-proxy/src/test/resources/rpc-json-output-check.json new file mode 100644 index 00000000000..701a06d82b2 --- /dev/null +++ b/metrics-proxy/src/test/resources/rpc-json-output-check.json @@ -0,0 +1 @@ +{"metrics":[{"status_code":1,"application":"test-system.qrserver","dimensions":{"metrictype":"health","instance":"qrserver"},"status_msg":"SOMETHING FAILED"}]}
\ No newline at end of file diff --git a/metrics-proxy/src/test/resources/yamas-array-no-routing.json b/metrics-proxy/src/test/resources/yamas-array-no-routing.json new file mode 100644 index 00000000000..8f21e8253b9 --- /dev/null +++ b/metrics-proxy/src/test/resources/yamas-array-no-routing.json @@ -0,0 +1,19 @@ +{ + "status_code" : 0, + "timestamp" : 1400047900, + "application" : "vespa.searchnode", + "metrics" : { + "cpu" : 55.5555555555555, + "memory_virt" : 22222222222, + "memory_rss" : 5555555555 + }, + "dimensions" : { + "applicationName" : "app", + "tenantName" : "tenant", + "metrictype" : "system", + "instance" : "searchnode", + "applicationInstance" : "default", + "clustername" : "cluster" + }, + "status_msg" : "Data collected successfully" +} diff --git a/metrics-proxy/src/test/resources/yamas-array.json b/metrics-proxy/src/test/resources/yamas-array.json new file mode 100644 index 00000000000..c9293623b25 --- /dev/null +++ b/metrics-proxy/src/test/resources/yamas-array.json @@ -0,0 +1,26 @@ +{ + "status_code" : 0, + "timestamp" : 1400047900, + "application" : "vespa.searchnode", + "metrics" : { + "cpu" : 55.5555555555555, + "memory_virt" : 22222222222, + "memory_rss" : 5555555555 + }, + "dimensions" : { + "applicationName" : "app", + "tenantName" : "tenant", + "metrictype" : "system", + "instance" : "searchnode", + "applicationInstance" : "default", + "clustername" : "cluster" + }, + "routing" : { + "yamas" : { + "namespaces" : [ + "Vespa" + ] + } + }, + "status_msg" : "Data collected successfully" +} |