Skip to content

Commit c376500

Browse files
authored
Return logs external_url for AWS and GCP (#2944)
1 parent 62e5b87 commit c376500

File tree

4 files changed

+33
-3
lines changed

4 files changed

+33
-3
lines changed

src/dstack/_internal/core/models/logs.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,4 +23,5 @@ class LogEvent(CoreModel):
2323

2424
class JobSubmissionLogs(CoreModel):
2525
logs: List[LogEvent]
26-
next_token: Optional[str]
26+
external_url: Optional[str] = None
27+
next_token: Optional[str] = None

src/dstack/_internal/server/services/logs/aws.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import itertools
22
import operator
3+
import urllib
4+
import urllib.parse
35
from contextlib import contextmanager
46
from datetime import datetime, timedelta, timezone
57
from typing import Iterator, List, Optional, Set, Tuple, TypedDict
@@ -64,6 +66,7 @@ def __init__(self, *, group: str, region: Optional[str] = None) -> None:
6466
self._client = session.client("logs")
6567
self._check_group_exists(group)
6668
self._group = group
69+
self._region = self._client.meta.region_name
6770
# Stores names of already created streams.
6871
# XXX: This set acts as an unbound cache. If this becomes a problem (in case of _very_ long
6972
# running server and/or lots of jobs, consider replacing it with an LRU cache, e.g.,
@@ -103,7 +106,11 @@ def poll_logs(self, project: ProjectModel, request: PollLogsRequest) -> JobSubmi
103106
)
104107
for cw_event in cw_events
105108
]
106-
return JobSubmissionLogs(logs=logs, next_token=next_token)
109+
return JobSubmissionLogs(
110+
logs=logs,
111+
external_url=self._get_stream_external_url(stream),
112+
next_token=next_token,
113+
)
107114

108115
def _get_log_events_with_retry(
109116
self, stream: str, request: PollLogsRequest
@@ -181,6 +188,11 @@ def _get_log_events(
181188

182189
return events, next_token
183190

191+
def _get_stream_external_url(self, stream: str) -> str:
192+
quoted_group = urllib.parse.quote(self._group, safe="")
193+
quoted_stream = urllib.parse.quote(stream, safe="")
194+
return f"https://console.aws.amazon.com/cloudwatch/home?region={self._region}#logsV2:log-groups/log-group/{quoted_group}/log-events/{quoted_stream}"
195+
184196
def write_logs(
185197
self,
186198
project: ProjectModel,

src/dstack/_internal/server/services/logs/gcp.py

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import urllib.parse
12
from typing import List
23
from uuid import UUID
34

@@ -48,6 +49,7 @@ class GCPLogStorage(LogStorage):
4849
# (https://cloud.google.com/logging/docs/analyze/custom-index).
4950

5051
def __init__(self, project_id: str):
52+
self.project_id = project_id
5153
try:
5254
self.client = logging_v2.Client(project=project_id)
5355
self.logger = self.client.logger(name=self.LOG_NAME)
@@ -106,7 +108,11 @@ def poll_logs(self, project: ProjectModel, request: PollLogsRequest) -> JobSubmi
106108
"GCP Logging read request limit exceeded."
107109
" It's recommended to increase default entries.list request quota from 60 per minute."
108110
)
109-
return JobSubmissionLogs(logs=logs, next_token=next_token if len(logs) > 0 else None)
111+
return JobSubmissionLogs(
112+
logs=logs,
113+
external_url=self._get_stream_extrnal_url(stream_name),
114+
next_token=next_token if len(logs) > 0 else None,
115+
)
110116

111117
def write_logs(
112118
self,
@@ -162,3 +168,12 @@ def _get_stream_name(
162168
self, project_name: str, run_name: str, job_submission_id: UUID, producer: LogProducer
163169
) -> str:
164170
return f"{project_name}-{run_name}-{job_submission_id}-{producer.value}"
171+
172+
def _get_stream_extrnal_url(self, stream_name: str) -> str:
173+
log_name_resource_name = self._get_log_name_resource_name()
174+
query = f'logName="{log_name_resource_name}" AND labels.stream="{stream_name}"'
175+
quoted_query = urllib.parse.quote(query, safe="")
176+
return f"https://console.cloud.google.com/logs/query;query={quoted_query}?project={self.project_id}"
177+
178+
def _get_log_name_resource_name(self) -> str:
179+
return f"projects/{self.project_id}/logs/{self.LOG_NAME}"

src/tests/_internal/server/routers/test_logs.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ async def test_returns_logs(
7575
"message": "IQ==",
7676
},
7777
],
78+
"external_url": None,
7879
"next_token": None,
7980
}
8081
response = await client.post(
@@ -96,5 +97,6 @@ async def test_returns_logs(
9697
"message": "IQ==",
9798
},
9899
],
100+
"external_url": None,
99101
"next_token": None,
100102
}

0 commit comments

Comments
 (0)