This is an automated email from the ASF dual-hosted git repository.
potiuk pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/airflow.git
The following commit(s) were added to refs/heads/main by this push:
new c82f0ee58a3 Add airflowctl tasks logs command for retrieving task
instance logs (#71763)
c82f0ee58a3 is described below
commit c82f0ee58a396e6b6ac595a8b5c082cd56166784
Author: BobSong <[email protected]>
AuthorDate: Sun Nov 1 22:12:00 2026 +0800
Add airflowctl tasks logs command for retrieving task instance logs (#71763)
* Add airflowctl tasks logs command
Without direct DB or webserver access, operators can list failed task
instances, check their state and clear them for retry, but retrieving the logs
needed to diagnose the failure still forced a fallback to the webserver UI or a
hand-rolled REST client. This closes that gap by fetching task instance logs
for a given try through the existing
/dags/{dag_id}/dagRuns/{dag_run_id}/taskInstances/{task_id}/logs/{try_number}
endpoint, defaulting to the latest attempt.
* Make --try-number required for airflowctl tasks logs command
The /logs/{try_number} endpoint types the path parameter as
NonNegativeInt and has no server-side latest-attempt fallback, so
the client-side -1 default cannot work. Require --try-number
explicitly, per review feedback, and update the tests that relied
on the default.
* Fetch the full log in airflowctl tasks logs
Without full_content the endpoint returns one chunk and a continuation
token, so a running or remotely stored log printed only partially. Also give
the auto-generated taskinstances logs command a help text and an integration
test, as main now requires for every operations method.
Generated-by: Claude Opus 5
* Regenerate airflowctl help images for the tasks logs commands
Generated-by: Claude Opus 5
---------
Co-authored-by: Jarek Potiuk <[email protected]>
---
.../airflowctl_tests/test_airflowctl_commands.py | 2 +
airflow-ctl/docs/images/command_hashes.txt | 4 +-
airflow-ctl/docs/images/output_taskinstances.svg | 86 ++++++++-------
airflow-ctl/docs/images/output_tasks.svg | 94 ++++++++--------
airflow-ctl/src/airflowctl/api/operations.py | 27 +++++
airflow-ctl/src/airflowctl/ctl/cli_config.py | 27 +++++
.../src/airflowctl/ctl/commands/task_command.py | 26 ++++-
airflow-ctl/src/airflowctl/ctl/help_texts.yaml | 1 +
.../tests/airflow_ctl/api/test_operations.py | 22 ++++
.../airflow_ctl/ctl/commands/test_task_command.py | 121 +++++++++++++++++++++
10 files changed, 321 insertions(+), 89 deletions(-)
diff --git
a/airflow-ctl-tests/tests/airflowctl_tests/test_airflowctl_commands.py
b/airflow-ctl-tests/tests/airflowctl_tests/test_airflowctl_commands.py
index 6ac690fc9d5..fef33b01081 100644
--- a/airflow-ctl-tests/tests/airflowctl_tests/test_airflowctl_commands.py
+++ b/airflow-ctl-tests/tests/airflowctl_tests/test_airflowctl_commands.py
@@ -113,10 +113,12 @@ TEST_COMMANDS = [
'tasks states-for-dag-run example_bash_operator --logical-date
"{date_param}"',
'tasks state example_bash_operator runme_0 "manual__{date_param}"',
'tasks state example_bash_operator runme_0 --logical-date "{date_param}"',
+ 'tasks logs example_bash_operator "manual__{date_param}" runme_0
--try-number 1',
'tasks clear example_bash_operator --dag-run-id "manual__{date_param}"
--task-ids runme_0 -o json',
# Task Instances commands
'taskinstances get example_bash_operator "manual__{date_param}" runme_0',
'taskinstances get-dependencies example_bash_operator
"manual__{date_param}" runme_0',
+ 'taskinstances logs example_bash_operator "manual__{date_param}" runme_0
1',
'taskinstances list example_bash_operator "manual__{date_param}"',
'taskinstances state-store set example_bash_operator
"manual__{date_param}" runme_0 test_key test_value',
'taskinstances state-store get example_bash_operator
"manual__{date_param}" runme_0 test_key',
diff --git a/airflow-ctl/docs/images/command_hashes.txt
b/airflow-ctl/docs/images/command_hashes.txt
index 7b30c1068bf..6b663dd3048 100644
--- a/airflow-ctl/docs/images/command_hashes.txt
+++ b/airflow-ctl/docs/images/command_hashes.txt
@@ -9,8 +9,8 @@ dagrun:07035226eaaff0a557d3ad9aab4b41b5
jobs:d4af478f28dae48ee18d43b998edc345
pools:19efe105b9515ab1926ebcaf0e028d71
providers:826e68c9de8254293ba810a5c67fa8c7
-taskinstances:af95eda24a2bc4c6bd87c9f1324dbc3b
-tasks:56fa16e067fea389a11727b21985ec2f
+taskinstances:ef73172ce155dcad1f4ee38c1050db8f
+tasks:79059c3c8de9114e73895d4b8e86ed00
variables:f8fc76d3d398b2780f4e97f7cd816646
version:31f4efdf8de0dbaaa4fac71ff7efecc3
plugins:4864fd8f356704bd2b3cd1aec3567e35
diff --git a/airflow-ctl/docs/images/output_taskinstances.svg
b/airflow-ctl/docs/images/output_taskinstances.svg
index 7c02563d8f2..f3d6be8f3e7 100644
--- a/airflow-ctl/docs/images/output_taskinstances.svg
+++ b/airflow-ctl/docs/images/output_taskinstances.svg
@@ -1,4 +1,4 @@
-<svg class="rich-terminal" viewBox="0 0 933 416.0"
xmlns="http://www.w3.org/2000/svg">
+<svg class="rich-terminal" viewBox="0 0 933 440.4"
xmlns="http://www.w3.org/2000/svg">
<!-- Generated with Rich https://www.textualize.io -->
<style>
@@ -19,98 +19,102 @@
font-weight: 700;
}
- .terminal-944627566-matrix {
+ .terminal-57093932-matrix {
font-family: Fira Code, monospace;
font-size: 20px;
line-height: 24.4px;
font-variant-east-asian: full-width;
}
- .terminal-944627566-title {
+ .terminal-57093932-title {
font-size: 18px;
font-weight: bold;
font-family: arial;
}
- .terminal-944627566-r1 { fill: #ff8700 }
-.terminal-944627566-r2 { fill: #c5c8c6 }
-.terminal-944627566-r3 { fill: #808080 }
-.terminal-944627566-r4 { fill: #68a0b3 }
+ .terminal-57093932-r1 { fill: #ff8700 }
+.terminal-57093932-r2 { fill: #c5c8c6 }
+.terminal-57093932-r3 { fill: #808080 }
+.terminal-57093932-r4 { fill: #68a0b3 }
</style>
<defs>
- <clipPath id="terminal-944627566-clip-terminal">
- <rect x="0" y="0" width="914.0" height="365.0" />
+ <clipPath id="terminal-57093932-clip-terminal">
+ <rect x="0" y="0" width="914.0" height="389.4" />
</clipPath>
- <clipPath id="terminal-944627566-line-0">
+ <clipPath id="terminal-57093932-line-0">
<rect x="0" y="1.5" width="915" height="24.65"/>
</clipPath>
-<clipPath id="terminal-944627566-line-1">
+<clipPath id="terminal-57093932-line-1">
<rect x="0" y="25.9" width="915" height="24.65"/>
</clipPath>
-<clipPath id="terminal-944627566-line-2">
+<clipPath id="terminal-57093932-line-2">
<rect x="0" y="50.3" width="915" height="24.65"/>
</clipPath>
-<clipPath id="terminal-944627566-line-3">
+<clipPath id="terminal-57093932-line-3">
<rect x="0" y="74.7" width="915" height="24.65"/>
</clipPath>
-<clipPath id="terminal-944627566-line-4">
+<clipPath id="terminal-57093932-line-4">
<rect x="0" y="99.1" width="915" height="24.65"/>
</clipPath>
-<clipPath id="terminal-944627566-line-5">
+<clipPath id="terminal-57093932-line-5">
<rect x="0" y="123.5" width="915" height="24.65"/>
</clipPath>
-<clipPath id="terminal-944627566-line-6">
+<clipPath id="terminal-57093932-line-6">
<rect x="0" y="147.9" width="915" height="24.65"/>
</clipPath>
-<clipPath id="terminal-944627566-line-7">
+<clipPath id="terminal-57093932-line-7">
<rect x="0" y="172.3" width="915" height="24.65"/>
</clipPath>
-<clipPath id="terminal-944627566-line-8">
+<clipPath id="terminal-57093932-line-8">
<rect x="0" y="196.7" width="915" height="24.65"/>
</clipPath>
-<clipPath id="terminal-944627566-line-9">
+<clipPath id="terminal-57093932-line-9">
<rect x="0" y="221.1" width="915" height="24.65"/>
</clipPath>
-<clipPath id="terminal-944627566-line-10">
+<clipPath id="terminal-57093932-line-10">
<rect x="0" y="245.5" width="915" height="24.65"/>
</clipPath>
-<clipPath id="terminal-944627566-line-11">
+<clipPath id="terminal-57093932-line-11">
<rect x="0" y="269.9" width="915" height="24.65"/>
</clipPath>
-<clipPath id="terminal-944627566-line-12">
+<clipPath id="terminal-57093932-line-12">
<rect x="0" y="294.3" width="915" height="24.65"/>
</clipPath>
-<clipPath id="terminal-944627566-line-13">
+<clipPath id="terminal-57093932-line-13">
<rect x="0" y="318.7" width="915" height="24.65"/>
</clipPath>
+<clipPath id="terminal-57093932-line-14">
+ <rect x="0" y="343.1" width="915" height="24.65"/>
+ </clipPath>
</defs>
- <rect fill="#292929" stroke="rgba(255,255,255,0.35)" stroke-width="1"
x="1" y="1" width="931" height="414" rx="8"/>
+ <rect fill="#292929" stroke="rgba(255,255,255,0.35)" stroke-width="1"
x="1" y="1" width="931" height="438.4" rx="8"/>
<g transform="translate(26,22)">
<circle cx="0" cy="0" r="7" fill="#ff5f57"/>
<circle cx="22" cy="0" r="7" fill="#febc2e"/>
<circle cx="44" cy="0" r="7" fill="#28c840"/>
</g>
- <g transform="translate(9, 41)"
clip-path="url(#terminal-944627566-clip-terminal)">
+ <g transform="translate(9, 41)"
clip-path="url(#terminal-57093932-clip-terminal)">
- <g class="terminal-944627566-matrix">
- <text class="terminal-944627566-r1" x="0" y="20" textLength="73.2"
clip-path="url(#terminal-944627566-line-0)">Usage:</text><text
class="terminal-944627566-r3" x="85.4" y="20" textLength="292.8"
clip-path="url(#terminal-944627566-line-0)">airflowctl taskinstances</text><text
class="terminal-944627566-r2" x="378.2" y="20" textLength="24.4"
clip-path="url(#terminal-944627566-line-0)"> [</text><text
class="terminal-944627566-r4" x="402.6" y="20" textLength="24.4" clip-path="ur
[...]
-</text><text class="terminal-944627566-r2" x="915" y="44.4" textLength="12.2"
clip-path="url(#terminal-944627566-line-1)">
-</text><text class="terminal-944627566-r2" x="0" y="68.8" textLength="390.4"
clip-path="url(#terminal-944627566-line-2)">Perform TaskInstances operations</text><text
class="terminal-944627566-r2" x="915" y="68.8" textLength="12.2"
clip-path="url(#terminal-944627566-line-2)">
-</text><text class="terminal-944627566-r2" x="915" y="93.2" textLength="12.2"
clip-path="url(#terminal-944627566-line-3)">
-</text><text class="terminal-944627566-r1" x="0" y="117.6" textLength="256.2"
clip-path="url(#terminal-944627566-line-4)">Positional Arguments:</text><text
class="terminal-944627566-r2" x="915" y="117.6" textLength="12.2"
clip-path="url(#terminal-944627566-line-4)">
-</text><text class="terminal-944627566-r4" x="24.4" y="142" textLength="85.4"
clip-path="url(#terminal-944627566-line-5)">COMMAND</text><text
class="terminal-944627566-r2" x="915" y="142" textLength="12.2"
clip-path="url(#terminal-944627566-line-5)">
-</text><text class="terminal-944627566-r4" x="48.8" y="166.4"
textLength="36.6" clip-path="url(#terminal-944627566-line-6)">get</text><text
class="terminal-944627566-r2" x="244" y="166.4" textLength="475.8"
clip-path="url(#terminal-944627566-line-6)">Get a task instance for a given Dag run</text><text
class="terminal-944627566-r2" x="915" y="166.4" textLength="12.2"
clip-path="url(#terminal-944627566-line-6)">
-</text><text class="terminal-944627566-r4" x="48.8" y="190.8"
textLength="195.2"
clip-path="url(#terminal-944627566-line-7)">get-dependencies</text><text
class="terminal-944627566-r2" x="915" y="190.8" textLength="12.2"
clip-path="url(#terminal-944627566-line-7)">
-</text><text class="terminal-944627566-r2" x="244" y="215.2"
textLength="634.4"
clip-path="url(#terminal-944627566-line-8)">Get unmet scheduler dependencies for a task instance</text><text
class="terminal-944627566-r2" x="915" y="215.2" textLength="12.2"
clip-path="url(#terminal-944627566-line-8)">
-</text><text class="terminal-944627566-r4" x="48.8" y="239.6"
textLength="48.8" clip-path="url(#terminal-944627566-line-9)">list</text><text
class="terminal-944627566-r2" x="244" y="239.6" textLength="524.6"
clip-path="url(#terminal-944627566-line-9)">List all task instances for a given Dag run</text><text
class="terminal-944627566-r2" x="915" y="239.6" textLength="12.2"
clip-path="url(#terminal-944627566-line-9)">
-</text><text class="terminal-944627566-r4" x="48.8" y="264" textLength="134.2"
clip-path="url(#terminal-944627566-line-10)">state-store</text><text
class="terminal-944627566-r2" x="244" y="264" textLength="500.2"
clip-path="url(#terminal-944627566-line-10)">Manage the state store of a task instance</text><text
class="terminal-944627566-r2" x="915" y="264" textLength="12.2"
clip-path="url(#terminal-944627566-line-10)">
-</text><text class="terminal-944627566-r2" x="915" y="288.4" textLength="12.2"
clip-path="url(#terminal-944627566-line-11)">
-</text><text class="terminal-944627566-r1" x="0" y="312.8" textLength="97.6"
clip-path="url(#terminal-944627566-line-12)">Options:</text><text
class="terminal-944627566-r2" x="915" y="312.8" textLength="12.2"
clip-path="url(#terminal-944627566-line-12)">
-</text><text class="terminal-944627566-r4" x="24.4" y="337.2"
textLength="24.4" clip-path="url(#terminal-944627566-line-13)">-h</text><text
class="terminal-944627566-r2" x="48.8" y="337.2" textLength="24.4"
clip-path="url(#terminal-944627566-line-13)">, </text><text
class="terminal-944627566-r4" x="73.2" y="337.2" textLength="73.2"
clip-path="url(#terminal-944627566-line-13)">--help</text><text
class="terminal-944627566-r2" x="244" y="337.2" textLength="378.2"
clip-path="url(#termin [...]
-</text><text class="terminal-944627566-r2" x="915" y="361.6" textLength="12.2"
clip-path="url(#terminal-944627566-line-14)">
+ <g class="terminal-57093932-matrix">
+ <text class="terminal-57093932-r1" x="0" y="20" textLength="73.2"
clip-path="url(#terminal-57093932-line-0)">Usage:</text><text
class="terminal-57093932-r3" x="85.4" y="20" textLength="292.8"
clip-path="url(#terminal-57093932-line-0)">airflowctl taskinstances</text><text
class="terminal-57093932-r2" x="378.2" y="20" textLength="24.4"
clip-path="url(#terminal-57093932-line-0)"> [</text><text
class="terminal-57093932-r4" x="402.6" y="20" textLength="24.4"
clip-path="url(#term [...]
+</text><text class="terminal-57093932-r2" x="915" y="44.4" textLength="12.2"
clip-path="url(#terminal-57093932-line-1)">
+</text><text class="terminal-57093932-r2" x="0" y="68.8" textLength="390.4"
clip-path="url(#terminal-57093932-line-2)">Perform TaskInstances operations</text><text
class="terminal-57093932-r2" x="915" y="68.8" textLength="12.2"
clip-path="url(#terminal-57093932-line-2)">
+</text><text class="terminal-57093932-r2" x="915" y="93.2" textLength="12.2"
clip-path="url(#terminal-57093932-line-3)">
+</text><text class="terminal-57093932-r1" x="0" y="117.6" textLength="256.2"
clip-path="url(#terminal-57093932-line-4)">Positional Arguments:</text><text
class="terminal-57093932-r2" x="915" y="117.6" textLength="12.2"
clip-path="url(#terminal-57093932-line-4)">
+</text><text class="terminal-57093932-r4" x="24.4" y="142" textLength="85.4"
clip-path="url(#terminal-57093932-line-5)">COMMAND</text><text
class="terminal-57093932-r2" x="915" y="142" textLength="12.2"
clip-path="url(#terminal-57093932-line-5)">
+</text><text class="terminal-57093932-r4" x="48.8" y="166.4" textLength="36.6"
clip-path="url(#terminal-57093932-line-6)">get</text><text
class="terminal-57093932-r2" x="244" y="166.4" textLength="475.8"
clip-path="url(#terminal-57093932-line-6)">Get a task instance for a given Dag run</text><text
class="terminal-57093932-r2" x="915" y="166.4" textLength="12.2"
clip-path="url(#terminal-57093932-line-6)">
+</text><text class="terminal-57093932-r4" x="48.8" y="190.8"
textLength="195.2"
clip-path="url(#terminal-57093932-line-7)">get-dependencies</text><text
class="terminal-57093932-r2" x="915" y="190.8" textLength="12.2"
clip-path="url(#terminal-57093932-line-7)">
+</text><text class="terminal-57093932-r2" x="244" y="215.2" textLength="634.4"
clip-path="url(#terminal-57093932-line-8)">Get unmet scheduler dependencies for a task instance</text><text
class="terminal-57093932-r2" x="915" y="215.2" textLength="12.2"
clip-path="url(#terminal-57093932-line-8)">
+</text><text class="terminal-57093932-r4" x="48.8" y="239.6" textLength="48.8"
clip-path="url(#terminal-57093932-line-9)">list</text><text
class="terminal-57093932-r2" x="244" y="239.6" textLength="524.6"
clip-path="url(#terminal-57093932-line-9)">List all task instances for a given Dag run</text><text
class="terminal-57093932-r2" x="915" y="239.6" textLength="12.2"
clip-path="url(#terminal-57093932-line-9)">
+</text><text class="terminal-57093932-r4" x="48.8" y="264" textLength="48.8"
clip-path="url(#terminal-57093932-line-10)">logs</text><text
class="terminal-57093932-r2" x="244" y="264" textLength="658.8"
clip-path="url(#terminal-57093932-line-10)">Get the logs of a task instance for a given try number</text><text
class="terminal-57093932-r2" x="915" y="264" textLength="12.2"
clip-path="url(#terminal-57093932-line-10)">
+</text><text class="terminal-57093932-r4" x="48.8" y="288.4"
textLength="134.2"
clip-path="url(#terminal-57093932-line-11)">state-store</text><text
class="terminal-57093932-r2" x="244" y="288.4" textLength="500.2"
clip-path="url(#terminal-57093932-line-11)">Manage the state store of a task instance</text><text
class="terminal-57093932-r2" x="915" y="288.4" textLength="12.2"
clip-path="url(#terminal-57093932-line-11)">
+</text><text class="terminal-57093932-r2" x="915" y="312.8" textLength="12.2"
clip-path="url(#terminal-57093932-line-12)">
+</text><text class="terminal-57093932-r1" x="0" y="337.2" textLength="97.6"
clip-path="url(#terminal-57093932-line-13)">Options:</text><text
class="terminal-57093932-r2" x="915" y="337.2" textLength="12.2"
clip-path="url(#terminal-57093932-line-13)">
+</text><text class="terminal-57093932-r4" x="24.4" y="361.6" textLength="24.4"
clip-path="url(#terminal-57093932-line-14)">-h</text><text
class="terminal-57093932-r2" x="48.8" y="361.6" textLength="24.4"
clip-path="url(#terminal-57093932-line-14)">, </text><text
class="terminal-57093932-r4" x="73.2" y="361.6" textLength="73.2"
clip-path="url(#terminal-57093932-line-14)">--help</text><text
class="terminal-57093932-r2" x="244" y="361.6" textLength="378.2"
clip-path="url(#terminal-5709 [...]
+</text><text class="terminal-57093932-r2" x="915" y="386" textLength="12.2"
clip-path="url(#terminal-57093932-line-15)">
</text>
</g>
</g>
diff --git a/airflow-ctl/docs/images/output_tasks.svg
b/airflow-ctl/docs/images/output_tasks.svg
index b1b5bf1014b..5af11113e74 100644
--- a/airflow-ctl/docs/images/output_tasks.svg
+++ b/airflow-ctl/docs/images/output_tasks.svg
@@ -1,4 +1,4 @@
-<svg class="rich-terminal" viewBox="0 0 933 464.79999999999995"
xmlns="http://www.w3.org/2000/svg">
+<svg class="rich-terminal" viewBox="0 0 933 489.2"
xmlns="http://www.w3.org/2000/svg">
<!-- Generated with Rich https://www.textualize.io -->
<style>
@@ -19,106 +19,110 @@
font-weight: 700;
}
- .terminal-4044056635-matrix {
+ .terminal-986165217-matrix {
font-family: Fira Code, monospace;
font-size: 20px;
line-height: 24.4px;
font-variant-east-asian: full-width;
}
- .terminal-4044056635-title {
+ .terminal-986165217-title {
font-size: 18px;
font-weight: bold;
font-family: arial;
}
- .terminal-4044056635-r1 { fill: #ff8700 }
-.terminal-4044056635-r2 { fill: #c5c8c6 }
-.terminal-4044056635-r3 { fill: #808080 }
-.terminal-4044056635-r4 { fill: #68a0b3 }
+ .terminal-986165217-r1 { fill: #ff8700 }
+.terminal-986165217-r2 { fill: #c5c8c6 }
+.terminal-986165217-r3 { fill: #808080 }
+.terminal-986165217-r4 { fill: #68a0b3 }
</style>
<defs>
- <clipPath id="terminal-4044056635-clip-terminal">
- <rect x="0" y="0" width="914.0" height="413.79999999999995" />
+ <clipPath id="terminal-986165217-clip-terminal">
+ <rect x="0" y="0" width="914.0" height="438.2" />
</clipPath>
- <clipPath id="terminal-4044056635-line-0">
+ <clipPath id="terminal-986165217-line-0">
<rect x="0" y="1.5" width="915" height="24.65"/>
</clipPath>
-<clipPath id="terminal-4044056635-line-1">
+<clipPath id="terminal-986165217-line-1">
<rect x="0" y="25.9" width="915" height="24.65"/>
</clipPath>
-<clipPath id="terminal-4044056635-line-2">
+<clipPath id="terminal-986165217-line-2">
<rect x="0" y="50.3" width="915" height="24.65"/>
</clipPath>
-<clipPath id="terminal-4044056635-line-3">
+<clipPath id="terminal-986165217-line-3">
<rect x="0" y="74.7" width="915" height="24.65"/>
</clipPath>
-<clipPath id="terminal-4044056635-line-4">
+<clipPath id="terminal-986165217-line-4">
<rect x="0" y="99.1" width="915" height="24.65"/>
</clipPath>
-<clipPath id="terminal-4044056635-line-5">
+<clipPath id="terminal-986165217-line-5">
<rect x="0" y="123.5" width="915" height="24.65"/>
</clipPath>
-<clipPath id="terminal-4044056635-line-6">
+<clipPath id="terminal-986165217-line-6">
<rect x="0" y="147.9" width="915" height="24.65"/>
</clipPath>
-<clipPath id="terminal-4044056635-line-7">
+<clipPath id="terminal-986165217-line-7">
<rect x="0" y="172.3" width="915" height="24.65"/>
</clipPath>
-<clipPath id="terminal-4044056635-line-8">
+<clipPath id="terminal-986165217-line-8">
<rect x="0" y="196.7" width="915" height="24.65"/>
</clipPath>
-<clipPath id="terminal-4044056635-line-9">
+<clipPath id="terminal-986165217-line-9">
<rect x="0" y="221.1" width="915" height="24.65"/>
</clipPath>
-<clipPath id="terminal-4044056635-line-10">
+<clipPath id="terminal-986165217-line-10">
<rect x="0" y="245.5" width="915" height="24.65"/>
</clipPath>
-<clipPath id="terminal-4044056635-line-11">
+<clipPath id="terminal-986165217-line-11">
<rect x="0" y="269.9" width="915" height="24.65"/>
</clipPath>
-<clipPath id="terminal-4044056635-line-12">
+<clipPath id="terminal-986165217-line-12">
<rect x="0" y="294.3" width="915" height="24.65"/>
</clipPath>
-<clipPath id="terminal-4044056635-line-13">
+<clipPath id="terminal-986165217-line-13">
<rect x="0" y="318.7" width="915" height="24.65"/>
</clipPath>
-<clipPath id="terminal-4044056635-line-14">
+<clipPath id="terminal-986165217-line-14">
<rect x="0" y="343.1" width="915" height="24.65"/>
</clipPath>
-<clipPath id="terminal-4044056635-line-15">
+<clipPath id="terminal-986165217-line-15">
<rect x="0" y="367.5" width="915" height="24.65"/>
</clipPath>
+<clipPath id="terminal-986165217-line-16">
+ <rect x="0" y="391.9" width="915" height="24.65"/>
+ </clipPath>
</defs>
- <rect fill="#292929" stroke="rgba(255,255,255,0.35)" stroke-width="1"
x="1" y="1" width="931" height="462.8" rx="8"/>
+ <rect fill="#292929" stroke="rgba(255,255,255,0.35)" stroke-width="1"
x="1" y="1" width="931" height="487.2" rx="8"/>
<g transform="translate(26,22)">
<circle cx="0" cy="0" r="7" fill="#ff5f57"/>
<circle cx="22" cy="0" r="7" fill="#febc2e"/>
<circle cx="44" cy="0" r="7" fill="#28c840"/>
</g>
- <g transform="translate(9, 41)"
clip-path="url(#terminal-4044056635-clip-terminal)">
+ <g transform="translate(9, 41)"
clip-path="url(#terminal-986165217-clip-terminal)">
- <g class="terminal-4044056635-matrix">
- <text class="terminal-4044056635-r1" x="0" y="20" textLength="73.2"
clip-path="url(#terminal-4044056635-line-0)">Usage:</text><text
class="terminal-4044056635-r3" x="85.4" y="20" textLength="195.2"
clip-path="url(#terminal-4044056635-line-0)">airflowctl tasks</text><text
class="terminal-4044056635-r2" x="280.6" y="20" textLength="24.4"
clip-path="url(#terminal-4044056635-line-0)"> [</text><text
class="terminal-4044056635-r4" x="305" y="20" textLength="24.4"
clip-path="url(# [...]
-</text><text class="terminal-4044056635-r2" x="915" y="44.4" textLength="12.2"
clip-path="url(#terminal-4044056635-line-1)">
-</text><text class="terminal-4044056635-r2" x="0" y="68.8" textLength="292.8"
clip-path="url(#terminal-4044056635-line-2)">Perform Tasks operations</text><text
class="terminal-4044056635-r2" x="915" y="68.8" textLength="12.2"
clip-path="url(#terminal-4044056635-line-2)">
-</text><text class="terminal-4044056635-r2" x="915" y="93.2" textLength="12.2"
clip-path="url(#terminal-4044056635-line-3)">
-</text><text class="terminal-4044056635-r1" x="0" y="117.6" textLength="256.2"
clip-path="url(#terminal-4044056635-line-4)">Positional Arguments:</text><text
class="terminal-4044056635-r2" x="915" y="117.6" textLength="12.2"
clip-path="url(#terminal-4044056635-line-4)">
-</text><text class="terminal-4044056635-r4" x="24.4" y="142" textLength="85.4"
clip-path="url(#terminal-4044056635-line-5)">COMMAND</text><text
class="terminal-4044056635-r2" x="915" y="142" textLength="12.2"
clip-path="url(#terminal-4044056635-line-5)">
-</text><text class="terminal-4044056635-r4" x="48.8" y="166.4" textLength="61"
clip-path="url(#terminal-4044056635-line-6)">clear</text><text
class="terminal-4044056635-r2" x="268.4" y="166.4" textLength="475.8"
clip-path="url(#terminal-4044056635-line-6)">Clear task instances of a Dag by its ID</text><text
class="terminal-4044056635-r2" x="915" y="166.4" textLength="12.2"
clip-path="url(#terminal-4044056635-line-6)">
-</text><text class="terminal-4044056635-r4" x="48.8" y="190.8"
textLength="134.2"
clip-path="url(#terminal-4044056635-line-7)">failed-deps</text><text
class="terminal-4044056635-r2" x="268.4" y="190.8" textLength="610"
clip-path="url(#terminal-4044056635-line-7)">Returns the unmet dependencies for a task instance</text><text
class="terminal-4044056635-r2" x="915" y="190.8" textLength="12.2"
clip-path="url(#terminal-4044056635-line-7)">
-</text><text class="terminal-4044056635-r4" x="48.8" y="215.2"
textLength="36.6" clip-path="url(#terminal-4044056635-line-8)">get</text><text
class="terminal-4044056635-r2" x="268.4" y="215.2" textLength="573.4"
clip-path="url(#terminal-4044056635-line-8)">Retrieve a task for a Dag by its ID and task ID</text><text
class="terminal-4044056635-r2" x="915" y="215.2" textLength="12.2"
clip-path="url(#terminal-4044056635-line-8)">
-</text><text class="terminal-4044056635-r4" x="48.8" y="239.6"
textLength="48.8" clip-path="url(#terminal-4044056635-line-9)">list</text><text
class="terminal-4044056635-r2" x="268.4" y="239.6" textLength="244"
clip-path="url(#terminal-4044056635-line-9)">List tasks for a Dag</text><text
class="terminal-4044056635-r2" x="915" y="239.6" textLength="12.2"
clip-path="url(#terminal-4044056635-line-9)">
-</text><text class="terminal-4044056635-r4" x="48.8" y="264" textLength="61"
clip-path="url(#terminal-4044056635-line-10)">state</text><text
class="terminal-4044056635-r2" x="268.4" y="264" textLength="390.4"
clip-path="url(#terminal-4044056635-line-10)">Get the state of a task instance</text><text
class="terminal-4044056635-r2" x="915" y="264" textLength="12.2"
clip-path="url(#terminal-4044056635-line-10)">
-</text><text class="terminal-4044056635-r4" x="48.8" y="288.4"
textLength="219.6"
clip-path="url(#terminal-4044056635-line-11)">states-for-dag-run</text><text
class="terminal-4044056635-r2" x="915" y="288.4" textLength="12.2"
clip-path="url(#terminal-4044056635-line-11)">
-</text><text class="terminal-4044056635-r2" x="268.4" y="312.8"
textLength="597.8"
clip-path="url(#terminal-4044056635-line-12)">Get the status of all task instances in a Dag run</text><text
class="terminal-4044056635-r2" x="915" y="312.8" textLength="12.2"
clip-path="url(#terminal-4044056635-line-12)">
-</text><text class="terminal-4044056635-r2" x="915" y="337.2"
textLength="12.2" clip-path="url(#terminal-4044056635-line-13)">
-</text><text class="terminal-4044056635-r1" x="0" y="361.6" textLength="97.6"
clip-path="url(#terminal-4044056635-line-14)">Options:</text><text
class="terminal-4044056635-r2" x="915" y="361.6" textLength="12.2"
clip-path="url(#terminal-4044056635-line-14)">
-</text><text class="terminal-4044056635-r4" x="24.4" y="386" textLength="24.4"
clip-path="url(#terminal-4044056635-line-15)">-h</text><text
class="terminal-4044056635-r2" x="48.8" y="386" textLength="24.4"
clip-path="url(#terminal-4044056635-line-15)">, </text><text
class="terminal-4044056635-r4" x="73.2" y="386" textLength="73.2"
clip-path="url(#terminal-4044056635-line-15)">--help</text><text
class="terminal-4044056635-r2" x="268.4" y="386" textLength="378.2"
clip-path="url(#termi [...]
-</text><text class="terminal-4044056635-r2" x="915" y="410.4"
textLength="12.2" clip-path="url(#terminal-4044056635-line-16)">
+ <g class="terminal-986165217-matrix">
+ <text class="terminal-986165217-r1" x="0" y="20" textLength="73.2"
clip-path="url(#terminal-986165217-line-0)">Usage:</text><text
class="terminal-986165217-r3" x="85.4" y="20" textLength="195.2"
clip-path="url(#terminal-986165217-line-0)">airflowctl tasks</text><text
class="terminal-986165217-r2" x="280.6" y="20" textLength="24.4"
clip-path="url(#terminal-986165217-line-0)"> [</text><text
class="terminal-986165217-r4" x="305" y="20" textLength="24.4"
clip-path="url(#termina [...]
+</text><text class="terminal-986165217-r2" x="915" y="44.4" textLength="12.2"
clip-path="url(#terminal-986165217-line-1)">
+</text><text class="terminal-986165217-r2" x="0" y="68.8" textLength="292.8"
clip-path="url(#terminal-986165217-line-2)">Perform Tasks operations</text><text
class="terminal-986165217-r2" x="915" y="68.8" textLength="12.2"
clip-path="url(#terminal-986165217-line-2)">
+</text><text class="terminal-986165217-r2" x="915" y="93.2" textLength="12.2"
clip-path="url(#terminal-986165217-line-3)">
+</text><text class="terminal-986165217-r1" x="0" y="117.6" textLength="256.2"
clip-path="url(#terminal-986165217-line-4)">Positional Arguments:</text><text
class="terminal-986165217-r2" x="915" y="117.6" textLength="12.2"
clip-path="url(#terminal-986165217-line-4)">
+</text><text class="terminal-986165217-r4" x="24.4" y="142" textLength="85.4"
clip-path="url(#terminal-986165217-line-5)">COMMAND</text><text
class="terminal-986165217-r2" x="915" y="142" textLength="12.2"
clip-path="url(#terminal-986165217-line-5)">
+</text><text class="terminal-986165217-r4" x="48.8" y="166.4" textLength="61"
clip-path="url(#terminal-986165217-line-6)">clear</text><text
class="terminal-986165217-r2" x="268.4" y="166.4" textLength="475.8"
clip-path="url(#terminal-986165217-line-6)">Clear task instances of a Dag by its ID</text><text
class="terminal-986165217-r2" x="915" y="166.4" textLength="12.2"
clip-path="url(#terminal-986165217-line-6)">
+</text><text class="terminal-986165217-r4" x="48.8" y="190.8"
textLength="134.2"
clip-path="url(#terminal-986165217-line-7)">failed-deps</text><text
class="terminal-986165217-r2" x="268.4" y="190.8" textLength="610"
clip-path="url(#terminal-986165217-line-7)">Returns the unmet dependencies for a task instance</text><text
class="terminal-986165217-r2" x="915" y="190.8" textLength="12.2"
clip-path="url(#terminal-986165217-line-7)">
+</text><text class="terminal-986165217-r4" x="48.8" y="215.2"
textLength="36.6" clip-path="url(#terminal-986165217-line-8)">get</text><text
class="terminal-986165217-r2" x="268.4" y="215.2" textLength="573.4"
clip-path="url(#terminal-986165217-line-8)">Retrieve a task for a Dag by its ID and task ID</text><text
class="terminal-986165217-r2" x="915" y="215.2" textLength="12.2"
clip-path="url(#terminal-986165217-line-8)">
+</text><text class="terminal-986165217-r4" x="48.8" y="239.6"
textLength="48.8" clip-path="url(#terminal-986165217-line-9)">list</text><text
class="terminal-986165217-r2" x="268.4" y="239.6" textLength="244"
clip-path="url(#terminal-986165217-line-9)">List tasks for a Dag</text><text
class="terminal-986165217-r2" x="915" y="239.6" textLength="12.2"
clip-path="url(#terminal-986165217-line-9)">
+</text><text class="terminal-986165217-r4" x="48.8" y="264" textLength="48.8"
clip-path="url(#terminal-986165217-line-10)">logs</text><text
class="terminal-986165217-r2" x="268.4" y="264" textLength="378.2"
clip-path="url(#terminal-986165217-line-10)">Get the logs of a task instance</text><text
class="terminal-986165217-r2" x="915" y="264" textLength="12.2"
clip-path="url(#terminal-986165217-line-10)">
+</text><text class="terminal-986165217-r4" x="48.8" y="288.4" textLength="61"
clip-path="url(#terminal-986165217-line-11)">state</text><text
class="terminal-986165217-r2" x="268.4" y="288.4" textLength="390.4"
clip-path="url(#terminal-986165217-line-11)">Get the state of a task instance</text><text
class="terminal-986165217-r2" x="915" y="288.4" textLength="12.2"
clip-path="url(#terminal-986165217-line-11)">
+</text><text class="terminal-986165217-r4" x="48.8" y="312.8"
textLength="219.6"
clip-path="url(#terminal-986165217-line-12)">states-for-dag-run</text><text
class="terminal-986165217-r2" x="915" y="312.8" textLength="12.2"
clip-path="url(#terminal-986165217-line-12)">
+</text><text class="terminal-986165217-r2" x="268.4" y="337.2"
textLength="597.8"
clip-path="url(#terminal-986165217-line-13)">Get the status of all task instances in a Dag run</text><text
class="terminal-986165217-r2" x="915" y="337.2" textLength="12.2"
clip-path="url(#terminal-986165217-line-13)">
+</text><text class="terminal-986165217-r2" x="915" y="361.6" textLength="12.2"
clip-path="url(#terminal-986165217-line-14)">
+</text><text class="terminal-986165217-r1" x="0" y="386" textLength="97.6"
clip-path="url(#terminal-986165217-line-15)">Options:</text><text
class="terminal-986165217-r2" x="915" y="386" textLength="12.2"
clip-path="url(#terminal-986165217-line-15)">
+</text><text class="terminal-986165217-r4" x="24.4" y="410.4"
textLength="24.4" clip-path="url(#terminal-986165217-line-16)">-h</text><text
class="terminal-986165217-r2" x="48.8" y="410.4" textLength="24.4"
clip-path="url(#terminal-986165217-line-16)">, </text><text
class="terminal-986165217-r4" x="73.2" y="410.4" textLength="73.2"
clip-path="url(#terminal-986165217-line-16)">--help</text><text
class="terminal-986165217-r2" x="268.4" y="410.4" textLength="378.2"
clip-path="url(#term [...]
+</text><text class="terminal-986165217-r2" x="915" y="434.8" textLength="12.2"
clip-path="url(#terminal-986165217-line-17)">
</text>
</g>
</g>
diff --git a/airflow-ctl/src/airflowctl/api/operations.py
b/airflow-ctl/src/airflowctl/api/operations.py
index ac934fe6f94..802f663abb4 100644
--- a/airflow-ctl/src/airflowctl/api/operations.py
+++ b/airflow-ctl/src/airflowctl/api/operations.py
@@ -83,6 +83,7 @@ from airflowctl.api.datamodels.generated import (
TaskDependencyCollectionResponse,
TaskInstanceCollectionResponse,
TaskInstanceResponse,
+ TaskInstancesLogResponse,
TaskResponse,
TaskStateStoreBody,
TaskStateStoreCollectionResponse,
@@ -879,6 +880,32 @@ class TaskInstancesOperations(BaseOperations):
)
return validate_response(self.response.content,
TaskDependencyCollectionResponse)
+ def logs(
+ self,
+ dag_id: str,
+ dag_run_id: str,
+ task_id: str,
+ try_number: int,
+ map_index: int | None = None,
+ *,
+ suppress_error_log: bool = False,
+ ) -> TaskInstancesLogResponse | ServerResponseError:
+ """Get the logs of a task instance for a given try number."""
+ path = _build_task_instance_path(
+ dag_id=dag_id, dag_run_id=dag_run_id, task_id=task_id,
map_index=None
+ )
+ # Without full_content the endpoint returns one chunk and a
continuation token, so a running or
+ # remotely stored log would print only partially.
+ params: dict[str, Any] = {"full_content": True}
+ if map_index is not None and map_index >= 0:
+ params["map_index"] = map_index
+ self.response = self.client.get(
+ f"{path}/logs/{try_number}",
+ params=params,
+ extensions={"airflowctl_suppress_error_log": suppress_error_log},
+ )
+ return
TaskInstancesLogResponse.model_validate_json(self.response.content)
+
def list(self, dag_id: str, dag_run_id: str) ->
TaskInstanceCollectionResponse | ServerResponseError:
"""List task instances for a Dag run."""
return super().execute_list(
diff --git a/airflow-ctl/src/airflowctl/ctl/cli_config.py
b/airflow-ctl/src/airflowctl/ctl/cli_config.py
index 25d5d6c870f..b7702e68d07 100755
--- a/airflow-ctl/src/airflowctl/ctl/cli_config.py
+++ b/airflow-ctl/src/airflowctl/ctl/cli_config.py
@@ -415,6 +415,20 @@ ARG_MAP_INDEX = Arg(
help="Mapped task index",
)
+# Task logs command args. Required primitive parameters stay positional per the
+# airflowctl parameter style consensus (#66768).
+ARG_TASKS_LOGS_DAG_RUN_ID = Arg(
+ flags=("dag_run_id",),
+ type=str,
+ help="The run ID of the Dag run",
+)
+ARG_TRY_NUMBER = Arg(
+ flags=("--try-number",),
+ type=int,
+ required=True,
+ help="The try number of the task instance logs to fetch",
+)
+
ARG_ACTION_ON_EXISTING_KEY = Arg(
flags=("-a", "--action-on-existing-key"),
type=str,
@@ -1430,6 +1444,19 @@ TASK_COMMANDS = (
ARG_OUTPUT,
),
),
+ ActionCommand(
+ name="logs",
+ help="Get the logs of a task instance",
+ description="Get the logs of a task instance for a given try number.",
+ func=lazy_load_command("airflowctl.ctl.commands.task_command.logs"),
+ args=(
+ ARG_DAG_ID,
+ ARG_TASKS_LOGS_DAG_RUN_ID,
+ ARG_TASK_ID,
+ ARG_MAP_INDEX,
+ ARG_TRY_NUMBER,
+ ),
+ ),
)
VARIABLE_COMMANDS = (
diff --git a/airflow-ctl/src/airflowctl/ctl/commands/task_command.py
b/airflow-ctl/src/airflowctl/ctl/commands/task_command.py
index f03a83e5103..dc756bd8056 100644
--- a/airflow-ctl/src/airflowctl/ctl/commands/task_command.py
+++ b/airflow-ctl/src/airflowctl/ctl/commands/task_command.py
@@ -23,7 +23,7 @@ from typing import TYPE_CHECKING
import rich
from airflowctl.api.client import NEW_API_CLIENT, ClientKind,
ServerResponseError, provide_api_client
-from airflowctl.api.datamodels.generated import TaskInstanceState
+from airflowctl.api.datamodels.generated import StructuredLogMessage,
TaskInstanceState
from airflowctl.ctl.console_formatting import AirflowConsole
from airflowctl.ctl.utils.dag_run import resolve_dag_run_id
@@ -158,3 +158,27 @@ def state(args, api_client=NEW_API_CLIENT) -> None:
# Unset states print as None to stay drop-in compatible with the deprecated
# `airflow tasks state`, which prints its nullable `ti.state` column
directly.
print(task_instance.state.value if task_instance.state else None)
+
+
+@provide_api_client(kind=ClientKind.CLI)
+def logs(args, api_client=NEW_API_CLIENT) -> None:
+ """Get the logs of a task instance for a given try."""
+ try:
+ response = api_client.task_instances.logs(
+ dag_id=args.dag_id,
+ dag_run_id=args.dag_run_id,
+ task_id=args.task_id,
+ try_number=args.try_number,
+ map_index=args.map_index,
+ suppress_error_log=True,
+ )
+ except ServerResponseError as e:
+ if e.response.status_code == 404:
+ rich.print(
+ f"[red]{_task_instance_not_found_message(args.dag_id,
args.dag_run_id, args.task_id, args.map_index, e)}[/red]"
+ )
+ sys.exit(1)
+ raise
+
+ for entry in response.content:
+ print(entry.event if isinstance(entry, StructuredLogMessage) else
entry)
diff --git a/airflow-ctl/src/airflowctl/ctl/help_texts.yaml
b/airflow-ctl/src/airflowctl/ctl/help_texts.yaml
index 26e6074f16d..ef0e0ddc7bb 100644
--- a/airflow-ctl/src/airflowctl/ctl/help_texts.yaml
+++ b/airflow-ctl/src/airflowctl/ctl/help_texts.yaml
@@ -96,6 +96,7 @@ taskinstances:
list: "List all task instances for a given Dag run"
get: "Get a task instance for a given Dag run"
get-dependencies: "Get unmet scheduler dependencies for a task instance"
+ logs: "Get the logs of a task instance for a given try number"
# state-store is the nested subcommand group. The *-state-store keys stay
because
# help is still looked up by the operation method name, not the CLI path.
state-store: "Manage the state store of a task instance"
diff --git a/airflow-ctl/tests/airflow_ctl/api/test_operations.py
b/airflow-ctl/tests/airflow_ctl/api/test_operations.py
index 400feaa9423..3642ed7473b 100644
--- a/airflow-ctl/tests/airflow_ctl/api/test_operations.py
+++ b/airflow-ctl/tests/airflow_ctl/api/test_operations.py
@@ -105,6 +105,7 @@ from airflowctl.api.datamodels.generated import (
TaskInletAssetReference,
TaskInstanceCollectionResponse,
TaskInstanceResponse,
+ TaskInstancesLogResponse,
TaskInstanceState,
TaskOutletAssetReference,
TaskResponse,
@@ -2450,6 +2451,27 @@ class TestTaskInstancesOperations:
)
assert response == self.task_dependency_collection_response
+ @pytest.mark.parametrize(
+ ("map_index", "expected_params"),
+ [
+ pytest.param(None, {"full_content": "true"}, id="unmapped"),
+ pytest.param(2, {"full_content": "true", "map_index": "2"},
id="mapped"),
+ ],
+ )
+ def test_logs_requests_full_content(self, map_index, expected_params):
+ log_response = TaskInstancesLogResponse(content=["line"],
continuation_token=None)
+
+ def handle_request(request: httpx.Request) -> httpx.Response:
+ assert request.url.path ==
"/api/v2/dags/dag_id/dagRuns/dag_run_id/taskInstances/task_id/logs/1"
+ assert dict(request.url.params) == expected_params
+ return httpx.Response(200,
json=json.loads(log_response.model_dump_json()))
+
+ client = make_api_client(transport=httpx.MockTransport(handle_request))
+ response = client.task_instances.logs(
+ dag_id="dag_id", dag_run_id="dag_run_id", task_id="task_id",
try_number=1, map_index=map_index
+ )
+ assert response == log_response
+
def test_get_dependencies_with_map_index(self):
client = self._make_client_asserting_path(
"/api/v2/dags/dag_id/dagRuns/dag_run_id/taskInstances/task_id/3/dependencies",
diff --git a/airflow-ctl/tests/airflow_ctl/ctl/commands/test_task_command.py
b/airflow-ctl/tests/airflow_ctl/ctl/commands/test_task_command.py
index d086f37abc5..c803a880221 100644
--- a/airflow-ctl/tests/airflow_ctl/ctl/commands/test_task_command.py
+++ b/airflow-ctl/tests/airflow_ctl/ctl/commands/test_task_command.py
@@ -24,10 +24,12 @@ import httpx
import pytest
from airflowctl.api.datamodels.generated import (
+ StructuredLogMessage,
TaskDependencyCollectionResponse,
TaskDependencyResponse,
TaskInstanceCollectionResponse,
TaskInstanceResponse,
+ TaskInstancesLogResponse,
TaskInstanceState,
)
from airflowctl.api.operations import ServerResponseError
@@ -909,3 +911,122 @@ class TestState:
self.parser.parse_args(["tasks", "state", self.dag_id,
self.task_id, self.run_id]),
api_client=api_client,
)
+
+
+class TestLogs:
+ parser = cli_parser.get_parser()
+ dag_id = "test_dag"
+ run_id = "test_run"
+ task_id = "test_task"
+
+ def _make_log_response(self, content: list[StructuredLogMessage] |
list[str]) -> TaskInstancesLogResponse:
+ return TaskInstancesLogResponse(content=content,
continuation_token=None)
+
+ def _make_structured_content(self) -> list[StructuredLogMessage]:
+ return [
+ StructuredLogMessage(
+ timestamp=datetime.datetime(2025, 1, 1, tzinfo=datetime.UTC),
+ event="INFO - hello",
+ ),
+ StructuredLogMessage(
+ timestamp=datetime.datetime(2025, 1, 1, tzinfo=datetime.UTC),
+ event="plain line",
+ ),
+ ]
+
+ def test_logs_with_try_number(self, capsys):
+ api_client = mock.MagicMock()
+ api_client.task_instances.logs.return_value =
self._make_log_response(self._make_structured_content())
+
+ task_command.logs(
+ self.parser.parse_args(
+ ["tasks", "logs", self.dag_id, self.run_id, self.task_id,
"--try-number", "1"]
+ ),
+ api_client=api_client,
+ )
+
+ api_client.task_instances.logs.assert_called_once_with(
+ dag_id=self.dag_id,
+ dag_run_id=self.run_id,
+ task_id=self.task_id,
+ try_number=1,
+ map_index=-1,
+ suppress_error_log=True,
+ )
+ assert capsys.readouterr().out == "INFO - hello\nplain line\n"
+
+ def test_logs_requires_try_number(self, capsys):
+ with pytest.raises(SystemExit, match="2"):
+ self.parser.parse_args(["tasks", "logs", self.dag_id, self.run_id,
self.task_id])
+ assert "--try-number" in capsys.readouterr().err
+
+ def test_logs_prints_plain_string_entries(self, capsys):
+ api_client = mock.MagicMock()
+ api_client.task_instances.logs.return_value =
self._make_log_response(["plain line"])
+
+ task_command.logs(
+ self.parser.parse_args(
+ ["tasks", "logs", self.dag_id, self.run_id, self.task_id,
"--try-number", "1"]
+ ),
+ api_client=api_client,
+ )
+
+ assert capsys.readouterr().out == "plain line\n"
+
+ def test_logs_with_explicit_try_number_and_map_index(self, capsys):
+ api_client = mock.MagicMock()
+ api_client.task_instances.logs.return_value =
self._make_log_response(self._make_structured_content())
+
+ task_command.logs(
+ self.parser.parse_args(
+ [
+ "tasks",
+ "logs",
+ self.dag_id,
+ self.run_id,
+ self.task_id,
+ "--try-number",
+ "2",
+ "--map-index",
+ "3",
+ ]
+ ),
+ api_client=api_client,
+ )
+
+ api_client.task_instances.logs.assert_called_once_with(
+ dag_id=self.dag_id,
+ dag_run_id=self.run_id,
+ task_id=self.task_id,
+ try_number=2,
+ map_index=3,
+ suppress_error_log=True,
+ )
+
+ def test_logs_task_instance_not_found(self, capsys):
+ api_client = mock.MagicMock()
+ api_client.task_instances.logs.side_effect = _make_server_error(404)
+
+ with pytest.raises(SystemExit, match="1"):
+ task_command.logs(
+ self.parser.parse_args(
+ ["tasks", "logs", self.dag_id, self.run_id, self.task_id,
"--try-number", "1"]
+ ),
+ api_client=api_client,
+ )
+
+ assert _normalize_rich_output(capsys.readouterr().out) == (
+ "Task instance for task 'test_task' in Dag run 'test_run' of Dag
'test_dag' not found"
+ )
+
+ def test_logs_reraises_non_404_error(self):
+ api_client = mock.MagicMock()
+ api_client.task_instances.logs.side_effect = _make_server_error(500)
+
+ with pytest.raises(ServerResponseError):
+ task_command.logs(
+ self.parser.parse_args(
+ ["tasks", "logs", self.dag_id, self.run_id, self.task_id,
"--try-number", "1"]
+ ),
+ api_client=api_client,
+ )