Skip to content

Commit b7ed360

Browse files
davanstrienclaudeWauplin
authored
[Spaces] Add fetch_space_logs + hf spaces logs command (#4091)
* [Spaces] Add fetch_space_logs + hf spaces logs command Agents and scripts currently have no way to read Space build/run logs programmatically — the endpoint is only reachable via raw curl. This adds a public API to close that gap. - HfApi.fetch_space_logs(repo_id, *, build=False, follow=False) yields log lines as Iterable[str]. build=True switches to container build logs; default is the running app's stdout/stderr. - `hf spaces logs <repo_id> [--build] [-f] [-n N]` mirrors the Python API at the CLI level, with 404/403 mapped to clean CLIError messages. The helper trusts the "stream close = done" server contract (confirmed against moon-landing's SpaceLogs.svelte onClose handler) and does not poll SpaceStage; read timeout + bounded retries handle the misbehaving-upstream case. Structure mirrors _fetch_running_job_sse but without the status-check backstop. Tests use the mock-based pattern from hf jobs logs (no new VCR cassettes). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Update generated CLI reference for hf spaces logs Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix: catch HfHubHTTPError instead of httpx.HTTPStatusError hf_raise_for_status() raises HfHubHTTPError (inherits HTTPError), not httpx.HTTPStatusError. The previous handler was dead code, causing 404/403 errors to fall through to the retry loop instead of raising immediately. Spotted by cursor bugbot on PR review. Note: the same bug exists in _fetch_running_job_sse — not fixed here to keep the diff focused, but worth a follow-up. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Update docs/source/en/guides/manage-spaces.md Co-authored-by: Lucain <lucainp@gmail.com> * Refactor: share SSE streaming loop between Spaces and Jobs Extracts `HfApi._stream_sse_events` to unify the retry/backoff/dedup loop previously duplicated across `_fetch_space_logs_sse` and `_fetch_running_job_sse`. Addresses Wauplin and Cursor Bugbot review comments on PR #4091. Also fixes a dead `except httpx.HTTPStatusError` handler that affected both Spaces and Jobs: `hf_raise_for_status` raises `HfHubHTTPError` (subclass of `httpx.HTTPError`, not `HTTPStatusError`), so 404/403 in follow mode used to fall through to the broad retry arm and stall for ~25s. The new helper catches `HfHubHTTPError` before the broad arm, so permanent errors fail fast. Live-verified: `hf spaces logs missing/x -f` now errors in <1s instead of ~25s. CLI cleanups on `hf spaces logs` per Wauplin: - Switch from `print()` to `out.text(line.strip())` (new mode-aware printer from #3979). - Drop the redundant local `HfHubHTTPError` block — it's already handled by the global CLI error mapper. Also tightens `_fetch_running_job_sse` typing by splitting the legacy `double_check_job_has_finished_on_status_code_or_error` mixed tuple into `tolerated_status_codes: tuple[int, ...]` and `tolerated_exception_types: tuple[type[Exception], ...]`, eliminating a runtime type-discrimination step. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Drop dead `httpx.ReadTimeout` entry from fetch_job_metrics The entry in `tolerated_exception_types` was never consulted: the SSE helper's `is_no_new_line_timeout` check short-circuits the tolerated tuple lookup for any ReadTimeout, so the tuple entry was dead code (pre-existing on `main` before #4091, preserved faithfully through the refactor). ReadTimeout tolerance continues to work via the `is_no_new_line_timeout` path. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Update src/huggingface_hub/cli/spaces.py Co-authored-by: Lucain <lucainp@gmail.com> * Update tests/test_cli.py Co-authored-by: Lucain <lucainp@gmail.com> * Update tests/test_cli.py Co-authored-by: Lucain <lucainp@gmail.com> * Address review feedback on fetch_space_logs - Promote "Debug a failing Space" heading to ### (matches PR #4108 style) - Add hint when run logs are empty, suggesting --build as alternative - Add tests covering the empty-logs hint for both run and build modes - Regenerate CLI reference docs to include hf spaces logs command * Update tests/test_cli.py --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Co-authored-by: Lucain <lucainp@gmail.com>
1 parent 6cace2b commit b7ed360

7 files changed

Lines changed: 358 additions & 60 deletions

File tree

