diff --git a/core/frontend/src/views/ExtensionManagerView.vue b/core/frontend/src/views/ExtensionManagerView.vue index 861fa7dfda..8f1364ceed 100644 --- a/core/frontend/src/views/ExtensionManagerView.vue +++ b/core/frontend/src/views/ExtensionManagerView.vue @@ -424,25 +424,41 @@ export default Vue.extend({ notifier.pushBackError('EXTENSIONS_INSTALLED_FETCH_FAIL', error) }) }, - async showLogs(extension: InstalledExtensionData): Promise { + async showLogs(extension: InstalledExtensionData) { this.setLoading(extension, true) - await back_axios({ + const ansi = new AnsiUp() + this.log_output = '' + + back_axios({ method: 'get', url: `${API_URL}/log`, params: { container_name: this.getContainerName(extension), }, + onDownloadProgress: (progressEvent) => { + const chunk = progressEvent.currentTarget.response + this.$set(this, 'log_output', this.log_output + ansi.ansi_to_html(chunk)) + this.show_log = true + this.setLoading(extension, false) + this.$nextTick(() => { + // TODO: find a better way to scroll to bottom + const output = document.querySelector( + '#app > div.v-dialog__content.v-dialog__content--active > div', + ) as HTMLInputElement + output.scrollTop = output.scrollHeight + }) + }, timeout: 30000, }) - .then((response) => { - const ansi = new AnsiUp() - this.log_output = ansi.ansi_to_html(response.data.join('')) - this.show_log = true + .then(() => { + this.setLoading(extension, false) }) .catch((error) => { notifier.pushBackError('EXTENSIONS_LOG_FETCH_FAIL', error) }) - this.setLoading(extension, false) + .finally(() => { + this.setLoading(extension, false) + }) }, showModal(extension: ExtensionData) { this.show_dialog = true diff --git a/core/services/kraken/kraken.py b/core/services/kraken/kraken.py index e7daf89358..fa44ab1627 100644 --- a/core/services/kraken/kraken.py +++ b/core/services/kraken/kraken.py @@ -280,11 +280,16 @@ async def list_containers(self) -> List[DockerContainer]: containers: List[DockerContainer] = await self.client.containers.list(filter='{"status": ["running"]}') # type: ignore return containers - async def load_logs(self, container_name: str) -> List[str]: + async def stream_logs(self, container_name: str, timeout: int = 30) -> AsyncGenerator[str, None]: containers = await self.client.containers.list(filters={"name": {container_name: True}}) # type: ignore if not containers: raise RuntimeError(f"Container not found: {container_name}") - return cast(List[str], await containers[0].log(stdout=True, stderr=True)) + + start_time = asyncio.get_event_loop().time() + async for log_line in containers[0].log(stdout=True, stderr=True, follow=True, stream=True): + if asyncio.get_event_loop().time() - start_time > timeout: + break + yield log_line # pylint: disable=too-many-locals async def load_stats(self) -> Dict[str, Any]: diff --git a/core/services/kraken/main.py b/core/services/kraken/main.py index b05f885c91..72191c99c8 100755 --- a/core/services/kraken/main.py +++ b/core/services/kraken/main.py @@ -2,13 +2,13 @@ import argparse import asyncio import logging -from typing import Any, List +from typing import Any, Iterable from commonwealth.utils.apis import GenericErrorHandlingRoute from commonwealth.utils.general import limit_ram_usage from commonwealth.utils.logs import InterceptHandler, init_logger from fastapi import FastAPI, HTTPException, status -from fastapi.responses import HTMLResponse, StreamingResponse +from fastapi.responses import HTMLResponse, PlainTextResponse, StreamingResponse from fastapi_versioning import VersionedFastAPI, version from loguru import logger from pydantic import BaseModel @@ -129,10 +129,10 @@ async def list_containers() -> Any: ] -@app.get("/log", status_code=status.HTTP_200_OK) +@app.get("/log", status_code=status.HTTP_200_OK, response_class=PlainTextResponse) @version(1, 0) -async def log_containers(container_name: str) -> List[str]: - return await kraken.load_logs(container_name) +async def log_containers(container_name: str) -> Iterable[bytes]: + return StreamingResponse(kraken.stream_logs(container_name)) # type: ignore @app.get("/stats", status_code=status.HTTP_200_OK)