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&#160;taskinstances</text><text
 class="terminal-944627566-r2" x="378.2" y="20" textLength="24.4" 
clip-path="url(#terminal-944627566-line-0)">&#160;[</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&#160;TaskInstances&#160;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&#160;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&#160;a&#160;task&#160;instance&#160;for&#160;a&#160;given&#160;Dag&#160;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&#160;unmet&#160;scheduler&#160;dependencies&#160;for&#160;a&#160;task&#160;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&#160;all&#160;task&#160;instances&#160;for&#160;a&#160;given&#160;Dag&#160;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&#160;the&#160;state&#160;store&#160;of&#160;a&#160;task&#160;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)">,&#160;</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&#160;taskinstances</text><text
 class="terminal-57093932-r2" x="378.2" y="20" textLength="24.4" 
clip-path="url(#terminal-57093932-line-0)">&#160;[</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&#160;TaskInstances&#160;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&#160;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&#160;a&#160;task&#160;instance&#160;for&#160;a&#160;given&#160;Dag&#160;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&#160;unmet&#160;scheduler&#160;dependencies&#160;for&#160;a&#160;task&#160;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&#160;all&#160;task&#160;instances&#160;for&#160;a&#160;given&#160;Dag&#160;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&#160;the&#160;logs&#160;of&#160;a&#160;task&#160;instance&#160;for&#160;a&#160;given&#160;try&#160;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&#160;the&#160;state&#160;store&#160;of&#160;a&#160;task&#160;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)">,&#160;</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&#160;tasks</text><text 
class="terminal-4044056635-r2" x="280.6" y="20" textLength="24.4" 
clip-path="url(#terminal-4044056635-line-0)">&#160;[</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&#160;Tasks&#160;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&#160;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&#160;task&#160;instances&#160;of&#160;a&#160;Dag&#160;by&#160;its&#160;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&#160;the&#160;unmet&#160;dependencies&#160;for&#160;a&#160;task&#160;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&#160;a&#160;task&#160;for&#160;a&#160;Dag&#160;by&#160;its&#160;ID&#160;and&#160;task&#160;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&#160;tasks&#160;for&#160;a&#160;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&#160;the&#160;state&#160;of&#160;a&#160;task&#160;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&#160;the&#160;status&#160;of&#160;all&#160;task&#160;instances&#160;in&#160;a&#160;Dag&#160;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)">,&#160;</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&#160;tasks</text><text 
class="terminal-986165217-r2" x="280.6" y="20" textLength="24.4" 
clip-path="url(#terminal-986165217-line-0)">&#160;[</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&#160;Tasks&#160;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&#160;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&#160;task&#160;instances&#160;of&#160;a&#160;Dag&#160;by&#160;its&#160;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&#160;the&#160;unmet&#160;dependencies&#160;for&#160;a&#160;task&#160;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&#160;a&#160;task&#160;for&#160;a&#160;Dag&#160;by&#160;its&#160;ID&#160;and&#160;task&#160;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&#160;tasks&#160;for&#160;a&#160;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&#160;the&#160;logs&#160;of&#160;a&#160;task&#160;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&#160;the&#160;state&#160;of&#160;a&#160;task&#160;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&#160;the&#160;status&#160;of&#160;all&#160;task&#160;instances&#160;in&#160;a&#160;Dag&#160;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)">,&#160;</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,
+            )

Reply via email to