Parvez Mohammed
11/07/2025, 8:58 AMMarvin
11/07/2025, 8:58 AMMarvin
11/07/2025, 9:02 AMprefect flow-run logs <FLOW_RUN_ID> > flow_run_logs.txt
- Tail only the most recent N logs:
prefect flow-run logs -t -n 500 <FLOW_RUN_ID> | tee flow_run_logs_tail.txt
- If you need to find the run ID:
prefect flow-run ls --flow-name "<YOUR_FLOW_NAME>" --limit 50
Docs: CLI: flow-run commands
2) Programmatic export with the Python client (robust, paginated)
This is the most reliable way to fetch very large log sets (with pagination) and write them to CSV/NDJSON/etc.
Example: export all logs for one flow run to CSV
import asyncio
import csv
from prefect.client.orchestration import get_client
from prefect.client.schemas.filters import LogFilter, LogFilterFlowRunId
from prefect.client.schemas.sorting import LogSort
async def export_flow_run_logs(flow_run_id: str, out_csv_path: str, page_size: int = 1000):
async with get_client() as client:
offset = 0
with open(out_csv_path, "w", newline="") as f:
writer = csv.writer(f)
writer.writerow(["timestamp", "level", "name", "message", "flow_run_id", "task_run_id"])
while True:
logs = await client.read_logs(
limit=page_size,
offset=offset,
logs=LogFilter(flow_run_id=LogFilterFlowRunId(any_=[flow_run_id])),
sort=LogSort.TIMESTAMP_ASC,
)
if not logs:
break
for log in logs:
writer.writerow([
log.timestamp.isoformat(),
log.level,
log.name,
log.message,
str(log.flow_run_id) if log.flow_run_id else "",
str(log.task_run_id) if log.task_run_id else "",
])
offset += len(logs)
print(f"Wrote logs to {out_csv_path}")
if __name__ == "__main__":
asyncio.run(export_flow_run_logs("<FLOW_RUN_ID>", "flow_run_logs.csv"))
- This paginates until it retrieves everything, avoiding UI download limits.
- The SDK handles backoff/retries automatically if you encounter rate limits.
API docs: REST: read logs, Logging handler API
3) Direct REST download endpoint (CSV)
- There is an endpoint that streams CSV:
GET /api/flow_runs/{flow_run_id}/logs/download
- Example curl (when PREFECT_API_URL already points to your workspace base URL):
curl -L \
-H "Authorization: Bearer $PREFECT_API_KEY" \
"$PREFECT_API_URL/flow_runs/<FLOW_RUN_ID>/logs/download" \
-o flow_run_logs.csv
If your log set is extremely large, this endpoint can still time out — in that case the paginated /api/logs approach in option 2 is more resilient.
Docs: REST: download flow run logs
If you frequently need big-log analysis
- Emit logs to another sink during runs so you’re not dependent on the Prefect UI or API export:
- Add a file handler to write all run logs locally or to network storage
- Ship to S3, CloudWatch, Datadog, ELK, etc., via standard Python logging handlers
- Prefect supports custom logging config via a YAML file (e.g., ~/.prefect/logging.yml) or the PREFECT_LOGGING_SETTINGS_PATH setting.
Docs: Advanced logging customization
Notes and limitsMarvin
11/07/2025, 9:02 AMParvez Mohammed
11/07/2025, 9:04 AMMarvin
11/07/2025, 9:05 AMMarvin
11/07/2025, 9:08 AM/api/logs/filter was used in older APIs. You’ll get a 422 from Prefect 3 if the path or the payload doesn’t match the current schema.
What to use instead
- Read logs (paginated): POST /api/logs
- Download CSV for a single flow run: GET /api/flow_runs/<FLOW_RUN_ID>/logs/download
Working examples
1) Paginated logs via REST (POST /api/logs)
curl -X POST <http://192.168.0.72:4201/api/logs> \
-H "Content-Type: application/json" \
-d '{
"limit": 1000,
"offset": 0,
"logs": {
"flow_run_id": { "any_": ["<FLOW_RUN_ID>"] }
},
"sort": "TIMESTAMP_ASC"
}'
- Increase offset by the number of logs returned to paginate until you get an empty list.
- Common 422 causes here:
- Wrong path (/api/logs/filter instead of /api/logs)
- Using GET instead of POST
- Invalid body shape (filters must be nested, e.g. {"flow_run_id": {"any_": ["uuid"]}})
- Invalid UUID format for flow_run_id
- Negative offset or non-integer limit
Docs: REST: read logs
2) CSV download for one run (GET /api/flow_runs/{id}/logs/download)
curl -L \
"<http://192.168.0.72:4201/api/flow_runs/<FLOW_RUN_ID>/logs/download>" \
-H "Accept: text/csv" \
-o flow_run_logs.csv
Docs: REST: download flow run logs
Alternative: CLI (often easiest)
prefect flow-run logs <FLOW_RUN_ID> > flow_run_logs.txt
- Find IDs if needed:
prefect flow-run ls --flow-name "<YOUR_FLOW_NAME>" --limit 50
CLI docs: CLI: flow-run commands
If you prefer Python, the SDK handles pagination and schema for you (fewer chances of 422). I can share a ready-to-run snippet that writes CSV/NDJSON.
If you can paste the exact request body you were sending to /api/logs/filter, I can point out the mismatched fields — but switching to the correct endpoint and body above should resolve the 422 on your self-hosted server.Parvez Mohammed
11/07/2025, 9:14 AMMarvin
11/07/2025, 9:14 AMMarvin
11/07/2025, 9:15 AM/api/logs/filter is not the logs read endpoint. Use POST /api/logs instead. Hitting the old path can return 422/500 depending on the server.
Quick fixes
- Minimal smoke test (POST /api/logs):
curl -X POST <http://192.168.0.72:4201/api/logs> \
-H "Content-Type: application/json" \
-d '{
"limit": 1,
"offset": 0
}'
- Filter by a specific flow run, with pagination:
curl -X POST <http://192.168.0.72:4201/api/logs> \
-H "Content-Type: application/json" \
-d '{
"limit": 1000,
"offset": 0,
"logs": {
"flow_run_id": { "any_": ["<FLOW_RUN_ID>"] }
},
"sort": "TIMESTAMP_ASC"
}'
- CSV for one flow run:
curl -L "<http://192.168.0.72:4201/api/flow_runs/<FLOW_RUN_ID>/logs/download>" \
-H "Accept: text/csv" \
-o flow_run_logs.csv
- Easiest path via CLI:
prefect flow-run logs <FLOW_RUN_ID> > flow_run_logs.txt
If you want a robust, paginated export in Python (avoids UI limits), this works in 3.4.14:
import asyncio
import csv
from prefect.client.orchestration import get_client
from prefect.client.schemas.filters import LogFilter, LogFilterFlowRunId
from prefect.client.schemas.sorting import LogSort
async def export_flow_run_logs(flow_run_id: str, out_csv_path: str, page_size: int = 1000):
async with get_client() as client:
offset = 0
with open(out_csv_path, "w", newline="") as f:
writer = csv.writer(f)
writer.writerow(["timestamp", "level", "name", "message", "flow_run_id", "task_run_id"])
while True:
logs = await client.read_logs(
limit=page_size,
offset=offset,
logs=LogFilter(flow_run_id=LogFilterFlowRunId(any_=[flow_run_id])),
sort=LogSort.TIMESTAMP_ASC,
)
if not logs:
break
for log in logs:
writer.writerow([
log.timestamp.isoformat(),
log.level,
log.name,
log.message,
str(log.flow_run_id) if log.flow_run_id else "",
str(log.task_run_id) if log.task_run_id else "",
])
offset += len(logs)
if __name__ == "__main__":
asyncio.run(export_flow_run_logs("<FLOW_RUN_ID>", "flow_run_logs.csv"))
References:
- REST: read logs
- REST: download flow run logs
- CLI: flow-run commands
If you still get a 500 on the correct endpoint (/api/logs), that usually indicates a server-side issue (e.g., DB error or very large unpaginated query). Try:
- Lower limit (e.g., 200–1000) and paginate via offset
- Add a narrower filter (flow_run_id and/or timestamp range)
- Confirm both your server and SDK are on 3.4.14 (mixed 2.x/3.x can cause odd failures)
- Check the Prefect server logs for the traceback when the 500 occurs
If you share:
- Whether you need logs for one run or many
- Your current payload and a sample flow run ID
I can tailor a ready-to-run command/script for your exact case.Parvez Mohammed
11/07/2025, 9:20 AMMarvin
11/07/2025, 9:20 AMMarvin
11/07/2025, 9:24 AMprefect version
- What version/tag your self-hosted server is running (container image tag or output of prefect version inside the server container)
- Your PREFECT_API_URL value
Immediate tests (no SDK involved)
- Minimal smoke test against a 3.x server:
curl -sS -X POST <http://192.168.0.72:4201/api/logs> \
-H "Content-Type: application/json" \
-d '{"limit": 1, "offset": 0}'
Expected: 200 with a JSON array (possibly empty). If you get 404, it’s very likely the server is 2.x. If 500, the server is finding the route but failing internally (DB/timeouts).
- CSV download for a single run (3.x):
curl -L \
"<http://192.168.0.72:4201/api/flow_runs/<FLOW_RUN_ID>/logs/download>" \
-H "Accept: text/csv" \
-o flow_run_logs.csv
This bypasses the SDK entirely and often succeeds for large outputs where the UI download stalls.
If your server is 3.x but your local SDK is older
You can still export logs without the SDK by calling the REST API directly:
import os, requests, csv
PREFECT_API_URL = os.environ.get("PREFECT_API_URL", "<http://192.168.0.72:4201/api>").rstrip("/")
def export_logs(flow_run_id: str, out_csv: str, page_size: int = 1000):
offset = 0
with open(out_csv, "w", newline="") as f:
w = csv.writer(f)
w.writerow(["timestamp","level","name","message","flow_run_id","task_run_id"])
while True:
payload = {
"limit": page_size,
"offset": offset,
"logs": {"flow_run_id": {"any_": [flow_run_id]}},
"sort": "TIMESTAMP_ASC"
}
r = <http://requests.post|requests.post>(f"{PREFECT_API_URL}/logs", json=payload, timeout=60)
r.raise_for_status()
logs = r.json()
if not logs:
break
for log in logs:
w.writerow([
log["timestamp"], log["level"], log["name"], log["message"],
log.get("flow_run_id") or "", log.get("task_run_id") or ""
])
offset += len(logs)
export_logs("<FLOW_RUN_ID>", "flow_run_logs.csv")
If your server is actually 2.x
- The 3.x client and 3.x filter schema won’t be compatible and can trigger 422/500 errors.
- You can either:
- Align both sides on 3.4.14 (recommended), or
- Use the 2.x-compatible route and payload (POST /api/logs/filter with 2.x schemas) and 2.x SDK.
Why you’re seeing 500
- Calling /api/logs/filter from a mismatched client/server can hit a handler that expects different filter shapes and fail server-side (DB or validation quirks), returning 500 instead of a clear 4xx.
- It can also occur if the DB query is too heavy. If you confirm the correct 3.x endpoint, reduce limit (e.g., 200–1000) and paginate.
References
- REST: read logs (3.x): https://docs-3.prefect.io/v3/api-ref/rest-api/server/logs/read-logs
- REST: download flow run logs (CSV): https://docs-3.prefect.io/v3/api-ref/rest-api/server/flow-runs/download-logs
- CLI: flow-run commands: https://docs-3.prefect.io/v3/api-ref/python/prefect-cli-flow_run
Once you share the exact prefect version outputs and your server version, I’ll give you a precise, ready-to-run export path for your setup.