This is an automated email from the ASF dual-hosted git repository.

dabla 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 71a19df58c5 Write container logs to the task log in deferrable 
AzureContainerInstancesOperator (#76330)
71a19df58c5 is described below

commit 71a19df58c50f12d5ad937a9c4a6c7f8373cc6b8
Author: Ankit Raj <[email protected]>
AuthorDate: Fri Nov 6 00:57:12 2026 +0530

    Write container logs to the task log in deferrable 
AzureContainerInstancesOperator (#76330)
    
    * Write container logs to the task log in deferrable 
AzureContainerInstancesOperator
    
    * Fail the task when logs needed for XCom cannot be read
---
 .../azure/operators/container_instances.py         |  26 +++-
 .../azure/operators/test_container_instances.py    | 143 +++++++++++++++++++++
 2 files changed, 163 insertions(+), 6 deletions(-)

diff --git 
a/providers/microsoft/azure/src/airflow/providers/microsoft/azure/operators/container_instances.py
 
b/providers/microsoft/azure/src/airflow/providers/microsoft/azure/operators/container_instances.py
index a196282502f..0b7df5bc821 100644
--- 
a/providers/microsoft/azure/src/airflow/providers/microsoft/azure/operators/container_instances.py
+++ 
b/providers/microsoft/azure/src/airflow/providers/microsoft/azure/operators/container_instances.py
@@ -467,6 +467,10 @@ class AzureContainerInstancesOperator(BaseOperator):
 
         exit_code: int = event.get("exit_code", 1)
 
+        # The trigger does not stream the container output, so write it to the 
task log here,
+        # before the container group may be removed.
+        logs = self._log_container_output()
+
         if event["status"] == "error":
             if self.remove_on_error:
                 self.on_kill()
@@ -479,13 +483,11 @@ class AzureContainerInstancesOperator(BaseOperator):
 
         try:
             if self.xcom_all is not None:
-                logs = self.hook.get_logs(self.resource_group, self.name)
                 if logs is None:
-                    context["ti"].xcom_push(key="logs", value=[])
-                elif self.xcom_all:
-                    context["ti"].xcom_push(key="logs", value=logs)
-                else:
-                    context["ti"].xcom_push(key="logs", value=logs[-1:])
+                    # The first fetch failed; fetch again so the error fails 
the task
+                    # instead of pushing empty logs to XCom.
+                    logs = self.hook.get_logs(self.resource_group, self.name)
+                context["ti"].xcom_push(key="logs", value=logs if 
self.xcom_all else logs[-1:])
 
             self.log.info("Container had exit code: %s", exit_code)
             return exit_code
@@ -563,6 +565,18 @@ class AzureContainerInstancesOperator(BaseOperator):
 
             time.sleep(1)
 
+    def _log_container_output(self) -> list | None:
+        """Write the container group's logs to the task log and return them."""
+        try:
+            logs = self.hook.get_logs(self.resource_group, self.name)
+        except Exception as e:
+            self.log.warning(
+                "Failed to fetch logs for container group %s/%s: %s", 
self.resource_group, self.name, e
+            )
+            return None
+        self._log_last(logs, None)
+        return logs
+
     def _log_last(self, logs: list | None, last_line_logged: Any) -> Any | 
None:
         if logs:
             # determine the last line which was logged before
diff --git 
a/providers/microsoft/azure/tests/unit/microsoft/azure/operators/test_container_instances.py
 
b/providers/microsoft/azure/tests/unit/microsoft/azure/operators/test_container_instances.py
index d21070de0c3..99ba304e3b4 100644
--- 
a/providers/microsoft/azure/tests/unit/microsoft/azure/operators/test_container_instances.py
+++ 
b/providers/microsoft/azure/tests/unit/microsoft/azure/operators/test_container_instances.py
@@ -799,6 +799,149 @@ class TestACIOperator:
             )
         assert aci_mock.return_value.delete.call_count == 1
 
+    @mock.patch(
+        
"airflow.providers.microsoft.azure.operators.container_instances.AzureContainerInstanceHook",
+        autospec=True,
+    )
+    def 
test_execute_complete_error_event_logs_container_output_before_removal(self, 
aci_mock, caplog):
+        """On error, the container output is written to the task log before 
the container group is removed."""
+        calls = []
+        aci_mock.return_value.get_logs.side_effect = lambda *args, **kwargs: (
+            calls.append("get_logs") or ["the real error\n"]
+        )
+        aci_mock.return_value.delete.side_effect = lambda *args, **kwargs: 
calls.append("delete")
+
+        aci = AzureContainerInstancesOperator(
+            ci_conn_id="azure_default",
+            registry_conn_id=None,
+            resource_group="resource-group",
+            name="container-name",
+            image="container-image",
+            region="region",
+            task_id="task",
+            deferrable=True,
+            remove_on_error=True,
+        )
+        with caplog.at_level("INFO"), pytest.raises(RuntimeError, 
match="Container group failed"):
+            aci.execute_complete(
+                context=None,
+                event={
+                    "status": "error",
+                    "exit_code": 1,
+                    "detail_status": "Failed",
+                    "message": "Container group failed with exit code 1",
+                    "resource_group": "resource-group",
+                    "name": "container-name",
+                },
+            )
+
+        assert "the real error" in caplog.text
+        assert calls == ["get_logs", "delete"]
+
+    @mock.patch(
+        
"airflow.providers.microsoft.azure.operators.container_instances.AzureContainerInstanceHook",
+        autospec=True,
+    )
+    def test_execute_complete_success_logs_container_output(self, aci_mock, 
caplog):
+        """On success, the container output is written to the task log."""
+        aci_mock.return_value.get_logs.return_value = ["hello from the 
container\n"]
+
+        aci = AzureContainerInstancesOperator(
+            ci_conn_id="azure_default",
+            registry_conn_id=None,
+            resource_group="resource-group",
+            name="container-name",
+            image="container-image",
+            region="region",
+            task_id="task",
+            deferrable=True,
+        )
+        with caplog.at_level("INFO"):
+            aci.execute_complete(
+                context=None,
+                event={
+                    "status": "success",
+                    "exit_code": 0,
+                    "detail_status": "Completed",
+                    "resource_group": "resource-group",
+                    "name": "container-name",
+                },
+            )
+
+        assert "hello from the container" in caplog.text
+        
aci_mock.return_value.get_logs.assert_called_once_with("resource-group", 
"container-name")
+
+    @mock.patch(
+        
"airflow.providers.microsoft.azure.operators.container_instances.AzureContainerInstanceHook",
+        autospec=True,
+    )
+    def test_execute_complete_log_fetch_failure_keeps_container_error(self, 
aci_mock, caplog):
+        """A failure while fetching logs does not replace the container's own 
error."""
+        aci_mock.return_value.get_logs.side_effect = Exception("cannot read 
logs")
+
+        aci = AzureContainerInstancesOperator(
+            ci_conn_id="azure_default",
+            registry_conn_id=None,
+            resource_group="resource-group",
+            name="container-name",
+            image="container-image",
+            region="region",
+            task_id="task",
+            deferrable=True,
+            remove_on_error=False,
+        )
+        with caplog.at_level("WARNING"), pytest.raises(RuntimeError, 
match="Container group failed"):
+            aci.execute_complete(
+                context=None,
+                event={
+                    "status": "error",
+                    "exit_code": 1,
+                    "detail_status": "Failed",
+                    "message": "Container group failed with exit code 1",
+                    "resource_group": "resource-group",
+                    "name": "container-name",
+                },
+            )
+
+        assert "Failed to fetch logs for container group 
resource-group/container-name" in caplog.text
+
+    @pytest.mark.parametrize("xcom_all", [True, False])
+    @mock.patch(
+        
"airflow.providers.microsoft.azure.operators.container_instances.AzureContainerInstanceHook",
+        autospec=True,
+    )
+    def 
test_execute_complete_success_log_fetch_failure_fails_task_when_xcom_needs_logs(
+        self, aci_mock, xcom_all
+    ):
+        """On success, if XCom needs the logs and they cannot be read, the 
task fails instead of pushing []."""
+        aci_mock.return_value.get_logs.side_effect = Exception("cannot read 
logs")
+
+        aci = AzureContainerInstancesOperator(
+            ci_conn_id="azure_default",
+            registry_conn_id=None,
+            resource_group="resource-group",
+            name="container-name",
+            image="container-image",
+            region="region",
+            task_id="task",
+            deferrable=True,
+            xcom_all=xcom_all,
+        )
+        context = Context(ti=XcomMock())
+        with pytest.raises(Exception, match="cannot read logs"):
+            aci.execute_complete(
+                context=context,
+                event={
+                    "status": "success",
+                    "exit_code": 0,
+                    "detail_status": "Completed",
+                    "resource_group": "resource-group",
+                    "name": "container-name",
+                },
+            )
+
+        assert "logs" not in context["ti"].values
+
     @mock.patch(
         
"airflow.providers.microsoft.azure.operators.container_instances.AzureContainerInstanceHook",
         autospec=True,

Reply via email to