docs/source/en/guides/manage-spaces.md

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,33 @@ it will go to sleep. Any visitor landing on your Space will start it back up. Yo
195195
Note: if you are using a 'cpu-basic' hardware, you cannot configure a custom sleep time. Your Space will automatically
196196
be paused after 48h of inactivity.
197197

198+
### Debug a failing Space by reading its logs
199+
200+
When a Space fails to build or crashes at runtime, the logs you normally view in the browser are also available programmatically via [`fetch_space_logs`]. This is particularly useful from scripts or agentic workflows where opening a browser is not an option.
201+
202+
```py
203+
# Drain the currently available run logs and return immediately (like `docker logs`)
204+
>>> for line in api.fetch_space_logs(repo_id=repo_id):
205+
... print(line, end="")
206+
207+
# Read the container build logs instead (useful when the Space is stuck in BUILD_ERROR)
208+
>>> for line in api.fetch_space_logs(repo_id=repo_id, build=True):
209+
... print(line, end="")
210+
211+
# Stream run logs in real time until the server closes the stream (Ctrl-C to stop)
212+
>>> for line in api.fetch_space_logs(repo_id=repo_id, follow=True):
213+
... print(line, end="")
214+
```
215+
216+
The same functionality is available from the CLI:
217+
218+
```bash
219+
hf spaces logs username/my-space # drain run logs
220+
hf spaces logs username/my-space --build # read build logs
221+
hf spaces logs username/my-space -f # stream in real time
222+
hf spaces logs username/my-space -n 50 # last 50 lines only
223+
```
224+
198225
**Bonus: set a sleep time while requesting hardware**
199226

200227
Upgraded hardware will be automatically assigned to your Space once it's built.

docs/source/en/package_reference/cli.md

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3403,6 +3403,7 @@ $ hf spaces [OPTIONS] COMMAND [ARGS]...
34033403
* `hot-reload`: Hot-reload any Python file of a Space...
34043404
* `info`: Get info about a space on the Hub.
34053405
* `list`: List spaces on the Hub. [alias: ls]
3406+
* `logs`: Fetch the run or build logs of a Space.
34063407
* `search`: Search spaces on the Hub using semantic...
34073408
* `volumes`: Manage volumes for a Space on the Hub.
34083409

@@ -3548,6 +3549,44 @@ Learn more
35483549
Read the documentation at https://huggingface.co/docs/huggingface_hub/en/guides/cli
35493550

35503551

3552+
### `hf spaces logs`
3553+
3554+
Fetch the run or build logs of a Space.
3555+
3556+
By default, prints currently available run logs and exits (non-blocking, like
3557+
`docker logs`). Use --follow/-f to stream until the server closes the stream.
3558+
Use --build to see the container build logs instead (useful when a Space is
3559+
stuck in BUILD_ERROR).
3560+
3561+
**Usage**:
3562+
3563+
```console
3564+
$ hf spaces logs [OPTIONS] SPACE_ID
3565+
```
3566+
3567+
**Arguments**:
3568+
3569+
* `SPACE_ID`: The space ID (e.g. `username/repo-name`). [required]
3570+
3571+
**Options**:
3572+
3573+
* `--build`: Fetch the container build logs instead of the run logs. Useful when a Space is stuck in BUILD_ERROR.
3574+
* `-f, --follow`: Follow log output (stream until the server closes the stream). Without this flag, only currently available logs are printed.
3575+
* `-n, --tail INTEGER`: Number of lines to show from the end of the logs.
3576+
* `--token TEXT`: A User Access Token generated from https://huggingface.co/settings/tokens.
3577+
* `--help`: Show this message and exit.
3578+
3579+
Examples
3580+
$ hf spaces logs username/my-space
3581+
$ hf spaces logs username/my-space --build
3582+
$ hf spaces logs -f username/my-space
3583+
$ hf spaces logs -n 50 username/my-space
3584+
3585+
Learn more
3586+
Use `hf <command> --help` for more information about a command.
3587+
Read the documentation at https://huggingface.co/docs/huggingface_hub/en/guides/cli
3588+
3589+
35513590
### `hf spaces search`
35523591

35533592
Search spaces on the Hub using semantic search.

docs/source/en/package_reference/space_runtime.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ Check the [`HfApi`] documentation page for the reference of methods to manage yo
88

