summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorOla Aunrønning <ola.aunroe@gmail.com>2018-08-24 14:54:27 +0200
committerOla Aunrønning <ola.aunroe@gmail.com>2018-09-10 13:07:15 +0200
commit44053e445e8f893514667325be40613830056e6e (patch)
treebd1f6be2b7ec2f419d2a9363de96fecb32988152
parent79fbe75a324084d7e871e4aa7b82500e7ccd35b3 (diff)
Add functionality for retrieving logs
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/ApplicationRepository.java16
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/http/LogRetriever.java41
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/http/v2/ApplicationHandler.java12
-rw-r--r--configserver/src/main/resources/configserver-app/services.xml2
-rw-r--r--configserver/src/test/java/com/yahoo/vespa/config/server/http/LogRetrieverTest.java46
-rw-r--r--container-core/src/main/java/com/yahoo/container/handler/LogHandler.java31
-rw-r--r--container-core/src/main/java/com/yahoo/container/handler/LogReader.java44
-rw-r--r--container-core/src/test/java/com/yahoo/container/handler/LogReaderTest.java36
-rw-r--r--container-core/src/test/resources/logfolder/log1.log1
-rw-r--r--container-core/src/test/resources/logfolder/subfolder/log2.log1
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/configserver/ConfigServer.java2
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/application/ApplicationApiHandler.java9
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/integration/ConfigServerMock.java16
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/ApplicationApiTest.java3
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/logs.json5
15 files changed, 264 insertions, 1 deletions
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/ApplicationRepository.java b/configserver/src/main/java/com/yahoo/vespa/config/server/ApplicationRepository.java
index a8b4844ca43..585a1a67f4d 100644
--- a/configserver/src/main/java/com/yahoo/vespa/config/server/ApplicationRepository.java
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/ApplicationRepository.java
@@ -36,6 +36,7 @@ import com.yahoo.vespa.config.server.configchange.RestartActions;
import com.yahoo.vespa.config.server.deploy.DeployHandlerLogger;
import com.yahoo.vespa.config.server.deploy.Deployment;
import com.yahoo.vespa.config.server.http.CompressedApplicationInputStream;
+import com.yahoo.vespa.config.server.http.LogRetriever;
import com.yahoo.vespa.config.server.http.SimpleHttpFetcher;
import com.yahoo.vespa.config.server.http.v2.PrepareResult;
import com.yahoo.vespa.config.server.provision.HostProvisionerProvider;
@@ -50,6 +51,7 @@ import com.yahoo.vespa.config.server.session.SilentDeployLogger;
import com.yahoo.vespa.config.server.tenant.Rotations;
import com.yahoo.vespa.config.server.tenant.Tenant;
import com.yahoo.vespa.config.server.tenant.TenantRepository;
+import com.yahoo.vespa.model.VespaModel;
import java.io.File;
import java.io.IOException;
@@ -477,6 +479,13 @@ public class ApplicationRepository implements com.yahoo.config.provision.Deploye
return convergeChecker.servicesToCheck(getApplication(applicationId), uri, timeout);
}
+ // ---------------- Logs ----------------------------------------------------------------
+
+ public HttpResponse getLogs(ApplicationId applicationId) {
+ String logServerHostName = getLogServerHostname(applicationId);
+ return LogRetriever.getLogs(logServerHostName);
+ }
+
// ---------------- Session operations ----------------------------------------------------------------
/**
@@ -690,6 +699,13 @@ public class ApplicationRepository implements com.yahoo.config.provision.Deploye
}
}
+ private String getLogServerHostname(ApplicationId applicationId) {
+ Application application = getApplication(applicationId);
+ VespaModel model = (VespaModel) application.getModel();
+ String logServerHostname = model.getAdmin().getLogserver().getHostName();
+ return logServerHostname;
+ }
+
/** Returns version to use when deploying application in given environment */
static Version decideVersion(ApplicationId application, Environment environment, Version targetVersion, boolean bootstrap) {
if (environment.isManuallyDeployed() &&
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/http/LogRetriever.java b/configserver/src/main/java/com/yahoo/vespa/config/server/http/LogRetriever.java
new file mode 100644
index 00000000000..f4952b5b9fc
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/http/LogRetriever.java
@@ -0,0 +1,41 @@
+package com.yahoo.vespa.config.server.http;
+
+import com.yahoo.container.jdisc.HttpResponse;
+import org.apache.http.client.methods.HttpGet;
+import org.apache.http.impl.client.CloseableHttpClient;
+import org.apache.http.impl.client.HttpClientBuilder;
+import org.apache.http.util.EntityUtils;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+
+public class LogRetriever {
+
+ private final static Logger log = Logger.getLogger(LogRetriever.class.getName());
+
+ public static HttpResponse getLogs(String logServerHostname) {
+ HttpGet get = new HttpGet(logServerHostname);
+ try (CloseableHttpClient httpClient = HttpClientBuilder.create().build()) {
+ org.apache.http.HttpResponse response = httpClient.execute(get);
+ String responseBody = EntityUtils.toString(response.getEntity(), "UTF-8");
+ return new HttpResponse(response.getStatusLine().getStatusCode()) {
+ @Override
+ public void render(OutputStream outputStream) throws IOException {
+ if (response.getEntity() != null ) outputStream.write(responseBody.getBytes());
+ }
+ };
+ } catch (IOException e) {
+ log.log(Level.WARNING, "Failed to retrieve logs from log server", e);
+ return new HttpResponse(404) {
+ @Override
+ public void render(OutputStream outputStream) throws IOException {
+ outputStream.write(e.toString().getBytes());
+ }
+ };
+ }
+
+ }
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/http/v2/ApplicationHandler.java b/configserver/src/main/java/com/yahoo/vespa/config/server/http/v2/ApplicationHandler.java
index 2004ab95144..b65cb370f93 100644
--- a/configserver/src/main/java/com/yahoo/vespa/config/server/http/v2/ApplicationHandler.java
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/http/v2/ApplicationHandler.java
@@ -96,6 +96,10 @@ public class ApplicationHandler extends HttpHandler {
return applicationRepository.filedistributionStatus(applicationId, timeout);
}
+ if (isLogRequest(request)) {
+ return applicationRepository.getLogs(applicationId);
+ }
+
return new GetApplicationResponse(Response.Status.OK, applicationRepository.getApplicationGeneration(applicationId));
}
@@ -140,7 +144,13 @@ public class ApplicationHandler extends HttpHandler {
"http://*/application/v2/tenant/*/application/*/environment/*/region/*/instance/*/serviceconverge/*",
"http://*/application/v2/tenant/*/application/*/environment/*/region/*/instance/*/clustercontroller/*/status/*",
"http://*/application/v2/tenant/*/application/*/environment/*/region/*/instance/*",
- "http://*/application/v2/tenant/*/application/*");
+ "http://*/application/v2/tenant/*/application/*",
+ "http://*/application/v2/tenant/*/application/*/logs");
+ }
+
+ private static boolean isLogRequest(HttpRequest request) {
+ return getBindingMatch(request).groupCount() == 4 &&
+ request.getUri().getPath().endsWith("/logs");
}
private static boolean isServiceConvergeListRequest(HttpRequest request) {
diff --git a/configserver/src/main/resources/configserver-app/services.xml b/configserver/src/main/resources/configserver-app/services.xml
index 8a99869e69a..60dd7b0cea2 100644
--- a/configserver/src/main/resources/configserver-app/services.xml
+++ b/configserver/src/main/resources/configserver-app/services.xml
@@ -147,6 +147,8 @@
<binding>https://*/application/v2/tenant/*/application/*/environment/*/region/*/instance/*</binding>
<binding>http://*/application/v2/tenant/*/application/*</binding>
<binding>https://*/application/v2/tenant/*/application/*</binding>
+ <binding>http://*/application/v2/tenant/*/application/*/logs</binding>
+ <binding>https://*/application/v2/tenant/*/application/*/logs</binding>
</handler>
<handler id='com.yahoo.vespa.config.server.http.v2.HttpGetConfigHandler' bundle='configserver'>
<binding>http://*/config/v2/tenant/*/application/*/*</binding>
diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/http/LogRetrieverTest.java b/configserver/src/test/java/com/yahoo/vespa/config/server/http/LogRetrieverTest.java
new file mode 100644
index 00000000000..2c3622e3dac
--- /dev/null
+++ b/configserver/src/test/java/com/yahoo/vespa/config/server/http/LogRetrieverTest.java
@@ -0,0 +1,46 @@
+package com.yahoo.vespa.config.server.http;
+
+import com.github.tomakehurst.wiremock.junit.WireMockRule;
+import com.yahoo.container.jdisc.HttpResponse;
+import org.junit.Rule;
+import org.junit.Test;
+
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+
+import static com.github.tomakehurst.wiremock.client.WireMock.aResponse;
+import static com.github.tomakehurst.wiremock.client.WireMock.get;
+import static com.github.tomakehurst.wiremock.client.WireMock.okJson;
+import static com.github.tomakehurst.wiremock.client.WireMock.stubFor;
+import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo;
+import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.options;
+import static org.junit.Assert.assertEquals;
+
+public class LogRetrieverTest {
+
+ private String logServerHostName = "http://localhost:8080/";
+ @Rule
+ public final WireMockRule wireMock = new WireMockRule(options().port(8080), true);
+
+ @Test
+ public void testThatLogHandlerPropagatesResponseBody() throws IOException {
+ String expectedBody = "{logs-json}";
+ stubFor(get(urlEqualTo("/")).willReturn(okJson(expectedBody)));
+ HttpResponse response = LogRetriever.getLogs(logServerHostName);
+ ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
+ response.render(byteArrayOutputStream);
+ assertEquals(expectedBody, byteArrayOutputStream.toString());
+ assertEquals(200, response.getStatus());
+ }
+
+ @Test
+ public void testThatNotFoundLogServerReturns404() throws IOException {
+ stubFor(get(urlEqualTo("/")).willReturn(aResponse().withStatus(200)));
+ HttpResponse response = LogRetriever.getLogs("http://wrong-host:8080/");
+ assertEquals(404, response.getStatus());
+ }
+
+
+
+} \ No newline at end of file
diff --git a/container-core/src/main/java/com/yahoo/container/handler/LogHandler.java b/container-core/src/main/java/com/yahoo/container/handler/LogHandler.java
new file mode 100644
index 00000000000..474d7d7321a
--- /dev/null
+++ b/container-core/src/main/java/com/yahoo/container/handler/LogHandler.java
@@ -0,0 +1,31 @@
+package com.yahoo.container.handler;
+
+import com.fasterxml.jackson.core.JsonFactory;
+import com.google.inject.Inject;
+import com.yahoo.container.jdisc.HttpRequest;
+import com.yahoo.container.jdisc.HttpResponse;
+import com.yahoo.container.jdisc.ThreadedHttpRequestHandler;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.util.concurrent.Executor;
+
+public class LogHandler extends ThreadedHttpRequestHandler {
+
+ private static final String LOG_DIRECTORY = "/home/y/logs/vespa/";
+
+ @Inject
+ public LogHandler(Executor executor) {
+ super(executor);
+ }
+
+ @Override
+ public HttpResponse handle(HttpRequest request) {
+
+ return new HttpResponse(200) {
+ @Override
+ public void render(OutputStream outputStream) throws IOException {
+ LogReader.writeToOutputStream(LOG_DIRECTORY, outputStream);
+ }
+ };
+ }
+}
diff --git a/container-core/src/main/java/com/yahoo/container/handler/LogReader.java b/container-core/src/main/java/com/yahoo/container/handler/LogReader.java
new file mode 100644
index 00000000000..e35ea748c0e
--- /dev/null
+++ b/container-core/src/main/java/com/yahoo/container/handler/LogReader.java
@@ -0,0 +1,44 @@
+package com.yahoo.container.handler;
+
+import com.fasterxml.jackson.core.JsonFactory;
+import com.fasterxml.jackson.core.JsonGenerator;
+import org.apache.commons.codec.binary.Base64;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import javax.xml.bind.DatatypeConverter;
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.nio.file.Files;
+
+public class LogReader {
+
+ protected static void writeToOutputStream(String logDirectory, OutputStream outputStream) throws IOException {
+ OutputStreamWriter outputStreamWriter = new OutputStreamWriter(outputStream);
+ JSONObject json = new JSONObject();
+ File root = new File(logDirectory);
+ try {
+ traverse_folder(root, json);
+ } catch (JSONException e) {
+ outputStreamWriter.write("Failed to create log JSON");
+ }
+ outputStreamWriter.write(json.toString());
+ outputStreamWriter.close();
+ }
+
+ private static void traverse_folder(File root, JSONObject json) throws IOException, JSONException {
+ for(File child : root.listFiles()) {
+ JSONObject childJson = new JSONObject();
+ if(child.isFile()) {
+ json.put(child.getName(), DatatypeConverter.printBase64Binary(Files.readAllBytes(child.toPath())));
+ }
+ else {
+ json.put(child.getName(), childJson);
+ traverse_folder(child, childJson);
+ }
+ }
+ }
+}
diff --git a/container-core/src/test/java/com/yahoo/container/handler/LogReaderTest.java b/container-core/src/test/java/com/yahoo/container/handler/LogReaderTest.java
new file mode 100644
index 00000000000..ff6ea74a411
--- /dev/null
+++ b/container-core/src/test/java/com/yahoo/container/handler/LogReaderTest.java
@@ -0,0 +1,36 @@
+package com.yahoo.container.handler;
+
+import org.junit.Before;
+import org.junit.Test;
+
+import java.io.ByteArrayOutputStream;
+
+import static org.junit.Assert.*;
+
+public class LogReaderTest {
+
+ ByteArrayOutputStream outputStream;
+
+ @Before
+ public void setup() {
+ outputStream = new ByteArrayOutputStream();
+ }
+
+ @Test
+ public void testThatFilesAreWrittenCorrectlyToOutputStream() throws Exception{
+ String logDirectory = "src/test/resources/logfolder/";
+ LogReader.writeToOutputStream(logDirectory, outputStream);
+ String expected = "{\"subfolder\":{\"log2.log\":\"VGhpcyBpcyBhbm90aGVyIGxvZyBmaWxl\"},\"log1.log\":\"VGhpcyBpcyBvbmUgbG9nIGZpbGU=\"}";
+ String actual = new String(outputStream.toByteArray());
+ assertEquals(expected, actual);
+ }
+
+ @Test
+ public void testNothingISWrittenToOutputStreamWithEmptyLogFolder() throws Exception {
+ String logDirectory = "src/test/resources/emptylogfolder/";
+ LogReader.writeToOutputStream(logDirectory, outputStream);
+ String expected = "{}";
+ String actual = new String(outputStream.toByteArray());
+ assertEquals(expected, actual);
+ }
+} \ No newline at end of file
diff --git a/container-core/src/test/resources/logfolder/log1.log b/container-core/src/test/resources/logfolder/log1.log
new file mode 100644
index 00000000000..bb85d5a4950
--- /dev/null
+++ b/container-core/src/test/resources/logfolder/log1.log
@@ -0,0 +1 @@
+This is one log file \ No newline at end of file
diff --git a/container-core/src/test/resources/logfolder/subfolder/log2.log b/container-core/src/test/resources/logfolder/subfolder/log2.log
new file mode 100644
index 00000000000..aee6eaca2e8
--- /dev/null
+++ b/container-core/src/test/resources/logfolder/subfolder/log2.log
@@ -0,0 +1 @@
+This is another log file \ No newline at end of file
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/configserver/ConfigServer.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/configserver/ConfigServer.java
index 54e057e4187..eb10c78f891 100644
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/configserver/ConfigServer.java
+++ b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/configserver/ConfigServer.java
@@ -1,6 +1,7 @@
// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
package com.yahoo.vespa.hosted.controller.api.integration.configserver;
+import com.yahoo.container.jdisc.HttpResponse;
import com.yahoo.vespa.hosted.controller.api.application.v4.model.DeployOptions;
import com.yahoo.vespa.hosted.controller.api.application.v4.model.EndpointStatus;
import com.yahoo.vespa.hosted.controller.api.identifiers.DeploymentId;
@@ -41,6 +42,7 @@ public interface ConfigServer {
Map<?,?> getServiceApiResponse(String tenantName, String applicationName, String instanceName, String environment, String region, String serviceName, String restPath);
+ HttpResponse getLogs(DeploymentId deployment);
/**
* Set new status on en endpoint in one zone.
*
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/application/ApplicationApiHandler.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/application/ApplicationApiHandler.java
index 22809ac18bf..556d019d440 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/application/ApplicationApiHandler.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/application/ApplicationApiHandler.java
@@ -169,6 +169,7 @@ public class ApplicationApiHandler extends LoggingRequestHandler {
if (path.matches("/application/v4/tenant/{tenant}")) return tenant(path.get("tenant"), request);
if (path.matches("/application/v4/tenant/{tenant}/application")) return applications(path.get("tenant"), request);
if (path.matches("/application/v4/tenant/{tenant}/application/{application}")) return application(path.get("tenant"), path.get("application"), request);
+ if (path.matches("/application/v4/tenant/{tenant}/application/{application}/environment/{environment}/region/{region}/instance/{instance}/logs")) return logs(path.get("tenant"), path.get("application"), path.get("instance"), path.get("environment"), path.get("region"));
if (path.matches("/application/v4/tenant/{tenant}/application/{application}/instance/{instance}/job")) return JobControllerApiHandlerHelper.jobTypeResponse(controller, appIdFromPath(path), request.getUri());
if (path.matches("/application/v4/tenant/{tenant}/application/{application}/instance/{instance}/job/{jobtype}")) return JobControllerApiHandlerHelper.runResponse(controller.jobController().runs(appIdFromPath(path), jobTypeFromPath(path)), request.getUri());
if (path.matches("/application/v4/tenant/{tenant}/application/{application}/instance/{instance}/job/{jobtype}/run/{number}")) return JobControllerApiHandlerHelper.runDetailsResponse(controller.jobController(), runIdFromPath(path), request.getProperty("after"));
@@ -346,6 +347,14 @@ public class ApplicationApiHandler extends LoggingRequestHandler {
return new SlimeJsonResponse(slime);
}
+ private HttpResponse logs(String tenantName, String applicationName, String instanceName, String environment, String region) {
+ ApplicationId application = ApplicationId.from(tenantName, applicationName, instanceName);
+ ZoneId zone = ZoneId.from(environment, region);
+ DeploymentId deployment = new DeploymentId(application, zone);
+ return controller.configServer().getLogs(deployment);
+ }
+
+
private void toSlime(Cursor object, Application application, HttpRequest request) {
object.setString("application", application.id().application().value());
object.setString("instance", application.id().instance().value());
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/integration/ConfigServerMock.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/integration/ConfigServerMock.java
index 458ba49f3e3..bd65465633e 100644
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/integration/ConfigServerMock.java
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/integration/ConfigServerMock.java
@@ -7,6 +7,7 @@ import com.yahoo.component.Version;
import com.yahoo.config.provision.ApplicationId;
import com.yahoo.config.provision.HostName;
import com.yahoo.config.provision.NodeType;
+import com.yahoo.container.jdisc.HttpResponse;
import com.yahoo.vespa.hosted.controller.api.application.v4.model.DeployOptions;
import com.yahoo.vespa.hosted.controller.api.application.v4.model.EndpointStatus;
import com.yahoo.vespa.hosted.controller.api.application.v4.model.configserverbindings.ConfigChangeActions;
@@ -25,7 +26,11 @@ import com.yahoo.vespa.hosted.controller.application.SystemApplication;
import com.yahoo.vespa.serviceview.bindings.ApplicationView;
import com.yahoo.vespa.serviceview.bindings.ClusterView;
import com.yahoo.vespa.serviceview.bindings.ServiceView;
+import org.json.JSONException;
+import org.json.JSONObject;
+import java.io.IOException;
+import java.io.OutputStream;
import java.net.URI;
import java.util.ArrayList;
import java.util.Arrays;
@@ -292,6 +297,17 @@ public class ConfigServerMock extends AbstractComponent implements ConfigServer
return endpoints.getOrDefault(endpoint, result);
}
+ @Override
+ public HttpResponse getLogs(DeploymentId deployment) {
+ return new HttpResponse(200) {
+ @Override
+ public void render(OutputStream outputStream) throws IOException {
+ outputStream.write("{\"subfolder\":{\"log2.log\":\"VGhpcyBpcyBhbm90aGVyIGxvZyBmaWxl\"},\"log1.log\":\"VGhpcyBpcyBvbmUgbG9nIGZpbGU=\"}".getBytes());
+ }
+ };
+
+ }
+
public static class Application {
private final ApplicationId id;
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/ApplicationApiTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/ApplicationApiTest.java
index 13092451d4b..03877cca30e 100644
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/ApplicationApiTest.java
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/ApplicationApiTest.java
@@ -330,6 +330,9 @@ public class ApplicationApiTest extends ControllerContainerTest {
.recursive("true"),
new File("application1-recursive.json"));
+ // GET logs
+ tester.assertResponse(request("/application/v4/tenant/tenant2/application//application1/environment/prod/region/corp-us-east-1/instance/default/logs", GET).userIdentity(USER_ID), new File("logs.json"));
+
// DELETE (cancel) ongoing change
tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/deploying", DELETE)
.userIdentity(HOSTED_VESPA_OPERATOR),
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/logs.json b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/logs.json
new file mode 100644
index 00000000000..398a62758ee
--- /dev/null
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/logs.json
@@ -0,0 +1,5 @@
+{
+ "subfolder": {
+ "log2.log":"VGhpcyBpcyBhbm90aGVyIGxvZyBmaWxl"},
+ "log1.log":"VGhpcyBpcyBvbmUgbG9nIGZpbGU="
+} \ No newline at end of file