Self Hosting / Task logs
Verified against Flow 5.0.0-beta.39
A task's log comes from one of two places: a copy core stored when the task finished, or the dispatcher still holding the task. This page is for operators setting up both, and Loki as an optional live source.
Every read goes through core, GET /api/v2/taskrun/{id}/log, which is what a task's View Log in the webapp calls. Core checks that the reader may see the run, and scrubs the run's secret values from the log as it relays it.
| Source | Kept | Used |
|---|---|---|
| The stored log | For Task Log Retention (days), 7 by default | Once the task has ended |
| The dispatcher's live route | While the task's pod exists | While the task runs, and after it ends when no copy was stored |
| A message | — | When neither answers |
While a task runs, the live route answers even when a copy is stored. A copy stored during this attempt answers instead only when the dispatcher registered no live route, or its route fails before answering. A requeued task never shows its earlier attempt's log.
When a task ends, Flow's Kubernetes dispatcher reads the pod's log before the Job or TaskRun can be deleted, and uploads it to core. Core keeps it in the artifact store as part of the run. It outlives the pod, needs no dispatcher to read it, and is purged with the run.
| Setting or property | Where | Default | Meaning |
|---|---|---|---|
Store Task Logs (logs.enabled) |
Admin › Settings › Artifacts | On | Core stores uploaded logs. Off tells dispatchers their logs aren't stored; logs already stored are served until they expire. |
Task Log Retention (days) (logs.retention.days) |
Admin › Settings › Artifacts | 7 |
Days a stored log is kept, apart from artifact retention |
flow.engine.task.log.max-bytes (FLOW_ENGINE_TASK_LOG_MAX_BYTES) |
Core | 10485760 (10 MiB) |
The largest log core stores. A larger one is refused whole, never truncated. |
flow.dispatcher.logs.upload (FLOW_DISPATCHER_LOGS_UPLOAD) |
Dispatcher | true |
Upload each finished task's pod log. false keeps logs in the pod only. |

A stored log isn't listed with the run's artifacts, can't be read through the artifact routes, and doesn't count toward artifact storage. Settings take effect without a restart.
The dispatcher logs TaskRun (<id>) log stored (<n> bytes). for each upload. When an upload fails, the task's outcome is unchanged:
| Dispatcher log | Cause | Fix |
|---|---|---|
The engine stores no task logs (no artifact store); sending no more. |
Core's artifact store is off. The dispatcher stops uploading for the rest of its life. | Turn the store on (FLOW_ARTIFACTS_ENABLED=true), then restart the dispatcher |
TaskRun (<id>) log not stored: log storage is switched off. |
Store Task Logs is off | Turn it on. The dispatcher keeps offering logs. |
TaskRun (<id>) log not stored: … with a 413 |
The log is over flow.engine.task.log.max-bytes |
Raise the limit on core, or log less. The log stays readable from the pod while it exists. |
Task (<id>) log not read from its pod: … |
The pod never existed or its log couldn't be read | Check the pod and the dispatcher's pods/log permission (Grant the Kubernetes permissions) |
Warning
Without a stored copy, a finished task's log lasts only as long as its pod. Under the Deletion Policy On Success or Always, the pod goes when the task ends, and so does its log. See Clean up finished tasks.
The dispatcher serves a running task's log at /api/v1/logs/stream, on port 7702. At start, it registers that route's address with core, with a bearer token it generates. The route accepts only that token, so only core can read it: a pod log isn't scrubbed until core relays it.
| Property | Environment variable | Default | Meaning |
|---|---|---|---|
flow.dispatcher.logs.url |
FLOW_DISPATCHER_LOGS_URL |
blank | The route's address as core reaches it, such as http://flow-dispatcher.flow.svc:7702/api/v1/logs/stream. Blank registers none, and running tasks have no live log. |
The earlier properties flow.agent.service.host and flow.agent.logstream.url are gone from core: nothing on core names a dispatcher. Don't route /api/v1/logs from your ingress; core reaches the dispatcher directly.
Waiting for the task's container to start., then follows the log once it starts. After five minutes it says the container hasn't started yet.When a read fails, the log shows one of these messages instead of an error:
| Message | Cause | Fix |
|---|---|---|
No log is available for this task: no stored log was uploaded, and its dispatcher registered no live log route. |
No stored copy, and flow.dispatcher.logs.url is blank |
Set FLOW_DISPATCHER_LOGS_URL on the dispatcher and restart it. Turn on stored logs for finished tasks. |
The live log cannot be read: the dispatcher running this task could not be reached or refused the read. … |
Core can't reach the registered address, or the dispatcher refused the token | Check the address from core's pod. With several replicas, register each pod's own address. |
The live log was cut off: the connection to the dispatcher failed. Open the log again to see the rest. |
The stream broke part way | Open the log again |
No Pod found yet for the requested Task. |
The task's pod doesn't exist yet, or was already deleted | Open the log again once the task starts. For finished tasks, use stored logs. |
Unable to retrieve logs. A server error has occurred. |
The dispatcher couldn't read the pod | Check the dispatcher's log and its pods/log permission |
The dispatcher's live route reads the task's pod by default. Set dispatcher.logging.type=loki and it queries Loki instead. Use it when task pods are removed quickly and no artifact store keeps their logs, since a finished task's stored copy wins over the live route.
| Property | Environment variable | Default | Meaning |
|---|---|---|---|
dispatcher.logging.type |
DISPATCHER_LOGGING_TYPE |
default |
default reads the pod; loki queries Loki |
dispatcher.logging.host |
DISPATCHER_LOGGING_HOST |
localhost |
Loki's host |
dispatcher.logging.port |
DISPATCHER_LOGGING_PORT |
9200 |
Loki's port. Set it: Loki usually listens on 3100. |
dispatcher.logging.type=loki
dispatcher.logging.host=loki.loki.svc.cluster.local
dispatcher.logging.port=3100The dispatcher calls GET http://<host>:<port>/loki/api/v1/query_range, forward from the start, with this stream selector:
{boomerang_io_taskrun_ref="<task run id>",boomerang_io_workflow_ref="<workflow id>",container=~"task|step-task"}The upload of a finished log still reads the pod, not Loki.
The dispatcher stamps each task pod with boomerang.io/* labels, but Loki doesn't index pod labels by itself. Your log shipper must write each stream with these labels:
| Loki label | From | Value |
|---|---|---|
boomerang_io_taskrun_ref |
The pod label boomerang.io/taskrun-ref |
The task run's ID |
boomerang_io_workflow_ref |
The pod label boomerang.io/workflow-ref |
The workflow's ID |
container |
The container's name | task on the kube-jobs executor, step-task on Tekton |
The names are what a promtail or Grafana Alloy labelmap on the pod's labels writes, since Kubernetes service discovery turns ., / and - into _. Keep both pod labels and the container name on every task pod's stream.
When no stream matches, the log reads Unable to retrieve logs. This could be due to data retention policy, activity cleanup, or unavailable in log store. Compare the labels on a task's stream in Loki with the table.
The Loki client has limits:
elastic, a v3 value, now answers only Unable to retrieve logs. The log option configured has been deprecated and no longer works.The promtail configuration published for v3 relabels v3's pod labels into bmrg_* names, and drops any pod without them. v5 task pods carry none of them, so it drops every v5 task pod, and the v5 query asks for different names. Remove the v3 scrape job and map the labels in the table above instead. Upgrading lists this step for a v3 upgrade.