@@ -43,6 +43,67 @@ def _env_bool(name: str, default: bool = False) -> bool:
4343 return value in {"1" , "true" , "yes" , "y" , "on" }
4444
4545
46+ def _enabled_value (value : Any , * , default : bool = True ) -> bool :
47+ if value is None :
48+ return default
49+ text = str (value ).strip ().lower ()
50+ if not text :
51+ return default
52+ if text in {"0" , "false" , "no" , "n" , "off" }:
53+ return False
54+ return True
55+
56+
57+ def _target_runtime_target (target : dict [str , Any ]) -> dict [str , Any ]:
58+ runtime_target = target .get ("runtime_target" ) or target .get ("runtime_target_json" )
59+ if isinstance (runtime_target , str ):
60+ try :
61+ runtime_target = json .loads (runtime_target )
62+ except json .JSONDecodeError :
63+ runtime_target = {}
64+ return runtime_target if isinstance (runtime_target , dict ) else {}
65+
66+
67+ def _target_account_scope (target : dict [str , Any ], runtime_target : dict [str , Any ]) -> str :
68+ for source in (target , runtime_target ):
69+ for key in (
70+ "account_scope" ,
71+ "account_group" ,
72+ "account_region" ,
73+ "ACCOUNT_GROUP" ,
74+ "ACCOUNT_REGION" ,
75+ ):
76+ value = source .get (key )
77+ if value :
78+ return str (value ).strip ()
79+ return ""
80+
81+
82+ def _target_matches_expected_scope (target : dict [str , Any ], runtime_target : dict [str , Any ]) -> bool :
83+ expected_scope = (os .environ .get ("RUNTIME_HEARTBEAT_ACCOUNT_SCOPE" ) or "" ).strip ().lower ()
84+ if not expected_scope :
85+ return True
86+ target_scope = _target_account_scope (target , runtime_target ).lower ()
87+ return not target_scope or target_scope == expected_scope
88+
89+
90+ def _target_enabled (target : dict [str , Any ], runtime_target : dict [str , Any ]) -> bool :
91+ value = target .get ("runtime_target_enabled" )
92+ if value is None :
93+ value = target .get ("RUNTIME_TARGET_ENABLED" )
94+ if value is None :
95+ value = runtime_target .get ("runtime_target_enabled" )
96+ return _enabled_value (value , default = True )
97+
98+
99+ def _target_service_values (target : dict [str , Any ], runtime_target : dict [str , Any ]) -> list [str ]:
100+ for key in ("service" , "service_name" , "cloud_run_service" ):
101+ value = target .get (key ) or runtime_target .get (key )
102+ if value :
103+ return _split_values (str (value ))
104+ return []
105+
106+
46107def _parse_timestamp (value : Any ) -> dt .datetime | None :
47108 if not value :
48109 return None
@@ -91,10 +152,56 @@ def _base_report_uris() -> list[str]:
91152 return unique
92153
93154
155+ def _load_target_service_candidates () -> tuple [list [str ], list [str ]]:
156+ disabled_target_services : list [str ] = []
157+ enabled_target_services : list [str ] = []
158+ raw_targets = (os .environ .get ("CLOUD_RUN_SERVICE_TARGETS_JSON" ) or "" ).strip ()
159+ if not raw_targets :
160+ return enabled_target_services , disabled_target_services
161+ try :
162+ payload = json .loads (raw_targets )
163+ targets = payload .get ("targets" ) if isinstance (payload , dict ) else payload
164+ if isinstance (targets , list ):
165+ for target in targets :
166+ if not isinstance (target , dict ):
167+ continue
168+ runtime_target = _target_runtime_target (target )
169+ if not _target_matches_expected_scope (target , runtime_target ):
170+ continue
171+ target_services = _target_service_values (target , runtime_target )
172+ if not target_services :
173+ continue
174+ if _target_enabled (target , runtime_target ):
175+ enabled_target_services .extend (target_services )
176+ else :
177+ disabled_target_services .extend (target_services )
178+ except json .JSONDecodeError :
179+ pass
180+ return _unique_values (enabled_target_services ), _unique_values (disabled_target_services )
181+
182+
183+ def _filter_disabled_services (
184+ services : list [str ],
185+ enabled_services : list [str ],
186+ disabled_services : list [str ],
187+ ) -> list [str ]:
188+ if not disabled_services :
189+ return services
190+ enabled_service_set = set (enabled_services )
191+ disabled_service_set = set (disabled_services ) - enabled_service_set
192+ return [service for service in services if service not in disabled_service_set ]
193+
194+
94195def _load_required_service_candidates () -> tuple [list [str ], bool ]:
196+ enabled_target_services , disabled_target_services = _load_target_service_candidates ()
95197 explicit_services = _split_values (os .environ .get ("RUNTIME_HEARTBEAT_REQUIRED_SERVICES" ))
96198 if explicit_services :
97- return _unique_values (explicit_services ), True
199+ services = _filter_disabled_services (
200+ explicit_services ,
201+ enabled_target_services ,
202+ disabled_target_services ,
203+ )
204+ return _unique_values (services ), True
98205
99206 services = []
100207 for name in (
@@ -103,30 +210,12 @@ def _load_required_service_candidates() -> tuple[list[str], bool]:
103210 ):
104211 services .extend (_split_values (os .environ .get (name )))
105212
106- raw_targets = (os .environ .get ("CLOUD_RUN_SERVICE_TARGETS_JSON" ) or "" ).strip ()
107- if raw_targets :
108- try :
109- payload = json .loads (raw_targets )
110- targets = payload .get ("targets" ) if isinstance (payload , dict ) else payload
111- if isinstance (targets , list ):
112- for target in targets :
113- if not isinstance (target , dict ):
114- continue
115- runtime_target = target .get ("runtime_target" ) or target .get ("runtime_target_json" )
116- if isinstance (runtime_target , str ):
117- try :
118- runtime_target = json .loads (runtime_target )
119- except json .JSONDecodeError :
120- runtime_target = {}
121- for key in ("service" , "service_name" , "cloud_run_service" ):
122- value = target .get (key ) or (
123- runtime_target .get (key ) if isinstance (runtime_target , dict ) else None
124- )
125- if value :
126- services .extend (_split_values (str (value )))
127- break
128- except json .JSONDecodeError :
129- pass
213+ services = _filter_disabled_services (
214+ services ,
215+ enabled_target_services ,
216+ disabled_target_services ,
217+ )
218+ services .extend (enabled_target_services )
130219
131220 return _unique_values (services ), False
132221
@@ -152,6 +241,8 @@ def _resolve_required_services(
152241 now : dt .datetime | None = None ,
153242) -> tuple [list [str ], str | None , bool ]:
154243 services , explicit = _load_required_service_candidates ()
244+ if explicit and not services :
245+ return [], "all explicitly required heartbeat services are disabled" , False
155246 if explicit or not services :
156247 return services , None , False
157248 if not _env_bool ("RUNTIME_HEARTBEAT_SCHEDULER_AWARE" , True ):
@@ -610,6 +701,9 @@ def main(now: dt.datetime | None = None) -> int:
610701 or os .environ .get ("GOOGLE_CLOUD_PROJECT" )
611702 )
612703 name = os .environ .get ("RUNTIME_HEARTBEAT_NAME" ) or os .environ .get ("GITHUB_REPOSITORY" ) or "runtime"
704+ if not _env_bool ("RUNTIME_TARGET_ENABLED" , True ):
705+ print (f"Execution report heartbeat skipped for { name } : runtime target is disabled" )
706+ return 0
613707 lookback_hours = float (os .environ .get ("RUNTIME_HEARTBEAT_LOOKBACK_HOURS" ) or "36" )
614708 max_reports = int (os .environ .get ("RUNTIME_HEARTBEAT_MAX_REPORTS_TO_READ" ) or "20" )
615709 fail_workflow = _env_bool ("RUNTIME_HEARTBEAT_FAIL_WORKFLOW_ON_ALERT" , True )
0 commit comments