@@ -102,6 +102,19 @@ def _run_gcloud(args: list[str]) -> subprocess.CompletedProcess[str]:
102102 return subprocess .run (args , text = True , capture_output = True , check = False )
103103
104104
105+ def _run_gcloud_json (args : list [str ], context : str ) -> Any :
106+ result = _run_gcloud (args )
107+ if result .returncode != 0 :
108+ detail = (result .stderr or result .stdout or "" ).strip ()
109+ raise RuntimeError (detail or f"gcloud { context } failed" )
110+ if not result .stdout .strip ():
111+ return None
112+ try :
113+ return json .loads (result .stdout )
114+ except json .JSONDecodeError as exc :
115+ raise RuntimeError (f"gcloud { context } returned invalid JSON: { exc } " ) from exc
116+
117+
105118def _run_gcloud_logging (project : str , log_filter : str , limit : int ) -> list [dict [str , Any ]]:
106119 command = [
107120 "gcloud" ,
@@ -126,6 +139,144 @@ def _run_gcloud_logging(project: str, log_filter: str, limit: int) -> list[dict[
126139 return payload if isinstance (payload , list ) else []
127140
128141
142+ def _parse_timestamp (value : Any ) -> dt .datetime | None :
143+ if not value :
144+ return None
145+ text = str (value ).strip ()
146+ if not text :
147+ return None
148+ if text .endswith ("Z" ):
149+ text = f"{ text [:- 1 ]} +00:00"
150+ try :
151+ parsed = dt .datetime .fromisoformat (text )
152+ except ValueError :
153+ return None
154+ if parsed .tzinfo is None :
155+ parsed = parsed .replace (tzinfo = dt .timezone .utc )
156+ return parsed .astimezone (dt .timezone .utc )
157+
158+
159+ def _format_timestamp (value : dt .datetime ) -> str :
160+ return value .astimezone (dt .timezone .utc ).isoformat ().replace ("+00:00" , "Z" )
161+
162+
163+ def _target_payloads () -> list [dict [str , Any ]]:
164+ raw_targets = (os .environ .get ("CLOUD_RUN_SERVICE_TARGETS_JSON" ) or "" ).strip ()
165+ if not raw_targets :
166+ return []
167+ try :
168+ payload = json .loads (raw_targets )
169+ except json .JSONDecodeError :
170+ return []
171+ targets = payload .get ("targets" ) if isinstance (payload , dict ) else payload
172+ if not isinstance (targets , list ):
173+ return []
174+ return [target for target in targets if isinstance (target , dict )]
175+
176+
177+ def _runtime_target (target : dict [str , Any ]) -> dict [str , Any ]:
178+ runtime_target = target .get ("runtime_target" ) or target .get ("runtime_target_json" )
179+ if isinstance (runtime_target , str ):
180+ try :
181+ runtime_target = json .loads (runtime_target )
182+ except json .JSONDecodeError :
183+ runtime_target = {}
184+ return runtime_target if isinstance (runtime_target , dict ) else {}
185+
186+
187+ def _target_service_names (target : dict [str , Any ]) -> list [str ]:
188+ runtime_target = _runtime_target (target )
189+ for key in ("service" , "service_name" , "cloud_run_service" ):
190+ value = target .get (key ) or runtime_target .get (key )
191+ if value :
192+ return _split_values (str (value ))
193+ return []
194+
195+
196+ def _region_for_service (service : str ) -> str :
197+ for target in _target_payloads ():
198+ if service not in _target_service_names (target ):
199+ continue
200+ runtime_target = _runtime_target (target )
201+ for key in ("region" , "cloud_run_region" , "location" ):
202+ value = target .get (key ) or runtime_target .get (key )
203+ if value :
204+ return str (value ).strip ()
205+ return (
206+ os .environ .get ("RUNTIME_GUARD_CLOUD_RUN_REGION" )
207+ or os .environ .get ("CLOUD_RUN_REGION" )
208+ or os .environ .get ("CLOUD_RUN_LOCATION" )
209+ or os .environ .get ("GOOGLE_CLOUD_REGION" )
210+ or ""
211+ ).strip ()
212+
213+
214+ def _latest_ready_revision_started_at (project : str , service : str ) -> dt .datetime | None :
215+ region = _region_for_service (service )
216+ if not region :
217+ return None
218+
219+ service_payload = _run_gcloud_json (
220+ [
221+ "gcloud" ,
222+ "run" ,
223+ "services" ,
224+ "describe" ,
225+ service ,
226+ "--project" ,
227+ project ,
228+ "--region" ,
229+ region ,
230+ "--format=json" ,
231+ ],
232+ f"run services describe { service } " ,
233+ )
234+ if not isinstance (service_payload , dict ):
235+ return None
236+ status = service_payload .get ("status" ) or {}
237+ if not isinstance (status , dict ):
238+ return None
239+ revision = str (status .get ("latestReadyRevisionName" ) or "" ).strip ()
240+ if not revision :
241+ return None
242+
243+ revision_payload = _run_gcloud_json (
244+ [
245+ "gcloud" ,
246+ "run" ,
247+ "revisions" ,
248+ "describe" ,
249+ revision ,
250+ "--project" ,
251+ project ,
252+ "--region" ,
253+ region ,
254+ "--format=json" ,
255+ ],
256+ f"run revisions describe { revision } " ,
257+ )
258+ if not isinstance (revision_payload , dict ):
259+ return None
260+ metadata = revision_payload .get ("metadata" ) or {}
261+ if not isinstance (metadata , dict ):
262+ return None
263+ return _parse_timestamp (metadata .get ("creationTimestamp" ))
264+
265+
266+ def _cloud_run_log_since (project : str , service : str , fallback : dt .datetime ) -> dt .datetime :
267+ try :
268+ revision_start = _latest_ready_revision_started_at (project , service )
269+ except RuntimeError as exc :
270+ print (
271+ f"Unable to resolve latest ready revision for { service } ; using lookback window: { exc } " ,
272+ file = sys .stderr ,
273+ )
274+ return fallback
275+ if revision_start and revision_start > fallback :
276+ return revision_start
277+ return fallback
278+
279+
129280def _status (entry : dict [str , Any ]) -> int | None :
130281 value = (entry .get ("httpRequest" ) or {}).get ("status" )
131282 try :
@@ -267,6 +418,7 @@ def main() -> int:
267418 require_success = _env_bool ("RUNTIME_GUARD_REQUIRE_SUCCESS" , False )
268419 fail_workflow = _env_bool ("RUNTIME_GUARD_FAIL_WORKFLOW_ON_ALERT" , True )
269420 check_scheduler = _env_bool ("RUNTIME_GUARD_CHECK_SCHEDULER" , True )
421+ ignore_pre_ready_logs = _env_bool ("RUNTIME_GUARD_IGNORE_PRE_READY_REVISION_LOGS" , True )
270422
271423 since = (
272424 dt .datetime .now (dt .timezone .utc ) - dt .timedelta (minutes = lookback_minutes )
@@ -288,10 +440,12 @@ def main() -> int:
288440 )
289441
290442 for service in services :
443+ service_since = _cloud_run_log_since (project , service , since ) if ignore_pre_ready_logs else since
444+ service_since_text = _format_timestamp (service_since )
291445 log_filter = (
292446 'resource.type="cloud_run_revision" '
293447 f'AND resource.labels.service_name="{ service } " '
294- f'AND timestamp >= "{ since_text } "'
448+ f'AND timestamp >= "{ service_since_text } "'
295449 )
296450 try :
297451 entries = _run_gcloud_logging (project , log_filter , limit )
0 commit comments