This is an automated email from the ASF dual-hosted git repository.
jason810496 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 918124cb417 Fix Cloudwatch remote logging crash API server if
ResourceNotFoundException (#68781)
918124cb417 is described below
commit 918124cb4171ecf739122365db4dee00a3d7a9f8
Author: Jason(Zhe-You) Liu <[email protected]>
AuthorDate: Tue Jun 23 11:34:41 2026 +0900
Fix Cloudwatch remote logging crash API server if ResourceNotFoundException
(#68781)
---
.../amazon/aws/log/cloudwatch_task_handler.py | 26 +++++++++++++++++++-
.../amazon/aws/log/test_cloudwatch_task_handler.py | 28 ++++++++++++++++++++++
2 files changed, 53 insertions(+), 1 deletion(-)
diff --git
a/providers/amazon/src/airflow/providers/amazon/aws/log/cloudwatch_task_handler.py
b/providers/amazon/src/airflow/providers/amazon/aws/log/cloudwatch_task_handler.py
index 5bc286cb963..a476491b317 100644
---
a/providers/amazon/src/airflow/providers/amazon/aws/log/cloudwatch_task_handler.py
+++
b/providers/amazon/src/airflow/providers/amazon/aws/log/cloudwatch_task_handler.py
@@ -31,6 +31,7 @@ from typing import TYPE_CHECKING, Any
import attrs
import watchtower
+from botocore.exceptions import ClientError
from airflow.providers.amazon.aws.hooks.logs import AwsLogsHook
from airflow.providers.amazon.aws.utils import datetime_to_epoch_utc_ms
@@ -229,12 +230,35 @@ class CloudWatchRemoteLogIO(LoggingMixin): # noqa: D101
if (end_date := getattr(task_instance, "end_date", None)) is None
else datetime_to_epoch_utc_ms(end_date + timedelta(seconds=30))
)
- return self.hook.get_log_events(
+ events = self.hook.get_log_events(
log_group=self.log_group,
log_stream_name=stream_name,
end_time=end_time,
)
+ def _iter_events() -> Generator[CloudWatchLogEvent, None, None]:
+ try:
+ yield from events
+ except ClientError as e:
+ # A missing log stream means no logs were written for this
stream
+ # (e.g. the task logged to stdout instead of remote storage,
or has
+ # not produced any logs). Surface a hint instead of a 500
error, and
+ # instead of an empty view that looks like remote logging
silently failed.
+ if e.response.get("Error", {}).get("Code") !=
"ResourceNotFoundException":
+ raise
+ notice_ts = end_time or
datetime_to_epoch_utc_ms(datetime.now(tz=timezone.utc))
+ yield {
+ "timestamp": notice_ts,
+ "ingestionTime": notice_ts,
+ "message": (
+ f"No log stream found in CloudWatch
(log_group={self.log_group}, "
+ f"log_stream={stream_name}). The task may have logged
to stdout only, "
+ f"not produced any logs yet, or remote logging may be
misconfigured."
+ ),
+ }
+
+ return _iter_events()
+
def _parse_log_event_as_dumped_json(self, event: CloudWatchLogEvent) ->
str:
event_dt = datetime.fromtimestamp(event["timestamp"] / 1000.0,
tz=timezone.utc).isoformat()
event_msg = event["message"]
diff --git
a/providers/amazon/tests/unit/amazon/aws/log/test_cloudwatch_task_handler.py
b/providers/amazon/tests/unit/amazon/aws/log/test_cloudwatch_task_handler.py
index c13515e0440..7a62e4caaf5 100644
--- a/providers/amazon/tests/unit/amazon/aws/log/test_cloudwatch_task_handler.py
+++ b/providers/amazon/tests/unit/amazon/aws/log/test_cloudwatch_task_handler.py
@@ -30,6 +30,7 @@ import boto3
import pendulum
import pytest
import time_machine
+from botocore.exceptions import ClientError
from moto import mock_aws
from pydantic import TypeAdapter
from watchtower import CloudWatchLogHandler
@@ -423,6 +424,33 @@ class TestCloudwatchTaskHandler:
end_time=expected_end_time,
)
+ @mock.patch.object(AwsLogsHook, "get_log_events")
+ def test_get_cloudwatch_logs_missing_stream_yields_hint(self,
mock_get_log_events):
+ # A missing log stream (no logs written for this try -- e.g. the task
logged
+ # to stdout instead of remote storage) must not raise (so the log
reader does
+ # not surface a 500) and must yield a hint instead of nothing, so the
reader
+ # does not show a blank view that looks like remote logging silently
failed.
+ def _raise_not_found(*args, **kwargs):
+ raise ClientError({"Error": {"Code":
"ResourceNotFoundException"}}, "GetLogEvents")
+ yield # pragma: no cover -- makes this a generator function
+
+ mock_get_log_events.side_effect = _raise_not_found
+ events =
list(self.cloudwatch_task_handler.io.get_cloudwatch_logs(self.remote_log_stream,
self.ti))
+ assert len(events) == 1
+ assert "No log stream found in CloudWatch" in events[0]["message"]
+ assert self.remote_log_stream in events[0]["message"]
+
+ @mock.patch.object(AwsLogsHook, "get_log_events")
+ def test_get_cloudwatch_logs_other_client_error_propagates(self,
mock_get_log_events):
+ # Errors other than a missing stream must still surface.
+ def _raise_access_denied(*args, **kwargs):
+ raise ClientError({"Error": {"Code": "AccessDeniedException"}},
"GetLogEvents")
+ yield # pragma: no cover -- makes this a generator function
+
+ mock_get_log_events.side_effect = _raise_access_denied
+ with pytest.raises(ClientError):
+
list(self.cloudwatch_task_handler.io.get_cloudwatch_logs(self.remote_log_stream,
self.ti))
+
@pytest.mark.parametrize(
("conf_json_serialize", "expected_serialized_output"),
[