diff options
author | Harald Musum <musum@oath.com> | 2018-09-11 09:08:49 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2018-09-11 09:08:49 +0200 |
commit | 4293fd9c05d9144de5d7a47e4910705d6d0c9b47 (patch) | |
tree | 857d14651e5e39d576810b5f4fed7f74e1bd822d | |
parent | 94e41451c54e74a236b4c7c3d9390ae60f52209b (diff) | |
parent | dd4f6af83ca7374d6458b4e31d2706a8684c43c8 (diff) |
Merge pull request #6871 from vespa-engine/olaa/log-retrieval-handler
Add functionality for retrieving logs
15 files changed, 286 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 79a8a3d8763..bcb51f335a3 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 @@ -10,6 +10,8 @@ import com.yahoo.config.FileReference; import com.yahoo.config.application.api.ApplicationFile; import com.yahoo.config.application.api.ApplicationMetaData; import com.yahoo.config.application.api.DeployLogger; +import com.yahoo.config.model.api.HostInfo; +import com.yahoo.config.model.api.ServiceInfo; import com.yahoo.config.provision.ApplicationId; import com.yahoo.config.provision.Environment; import com.yahoo.config.provision.HostFilter; @@ -36,6 +38,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 +53,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; @@ -61,6 +65,7 @@ import java.time.Clock; import java.time.Duration; import java.time.Instant; import java.util.Arrays; +import java.util.Collection; import java.util.HashSet; import java.util.List; import java.util.Optional; @@ -477,6 +482,14 @@ 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); + LogRetriever logRetriever = new LogRetriever(); + return logRetriever.getLogs(logServerHostName); + } + // ---------------- Session operations ---------------------------------------------------------------- /** @@ -690,6 +703,29 @@ 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(); + Collection<HostInfo> hostInfos = application.getModel().getHosts(); + + HostInfo logServerHostInfo = hostInfos.stream() + .filter(host -> host.getHostname().equals(logServerHostname)) + .findFirst().orElseThrow(() -> new IllegalArgumentException("Could not find HostInfo")); + + ServiceInfo serviceInfo = logServerHostInfo.getServices().stream() + .filter(service -> service.getServiceType().equals("container")) + .findFirst().orElseThrow(() -> new IllegalArgumentException("No container running on logserver host")); + + int port = serviceInfo.getPorts().stream() + .filter(portInfo -> portInfo.getTags().stream() + .filter(tag -> tag.equalsIgnoreCase("http")).count() > 0) + .findFirst().orElseThrow(() -> new IllegalArgumentException("Could not find HTTP port")) + .getPort(); + + return logServerHostname + ":" + port + "/logs"; + } + /** Returns version to use when deploying application in given environment */ static Version decideVersion(ApplicationId application, Environment environment, Version sessionVersion, 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..dd60d158313 --- /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 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..eb819053c05 --- /dev/null +++ b/configserver/src/test/java/com/yahoo/vespa/config/server/http/LogRetrieverTest.java @@ -0,0 +1,54 @@ +package com.yahoo.vespa.config.server.http; + +import com.github.tomakehurst.wiremock.junit.WireMockRule; +import com.yahoo.container.jdisc.HttpResponse; +import org.junit.Before; +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/"; + private LogRetriever logRetriever; + + @Rule + public final WireMockRule wireMock = new WireMockRule(options().port(8080), true); + + @Before + public void setup() { + logRetriever = new LogRetriever(); + } + + @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..4183b642af1 --- /dev/null +++ b/container-core/src/main/java/com/yahoo/container/handler/LogHandler.java @@ -0,0 +1,45 @@ +package com.yahoo.container.handler; + +import com.google.inject.Inject; +import com.yahoo.container.jdisc.HttpRequest; +import com.yahoo.container.jdisc.HttpResponse; +import com.yahoo.container.jdisc.ThreadedHttpRequestHandler; +import org.json.JSONException; +import org.json.JSONObject; + +import java.io.IOException; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +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) { + JSONObject logJson; + + try { + logJson = LogReader.readLogs(LOG_DIRECTORY); + } catch (IOException | JSONException e) { + return new HttpResponse(404) { + @Override + public void render(OutputStream outputStream) {} + }; + } + return new HttpResponse(200) { + @Override + public void render(OutputStream outputStream) throws IOException { + OutputStreamWriter outputStreamWriter = new OutputStreamWriter(outputStream); + outputStreamWriter.write(logJson.toString()); + outputStreamWriter.close(); + } + }; + } +} 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..eb00446dd0e --- /dev/null +++ b/container-core/src/main/java/com/yahoo/container/handler/LogReader.java @@ -0,0 +1,32 @@ +package com.yahoo.container.handler; + +import org.json.JSONException; +import org.json.JSONObject; + +import javax.xml.bind.DatatypeConverter; +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; + +public class LogReader { + + protected static JSONObject readLogs(String logDirectory) throws IOException, JSONException { + JSONObject json = new JSONObject(); + File root = new File(logDirectory); + traverse_folder(root, json); + return json; + } + + 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..e5302ee43ee --- /dev/null +++ b/container-core/src/test/java/com/yahoo/container/handler/LogReaderTest.java @@ -0,0 +1,28 @@ +package com.yahoo.container.handler; + +import org.json.JSONObject; +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/"; + JSONObject json = LogReader.readLogs(logDirectory); + String expected = "{\"subfolder\":{\"log2.log\":\"VGhpcyBpcyBhbm90aGVyIGxvZyBmaWxl\"},\"log1.log\":\"VGhpcyBpcyBvbmUgbG9nIGZpbGU=\"}"; + String actual = json.toString(); + 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 |