99
- Duplicate a Space: [`duplicate_space`]
1010
- Fetch current runtime: [`get_space_runtime`]
11+
- Fetch build or run logs: [`fetch_space_logs`]
1112
- Manage secrets: [`add_space_secret`] and [`delete_space_secret`]
1213
- Manage hardware: [`request_space_hardware`]
1314
- Manage state: [`pause_space`], [`restart_space`], [`set_space_sleep_time`]

src/huggingface_hub/__init__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -245,6 +245,7 @@
245245
"enable_webhook",
246246
"fetch_job_logs",
247247
"fetch_job_metrics",
248+
"fetch_space_logs",
248249
"file_exists",
249250
"get_bucket_file_metadata",
250251
"get_bucket_paths_info",
@@ -954,6 +955,7 @@
954955
"export_folder_as_dduf",
955956
"fetch_job_logs",
956957
"fetch_job_metrics",
958+
"fetch_space_logs",
957959
"file_exists",
958960
"from_pretrained_fastai",
959961
"get_async_session",
@@ -1379,6 +1381,7 @@ def __dir__():
13791381
enable_webhook, # noqa: F401
13801382
fetch_job_logs, # noqa: F401
13811383
fetch_job_metrics, # noqa: F401
1384+
fetch_space_logs, # noqa: F401
13821385
file_exists, # noqa: F401
13831386
get_bucket_file_metadata, # noqa: F401
13841387
get_bucket_paths_info, # noqa: F401

src/huggingface_hub/cli/spaces.py

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
import sys
3535
import tempfile
3636
import time
37+
from collections import deque
3738
from typing import Annotated, Literal, get_args
3839

3940
import typer
@@ -264,6 +265,68 @@ def dev_mode(
264265
print("PS: Dev mode stops after 48h of inactivity, don't forget to save your changes regularly.")
265266

266267

268+
@spaces_cli.command(
269+
"logs",
270+
examples=[
271+
"hf spaces logs username/my-space",
272+
"hf spaces logs username/my-space --build",
273+
"hf spaces logs -f username/my-space",
274+
"hf spaces logs -n 50 username/my-space",
275+
],
276+
)
277+
def spaces_logs(
278+
space_id: Annotated[str, typer.Argument(help="The space ID (e.g. `username/repo-name`).")],
279+
build: Annotated[
280+
bool,
281+
typer.Option(
282+
"--build",
283+
help="Fetch the container build logs instead of the run logs. Useful when a Space is stuck in BUILD_ERROR.",
284+
),
285+
] = False,
286+
follow: Annotated[
287+
bool,
288+
typer.Option(
289+
"-f",
290+
"--follow",
291+
help="Follow log output (stream until the server closes the stream). Without this flag, only currently available logs are printed.",
292+
),
293+
] = False,
294+
tail: Annotated[
295+
int | None,
296+
typer.Option(
297+
"-n",
298+
"--tail",
299+
help="Number of lines to show from the end of the logs.",
300+
),
301+
] = None,
302+
token: TokenOpt = None,
303+
) -> None:
304+
"""Fetch the run or build logs of a Space.
305+
306+
By default, prints currently available run logs and exits (non-blocking, like
307+
`docker logs`). Use --follow/-f to stream until the server closes the stream.
308+
Use --build to see the container build logs instead (useful when a Space is
309+
stuck in BUILD_ERROR).
310+
"""
311+
if follow and tail is not None:
312+
raise CLIError(
313+
"Cannot use --follow and --tail together. Use --follow to stream logs or --tail to show recent logs."
314+
)
315+
316+
api = get_hf_api(token=token)
317+
logs = api.fetch_space_logs(space_id, build=build, follow=follow)
318+
if tail is not None:
319+
logs = deque(logs, maxlen=tail)
320+
found_logs = False
321+
for line in logs:
322+
clean_line = line.strip()
323+
out.text(clean_line)
324+
if clean_line:
325+
found_logs = True
326+
if not found_logs and not build:
327+
out.hint(f"No run logs found for space {space_id}. Try passing --build to fetch build logs instead.")
328+
329+
267330
@spaces_cli.command(
268331
"hot-reload",
269332
examples=[

0 commit comments

Comments
 (0)