99import re
1010import subprocess
1111import sys
12+ import time
1213import urllib .parse
1314import urllib .request
1415from typing import Any
2425 "URL_UNREACHABLE" ,
2526)
2627SCHEDULER_CLOUD_RUN_DEDUP_SECONDS = 120
28+ DEFAULT_LOG_QUERY_MAX_ATTEMPTS = 3
29+ DEFAULT_LOG_QUERY_RETRY_SECONDS = 1.0
30+ _RETRYABLE_LOG_QUERY_MARKERS = (
31+ "http 429" ,
32+ "http 500" ,
33+ "http 502" ,
34+ "http 503" ,
35+ "http 504" ,
36+ '"code": 429' ,
37+ '"code": 500' ,
38+ '"code": 502' ,
39+ '"code": 503' ,
40+ '"code": 504' ,
41+ "internal error" ,
42+ "unavailable" ,
43+ "timed out" ,
44+ "network connectivity" ,
45+ "connection reset" ,
46+ "rate limit" ,
47+ )
2748
2849
2950def _split_values (raw : str | None ) -> list [str ]:
@@ -200,6 +221,31 @@ def _run_gcloud_json(args: list[str], context: str) -> Any:
200221 raise RuntimeError (f"gcloud { context } returned invalid JSON: { exc } " ) from exc
201222
202223
224+ def _log_query_retry_config () -> tuple [int , float ]:
225+ try :
226+ attempts = int (
227+ os .environ .get ("RUNTIME_GUARD_LOG_QUERY_MAX_ATTEMPTS" )
228+ or DEFAULT_LOG_QUERY_MAX_ATTEMPTS
229+ )
230+ except ValueError :
231+ attempts = DEFAULT_LOG_QUERY_MAX_ATTEMPTS
232+ try :
233+ retry_seconds = float (
234+ os .environ .get ("RUNTIME_GUARD_LOG_QUERY_RETRY_SECONDS" )
235+ or DEFAULT_LOG_QUERY_RETRY_SECONDS
236+ )
237+ except ValueError :
238+ retry_seconds = DEFAULT_LOG_QUERY_RETRY_SECONDS
239+ return max (1 , min (attempts , 5 )), max (0.0 , min (retry_seconds , 10.0 ))
240+
241+
242+ def _is_retryable_log_query_error (detail : str ) -> bool :
243+ normalized = detail .lower ()
244+ if "403" in normalized or "permission_denied" in normalized :
245+ return False
246+ return any (marker in normalized for marker in _RETRYABLE_LOG_QUERY_MARKERS )
247+
248+
203249def _run_gcloud_logging (project : str , log_filter : str , limit : int ) -> list [dict [str , Any ]]:
204250 command = [
205251 "gcloud" ,
@@ -211,17 +257,24 @@ def _run_gcloud_logging(project: str, log_filter: str, limit: int) -> list[dict[
211257 "--format=json" ,
212258 f"--limit={ limit } " ,
213259 ]
214- result = subprocess .run (command , text = True , capture_output = True , check = False )
215- if result .returncode != 0 :
216- detail = (result .stderr or result .stdout or "" ).strip ()
217- raise RuntimeError (detail or "gcloud logging read failed" )
218- if not result .stdout .strip ():
219- return []
220- try :
221- payload = json .loads (result .stdout )
222- except json .JSONDecodeError as exc :
223- raise RuntimeError (f"gcloud returned invalid JSON: { exc } " ) from exc
224- return payload if isinstance (payload , list ) else []
260+ max_attempts , retry_seconds = _log_query_retry_config ()
261+ last_detail = ""
262+ for attempt in range (1 , max_attempts + 1 ):
263+ result = _run_gcloud (command )
264+ if result .returncode == 0 :
265+ if not result .stdout .strip ():
266+ return []
267+ try :
268+ payload = json .loads (result .stdout )
269+ except json .JSONDecodeError as exc :
270+ raise RuntimeError (f"gcloud returned invalid JSON: { exc } " ) from exc
271+ return payload if isinstance (payload , list ) else []
272+ last_detail = (result .stderr or result .stdout or "" ).strip ()
273+ if attempt >= max_attempts or not _is_retryable_log_query_error (last_detail ):
274+ break
275+ time .sleep (retry_seconds * attempt )
276+ suffix = f" after { max_attempts } attempt(s)" if max_attempts > 1 else ""
277+ raise RuntimeError ((last_detail or "gcloud logging read failed" ) + suffix )
225278
226279
227280def _parse_timestamp (value : Any ) -> dt .datetime | None :
0 commit comments