@@ -15,16 +15,50 @@ def exception_handler(exception_type, exception, traceback):
1515sys .excepthook = exception_handler
1616
1717
18- def collect_all_sources (payload ):
19- # Return a flat list of every source string found in payload['content'].
18+ def at_least_3_2 (airflow_version : str ) -> bool :
19+ # The log API response shape changed in Airflow 3.2 (see collect_all_sources).
20+ try :
21+ major , minor = (int (p ) for p in airflow_version .split ("." )[:2 ])
22+ except (ValueError , IndexError ):
23+ # Unknown/garbled version: assume the current (>= 3.2) format.
24+ return True
25+ return (major , minor ) >= (3 , 2 )
26+
27+
28+ def collect_all_sources (payload , airflow_version ):
29+ # Return a flat list of every log-source string found in payload['content'].
30+ #
31+ # Airflow >= 3.2 exposes the (remote) log location as `event` lines wrapped in a
32+ # "Log message source details" group, e.g.:
33+ # {'event': '::group::Log message source details'}
34+ # {'event': 's3://my-bucket/.../attempt=1.log'}
35+ # {'event': '::endgroup::'}
36+ # Older Airflow 3.x returned the locations in a `sources` list per entry instead.
37+ if at_least_3_2 (airflow_version ):
38+ sources = []
39+ in_source_group = False
40+ for entry in payload .get ("content" , []):
41+ if not isinstance (entry , dict ):
42+ continue
43+ event = entry .get ("event" , "" )
44+ if event == "::group::Log message source details" :
45+ in_source_group = True
46+ elif event == "::endgroup::" :
47+ in_source_group = False
48+ elif in_source_group :
49+ sources .append (event )
50+ return sources
51+
2052 sources = []
2153 for entry in payload .get ("content" , []):
2254 if isinstance (entry , dict ) and isinstance (entry .get ("sources" ), list ):
2355 sources .extend (entry ["sources" ])
2456 return sources
2557
2658
27- def prefix_is_matched (tasks , dag_run_id_root_url , headers , required_prefix ) -> bool :
59+ def prefix_is_matched (
60+ tasks , dag_run_id_root_url , headers , required_prefix , airflow_version
61+ ) -> bool :
2862 for ti in tasks ["task_instances" ]:
2963 task_id = ti ["task_id" ]
3064 try_number = ti ["try_number" ]
@@ -34,7 +68,7 @@ def prefix_is_matched(tasks, dag_run_id_root_url, headers, required_prefix) -> b
3468 params = {"full_content" : "true" },
3569 ).json ()
3670 print (f"Logs/full-content: { logs } " )
37- all_sources = collect_all_sources (logs )
71+ all_sources = collect_all_sources (logs , airflow_version )
3872 if not all_sources :
3973 print ("No 'sources' arrays were found yet in the payload..." )
4074 else :
@@ -51,7 +85,7 @@ def prefix_is_matched(tasks, dag_run_id_root_url, headers, required_prefix) -> b
5185 return False
5286
5387
54- def remote_logging () -> None :
88+ def remote_logging (airflow_version : str ) -> None :
5589 now = datetime .now (timezone .utc )
5690 ts = now .strftime ("%Y-%m-%dT%H:%M:%S.%f" ) + now .strftime ("%z" )
5791
@@ -123,7 +157,9 @@ def remote_logging() -> None:
123157 f"{ dag_run_id_root_url } " ,
124158 headers = headers ,
125159 ).json ()
126- if prefix_is_matched (tasks , dag_run_id_root_url , headers , required_prefix ):
160+ if prefix_is_matched (
161+ tasks , dag_run_id_root_url , headers , required_prefix , airflow_version
162+ ):
127163 break
128164 time .sleep (10 )
129165 loop += 1
@@ -145,7 +181,7 @@ def remote_logging() -> None:
145181 opts = parser .parse_args ()
146182
147183 if opts .airflow_version and not opts .airflow_version .startswith ("2." ):
148- remote_logging ()
184+ remote_logging (opts . airflow_version )
149185 else :
150186 # should not happen as we are using airflow-latest
151187 print ("Remote logging is not tested for version < 3.x!" )
0 commit comments