From 5de8e12a6a87aad71791df33ba87bb9f37636c48 Mon Sep 17 00:00:00 2001 From: Victor Skvortsov Date: Wed, 16 Jul 2025 16:53:19 +0500 Subject: [PATCH 1/4] Return logs in base64 for backward compatibility --- frontend/src/libs/index.ts | 9 +++++++++ frontend/src/pages/Runs/Details/Logs/index.tsx | 2 +- frontend/src/services/project.ts | 3 ++- frontend/src/types/log.d.ts | 2 +- src/dstack/_internal/server/services/logs/__init__.py | 11 +++++++++-- src/dstack/api/_public/runs.py | 3 ++- src/tests/_internal/server/routers/test_logs.py | 8 ++++---- 7 files changed, 28 insertions(+), 10 deletions(-) diff --git a/frontend/src/libs/index.ts b/frontend/src/libs/index.ts index f8a70f146e..523923ab4b 100644 --- a/frontend/src/libs/index.ts +++ b/frontend/src/libs/index.ts @@ -91,6 +91,15 @@ export const riseRouterException = (status = 404, json = 'Not Found'): never => throw new Response(json, { status }); }; +export const base64ToArrayBuffer = (base64: string) => { + const binaryString = atob(base64); + const bytes = new Uint8Array(binaryString.length); + for (let i = 0; i < binaryString.length; i++) { + bytes[i] = binaryString.charCodeAt(i); + } + return bytes; +}; + export const isValidUrl = (urlString: string) => { try { return Boolean(new URL(urlString)); diff --git a/frontend/src/pages/Runs/Details/Logs/index.tsx b/frontend/src/pages/Runs/Details/Logs/index.tsx index 6fc5501039..aaffbf41cc 100644 --- a/frontend/src/pages/Runs/Details/Logs/index.tsx +++ b/frontend/src/pages/Runs/Details/Logs/index.tsx @@ -31,7 +31,7 @@ export const Logs: React.FC = ({ className, projectName, runName, jobSub const writeDataToTerminal = (logs: ILogItem[]) => { logs.forEach((logItem) => { - terminalInstance.current.write(logItem.message.replace(/(? { const logs = response.logs.map((logItem) => ({ ...logItem, - message: logItem.message, + message: base64ToArrayBuffer(logItem.message as string), })); return { diff --git a/frontend/src/types/log.d.ts b/frontend/src/types/log.d.ts index 99e9532c8c..eec182c1ec 100644 --- a/frontend/src/types/log.d.ts +++ b/frontend/src/types/log.d.ts @@ -1,7 +1,7 @@ declare interface ILogItem { log_source: 'stdout' | 'stderr'; timestamp: string; - message: string; + message: string | Uint8Array; } declare type TRequestLogsParams = { diff --git a/src/dstack/_internal/server/services/logs/__init__.py b/src/dstack/_internal/server/services/logs/__init__.py index a3623a19df..d38a15014a 100644 --- a/src/dstack/_internal/server/services/logs/__init__.py +++ b/src/dstack/_internal/server/services/logs/__init__.py @@ -8,7 +8,7 @@ from dstack._internal.server.schemas.logs import PollLogsRequest from dstack._internal.server.schemas.runner import LogEvent as RunnerLogEvent from dstack._internal.server.services.logs.aws import BOTO_AVAILABLE, CloudWatchLogStorage -from dstack._internal.server.services.logs.base import LogStorage, LogStorageError +from dstack._internal.server.services.logs.base import LogStorage, LogStorageError, b64encode_raw_message from dstack._internal.server.services.logs.filelog import FileLogStorage from dstack._internal.server.services.logs.gcp import GCP_LOGGING_AVAILABLE, GCPLogStorage from dstack._internal.utils.common import run_async @@ -75,4 +75,11 @@ def write_logs( async def poll_logs_async(project: ProjectModel, request: PollLogsRequest) -> JobSubmissionLogs: - return await run_async(get_log_storage().poll_logs, project=project, request=request) + job_submission_logs = await run_async(get_log_storage().poll_logs, project=project, request=request) + # Logs are stored in plaintext but transmitted in base64 for API/CLI backward compatibility. + # Old logs stored in base64 are encoded twice for transmission and shown as base64 in CLI/UI. + # We live with that. + # TODO: Drop base64 encoding in 0.20. + for log_event in job_submission_logs.logs: + log_event.message = b64encode_raw_message(log_event.message.encode()) + return job_submission_logs diff --git a/src/dstack/api/_public/runs.py b/src/dstack/api/_public/runs.py index 6a19cb459d..179012b667 100644 --- a/src/dstack/api/_public/runs.py +++ b/src/dstack/api/_public/runs.py @@ -1,6 +1,7 @@ import queue import tempfile import threading +import base64 import time from abc import ABC from collections.abc import Iterator @@ -228,7 +229,7 @@ def logs( ), ) for log in resp.logs: - yield log.message.encode() + yield base64.b64decode(log.message) next_token = resp.next_token if next_token is None: break diff --git a/src/tests/_internal/server/routers/test_logs.py b/src/tests/_internal/server/routers/test_logs.py index 11f0da8daf..0364edee46 100644 --- a/src/tests/_internal/server/routers/test_logs.py +++ b/src/tests/_internal/server/routers/test_logs.py @@ -62,17 +62,17 @@ async def test_returns_logs( { "timestamp": "2023-10-06T10:01:53.234234+00:00", "log_source": "stdout", - "message": "Hello", + "message": "SGVsbG8=", }, { "timestamp": "2023-10-06T10:01:53.234235+00:00", "log_source": "stdout", - "message": "World", + "message": "V29ybGQ=", }, { "timestamp": "2023-10-06T10:01:53.234236+00:00", "log_source": "stdout", - "message": "!", + "message": "IQ==", }, ], "next_token": None, @@ -93,7 +93,7 @@ async def test_returns_logs( { "timestamp": "2023-10-06T10:01:53.234236+00:00", "log_source": "stdout", - "message": "!", + "message": "IQ==", }, ], "next_token": None, From 5fb06ba893fb2fd4b7fe308f74d9eaf5058a783a Mon Sep 17 00:00:00 2001 From: Victor Skvortsov Date: Wed, 16 Jul 2025 17:08:07 +0500 Subject: [PATCH 2/4] Fix newlines in logs UI --- frontend/src/pages/Runs/Details/Logs/index.tsx | 2 +- frontend/src/services/project.ts | 4 +++- frontend/src/types/log.d.ts | 2 +- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/frontend/src/pages/Runs/Details/Logs/index.tsx b/frontend/src/pages/Runs/Details/Logs/index.tsx index aaffbf41cc..6fc5501039 100644 --- a/frontend/src/pages/Runs/Details/Logs/index.tsx +++ b/frontend/src/pages/Runs/Details/Logs/index.tsx @@ -31,7 +31,7 @@ export const Logs: React.FC = ({ className, projectName, runName, jobSub const writeDataToTerminal = (logs: ILogItem[]) => { logs.forEach((logItem) => { - terminalInstance.current.write(logItem.message); + terminalInstance.current.write(logItem.message.replace(/(? ({ @@ -131,7 +133,7 @@ export const projectApi = createApi({ transformResponse: (response: { logs: ILogItem[]; next_token: string }) => { const logs = response.logs.map((logItem) => ({ ...logItem, - message: base64ToArrayBuffer(logItem.message as string), + message: decoder.decode(base64ToArrayBuffer(logItem.message)), })); return { diff --git a/frontend/src/types/log.d.ts b/frontend/src/types/log.d.ts index eec182c1ec..99e9532c8c 100644 --- a/frontend/src/types/log.d.ts +++ b/frontend/src/types/log.d.ts @@ -1,7 +1,7 @@ declare interface ILogItem { log_source: 'stdout' | 'stderr'; timestamp: string; - message: string | Uint8Array; + message: string; } declare type TRequestLogsParams = { From 87bba3d3d1e762c376b5dfa41c92250e24244862 Mon Sep 17 00:00:00 2001 From: Victor Skvortsov Date: Wed, 16 Jul 2025 17:13:32 +0500 Subject: [PATCH 3/4] Fix lint --- src/dstack/_internal/server/services/logs/__init__.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/dstack/_internal/server/services/logs/__init__.py b/src/dstack/_internal/server/services/logs/__init__.py index d38a15014a..b38264980d 100644 --- a/src/dstack/_internal/server/services/logs/__init__.py +++ b/src/dstack/_internal/server/services/logs/__init__.py @@ -8,7 +8,11 @@ from dstack._internal.server.schemas.logs import PollLogsRequest from dstack._internal.server.schemas.runner import LogEvent as RunnerLogEvent from dstack._internal.server.services.logs.aws import BOTO_AVAILABLE, CloudWatchLogStorage -from dstack._internal.server.services.logs.base import LogStorage, LogStorageError, b64encode_raw_message +from dstack._internal.server.services.logs.base import ( + LogStorage, + LogStorageError, + b64encode_raw_message, +) from dstack._internal.server.services.logs.filelog import FileLogStorage from dstack._internal.server.services.logs.gcp import GCP_LOGGING_AVAILABLE, GCPLogStorage from dstack._internal.utils.common import run_async @@ -75,7 +79,9 @@ def write_logs( async def poll_logs_async(project: ProjectModel, request: PollLogsRequest) -> JobSubmissionLogs: - job_submission_logs = await run_async(get_log_storage().poll_logs, project=project, request=request) + job_submission_logs = await run_async( + get_log_storage().poll_logs, project=project, request=request + ) # Logs are stored in plaintext but transmitted in base64 for API/CLI backward compatibility. # Old logs stored in base64 are encoded twice for transmission and shown as base64 in CLI/UI. # We live with that. From a3b0e71160c69755369f60159429480778f3859e Mon Sep 17 00:00:00 2001 From: Victor Skvortsov Date: Wed, 16 Jul 2025 17:15:00 +0500 Subject: [PATCH 4/4] Fix lint --- src/dstack/api/_public/runs.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/dstack/api/_public/runs.py b/src/dstack/api/_public/runs.py index 179012b667..e1992068d0 100644 --- a/src/dstack/api/_public/runs.py +++ b/src/dstack/api/_public/runs.py @@ -1,7 +1,7 @@ +import base64 import queue import tempfile import threading -import base64 import time from abc import ABC from collections.abc import Iterator