Skip to content

Commit 2471d4b

Browse files
authored
Support --since arg for dstack logs command (#3258)
1 parent c4212d1 commit 2471d4b

File tree

2 files changed

+39
-4
lines changed

2 files changed

+39
-4
lines changed

src/dstack/_internal/cli/commands/logs.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
import argparse
22
import sys
3+
from datetime import datetime
4+
from typing import Optional
35

46
from dstack._internal.cli.commands import APIBaseCommand
57
from dstack._internal.cli.services.completion import RunNameCompleter
68
from dstack._internal.core.errors import CLIError
9+
from dstack._internal.utils.common import parse_since
710
from dstack._internal.utils.logging import get_logger
811

912
logger = get_logger(__name__)
@@ -30,14 +33,25 @@ def _register(self):
3033
type=int,
3134
default=0,
3235
)
36+
self._parser.add_argument(
37+
"--since",
38+
help=(
39+
"Show only logs newer than the specified date."
40+
" Can be a duration (e.g. 10s, 5m, 1d) or an RFC 3339 string (e.g. 2023-09-24T15:30:00Z)."
41+
),
42+
type=str,
43+
)
3344
self._parser.add_argument("run_name").completer = RunNameCompleter(all=True) # type: ignore[attr-defined]
3445

3546
def _command(self, args: argparse.Namespace):
3647
super()._command(args)
3748
run = self.api.runs.get(args.run_name)
3849
if run is None:
3950
raise CLIError(f"Run {args.run_name} not found")
51+
52+
start_time = _get_start_time(args.since)
4053
logs = run.logs(
54+
start_time=start_time,
4155
diagnose=args.diagnose,
4256
replica_num=args.replica,
4357
job_num=args.job,
@@ -48,3 +62,12 @@ def _command(self, args: argparse.Namespace):
4862
sys.stdout.buffer.flush()
4963
except KeyboardInterrupt:
5064
pass
65+
66+
67+
def _get_start_time(since: Optional[str]) -> Optional[datetime]:
68+
if since is None:
69+
return None
70+
try:
71+
return parse_since(since)
72+
except ValueError as e:
73+
raise CLIError(e.args[0])

src/dstack/_internal/utils/common.py

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -144,22 +144,34 @@ def pretty_resources(
144144
return " ".join(parts)
145145

146146

147-
def since(timestamp: str) -> datetime:
147+
def parse_since(value: str) -> datetime:
148+
"""
149+
Returns a timestamp given an RFC 3339 string (e.g. 2023-09-24T15:30:00Z)
150+
or a duration (e.g. 10s, 5m, 1d) between the timestamp and now.
151+
"""
148152
try:
149-
seconds = parse_pretty_duration(timestamp)
153+
seconds = parse_pretty_duration(value)
150154
return get_current_datetime() - timedelta(seconds=seconds)
151155
except ValueError:
152156
pass
153157
try:
154-
return datetime.fromisoformat(timestamp)
158+
res = datetime.fromisoformat(value)
155159
except ValueError:
156160
pass
161+
else:
162+
return check_time_offset_aware(res)
157163
try:
158-
return datetime.fromtimestamp(int(timestamp))
164+
return datetime.fromtimestamp(int(value), tz=timezone.utc)
159165
except Exception:
160166
raise ValueError("Invalid datetime format")
161167

162168

169+
def check_time_offset_aware(time: datetime) -> datetime:
170+
if time.tzinfo is None:
171+
raise ValueError("Timestamp is not offset-aware. Specify timezone.")
172+
return time
173+
174+
163175
def parse_pretty_duration(duration: str) -> int:
164176
regex = re.compile(r"(?P<amount>\d+)(?P<unit>s|m|h|d|w)$")
165177
re_match = regex.match(duration)

0 commit comments

Comments
 (0)