Skip to content

Fix CloudWatch remote logging for ephemeral lifecycle executor - #68779

Merged
jason810496 merged 4 commits into
apache:mainfrom
jason810496:fix/cloudwatch/remote-logging-k8s-executor
Jun 25, 2026
Merged

Fix CloudWatch remote logging for ephemeral lifecycle executor#68779
jason810496 merged 4 commits into
apache:mainfrom
jason810496:fix/cloudwatch/remote-logging-k8s-executor

Conversation

@jason810496

@jason810496 jason810496 commented Jun 20, 2026

Copy link
Copy Markdown
Member

Why

While trying to setup cloudwatch remote logging in #68709 in order to persist the logs in real time. I encounter the same errors as above listed issues.

The root cause I found is same as #66475 (comment) pointed out. The configure_logging -> dictConfig -> _clearExistingHandlers call chain shutdown the watchtower handler.

How

I went to the another than #66633, instead of configuring the processors after the dictConfig call. We could make the cloudwatch remote logger itself self-healing by creating the fresh instance if previous instance was shutdown by the dictConfig call but also ensure the .close semantic by guarding with the _close state.

What

Fix the lifecycle issue of cloudwatch remote logging and verify with breeze k8s system test with provider only changes without touching the Task-SDK changes.

Screenshot 2026-06-19 at 11 12 16 PM Screenshot 2026-06-19 at 11 12 46 PM

@jason810496

jason810496 commented Jun 20, 2026

Copy link
Copy Markdown
Member Author

cc @sarvesh371, @seanghaeli Could you verify this patch for your setup when you have a moment? Since it likely #66633 won't catch the 3.3 release (we're close to dev freeze for 3.3), so we might release this provider-only patch first. Thanks.

The streaming CloudWatch handler is rebuilt whenever it reports
shutting_down, so logs survive configure_logging() closing it. But
shutting_down alone cannot tell a mid-task close apart from genuine
teardown, so a record arriving after teardown would spin up an orphan
handler and its background queue thread that nobody flushes or closes.

The supervisor lifecycle makes the two cases distinguishable in time:

  1. configure_logging() builds the handler via remote.processors
     (processors does `_ = self.handler`), registering it in
     logging._handlerList.
  2. The same call then runs dictConfig, whose non-incremental reset
     closes that handler -> watchtower sets shutting_down=True.
  3. Child log records stream through proc -> self.handler, which sees
     shutting_down and rebuilds. This is the case we must keep working.
  4. At the last possible moment _upload_logs() -> upload() -> close()
     flushes; nothing logs after this.

shutting_down is watchtower's flag set by dictConfig (step 2); the new
_closed flag is ours, set only by close() (step 4). dictConfig never
touches _closed, so the rebuild in step 3 still fires, while a late
record after step 4 keeps the closed handler instead of orphaning a new
one. close() on the outer CloudwatchTaskHandler now closes the handler
the IO is currently using rather than the reference captured in
set_context(), which dictConfig may have closed and the IO since rebuilt.
@jason810496
jason810496 force-pushed the fix/cloudwatch/remote-logging-k8s-executor branch from a92793f to f28925a Compare June 20, 2026 06:18
@ferruzzi
ferruzzi requested a review from vincbeck June 22, 2026 19:51

@ferruzzi ferruzzi left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I haven't played with CloudWatch much, but I left some style nitpicks. Also, Claude loves to over-comment code, you may want to clean some of those up/

Comment thread providers/amazon/src/airflow/providers/amazon/aws/log/cloudwatch_task_handler.py Outdated
Comment thread providers/amazon/src/airflow/providers/amazon/aws/log/cloudwatch_task_handler.py Outdated
@jason810496

Copy link
Copy Markdown
Member Author

Thanks all for the review, then I guess it's fine to merge this?

@seanghaeli seanghaeli left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verified it in breeze just like the upstream PR and seeing the cloudwatch logs work e2e

@eladkal eladkal left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM but lets get +1 from @o-nikolas / @vincbeck / @ferruzzi before merge

@ferruzzi ferruzzi left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like my comments were addressed. Thanks.

@jason810496
jason810496 merged commit 7286fa9 into apache:main Jun 25, 2026
81 checks passed
karenbraganz pushed a commit to karenbraganz/airflow that referenced this pull request Jun 30, 2026
korex-f added a commit to korex-f/airflow that referenced this pull request Jul 11, 2026
…nges

Per reviewer guidance, this PR targets task-sdk changes only.
Provider-side fixes are covered by apache#68779.
korex-f added a commit to korex-f/airflow that referenced this pull request Jul 21, 2026
…nges

Per reviewer guidance, this PR targets task-sdk changes only.
Provider-side fixes are covered by apache#68779.
korex-f added a commit to korex-f/airflow that referenced this pull request Aug 1, 2026
…nfigure_logging

In configure_logging(), getattr(remote, 'processors') was called before
dictConfig() ran. dictConfig() calls _clearExistingHandlers() which closes
every handler in logging._handlerList — including the remote handler built
moments earlier. This caused CloudWatch/Watchtower logs to be silently
dropped when using the Task SDK with remote logging on ECS/Kubernetes workers.

Fix: move the remote processor injection to after dictConfig() has run via
a second structlog.configure() call. Also gate on not sending_to_supervisor
to avoid unmasked events from the task subprocess, and add a None default
to getattr() to handle third-party RemoteLogIO objects.

Provider-side fix: apache#68779

Closes apache#66475
korex-f added a commit to korex-f/airflow that referenced this pull request Aug 3, 2026
…nfigure_logging

In configure_logging(), getattr(remote, 'processors') was called before
dictConfig() ran. dictConfig() calls _clearExistingHandlers() which closes
every handler in logging._handlerList — including the remote handler built
moments earlier. This caused CloudWatch/Watchtower logs to be silently
dropped when using the Task SDK with remote logging on ECS/Kubernetes workers.

Fix: move the remote processor injection to after dictConfig() has run via
a second structlog.configure() call. Also gate on not sending_to_supervisor
to avoid unmasked events from the task subprocess, and add a None default
to getattr() to handle third-party RemoteLogIO objects.

Provider-side fix: apache#68779

Closes apache#66475
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Cloudwatch remote logging does not work for ECS Executor Cloudwatch logging broken in Airflow 3.1

6 participants