summaryrefslogtreecommitdiffstats
path: root/controller-server
diff options
context:
space:
mode:
Diffstat (limited to 'controller-server')
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/RoutingController.java4
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/horizon/HorizonApiHandler.java99
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/horizon/TsdbQueryRewriter.java102
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/certificate/EndpointCertificatesTest.java3
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/DeploymentExpirerTest.java2
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/horizon/TsdbQueryRewriterTest.java55
-rw-r--r--controller-server/src/test/resources/horizon/filter-in-execution-graph.expected.json37
-rw-r--r--controller-server/src/test/resources/horizon/filter-in-execution-graph.expected.operator.json32
-rw-r--r--controller-server/src/test/resources/horizon/filter-in-execution-graph.json21
-rw-r--r--controller-server/src/test/resources/horizon/filters-complex.expected.json56
-rw-r--r--controller-server/src/test/resources/horizon/filters-complex.json46
-rw-r--r--controller-server/src/test/resources/horizon/no-filters.expected.json32
-rw-r--r--controller-server/src/test/resources/horizon/no-filters.json16
13 files changed, 495 insertions, 10 deletions
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/RoutingController.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/RoutingController.java
index 1b1df28c201..990549b6d8c 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/RoutingController.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/RoutingController.java
@@ -72,7 +72,6 @@ public class RoutingController {
private final RoutingPolicies routingPolicies;
private final RotationRepository rotationRepository;
private final BooleanFlag hideSharedRoutingEndpoint;
- private final BooleanFlag vespaAppDomainInCertificate;
public RoutingController(Controller controller, RotationsConfig rotationsConfig) {
this.controller = Objects.requireNonNull(controller, "controller must be non-null");
@@ -80,7 +79,6 @@ public class RoutingController {
this.rotationRepository = new RotationRepository(rotationsConfig, controller.applications(),
controller.curator());
this.hideSharedRoutingEndpoint = Flags.HIDE_SHARED_ROUTING_ENDPOINT.bindTo(controller.flagSource());
- this.vespaAppDomainInCertificate = Flags.VESPA_APP_DOMAIN_IN_CERTIFICATE.bindTo(controller.flagSource());
}
public RoutingPolicies policies() {
@@ -180,7 +178,7 @@ public class RoutingController {
builder = builder.routingMethod(RoutingMethod.exclusive)
.on(Port.tls());
Endpoint endpoint = builder.in(controller.system());
- if (controller.system().isPublic() && vespaAppDomainInCertificate.with(FetchVector.Dimension.APPLICATION_ID, deployment.applicationId().serializedForm()).value()) {
+ if (controller.system().isPublic()) {
Endpoint legacyEndpoint = builder.legacy().in(controller.system());
endpointDnsNames.add(legacyEndpoint.dnsName());
}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/horizon/HorizonApiHandler.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/horizon/HorizonApiHandler.java
index 83efccbf1e5..422b8f22000 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/horizon/HorizonApiHandler.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/horizon/HorizonApiHandler.java
@@ -1,10 +1,23 @@
+// Copyright Verizon Media. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
package com.yahoo.vespa.hosted.controller.restapi.horizon;
import com.google.inject.Inject;
+import com.yahoo.config.provision.SystemName;
import com.yahoo.container.jdisc.HttpRequest;
import com.yahoo.container.jdisc.HttpResponse;
import com.yahoo.container.jdisc.LoggingRequestHandler;
-import com.yahoo.restapi.MessageResponse;
+import com.yahoo.restapi.ErrorResponse;
+import com.yahoo.restapi.Path;
+import com.yahoo.vespa.hosted.controller.Controller;
+import com.yahoo.vespa.hosted.controller.api.integration.horizon.HorizonClient;
+import com.yahoo.vespa.hosted.controller.api.role.SecurityContext;
+import com.yahoo.yolean.Exceptions;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.Optional;
+import java.util.logging.Level;
/**
* Proxies metrics requests from Horizon UI
@@ -13,13 +26,93 @@ import com.yahoo.restapi.MessageResponse;
*/
public class HorizonApiHandler extends LoggingRequestHandler {
+ private final SystemName systemName;
+ private final HorizonClient client;
+
@Inject
- public HorizonApiHandler(LoggingRequestHandler.Context parentCtx) {
+ public HorizonApiHandler(LoggingRequestHandler.Context parentCtx, Controller controller) {
super(parentCtx);
+ this.systemName = controller.system();
+ this.client = controller.serviceRegistry().horizonClient();
}
@Override
public HttpResponse handle(HttpRequest request) {
- return new MessageResponse("OK");
+ try {
+ switch (request.getMethod()) {
+ case GET: return get(request);
+ case POST: return post(request);
+ case PUT: return put(request);
+ default: return ErrorResponse.methodNotAllowed("Method '" + request.getMethod() + "' is not supported");
+ }
+ }
+ catch (IllegalArgumentException e) {
+ return ErrorResponse.badRequest(Exceptions.toMessageString(e));
+ }
+ catch (RuntimeException e) {
+ log.log(Level.WARNING, "Unexpected error handling '" + request.getUri() + "'", e);
+ return ErrorResponse.internalServerError(Exceptions.toMessageString(e));
+ }
+ }
+
+ private HttpResponse get(HttpRequest request) {
+ Path path = new Path(request.getUri());
+ if (path.matches("/horizon/v1/config/dashboard/topFolders")) return new JsonInputStreamResponse(client.getTopFolders());
+ if (path.matches("/horizon/v1/config/dashboard/file/{id}")) return new JsonInputStreamResponse(client.getDashboard(path.get("id")));
+ if (path.matches("/horizon/v1/config/dashboard/favorite")) return new JsonInputStreamResponse(client.getFavorite(request.getProperty("user")));
+ if (path.matches("/horizon/v1/config/dashboard/recent")) return new JsonInputStreamResponse(client.getRecent(request.getProperty("user")));
+ return ErrorResponse.notFoundError("Nothing at " + path);
+ }
+
+ private HttpResponse post(HttpRequest request) {
+ Path path = new Path(request.getUri());
+ if (path.matches("/horizon/v1/tsdb/api/query/graph")) return tsdbQuery(request);
+ if (path.matches("/horizon/v1/meta/search/timeseries")) assert true; // TODO
+ return ErrorResponse.notFoundError("Nothing at " + path);
+ }
+
+ private HttpResponse put(HttpRequest request) {
+ Path path = new Path(request.getUri());
+ if (path.matches("/horizon/v1/config/user")) return new JsonInputStreamResponse(client.getUser());
+ return ErrorResponse.notFoundError("Nothing at " + path);
+ }
+
+ private HttpResponse tsdbQuery(HttpRequest request) {
+ SecurityContext securityContext = getAttribute(request, SecurityContext.ATTRIBUTE_NAME, SecurityContext.class);
+ try {
+ byte[] data = TsdbQueryRewriter.rewrite(request.getData().readAllBytes(), securityContext.roles(), systemName);
+ return new JsonInputStreamResponse(client.getMetrics(data));
+ } catch (TsdbQueryRewriter.UnauthorizedException e) {
+ return ErrorResponse.forbidden("Access denied");
+ } catch (IOException e) {
+ return ErrorResponse.badRequest("Failed to parse request body: " + e.getMessage());
+ }
+ }
+
+ private static <T> T getAttribute(HttpRequest request, String attributeName, Class<T> clazz) {
+ return Optional.ofNullable(request.getJDiscRequest().context().get(attributeName))
+ .filter(clazz::isInstance)
+ .map(clazz::cast)
+ .orElseThrow(() -> new IllegalArgumentException("Attribute '" + attributeName + "' was not set on request"));
+ }
+
+ private static class JsonInputStreamResponse extends HttpResponse {
+
+ private final InputStream jsonInputStream;
+
+ public JsonInputStreamResponse(InputStream jsonInputStream) {
+ super(200);
+ this.jsonInputStream = jsonInputStream;
+ }
+
+ @Override
+ public String getContentType() {
+ return "application/json";
+ }
+
+ @Override
+ public void render(OutputStream outputStream) throws IOException {
+ jsonInputStream.transferTo(outputStream);
+ }
}
}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/horizon/TsdbQueryRewriter.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/horizon/TsdbQueryRewriter.java
new file mode 100644
index 00000000000..40bc9145ce2
--- /dev/null
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/horizon/TsdbQueryRewriter.java
@@ -0,0 +1,102 @@
+// Copyright Verizon Media. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.hosted.controller.restapi.horizon;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.node.ArrayNode;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import com.yahoo.config.provision.SystemName;
+import com.yahoo.config.provision.TenantName;
+import com.yahoo.vespa.hosted.controller.api.role.Role;
+import com.yahoo.vespa.hosted.controller.api.role.RoleDefinition;
+import com.yahoo.vespa.hosted.controller.api.role.TenantRole;
+
+import java.io.IOException;
+import java.util.EnumSet;
+import java.util.Optional;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+/**
+ * @author valerijf
+ */
+public class TsdbQueryRewriter {
+
+ private static final ObjectMapper mapper = new ObjectMapper();
+ private static final EnumSet<RoleDefinition> operatorRoleDefinitions =
+ EnumSet.of(RoleDefinition.hostedOperator, RoleDefinition.hostedSupporter);
+
+ public static byte[] rewrite(byte[] data, Set<Role> roles, SystemName systemName) throws IOException {
+ boolean operator = roles.stream().map(Role::definition).anyMatch(operatorRoleDefinitions::contains);
+
+ // Anyone with any tenant relation can view metrics for apps within those tenants
+ Set<TenantName> authorizedTenants = roles.stream()
+ .filter(TenantRole.class::isInstance)
+ .map(role -> ((TenantRole) role).tenant())
+ .collect(Collectors.toUnmodifiableSet());
+ if (!operator && authorizedTenants.isEmpty())
+ throw new UnauthorizedException();
+
+ JsonNode root = mapper.readTree(data);
+ getField(root, "executionGraph", ArrayNode.class)
+ .ifPresent(graph -> rewriteExecutionGraph(graph, authorizedTenants, operator, systemName));
+ getField(root, "filters", ArrayNode.class)
+ .ifPresent(filters -> rewriteFilters(filters, authorizedTenants, operator, systemName));
+
+ return mapper.writeValueAsBytes(root);
+ }
+
+ private static void rewriteExecutionGraph(ArrayNode executionGraph, Set<TenantName> tenantNames, boolean operator, SystemName systemName) {
+ for (int i = 0; i < executionGraph.size(); i++) {
+ JsonNode execution = executionGraph.get(i);
+
+ // Will be handled by rewriteFilters()
+ if (execution.has("filterId")) continue;
+
+ rewriteFilter((ObjectNode) execution, tenantNames, operator, systemName);
+ }
+ }
+
+ private static void rewriteFilters(ArrayNode filters, Set<TenantName> tenantNames, boolean operator, SystemName systemName) {
+ for (int i = 0; i < filters.size(); i++)
+ rewriteFilter((ObjectNode) filters.get(i), tenantNames, operator, systemName);
+ }
+
+ private static void rewriteFilter(ObjectNode parent, Set<TenantName> tenantNames, boolean operator, SystemName systemName) {
+ ObjectNode prev = ((ObjectNode) parent.get("filter"));
+ ArrayNode filters;
+ // If we dont already have a filter object, or the object that we have is not an AND filter
+ if (prev == null || !"Chain".equals(prev.get("type").asText()) || !"AND".equals(prev.get("op").asText())) {
+ // Create new filter object
+ filters = parent.putObject("filter")
+ .put("type", "Chain")
+ .put("op", "AND")
+ .putArray("filters");
+
+ // Add the previous filter to the AND expression
+ if (prev != null) filters.add(prev);
+ } else filters = (ArrayNode) prev.get("filters");
+
+ // Make sure we only show metrics in the relevant system
+ ObjectNode systemFilter = filters.addObject();
+ systemFilter.put("type", "TagValueLiteralOr");
+ systemFilter.put("filter", systemName.name());
+ systemFilter.put("tagKey", "system");
+
+ // Make sure non-operators cannot see metrics outside of their tenants
+ if (!operator) {
+ ObjectNode appFilter = filters.addObject();
+ appFilter.put("type", "TagValueRegex");
+ appFilter.put("filter",
+ tenantNames.stream().map(TenantName::value).sorted().collect(Collectors.joining("|", "(", ")\\..*")));
+ appFilter.put("tagKey", "applicationId");
+ }
+ }
+
+ private static <T extends JsonNode> Optional<T> getField(JsonNode object, String fieldName, Class<T> clazz) {
+ return Optional.ofNullable(object.get(fieldName)).filter(clazz::isInstance).map(clazz::cast);
+ }
+
+ static class UnauthorizedException extends RuntimeException { }
+
+}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/certificate/EndpointCertificatesTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/certificate/EndpointCertificatesTest.java
index d8544ff3947..a3580a9fda3 100644
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/certificate/EndpointCertificatesTest.java
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/certificate/EndpointCertificatesTest.java
@@ -13,8 +13,6 @@ import com.yahoo.security.SignatureAlgorithm;
import com.yahoo.security.X509CertificateBuilder;
import com.yahoo.security.X509CertificateUtils;
import com.yahoo.test.ManualClock;
-import com.yahoo.vespa.flags.Flags;
-import com.yahoo.vespa.flags.InMemoryFlagSource;
import com.yahoo.vespa.hosted.controller.ControllerTester;
import com.yahoo.vespa.hosted.controller.Instance;
import com.yahoo.vespa.hosted.controller.api.integration.certificates.EndpointCertificateMetadata;
@@ -133,7 +131,6 @@ public class EndpointCertificatesTest {
@Test
public void provisions_new_certificate_in_public_prod() {
ControllerTester tester = new ControllerTester(SystemName.Public);
- ((InMemoryFlagSource) tester.controller().flagSource()).withBooleanFlag(Flags.VESPA_APP_DOMAIN_IN_CERTIFICATE.id(), true);
EndpointCertificateValidatorImpl endpointCertificateValidator = new EndpointCertificateValidatorImpl(secretStore, clock);
EndpointCertificates endpointCertificates = new EndpointCertificates(tester.controller(), endpointCertificateMock, endpointCertificateValidator);
List<String> expectedSans = List.of(
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/DeploymentExpirerTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/DeploymentExpirerTest.java
index 232521c9609..ffc82f90ad4 100644
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/DeploymentExpirerTest.java
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/DeploymentExpirerTest.java
@@ -70,7 +70,7 @@ public class DeploymentExpirerTest {
assertEquals(1, permanentDeployments(prodApp.instance()));
// Dev application expires when enough time has passed since most recent attempt
- tester.clock().advance(Duration.ofDays(12));
+ tester.clock().advance(Duration.ofDays(12).plus(Duration.ofSeconds(1)));
expirer.maintain();
assertEquals(0, permanentDeployments(devApp.instance()));
assertEquals(1, permanentDeployments(prodApp.instance()));
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/horizon/TsdbQueryRewriterTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/horizon/TsdbQueryRewriterTest.java
new file mode 100644
index 00000000000..937b0e95440
--- /dev/null
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/horizon/TsdbQueryRewriterTest.java
@@ -0,0 +1,55 @@
+package com.yahoo.vespa.hosted.controller.restapi.horizon;
+
+import com.yahoo.config.provision.SystemName;
+import com.yahoo.config.provision.TenantName;
+import com.yahoo.slime.JsonFormat;
+import com.yahoo.slime.SlimeUtils;
+import com.yahoo.vespa.hosted.controller.api.role.Role;
+import org.junit.Test;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Paths;
+import java.util.Set;
+
+import static org.junit.Assert.assertEquals;
+
+/**
+ * @author valerijf
+ */
+public class TsdbQueryRewriterTest {
+
+ @Test
+ public void rewrites_query() throws IOException {
+ assertRewrite("filters-complex.json", "filters-complex.expected.json", Role.reader(TenantName.from("tenant2")));
+
+ assertRewrite("filter-in-execution-graph.json",
+ "filter-in-execution-graph.expected.json",
+ Role.reader(TenantName.from("tenant2")), Role.athenzTenantAdmin(TenantName.from("tenant3")));
+
+ assertRewrite("filter-in-execution-graph.json",
+ "filter-in-execution-graph.expected.operator.json",
+ Role.reader(TenantName.from("tenant2")), Role.athenzTenantAdmin(TenantName.from("tenant3")), Role.hostedOperator());
+
+ assertRewrite("no-filters.json",
+ "no-filters.expected.json",
+ Role.reader(TenantName.from("tenant2")), Role.athenzTenantAdmin(TenantName.from("tenant3")));
+ }
+
+ @Test(expected = TsdbQueryRewriter.UnauthorizedException.class)
+ public void throws_if_no_roles() throws IOException {
+ assertRewrite("filters-complex.json", "filters-complex.expected.json");
+ }
+
+ private static void assertRewrite(String initialFilename, String expectedFilename, Role... roles) throws IOException {
+ byte[] data = Files.readAllBytes(Paths.get("src/test/resources/horizon", initialFilename));
+ data = TsdbQueryRewriter.rewrite(data, Set.of(roles), SystemName.Public);
+
+ ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ new JsonFormat(false).encode(baos, SlimeUtils.jsonToSlime(data));
+ String expectedJson = Files.readString(Paths.get("src/test/resources/horizon", expectedFilename));
+
+ assertEquals(expectedJson, baos.toString());
+ }
+} \ No newline at end of file
diff --git a/controller-server/src/test/resources/horizon/filter-in-execution-graph.expected.json b/controller-server/src/test/resources/horizon/filter-in-execution-graph.expected.json
new file mode 100644
index 00000000000..7c279442f1d
--- /dev/null
+++ b/controller-server/src/test/resources/horizon/filter-in-execution-graph.expected.json
@@ -0,0 +1,37 @@
+{
+ "start": 1619301600000,
+ "end": 1623161217471,
+ "executionGraph": [
+ {
+ "id": "q1_m1",
+ "type": "TimeSeriesDataSource",
+ "metric": {
+ "type": "MetricLiteral",
+ "metric": "Vespa.vespa.distributor.vds.distributor.docsstored.average"
+ },
+ "sourceId": null,
+ "fetchLast": false,
+ "filter": {
+ "type": "Chain",
+ "op": "AND",
+ "filters": [
+ {
+ "type": "TagValueLiteralOr",
+ "filter": "tenant1.application1.instance1",
+ "tagKey": "applicationId"
+ },
+ {
+ "type": "TagValueLiteralOr",
+ "filter": "Public",
+ "tagKey": "system"
+ },
+ {
+ "type": "TagValueRegex",
+ "filter": "(tenant2|tenant3)\\..*",
+ "tagKey": "applicationId"
+ }
+ ]
+ }
+ }
+ ]
+}
diff --git a/controller-server/src/test/resources/horizon/filter-in-execution-graph.expected.operator.json b/controller-server/src/test/resources/horizon/filter-in-execution-graph.expected.operator.json
new file mode 100644
index 00000000000..5eb5b403b2f
--- /dev/null
+++ b/controller-server/src/test/resources/horizon/filter-in-execution-graph.expected.operator.json
@@ -0,0 +1,32 @@
+{
+ "start": 1619301600000,
+ "end": 1623161217471,
+ "executionGraph": [
+ {
+ "id": "q1_m1",
+ "type": "TimeSeriesDataSource",
+ "metric": {
+ "type": "MetricLiteral",
+ "metric": "Vespa.vespa.distributor.vds.distributor.docsstored.average"
+ },
+ "sourceId": null,
+ "fetchLast": false,
+ "filter": {
+ "type": "Chain",
+ "op": "AND",
+ "filters": [
+ {
+ "type": "TagValueLiteralOr",
+ "filter": "tenant1.application1.instance1",
+ "tagKey": "applicationId"
+ },
+ {
+ "type": "TagValueLiteralOr",
+ "filter": "Public",
+ "tagKey": "system"
+ }
+ ]
+ }
+ }
+ ]
+}
diff --git a/controller-server/src/test/resources/horizon/filter-in-execution-graph.json b/controller-server/src/test/resources/horizon/filter-in-execution-graph.json
new file mode 100644
index 00000000000..6a2512c3642
--- /dev/null
+++ b/controller-server/src/test/resources/horizon/filter-in-execution-graph.json
@@ -0,0 +1,21 @@
+{
+ "start": 1619301600000,
+ "end": 1623161217471,
+ "executionGraph": [
+ {
+ "id": "q1_m1",
+ "type": "TimeSeriesDataSource",
+ "metric": {
+ "type": "MetricLiteral",
+ "metric": "Vespa.vespa.distributor.vds.distributor.docsstored.average"
+ },
+ "sourceId": null,
+ "fetchLast": false,
+ "filter": {
+ "type": "TagValueLiteralOr",
+ "filter": "tenant1.application1.instance1",
+ "tagKey": "applicationId"
+ }
+ }
+ ]
+} \ No newline at end of file
diff --git a/controller-server/src/test/resources/horizon/filters-complex.expected.json b/controller-server/src/test/resources/horizon/filters-complex.expected.json
new file mode 100644
index 00000000000..333e79150e4
--- /dev/null
+++ b/controller-server/src/test/resources/horizon/filters-complex.expected.json
@@ -0,0 +1,56 @@
+{
+ "start": 1623080040000,
+ "end": 1623166440000,
+ "executionGraph": [
+ {
+ "id": "q1_m1",
+ "type": "TimeSeriesDataSource",
+ "metric": {
+ "type": "MetricLiteral",
+ "metric": "Vespa.vespa.qrserver.documents_covered.count"
+ },
+ "sourceId": null,
+ "fetchLast": false,
+ "filterId": "filter-ni8"
+ }
+ ],
+ "filters": [
+ {
+ "filter": {
+ "type": "Chain",
+ "op": "AND",
+ "filters": [
+ {
+ "type": "NOT",
+ "filter": {
+ "type": "TagValueLiteralOr",
+ "filter": "tenant1.app1.instance1",
+ "tagKey": "applicationId"
+ }
+ },
+ {
+ "type": "TagValueLiteralOr",
+ "filter": "Public",
+ "tagKey": "system"
+ },
+ {
+ "type": "TagValueRegex",
+ "filter": "(tenant2)\\..*",
+ "tagKey": "applicationId"
+ }
+ ]
+ },
+ "id": "filter-ni8"
+ }
+ ],
+ "serdesConfigs": [
+ {
+ "id": "JsonV3QuerySerdes",
+ "filter": [
+ "summarizer"
+ ]
+ }
+ ],
+ "logLevel": "ERROR",
+ "cacheMode": null
+}
diff --git a/controller-server/src/test/resources/horizon/filters-complex.json b/controller-server/src/test/resources/horizon/filters-complex.json
new file mode 100644
index 00000000000..3acc7fe5044
--- /dev/null
+++ b/controller-server/src/test/resources/horizon/filters-complex.json
@@ -0,0 +1,46 @@
+{
+ "start": 1623080040000,
+ "end": 1623166440000,
+ "executionGraph": [
+ {
+ "id": "q1_m1",
+ "type": "TimeSeriesDataSource",
+ "metric": {
+ "type": "MetricLiteral",
+ "metric": "Vespa.vespa.qrserver.documents_covered.count"
+ },
+ "sourceId": null,
+ "fetchLast": false,
+ "filterId": "filter-ni8"
+ }
+ ],
+ "filters": [
+ {
+ "filter": {
+ "type": "Chain",
+ "op": "AND",
+ "filters": [
+ {
+ "type": "NOT",
+ "filter": {
+ "type": "TagValueLiteralOr",
+ "filter": "tenant1.app1.instance1",
+ "tagKey": "applicationId"
+ }
+ }
+ ]
+ },
+ "id": "filter-ni8"
+ }
+ ],
+ "serdesConfigs": [
+ {
+ "id": "JsonV3QuerySerdes",
+ "filter": [
+ "summarizer"
+ ]
+ }
+ ],
+ "logLevel": "ERROR",
+ "cacheMode": null
+}
diff --git a/controller-server/src/test/resources/horizon/no-filters.expected.json b/controller-server/src/test/resources/horizon/no-filters.expected.json
new file mode 100644
index 00000000000..d2d6407e6b4
--- /dev/null
+++ b/controller-server/src/test/resources/horizon/no-filters.expected.json
@@ -0,0 +1,32 @@
+{
+ "start": 1619301600000,
+ "end": 1623161217471,
+ "executionGraph": [
+ {
+ "id": "q1_m1",
+ "type": "TimeSeriesDataSource",
+ "metric": {
+ "type": "MetricLiteral",
+ "metric": "Vespa.vespa.distributor.vds.distributor.docsstored.average"
+ },
+ "sourceId": null,
+ "fetchLast": false,
+ "filter": {
+ "type": "Chain",
+ "op": "AND",
+ "filters": [
+ {
+ "type": "TagValueLiteralOr",
+ "filter": "Public",
+ "tagKey": "system"
+ },
+ {
+ "type": "TagValueRegex",
+ "filter": "(tenant2|tenant3)\\..*",
+ "tagKey": "applicationId"
+ }
+ ]
+ }
+ }
+ ]
+}
diff --git a/controller-server/src/test/resources/horizon/no-filters.json b/controller-server/src/test/resources/horizon/no-filters.json
new file mode 100644
index 00000000000..3ff80feba02
--- /dev/null
+++ b/controller-server/src/test/resources/horizon/no-filters.json
@@ -0,0 +1,16 @@
+{
+ "start": 1619301600000,
+ "end": 1623161217471,
+ "executionGraph": [
+ {
+ "id": "q1_m1",
+ "type": "TimeSeriesDataSource",
+ "metric": {
+ "type": "MetricLiteral",
+ "metric": "Vespa.vespa.distributor.vds.distributor.docsstored.average"
+ },
+ "sourceId": null,
+ "fetchLast": false
+ }
+ ]
+} \ No newline at end of file