summaryrefslogtreecommitdiffstats
path: root/tenant-cd
diff options
context:
space:
mode:
authorJon Marius Venstad <jvenstad@yahoo-inc.com>2019-04-15 14:53:15 +0200
committerJon Marius Venstad <jvenstad@yahoo-inc.com>2019-06-06 13:03:27 +0200
commit5e326ad275e3f45ebae1e137271f63cb3bdaabe9 (patch)
tree980ff8021c0b7fd2f2aa09b3f661d03329e2490c /tenant-cd
parentdf757401715013727123f58f3b9e1aac6dae7f87 (diff)
Initial dump
Diffstat (limited to 'tenant-cd')
-rw-r--r--tenant-cd/pom.xml6
-rw-r--r--tenant-cd/src/main/java/ai/vespa/hosted/cd/ProductionTest.java19
-rw-r--r--tenant-cd/src/main/java/com/yahoo/vespa/tenant/cd/Deployment.java16
-rw-r--r--tenant-cd/src/main/java/com/yahoo/vespa/tenant/cd/Digest.java28
-rw-r--r--tenant-cd/src/main/java/com/yahoo/vespa/tenant/cd/Document.java16
-rw-r--r--tenant-cd/src/main/java/com/yahoo/vespa/tenant/cd/DocumentId.java71
-rw-r--r--tenant-cd/src/main/java/com/yahoo/vespa/tenant/cd/Endpoint.java21
-rw-r--r--tenant-cd/src/main/java/com/yahoo/vespa/tenant/cd/Feed.java25
-rw-r--r--tenant-cd/src/main/java/com/yahoo/vespa/tenant/cd/FunctionalTest.java31
-rw-r--r--tenant-cd/src/main/java/com/yahoo/vespa/tenant/cd/Query.java10
-rw-r--r--tenant-cd/src/main/java/com/yahoo/vespa/tenant/cd/Search.java24
-rw-r--r--tenant-cd/src/main/java/com/yahoo/vespa/tenant/cd/Selection.java10
-rw-r--r--tenant-cd/src/main/java/com/yahoo/vespa/tenant/cd/TestDeployment.java14
-rw-r--r--tenant-cd/src/main/java/com/yahoo/vespa/tenant/cd/TestEndpoint.java15
-rw-r--r--tenant-cd/src/main/java/com/yahoo/vespa/tenant/cd/UpgradeTest.java23
-rw-r--r--tenant-cd/src/main/java/com/yahoo/vespa/tenant/cd/Visit.java17
-rw-r--r--tenant-cd/src/main/java/com/yahoo/vespa/tenant/cd/VisitEndpoint.java10
-rw-r--r--tenant-cd/src/main/java/com/yahoo/vespa/tenant/cd/http/HttpEndpoint.java9
-rw-r--r--tenant-cd/src/main/java/com/yahoo/vespa/tenant/cd/metrics/Metric.java74
-rw-r--r--tenant-cd/src/main/java/com/yahoo/vespa/tenant/cd/metrics/Metrics.java76
-rw-r--r--tenant-cd/src/main/java/com/yahoo/vespa/tenant/cd/metrics/Statistic.java50
-rw-r--r--tenant-cd/src/main/java/com/yahoo/vespa/tenant/cd/metrics/Type.java32
22 files changed, 596 insertions, 1 deletions
diff --git a/tenant-cd/pom.xml b/tenant-cd/pom.xml
index 8907e56762c..99350c332c2 100644
--- a/tenant-cd/pom.xml
+++ b/tenant-cd/pom.xml
@@ -20,6 +20,12 @@
<dependencies>
<dependency>
+ <groupId>com.yahoo.vespa</groupId>
+ <artifactId>vespajlib</artifactId>
+ <version>7-SNAPSHOT</version>
+ </dependency>
+
+ <dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
diff --git a/tenant-cd/src/main/java/ai/vespa/hosted/cd/ProductionTest.java b/tenant-cd/src/main/java/ai/vespa/hosted/cd/ProductionTest.java
index a756b665c1a..6cf5fb07f58 100644
--- a/tenant-cd/src/main/java/ai/vespa/hosted/cd/ProductionTest.java
+++ b/tenant-cd/src/main/java/ai/vespa/hosted/cd/ProductionTest.java
@@ -1,6 +1,23 @@
// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
package ai.vespa.hosted.cd;
-public class ProductionTest {
+/**
+ * Tests that verify the health of production deployments of Vespa applications.
+ *
+ * These tests are typically run some time after deployment to a production zone, to ensure
+ * the deployment is still healthy and working as expected. When these tests fail, deployment
+ * of the tested change is halted until it succeeds, or is superseded by a remedying change.
+ *
+ * A typical production test is to verify that a set of metrics, measured by the Vespa
+ * deployment itself, are within specified parameters, or that some higher-level measure
+ * of quality, such as engagement among end users of the application, is as expected.
+ *
+ * @author jonmv
+ */
+public interface ProductionTest {
+
+ // Want to verify metrics (Vespa).
+ // Want to verify external metrics (YAMAS, other).
+ // May want to verify search gives expected results.
}
diff --git a/tenant-cd/src/main/java/com/yahoo/vespa/tenant/cd/Deployment.java b/tenant-cd/src/main/java/com/yahoo/vespa/tenant/cd/Deployment.java
new file mode 100644
index 00000000000..02627ae7c9e
--- /dev/null
+++ b/tenant-cd/src/main/java/com/yahoo/vespa/tenant/cd/Deployment.java
@@ -0,0 +1,16 @@
+package com.yahoo.vespa.tenant.cd;
+
+/**
+ * A deployment of a Vespa application, which contains endpoints for document and metrics retrieval.
+ *
+ * @author jonmv
+ */
+public interface Deployment {
+
+ /** Returns an Endpoint in the cluster with the "default" id. */
+ Endpoint endpoint();
+
+ /** Returns an Endpoint in the cluster with the given id. */
+ Endpoint endpoint(String id);
+
+}
diff --git a/tenant-cd/src/main/java/com/yahoo/vespa/tenant/cd/Digest.java b/tenant-cd/src/main/java/com/yahoo/vespa/tenant/cd/Digest.java
new file mode 100644
index 00000000000..73b804dc33c
--- /dev/null
+++ b/tenant-cd/src/main/java/com/yahoo/vespa/tenant/cd/Digest.java
@@ -0,0 +1,28 @@
+package com.yahoo.vespa.tenant.cd;
+
+import java.util.Set;
+
+/**
+ * An immutable report of the outcome of a {@link Feed} sent to a {@link TestEndpoint}.
+ *
+ * @author jonmv
+ */
+public class Digest {
+
+ public Set<DocumentId> created() {
+ return null;
+ }
+
+ public Set<DocumentId> updated() {
+ return null;
+ }
+
+ public Set<DocumentId> deleted() {
+ return null;
+ }
+
+ public Set<DocumentId> failed() {
+ return null;
+ }
+
+}
diff --git a/tenant-cd/src/main/java/com/yahoo/vespa/tenant/cd/Document.java b/tenant-cd/src/main/java/com/yahoo/vespa/tenant/cd/Document.java
new file mode 100644
index 00000000000..acb1ea08623
--- /dev/null
+++ b/tenant-cd/src/main/java/com/yahoo/vespa/tenant/cd/Document.java
@@ -0,0 +1,16 @@
+package com.yahoo.vespa.tenant.cd;
+
+/**
+ * A schema-less representation of a generic Vespa document.
+ *
+ * @author jonmv
+ */
+public class Document {
+
+
+ /** Returns a copy of this document, updated with the data in the given document. */
+ public Document updatedBy(Document update) {
+ return null;
+ }
+
+}
diff --git a/tenant-cd/src/main/java/com/yahoo/vespa/tenant/cd/DocumentId.java b/tenant-cd/src/main/java/com/yahoo/vespa/tenant/cd/DocumentId.java
new file mode 100644
index 00000000000..c4fc7103b05
--- /dev/null
+++ b/tenant-cd/src/main/java/com/yahoo/vespa/tenant/cd/DocumentId.java
@@ -0,0 +1,71 @@
+package com.yahoo.vespa.tenant.cd;
+
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * Unique, immutable ID of a Vespa document, which contains information pertinent to its storage.
+ *
+ * @author jonmv
+ */
+public class DocumentId {
+
+ private final String namespace;
+ private final String documentType;
+ private final String group;
+ private final Long number;
+ private final String userDefined;
+
+ private DocumentId(String namespace, String documentType, String group, Long number, String userDefined) {
+ this.namespace = namespace;
+ this.documentType = documentType;
+ this.group = group;
+ this.number = number;
+ this.userDefined = userDefined;
+ }
+
+ public static DocumentId of(String namespace, String documentType, String id) {
+ return new DocumentId(requireNonEmpty(namespace), requireNonEmpty(documentType), null, null, requireNonEmpty(id));
+ }
+
+ public static DocumentId of(String namespace, String documentType, String group, String id) {
+ return new DocumentId(requireNonEmpty(namespace), requireNonEmpty(documentType), requireNonEmpty(group), null, requireNonEmpty(id));
+ }
+
+ public static DocumentId of(String namespace, String documentType, long number, String id) {
+ return new DocumentId(requireNonEmpty(namespace), requireNonEmpty(documentType), null, number, requireNonEmpty(id));
+ }
+
+ public static DocumentId ofValue(String value) {
+ List<String> parts = Arrays.asList(value.split(":"));
+ String id = String.join(":", parts.subList(4, parts.size()));
+ if ( parts.size() < 5
+ || ! parts.get(0).equals("id")
+ || id.isEmpty()
+ || ! parts.get(3).matches("((n=\\d+)|(g=\\w+))?"))
+ throw new IllegalArgumentException("Document id must be on the form" +
+ " 'id:<namespace>:<document type>:n=<integer>|g=<name>|<empty>:<user defined id>'," +
+ " but was '" + value + "'.");
+
+ if (parts.get(3).matches("n=\\d+"))
+ return of(parts.get(1), parts.get(2), Long.parseLong(parts.get(3).substring(2)), id);
+ if (parts.get(3).matches("g=\\w+"))
+ return of(parts.get(1), parts.get(2), parts.get(3).substring(2), id);
+ return of(parts.get(1), parts.get(2), id);
+ }
+
+ public String asValue() {
+ return "id:" + namespace + ":" + documentType + ":" + grouper() + ":" + userDefined;
+ }
+
+ private String grouper() {
+ return group != null ? group : number != null ? number.toString() : "";
+ }
+
+ private static String requireNonEmpty(String string) {
+ if (string.isEmpty())
+ throw new IllegalArgumentException("The empty string is not allowed.");
+ return string;
+ }
+
+}
diff --git a/tenant-cd/src/main/java/com/yahoo/vespa/tenant/cd/Endpoint.java b/tenant-cd/src/main/java/com/yahoo/vespa/tenant/cd/Endpoint.java
new file mode 100644
index 00000000000..e238d38112b
--- /dev/null
+++ b/tenant-cd/src/main/java/com/yahoo/vespa/tenant/cd/Endpoint.java
@@ -0,0 +1,21 @@
+package com.yahoo.vespa.tenant.cd;
+
+import com.yahoo.vespa.tenant.cd.metrics.Metrics;
+
+/**
+ * An endpoint in a Vespa application {@link Deployment}, which allows document and metrics retrieval.
+ *
+ * The endpoint translates {@link Query}s to {@link Search}s, and {@link Selection}s to {@link Visit}s.
+ * It also supplies {@link Metrics}.
+ *
+ * @author jonmv
+ */
+public interface Endpoint {
+
+ Search search(Query query);
+
+ Visit visit(Selection selection);
+
+ Metrics metrics();
+
+}
diff --git a/tenant-cd/src/main/java/com/yahoo/vespa/tenant/cd/Feed.java b/tenant-cd/src/main/java/com/yahoo/vespa/tenant/cd/Feed.java
new file mode 100644
index 00000000000..0b6b1702c80
--- /dev/null
+++ b/tenant-cd/src/main/java/com/yahoo/vespa/tenant/cd/Feed.java
@@ -0,0 +1,25 @@
+package com.yahoo.vespa.tenant.cd;
+
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * An immutable set of document feed / update / delete operations, which can be sent to a Vespa {@link TestEndpoint}.
+ *
+ * @author jonmv
+ */
+public class Feed {
+
+ Map<DocumentId, Document> creations() {
+ return null;
+ }
+
+ Map<DocumentId, Document> updates() {
+ return null;
+ }
+
+ Set<DocumentId> deletions() {
+ return null;
+ }
+
+}
diff --git a/tenant-cd/src/main/java/com/yahoo/vespa/tenant/cd/FunctionalTest.java b/tenant-cd/src/main/java/com/yahoo/vespa/tenant/cd/FunctionalTest.java
new file mode 100644
index 00000000000..3c48e2d32f0
--- /dev/null
+++ b/tenant-cd/src/main/java/com/yahoo/vespa/tenant/cd/FunctionalTest.java
@@ -0,0 +1,31 @@
+package com.yahoo.vespa.tenant.cd;
+
+/**
+ * Tests that compare the behaviour of a Vespa application deployment against a fixed specification.
+ *
+ * These tests are run whenever a change is pushed to a Vespa application, and whenever the Vespa platform
+ * is upgraded, and before any deployments to production zones. When these tests fails, the tested change to
+ * the Vespa application is not rolled out.
+ *
+ * A typical functional test is to feed some documents, optionally verifying that the documents have been processed
+ * as expected, and then to see that queries give the expected results. Another common use is to verify integration
+ * with external services.
+ *
+ * @author jonmv
+ */
+public interface FunctionalTest {
+
+ // Want to feed some documents.
+ // Want to verify document processing and routing is as expected.
+ // Want to check recall on those documents.
+ // Want to verify queries give expected documents.
+ // Want to verify searchers.
+ // Want to verify updates.
+ // Want to verify deletion.
+ // May want to verify reprocessing.
+ // Must likely delete documents between tests.
+ // Must be able to feed documents, setting route.
+ // Must be able to search.
+ // Must be able to visit.
+
+}
diff --git a/tenant-cd/src/main/java/com/yahoo/vespa/tenant/cd/Query.java b/tenant-cd/src/main/java/com/yahoo/vespa/tenant/cd/Query.java
new file mode 100644
index 00000000000..e7b6baa1406
--- /dev/null
+++ b/tenant-cd/src/main/java/com/yahoo/vespa/tenant/cd/Query.java
@@ -0,0 +1,10 @@
+package com.yahoo.vespa.tenant.cd;
+
+/**
+ * An immutable query to send to a Vespa {@link Endpoint}, to receive a {@link Search}.
+ *
+ * @author jonmv
+ */
+public class Query {
+
+}
diff --git a/tenant-cd/src/main/java/com/yahoo/vespa/tenant/cd/Search.java b/tenant-cd/src/main/java/com/yahoo/vespa/tenant/cd/Search.java
new file mode 100644
index 00000000000..c53553628d9
--- /dev/null
+++ b/tenant-cd/src/main/java/com/yahoo/vespa/tenant/cd/Search.java
@@ -0,0 +1,24 @@
+package com.yahoo.vespa.tenant.cd;
+
+import java.util.SortedMap;
+
+/**
+ * The immutable result of sending a {@link Query} to a Vespa {@link Endpoint}.
+ *
+ * @author jonmv
+ */
+public class Search {
+
+ // hits
+ // coverage
+ // searched
+ // full?
+ // results?
+ // resultsFull?
+
+ /** Returns the documents that were returned as the result, with an iteration order of decreasing relevance. */
+ SortedMap<DocumentId, Document> documents() {
+ return null;
+ }
+
+}
diff --git a/tenant-cd/src/main/java/com/yahoo/vespa/tenant/cd/Selection.java b/tenant-cd/src/main/java/com/yahoo/vespa/tenant/cd/Selection.java
new file mode 100644
index 00000000000..b06ac67950a
--- /dev/null
+++ b/tenant-cd/src/main/java/com/yahoo/vespa/tenant/cd/Selection.java
@@ -0,0 +1,10 @@
+package com.yahoo.vespa.tenant.cd;
+
+/**
+ * A document selection expression, used for streaming search and visit against a {@link Endpoint}.
+ *
+ * @author jonmv
+ */
+public class Selection {
+
+}
diff --git a/tenant-cd/src/main/java/com/yahoo/vespa/tenant/cd/TestDeployment.java b/tenant-cd/src/main/java/com/yahoo/vespa/tenant/cd/TestDeployment.java
new file mode 100644
index 00000000000..21bae8545e4
--- /dev/null
+++ b/tenant-cd/src/main/java/com/yahoo/vespa/tenant/cd/TestDeployment.java
@@ -0,0 +1,14 @@
+package com.yahoo.vespa.tenant.cd;
+
+/**
+ * A deployment of a Vespa application, which also contains endpoints for document manipulation.
+ *
+ * @author jonmv
+ */
+public interface TestDeployment extends Deployment {
+
+ TestEndpoint endpoint();
+
+ TestEndpoint endpoint(String id);
+
+}
diff --git a/tenant-cd/src/main/java/com/yahoo/vespa/tenant/cd/TestEndpoint.java b/tenant-cd/src/main/java/com/yahoo/vespa/tenant/cd/TestEndpoint.java
new file mode 100644
index 00000000000..479ae1d17d2
--- /dev/null
+++ b/tenant-cd/src/main/java/com/yahoo/vespa/tenant/cd/TestEndpoint.java
@@ -0,0 +1,15 @@
+package com.yahoo.vespa.tenant.cd;
+
+import java.util.concurrent.Future;
+
+/**
+ * An endpoint in a Vespa application {@link TestDeployment}, which also translates {@link Feed}s to {@link Digest}s.
+ *
+ * @author jonmv
+ */
+public interface TestEndpoint extends Endpoint {
+
+ /** Sends the given Feed to this TestEndpoint, blocking until it is digested, and returns a feed report. */
+ Digest digest(Feed feed);
+
+}
diff --git a/tenant-cd/src/main/java/com/yahoo/vespa/tenant/cd/UpgradeTest.java b/tenant-cd/src/main/java/com/yahoo/vespa/tenant/cd/UpgradeTest.java
new file mode 100644
index 00000000000..329e58b5a89
--- /dev/null
+++ b/tenant-cd/src/main/java/com/yahoo/vespa/tenant/cd/UpgradeTest.java
@@ -0,0 +1,23 @@
+package com.yahoo.vespa.tenant.cd;
+
+/**
+ * Tests that assert continuity of behaviour for Vespa application deployments, through upgrades.
+ *
+ * These tests are run whenever a change is pushed to a Vespa application, and whenever the Vespa platform
+ * is upgraded, and before any deployments to production zones. When these tests fails, the tested change to
+ * the Vespa application is not rolled out.
+ *
+ * A typical upgrade test is to do some operations against a test deployment prior to upgrade, like feed and
+ * search for some documents, perhaps recording some metrics from the deployment, and then to upgrade it,
+ * repeat the exercise, and compare the results from pre and post upgrade.
+ *
+ * TODO Split in platform upgrades and application upgrades?
+ *
+ * @author jonmv
+ */
+public interface UpgradeTest {
+
+ // Want to verify documents are not damaged by upgrade.
+ // May want to verify metrics during upgrade.
+
+}
diff --git a/tenant-cd/src/main/java/com/yahoo/vespa/tenant/cd/Visit.java b/tenant-cd/src/main/java/com/yahoo/vespa/tenant/cd/Visit.java
new file mode 100644
index 00000000000..3e36b9434aa
--- /dev/null
+++ b/tenant-cd/src/main/java/com/yahoo/vespa/tenant/cd/Visit.java
@@ -0,0 +1,17 @@
+package com.yahoo.vespa.tenant.cd;
+
+import java.util.Map;
+
+/**
+ * A stateful visit operation against a {@link Endpoint}.
+ *
+ * @author jonmv
+ */
+public class Visit {
+
+ // Delegate to a blocking iterator, which can be used for iteration as visit is ongoing.
+ public Map<DocumentId, Document> documents() {
+ return null;
+ }
+
+}
diff --git a/tenant-cd/src/main/java/com/yahoo/vespa/tenant/cd/VisitEndpoint.java b/tenant-cd/src/main/java/com/yahoo/vespa/tenant/cd/VisitEndpoint.java
new file mode 100644
index 00000000000..6eb4df625aa
--- /dev/null
+++ b/tenant-cd/src/main/java/com/yahoo/vespa/tenant/cd/VisitEndpoint.java
@@ -0,0 +1,10 @@
+package com.yahoo.vespa.tenant.cd;
+
+/**
+ * A remote endpoint in a Vespa application {@link Deployment}, which translates {@link Selection}s to {@link Visit}s.
+ *
+ * @author jonmv
+ */
+public interface VisitEndpoint {
+
+}
diff --git a/tenant-cd/src/main/java/com/yahoo/vespa/tenant/cd/http/HttpEndpoint.java b/tenant-cd/src/main/java/com/yahoo/vespa/tenant/cd/http/HttpEndpoint.java
new file mode 100644
index 00000000000..c736244e7a8
--- /dev/null
+++ b/tenant-cd/src/main/java/com/yahoo/vespa/tenant/cd/http/HttpEndpoint.java
@@ -0,0 +1,9 @@
+package com.yahoo.vespa.tenant.cd.http;
+
+import java.net.http.HttpClient;
+
+public class HttpEndpoint {
+
+ HttpClient client = HttpClient.newBuilder().build();
+
+}
diff --git a/tenant-cd/src/main/java/com/yahoo/vespa/tenant/cd/metrics/Metric.java b/tenant-cd/src/main/java/com/yahoo/vespa/tenant/cd/metrics/Metric.java
new file mode 100644
index 00000000000..5da4c3fb750
--- /dev/null
+++ b/tenant-cd/src/main/java/com/yahoo/vespa/tenant/cd/metrics/Metric.java
@@ -0,0 +1,74 @@
+package com.yahoo.vespa.tenant.cd.metrics;
+
+import java.util.Map;
+import java.util.NoSuchElementException;
+import java.util.Set;
+import java.util.StringJoiner;
+
+import static java.util.Map.copyOf;
+import static java.util.stream.Collectors.toUnmodifiableMap;
+
+/**
+ * A set of statistics for a metric, for points over a String-indexed space.
+ *
+ * @author jonmv
+ */
+public class Metric {
+
+ private final Map<Map<String, ?>, Statistic> statistics;
+
+ private Metric(Map<Map<String, ?>, Statistic> statistics) {
+ this.statistics = copyOf(statistics);
+ }
+
+ /** Creates a new Metric with a copy of the given data. */
+ public static Metric of(Map<Map<String, ?>, Statistic> data) {
+ if (data.isEmpty())
+ throw new IllegalArgumentException("No data given.");
+
+ Set<String> dimensions = data.keySet().iterator().next().keySet();
+ for (Map<String, ?> point : data.keySet()) {
+ if (point.keySet().contains(null))
+ throw new IllegalArgumentException("Dimensions may not be null: '" + point.keySet() + "'.");
+
+ if ( ! point.keySet().equals(dimensions))
+ throw new IllegalArgumentException("Given data has inconsistent dimensions: '" + dimensions + "' vs '" + point.keySet() + "'.");
+
+ if (point.values().contains(null))
+ throw new IllegalArgumentException("Position along a dimension may not be null: '" + point + "'.");
+ }
+
+ return new Metric(data);
+ }
+
+ /** Returns a Metric view of the subset of points in the given hyperplane; its dimensions must be a subset of those of this Metric. */
+ public Metric at(Map<String, ?> hyperplane) {
+ return new Metric(statistics.keySet().stream()
+ .filter(point -> point.entrySet().containsAll(hyperplane.entrySet()))
+ .collect(toUnmodifiableMap(point -> point, statistics::get)));
+ }
+
+ /** If this Metric contains a single point, returns the Statistic of that point; otherwise, throws an exception. */
+ public Statistic statistic() {
+ if (statistics.size() == 1)
+ return statistics.values().iterator().next();
+
+ if (statistics.isEmpty())
+ throw new NoSuchElementException("This Metric has no data.");
+
+ throw new IllegalStateException("This Metric has more than one point of data.");
+ }
+
+ /** Returns the underlying, unmodifiable Map. */
+ public Map<Map<String, ?>, Statistic> asMap() {
+ return statistics;
+ }
+
+ @Override
+ public String toString() {
+ return new StringJoiner(", ", Metric.class.getSimpleName() + "[", "]")
+ .add("statistics=" + statistics)
+ .toString();
+ }
+
+}
diff --git a/tenant-cd/src/main/java/com/yahoo/vespa/tenant/cd/metrics/Metrics.java b/tenant-cd/src/main/java/com/yahoo/vespa/tenant/cd/metrics/Metrics.java
new file mode 100644
index 00000000000..c080d82a343
--- /dev/null
+++ b/tenant-cd/src/main/java/com/yahoo/vespa/tenant/cd/metrics/Metrics.java
@@ -0,0 +1,76 @@
+package com.yahoo.vespa.tenant.cd.metrics;
+
+import com.yahoo.vespa.tenant.cd.Endpoint;
+
+import java.time.Instant;
+import java.util.Map;
+import java.util.NoSuchElementException;
+import java.util.StringJoiner;
+
+import static java.util.Map.copyOf;
+
+/**
+ * Metrics from a Vespa application {@link Endpoint}, indexed by their names, and optionally by a set of custom dimensions.
+ *
+ * Metrics are collected from the <a href="https://docs.vespa.ai/documentation/reference/metrics-health-format.html>metrics</a>
+ * API of a Vespa endpoint, and contain the current health status of the endpoint, values for all configured metrics in
+ * that endpoint, and the time interval from which these metrics were sampled.
+ *
+ * Each metric is indexed by a name, and, optionally, along a custom set of dimensions, given by a {@code Map<String, String>}.
+ *
+ * @author jonmv
+ */
+public class Metrics {
+
+ private final Instant start, end;
+ private final Map<String, Metric> metrics;
+
+ private Metrics(Instant start, Instant end, Map<String, Metric> metrics) {
+ this.start = start;
+ this.end = end;
+ this.metrics = copyOf(metrics);
+ }
+
+ public static Metrics of(Instant start, Instant end, Map<String, Metric> metrics) {
+ if (metrics.containsKey(null) || metrics.containsValue(null))
+ throw new IllegalArgumentException("Metrics may not contain null keys or values: '" + metrics + "'.");
+
+ if ( ! start.isBefore(end))
+ throw new IllegalArgumentException("Given time interval must be positive: '" + start + "' to '" + end + "'.");
+
+ return new Metrics(start, end, metrics);
+ }
+
+ /** Returns the start of the time window from which these metrics were sampled, or throws if the status is {@code Status.down}. */
+ public Instant start() {
+ return start;
+ }
+
+ /** Returns the end of the time window from which these metrics were sampled, or throws if the status is {@code Status.down}. */
+ public Instant end() {
+ return end;
+ }
+
+ /** Returns the metric with the given name, or throws a NoSuchElementException if no such Metric is known. */
+ public Metric get(String name) {
+ if ( ! metrics.containsKey(name))
+ throw new NoSuchElementException("No metric with name '" + name + "'.");
+
+ return metrics.get(name);
+ }
+
+ /** Returns the underlying, unmodifiable Map. */
+ public Map<String, Metric> asMap() {
+ return metrics;
+ }
+
+ @Override
+ public String toString() {
+ return new StringJoiner(", ", Metrics.class.getSimpleName() + "[", "]")
+ .add("start=" + start)
+ .add("end=" + end)
+ .add("metrics=" + metrics)
+ .toString();
+ }
+
+}
diff --git a/tenant-cd/src/main/java/com/yahoo/vespa/tenant/cd/metrics/Statistic.java b/tenant-cd/src/main/java/com/yahoo/vespa/tenant/cd/metrics/Statistic.java
new file mode 100644
index 00000000000..65c6dc3ed3f
--- /dev/null
+++ b/tenant-cd/src/main/java/com/yahoo/vespa/tenant/cd/metrics/Statistic.java
@@ -0,0 +1,50 @@
+package com.yahoo.vespa.tenant.cd.metrics;
+
+import java.util.Map;
+import java.util.NoSuchElementException;
+import java.util.StringJoiner;
+
+import static java.util.Map.copyOf;
+
+/**
+ * Known statistic about a metric, at a certain point.
+ *
+ * @author jonmv
+ */
+public class Statistic {
+
+ private final Map<Type, Double> data;
+
+ /** Creates a new Statistic with a copy of the given data. */
+ private Statistic(Map<Type, Double> data) {
+ this.data = copyOf(data);
+ }
+
+ public static Statistic of(Map<Type, Double> data) {
+ if (data.containsKey(null) || data.containsValue(null))
+ throw new IllegalArgumentException("Data may not contain null keys or values: '" + data + "'.");
+
+ return new Statistic(data);
+ }
+
+ /** Returns the value of the given type, or throws a NoSuchElementException if this isn't known. */
+ public double get(Type key) {
+ if ( ! data.containsKey(key))
+ throw new NoSuchElementException("No value with key '" + key + "' is known.");
+
+ return data.get(key);
+ }
+
+ /** Returns the underlying, unmodifiable Map. */
+ public Map<Type, Double> asMap() {
+ return data;
+ }
+
+ @Override
+ public String toString() {
+ return new StringJoiner(", ", Statistic.class.getSimpleName() + "[", "]")
+ .add("data=" + data)
+ .toString();
+ }
+
+}
diff --git a/tenant-cd/src/main/java/com/yahoo/vespa/tenant/cd/metrics/Type.java b/tenant-cd/src/main/java/com/yahoo/vespa/tenant/cd/metrics/Type.java
new file mode 100644
index 00000000000..b8b168cc649
--- /dev/null
+++ b/tenant-cd/src/main/java/com/yahoo/vespa/tenant/cd/metrics/Type.java
@@ -0,0 +1,32 @@
+package com.yahoo.vespa.tenant.cd.metrics;
+
+/**
+ * Known statistic types.
+ */
+public enum Type {
+
+ /** 95th percentile measurement. */
+ percentile95,
+
+ /** 99th percentile measurement. */
+ percentile99,
+
+ /** Average over all measurements. */
+ average,
+
+ /** Number of measurements. */
+ count,
+
+ /** Last measurement. */
+ last,
+
+ /** Maximum measurement. */
+ max,
+
+ /** Minimum measurement. */
+ min,
+
+ /** Number of measurements per second. */
+ rate;
+
+}