Tamil Selvan
11/04/2025, 10:16 AMMarvin
11/04/2025, 10:16 AMMarvin
11/04/2025, 10:24 AMasync for log in subscriber only enters the body when a new log arrives. After the flow finishes, no new logs are emitted, so your state check never runs and the websocket stays open.
A robust pattern is to:
- Use get_logs_subscriber(...) (auto-detects OSS vs Cloud)
- Poll the flow’s state on a timeout when no new logs arrive
- Optionally fetch historical logs first (the subscriber only streams new logs)
Here’s a complete example you can drop in. It streams historical logs first, then live logs, and exits promptly once the flow run is final even if no new logs arrive.
import asyncio
import logging
from prefect.logging.clients import get_logs_subscriber
from prefect.client.orchestration import get_client
from prefect.client.schemas.filters import (
LogFilter,
LogFilterLevel,
LogFilterFlowRunId,
)
async def stream_flow_run_logs(flow_run_id, state_check_interval=1.0):
# Make sure PREFECT_API_URL is set for OSS, e.g. <http://localhost:4200/api>
log_filter = LogFilter(
flow_run_id=LogFilterFlowRunId(any_=[flow_run_id]),
level=LogFilterLevel(ge_=<http://logging.INFO|logging.INFO>),
)
async with get_client() as client:
# 1) Optionally send historical logs that already exist
history = await client.read_logs(filter=log_filter)
for log in history:
yield f"event: message\ndata: {log.message}\n\n"
# 2) Subscribe to live logs and poll state on timeout
async with get_logs_subscriber(filter=log_filter) as sub:
while True:
try:
log = await asyncio.wait_for(sub.__anext__(), timeout=state_check_interval)
yield f"event: message\ndata: {log.message}\n\n"
except asyncio.TimeoutError:
flow_run = await client.read_flow_run(flow_run_id)
if flow_run.state and flow_run.state.is_final():
# No new logs and run is final — exit cleanly
break
Common gotchas:
- Start the subscriber before or during the run if you want to see “live” logs; it does not replay past logs unless you fetch them via read_logs first.
- Ensure your filter isn’t too restrictive. For a single flow run, use:
LogFilter(flow_run_id=LogFilterFlowRunId(any_=[flow_run_id]))
and add a level filter only if you want to suppress DEBUG/INFO.
- Make sure your OSS API URL is correct and set in the environment where this code runs:
PREFECT_API_URL=http://<your-server>:4200/api
- Verify logs actually exist for the run by calling `client.read_logs(filter=log_filter)`; if this returns nothing, either your filter is wrong or the run didn’t emit logs.
References:
- Logging clients (PrefectLogsSubscriber): API: logging clients
- Filters (LogFilter, LogFilterLevel, LogFilterFlowRunId): API: filters
- Server logs API (read logs): REST: read logs
If you can share:
- Your log_filter construction
- When you start subscribing relative to the run
- Prefect version and your PREFECT_API_URL
…I can help tailor this further to your setup.Tamil Selvan
11/05/2025, 11:00 AMMarvin
11/05/2025, 11:00 AMMarvin
11/05/2025, 11:02 AMread_logs works), the worker is fine.
- If you’re on OSS behind a proxy/ingress, the most common reason for “no logs” with the subscriber is that WebSocket upgrade is blocked by the proxy/ingress. REST read_logs will still work in that case, but streaming won’t.
- In your snippet, subscriber._anext_() looks incorrect — you should call sub.__anext__() or just use async for. Also, make sure you’re using the documented filter classes (LogFilter, LogFilterFlowRunId, etc.).
A corrected minimal version
This streams historical logs first, then listens for new logs and exits promptly when the flow is final. It uses the public __anext__() and the documented filter classes.
import asyncio
import logging
from typing import AsyncGenerator
from prefect.client.orchestration import get_client, PrefectClient
from prefect.client.schemas.filters import (
LogFilter,
LogFilterFlowRunId,
LogFilterLevel,
)
from prefect.logging.clients import get_logs_subscriber
async def event_generator(flow_run_id: str) -> AsyncGenerator[str, None]:
try:
log_filter = LogFilter(
flow_run_id=LogFilterFlowRunId(any_=[flow_run_id]),
# optional: include to suppress DEBUG
level=LogFilterLevel(ge_=<http://logging.INFO|logging.INFO>),
)
async with get_client() as client:
client: PrefectClient # type: ignore
# 1) Send historical logs
history_logs = await client.read_logs(filter=log_filter)
for log in history_logs:
yield f"event: message\ndata: {log.message.replace('USER_OUTPUT: ', '')}\n\n"
# 2) Stream live logs with timeout + state check
async with get_logs_subscriber(filter=log_filter) as sub:
while True:
try:
log = await asyncio.wait_for(sub.__anext__(), timeout=1.0)
yield f"event: message\ndata: {log.message.replace('USER_OUTPUT: ', '')}\n\n"
except asyncio.TimeoutError:
flow_run = await client.read_flow_run(flow_run_id)
if flow_run.state and flow_run.state.is_final():
break
yield "event: message\ndata: Execution completed successfully.\n\n"
except Exception as e:
yield f"event: error\ndata: {str(e)}\n\n"
finally:
yield "event: close\ndata: Connection closing.\n\n"
Troubleshooting checklist
1) Verify you’re using the correct filter classes
- Prefer the documented classes from `prefect.client.schemas.filters`: LogFilter, LogFilterFlowRunId, LogFilterLevel, etc.
- If read_logs is working for you already, your filter is probably fine — but using the documented classes helps avoid mismatch in the WebSocket payload.
2) Make sure the WebSocket is reachable
- For OSS, the subscriber connects to a WebSocket derived from your PREFECT_API_URL. Ensure the process running this code has PREFECT_API_URL set, e.g. http://<host>:4200/api.
- Behind a proxy/ingress, you must allow WebSocket upgrades. Typical NGINX requirements:
- proxy_set_header Upgrade $http_upgrade;
- proxy_set_header Connection "upgrade";
- Adequate proxy_read_timeout
- Quick way to see what URL the subscriber will use:
from prefect.logging.clients import logs_out_socket_from_api_url
from prefect.settings import PREFECT_API_URL
print(logs_out_socket_from_api_url(PREFECT_API_URL.value()))
Then test with a WebSocket client (e.g., wscat) from the same network as the subscriber process.
3) Add debug logging in the subscriber processMarvin
11/05/2025, 11:02 AMPREFECT_LOGGING_LEVEL=DEBUG in the environment running this generator. Connection/retry messages can reveal if the WebSocket can’t connect or is being closed by an intermediary.
4) Subscribe early enough if you need “live” logs
- The subscriber only streams new logs. You’re already reading history first, which is good. If the flow is already finished and no new logs are emitted after you subscribe, you’ll only see timeouts — your timeout + state check will then exit.
5) Sanity checks
- Try loosening the filter temporarily (e.g., remove level filter) to ensure it’s not too restrictive.
- If you have SSL/HTTPS with a self-signed cert, ensure the client trusts it; otherwise wss:// connection may fail.
References
- Logging clients (PrefectLogsSubscriber): API: logging clients
- Filters (LogFilter, LogFilterLevel, LogFilterFlowRunId): API: filters
If you can share:
- Your PREFECT_API_URL
- Whether you’re behind an ingress/proxy and which one
- Any DEBUG logs from the subscriber process
…I can help pinpoint whether this is a connectivity (WebSocket) issue vs a code/path issue.Marvin
11/05/2025, 11:33 AMTamil Selvan
11/05/2025, 11:35 AMMarvin
11/05/2025, 11:35 AMMarvin
11/05/2025, 11:39 AM<ws //prefect 4200/api/logs/out> and earlier <http //prefect 4200/api/>
- That suggests the scheme separator :// may be missing in your PREFECT_API_URL.
- Please double-check PREFECT_API_URL is exactly like:
- <http://prefect:4200/api> (for OSS without TLS)
- <https://your-domain/api> (if behind TLS)
- To confirm what the subscriber will actually use, print the repr to avoid formatting:
from prefect.logging.clients import logs_out_socket_from_api_url
from prefect.settings import PREFECT_API_URL
print(repr(PREFECT_API_URL.value()))
print(repr(logs_out_socket_from_api_url(PREFECT_API_URL.value())))
Expected: '<http://prefect:4200/api>' and '<ws://prefect:4200/api/logs/out>'
2) Use the documented filter classes and public iteration API
- In Prefect 3.x, use LogFilter and LogFilterFlowRunId (no “ClientSchema” suffix), and iterate with __anext__() or async for.
- Also, the read_logs call takes filter=..., not log_filter=....
- A corrected version of your generator:
import asyncio
import logging
from typing import AsyncGenerator
from prefect.client.orchestration import get_client, PrefectClient
from prefect.client.schemas.filters import LogFilter, LogFilterFlowRunId, LogFilterLevel
from prefect.logging.clients import get_logs_subscriber
async def event_generator(flow_run_id: str) -> AsyncGenerator[str, None]:
try:
log_filter = LogFilter(
flow_run_id=LogFilterFlowRunId(any_=[flow_run_id]),
level=LogFilterLevel(ge_=<http://logging.INFO|logging.INFO>), # optional
)
async with get_client() as client:
client: PrefectClient # type: ignore
# 1) Historical logs
history_logs = await client.read_logs(filter=log_filter)
for log in history_logs:
yield f"event: message\ndata: {log.message.replace('USER_OUTPUT: ', '')}\n\n"
# 2) Live stream + state check timeout
async with get_logs_subscriber(filter=log_filter) as subscriber:
while True:
try:
log = await asyncio.wait_for(subscriber.__anext__(), timeout=1.0)
yield f"event: message\ndata: {log.message.replace('USER_OUTPUT: ', '')}\n\n"
except asyncio.TimeoutError:
flow_run = await client.read_flow_run(flow_run_id)
if flow_run.state and flow_run.state.is_final():
break
yield "event: message\ndata: Execution completed successfully.\n\n"
except Exception as e:
yield f"event: error\ndata: {str(e)}\n\n"
finally:
yield "event: close\ndata: Connection closing.\n\n"
3) No special env is required on Docker/K8s workers for streaming
- Workers just send logs to the API (which is working since read_logs returns data).
- Streaming is between your subscriber process and the Prefect API.
4) If you are behind an ingress/proxy, ensure WebSocket upgrades are allowed
- REST log reads can work while WebSockets are blocked.
- For NGINX, confirm headers and timeouts:
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_read_timeout 600s;
proxy_send_timeout 600s;
- Then test the handshake from the subscriber’s network:
from prefect.logging.clients import logs_out_socket_from_api_url
from prefect.settings import PREFECT_API_URL
print("logs ws:", repr(logs_out_socket_from_api_url(PREFECT_API_URL.value())))
# Try connecting with a ws client (e.g., wscat) to the printed URL to confirm upgrades work
5) Turn up debug specifically for logging clients
- You already set PREFECT_LOGGING_LEVEL=DEBUG which is great. To be extra sure you see subscriber logs, you can add:
```Marvin
11/05/2025, 11:39 AMNext steps to pinpoint the issue
- Please share the exact repr output of:
repr(PREFECT_API_URL.value())
repr(logs_out_socket_from_api_url(PREFECT_API_URL.value()))
```
- Are you running the subscriber behind an ingress/proxy? If so, which one (NGINX/ALB/etc.)?
- Confirm your Prefect version.
References
- Logging clients (streams): API: logging clients
- Filters (LogFilter, LogFilterFlowRunId, LogFilterLevel): API: filters