Skip to content

Commit 09a05f0

Browse files
authored
Merge pull request #1932 from transformerlab/fix/job-logs-cli-messages
Fix error messages for CLI when logs are not available yet
2 parents 8a3cd76 + de50aae commit 09a05f0

5 files changed

Lines changed: 176 additions & 3 deletions

File tree

api/transformerlab/routers/experiment/jobs.py

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
from fastapi.responses import StreamingResponse, FileResponse
1212
from json import JSONDecodeError
1313
from lab import Job, storage
14+
from lab.job_status import JobStatus
1415
from sqlalchemy.ext.asyncio import AsyncSession
1516
from werkzeug.utils import secure_filename
1617

@@ -69,6 +70,30 @@ def _eval_results_storage_path(stored_path: str, eval_dir: str) -> str:
6970
return p
7071

7172

73+
def _job_logs_not_ready_payload(
74+
job_data: dict,
75+
tail_lines: int,
76+
message: str,
77+
*,
78+
include_request_id: bool = False,
79+
) -> dict:
80+
payload = {
81+
"cluster_name": job_data.get("cluster_name"),
82+
"provider_id": job_data.get("provider_id"),
83+
"provider_job_id": None,
84+
"provider_name": job_data.get("provider_name"),
85+
"tail_lines": tail_lines,
86+
"logs": "",
87+
"job_candidates": [],
88+
"message": message,
89+
"retryable": True,
90+
"retry_after_seconds": 10,
91+
}
92+
if include_request_id:
93+
payload["request_id"] = None
94+
return payload
95+
96+
7297
@router.get("/list")
7398
async def jobs_get_all(
7499
experimentId: str,
@@ -271,15 +296,25 @@ async def get_provider_job_logs(
271296
raise HTTPException(status_code=404, detail="Job not found")
272297

273298
job_data = job.get("job_data") or {}
299+
job_status = str(job.get("status") or "").upper()
274300
if not isinstance(job_data, dict):
275301
try:
276302
job_data = json.loads(job_data)
277303
except JSONDecodeError:
278304
job_data = {}
279305

306+
def _logs_not_ready_response() -> dict:
307+
return _job_logs_not_ready_payload(
308+
job_data,
309+
tail_lines,
310+
"Machine logs are not available yet. Please try again shortly.",
311+
)
312+
280313
provider_id = job_data.get("provider_id")
281314
cluster_name = job_data.get("cluster_name")
282315
if not provider_id or not cluster_name:
316+
if job_status in {JobStatus.LAUNCHING.value, JobStatus.WAITING.value, "CREATED"}:
317+
return _logs_not_ready_response()
283318
raise HTTPException(
284319
status_code=400, detail="Job does not contain provider metadata (provider_id/cluster_name missing)"
285320
)
@@ -372,6 +407,8 @@ async def get_provider_job_logs(
372407
]
373408

374409
if provider_job_id is None:
410+
if job_status in {JobStatus.LAUNCHING.value, JobStatus.WAITING.value, "CREATED"}:
411+
return _logs_not_ready_response()
375412
raise HTTPException(status_code=404, detail="Unable to determine provider job id for this job")
376413

377414
# For local provider, set workspace_dir (job dir) so LocalProvider can read logs.
@@ -437,6 +474,7 @@ async def get_request_logs(
437474
raise HTTPException(status_code=404, detail="Job not found")
438475

439476
job_data = job.get("job_data") or {}
477+
job_status = str(job.get("status") or "").upper()
440478
if not isinstance(job_data, dict):
441479
try:
442480
job_data = json.loads(job_data)
@@ -445,6 +483,13 @@ async def get_request_logs(
445483

446484
provider_id = job_data.get("provider_id")
447485
if not provider_id:
486+
if job_status in {JobStatus.LAUNCHING.value, JobStatus.WAITING.value, "CREATED"}:
487+
return _job_logs_not_ready_payload(
488+
job_data,
489+
tail_lines,
490+
"Request logs are not available yet. Please try again shortly.",
491+
include_request_id=True,
492+
)
448493
raise HTTPException(status_code=400, detail="Job does not contain provider metadata (provider_id missing)")
449494

450495
provider_launch_result = job_data.get("provider_launch_result")
@@ -454,6 +499,13 @@ async def get_request_logs(
454499
if not request_id:
455500
request_id = job_data.get("orchestrator_request_id")
456501
if not request_id:
502+
if job_status in {JobStatus.LAUNCHING.value, JobStatus.WAITING.value, "CREATED"}:
503+
return _job_logs_not_ready_payload(
504+
job_data,
505+
tail_lines,
506+
"Request logs are not available yet. Please try again shortly.",
507+
include_request_id=True,
508+
)
457509
raise HTTPException(status_code=400, detail="Job does not have a provider request ID")
458510

459511
team_id = user_and_team["team_id"]
@@ -695,6 +747,7 @@ async def get_job_task_logs(
695747
raise HTTPException(status_code=404, detail="Job not found")
696748

697749
job_data = job.get("job_data") or {}
750+
job_status = str(job.get("status") or "").upper()
698751
if not isinstance(job_data, dict):
699752
try:
700753
job_data = json.loads(job_data)
@@ -711,9 +764,21 @@ async def get_job_task_logs(
711764
try:
712765
output_file_name = await shared.get_job_output_file_name(job_id, experiment_name=experimentId)
713766
except (ValueError, FileNotFoundError):
767+
if job_status in {JobStatus.LAUNCHING.value, JobStatus.WAITING.value, "CREATED"}:
768+
return _job_logs_not_ready_payload(
769+
job_data,
770+
tail_lines,
771+
"Task logs are not available yet. Please try again shortly.",
772+
)
714773
return {"logs": "", "tail_lines": tail_lines}
715774

716775
if not await storage.exists(output_file_name):
776+
if job_status in {JobStatus.LAUNCHING.value, JobStatus.WAITING.value, "CREATED"}:
777+
return _job_logs_not_ready_payload(
778+
job_data,
779+
tail_lines,
780+
"Task logs are not available yet. Please try again shortly.",
781+
)
717782
return {"logs": "", "tail_lines": tail_lines}
718783

719784
async with await storage.open(output_file_name, "r") as f:

cli/pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
44

55
[project]
66
name = "transformerlab-cli"
7-
version = "0.0.41"
7+
version = "0.0.42"
88
description = "Transformer Lab CLI"
99
requires-python = ">=3.10"
1010
authors = [{ name = "Transformer Lab", email = "hello@transformerlab.ai" }]

cli/src/transformerlab_cli/commands/job.py

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -760,16 +760,48 @@ def _print_logs(experiment_id: str, job_id: str, output_format: str, fetch_fn, l
760760
response = fetch_fn(experiment_id, job_id)
761761

762762
if response.status_code != 200:
763+
detail = ""
764+
try:
765+
payload = response.json()
766+
if isinstance(payload, dict):
767+
detail = str(payload.get("detail") or payload.get("message") or "")
768+
except Exception:
769+
detail = ""
770+
763771
if output_format == "json":
764-
print(json.dumps({"error": f"Failed to fetch {label}. Status code: {response.status_code}"}))
772+
body = {"error": f"Failed to fetch {label}. Status code: {response.status_code}"}
773+
if detail:
774+
body["detail"] = detail
775+
print(json.dumps(body))
765776
else:
766777
console.print(f"[red]Error:[/red] Failed to fetch {label}. Status code: {response.status_code}")
778+
if detail:
779+
console.print(f"[red]Detail:[/red] {detail}")
767780
raise typer.Exit(1)
768781

769782
data = response.json()
770783
logs_text = data.get("logs", "") if isinstance(data, dict) else ""
784+
wait_message = data.get("message", "") if isinstance(data, dict) else ""
785+
retryable = bool(data.get("retryable")) if isinstance(data, dict) else False
771786

772787
if not logs_text or "No log files found" in logs_text:
788+
if wait_message and retryable:
789+
if output_format == "json":
790+
print(
791+
json.dumps(
792+
{
793+
"job_id": job_id,
794+
"logs": logs_text,
795+
"line_count": 0,
796+
"message": wait_message,
797+
"retryable": True,
798+
"retry_after_seconds": data.get("retry_after_seconds"),
799+
}
800+
)
801+
)
802+
else:
803+
console.print(f"[yellow]{wait_message}[/yellow]")
804+
return
773805
exit_with_no_results(output_format, f"No {label} found for this job")
774806

775807
if output_format == "json":

cli/tests/commands/test_job.py

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -390,6 +390,46 @@ def test_machine_logs_error(_mock_require, _mock_fetch):
390390
assert result.exit_code == 1
391391

392392

393+
@patch(
394+
"transformerlab_cli.commands.job.fetch_logs",
395+
return_value=_mock_logs_response(""),
396+
)
397+
@patch("transformerlab_cli.commands.job.require_current_experiment", return_value="exp1")
398+
def test_machine_logs_not_ready_message_pretty(_mock_require, _mock_fetch):
399+
"""machine-logs shows retry message from API and exits cleanly when logs are not ready yet."""
400+
_mock_fetch.return_value.json.return_value = {
401+
"logs": "",
402+
"message": "Machine logs are not available yet while the job is still launching. Please try again shortly.",
403+
"retryable": True,
404+
"retry_after_seconds": 10,
405+
}
406+
result = runner.invoke(app, ["job", "machine-logs", "42"])
407+
assert result.exit_code == 0
408+
assert "not available yet" in strip_ansi(result.output)
409+
410+
411+
@patch(
412+
"transformerlab_cli.commands.job.fetch_logs",
413+
return_value=_mock_logs_response(""),
414+
)
415+
@patch("transformerlab_cli.commands.job.require_current_experiment", return_value="exp1")
416+
def test_machine_logs_not_ready_message_json(_mock_require, _mock_fetch):
417+
"""machine-logs --format json preserves retry metadata when logs are not ready yet."""
418+
_mock_fetch.return_value.json.return_value = {
419+
"logs": "",
420+
"message": "Machine logs are not available yet while the job is still launching. Please try again shortly.",
421+
"retryable": True,
422+
"retry_after_seconds": 10,
423+
}
424+
result = runner.invoke(app, ["--format", "json", "job", "machine-logs", "42"])
425+
assert result.exit_code == 0
426+
data = json.loads(result.output.strip())
427+
assert data["job_id"] == "42"
428+
assert data["line_count"] == 0
429+
assert data["retryable"] is True
430+
assert "not available yet" in data["message"]
431+
432+
393433
@patch(
394434
"transformerlab_cli.commands.job.fetch_task_logs",
395435
return_value=_mock_logs_response("sdk output line 1\nsdk output line 2"),
@@ -412,6 +452,24 @@ def test_task_logs_json(_mock_require, _mock_fetch):
412452
assert "sdk line" in data["logs"]
413453

414454

455+
@patch("transformerlab_cli.commands.job.fetch_task_logs", return_value=_mock_logs_response(""))
456+
@patch("transformerlab_cli.commands.job.require_current_experiment", return_value="exp1")
457+
def test_task_logs_not_ready_message_json(_mock_require, _mock_fetch):
458+
"""task-logs --format json preserves retry metadata when logs are not ready yet."""
459+
_mock_fetch.return_value.json.return_value = {
460+
"logs": "",
461+
"message": "Task logs are not available yet while the job is still launching. Please try again shortly.",
462+
"retryable": True,
463+
"retry_after_seconds": 10,
464+
}
465+
result = runner.invoke(app, ["--format", "json", "job", "task-logs", "42"])
466+
assert result.exit_code == 0
467+
data = json.loads(result.output.strip())
468+
assert data["job_id"] == "42"
469+
assert data["retryable"] is True
470+
assert "not available yet" in data["message"]
471+
472+
415473
@patch("transformerlab_cli.commands.job.api.get", return_value=_mock_logs_response("sdk line"))
416474
@patch("transformerlab_cli.commands.job.require_current_experiment", return_value="exp1")
417475
def test_task_logs_hits_task_logs_endpoint(_mock_require, mock_get):
@@ -452,6 +510,24 @@ def test_request_logs_json(_mock_require, _mock_fetch):
452510
assert "launch log" in data["logs"]
453511

454512

513+
@patch("transformerlab_cli.commands.job.fetch_request_logs", return_value=_mock_logs_response(""))
514+
@patch("transformerlab_cli.commands.job.require_current_experiment", return_value="exp1")
515+
def test_request_logs_not_ready_message_json(_mock_require, _mock_fetch):
516+
"""request-logs --format json preserves retry metadata when logs are not ready yet."""
517+
_mock_fetch.return_value.json.return_value = {
518+
"logs": "",
519+
"message": "Request logs are not available yet while the job is still launching. Please try again shortly.",
520+
"retryable": True,
521+
"retry_after_seconds": 10,
522+
}
523+
result = runner.invoke(app, ["--format", "json", "job", "request-logs", "42"])
524+
assert result.exit_code == 0
525+
data = json.loads(result.output.strip())
526+
assert data["job_id"] == "42"
527+
assert data["retryable"] is True
528+
assert "not available yet" in data["message"]
529+
530+
455531
@patch("transformerlab_cli.commands.job.fetch_request_logs", return_value=_mock_error_response(400))
456532
@patch("transformerlab_cli.commands.job.require_current_experiment", return_value="exp1")
457533
def test_request_logs_error(_mock_require, _mock_fetch):

cli/uv.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)