1+ # functions_tabular_model_diagnostics.py
2+ """Opt-in, metadata-only HTTP diagnostics for foreground tabular model calls."""
3+
4+ import asyncio
5+ import hashlib
6+ import logging
7+ import math
8+ import os
9+ import re
10+ import time
11+ import uuid
12+
13+ from openai import DefaultAsyncHttpxClient
14+
15+ from functions_appinsights import log_event
16+
17+
18+ DIAGNOSTICS_ENV = 'SIMPLECHAT_TABULAR_HTTP_DIAGNOSTICS'
19+ TIMEOUT_ENV = 'SIMPLECHAT_TABULAR_HTTP_TIMEOUT_SECONDS'
20+ RETRIES_ENV = 'SIMPLECHAT_TABULAR_HTTP_MAX_RETRIES'
21+ BUDGET_ENV = 'SIMPLECHAT_TABULAR_HTTP_BUDGET_SECONDS'
22+
23+
24+ def _diagnostic_number (name , default , minimum , maximum , integer = False ):
25+ try :
26+ raw_value = os .environ .get (name , str (default ))
27+ value = int (raw_value ) if integer else float (raw_value )
28+ if math .isfinite (value ) and minimum <= value <= maximum :
29+ return value
30+ except (ValueError , TypeError , OverflowError ):
31+ pass
32+ return default
33+
34+
35+ def _safe_provider_request_id (response ):
36+ for name in ('x-request-id' , 'apim-request-id' , 'x-ms-request-id' ):
37+ value = response .headers .get (name , '' )
38+ if re .fullmatch (r'[A-Za-z0-9_-]{1,128}' , value ):
39+ return value
40+ return ''
41+
42+
43+ class TabularDiagnosticAsyncClient (DefaultAsyncHttpxClient ):
44+ """Observe SDK HTTP attempts without inspecting request or response bodies."""
45+
46+ def __init__ (self , * , budget_seconds , ** kwargs ):
47+ super ().__init__ (** kwargs )
48+ self .diagnostic_run_id = uuid .uuid4 ().hex
49+ self .budget_seconds = budget_seconds
50+ self ._first_attempt_at = None
51+ self ._attempt_sequence = 0
52+
53+ def _emit (self , operation , properties , level = logging .INFO ):
54+ try :
55+ log_event (
56+ f'[TABULAR_MODEL_HTTP] { operation } ' ,
57+ extra = {
58+ 'diagnostic_run_id' : self .diagnostic_run_id ,
59+ 'operation' : operation ,
60+ 'service' : 'tabular-analysis' ,
61+ ** properties ,
62+ },
63+ level = level ,
64+ )
65+ except Exception :
66+ pass
67+
68+ async def send (self , request , ** kwargs ):
69+ started_at = time .monotonic ()
70+ if self ._first_attempt_at is None :
71+ self ._first_attempt_at = started_at
72+ self ._attempt_sequence += 1
73+ try :
74+ retry_count = max (0 , int (request .headers .get ('x-stainless-retry-count' , '0' )))
75+ except (ValueError , TypeError ):
76+ retry_count = 0
77+ endpoint_identity = f'{ request .url .host } :{ request .url .port } { request .url .path } '
78+ properties = {
79+ 'http_attempt_id' : uuid .uuid4 ().hex ,
80+ 'attempt_sequence' : self ._attempt_sequence ,
81+ 'retry_count' : retry_count ,
82+ 'endpoint_fingerprint' : hashlib .sha256 (endpoint_identity .encode ('utf-8' )).hexdigest ()[:16 ],
83+ 'budget_seconds' : self .budget_seconds ,
84+ }
85+ self ._emit ('attempt_started' , properties )
86+ try :
87+ remaining = self .budget_seconds - (started_at - self ._first_attempt_at )
88+ if remaining <= 0 :
89+ raise TimeoutError ('Tabular diagnostic HTTP budget exhausted' )
90+ async with asyncio .timeout (remaining ):
91+ response = await super ().send (request , ** kwargs )
92+ except (Exception , asyncio .CancelledError ) as exc :
93+ cause = exc
94+ seen = set ()
95+ while cause .__cause__ is not None and id (cause ) not in seen :
96+ seen .add (id (cause ))
97+ cause = cause .__cause__
98+ self ._emit ('attempt_failed' , {
99+ ** properties ,
100+ 'duration_ms' : round ((time .monotonic () - started_at ) * 1000 , 3 ),
101+ 'error_type' : type (exc ).__name__ ,
102+ 'reason' : type (cause ).__name__ ,
103+ 'budget_exhausted' : time .monotonic () - self ._first_attempt_at >= self .budget_seconds ,
104+ }, level = logging .WARNING )
105+ raise
106+ self ._emit ('attempt_completed' , {
107+ ** properties ,
108+ 'duration_ms' : round ((time .monotonic () - started_at ) * 1000 , 3 ),
109+ 'status_code' : response .status_code ,
110+ 'success' : response .is_success ,
111+ 'provider_request_id' : _safe_provider_request_id (response ),
112+ })
113+ return response
114+
115+
116+ def build_tabular_model_http_options (service_id ):
117+ """Return SDK options only for explicitly enabled tabular-analysis clients."""
118+ enabled = os .environ .get (DIAGNOSTICS_ENV , '' ).strip ().lower () in {'1' , 'true' , 'yes' , 'on' }
119+ if service_id != 'tabular-analysis' or not enabled :
120+ return {}
121+ timeout = _diagnostic_number (TIMEOUT_ENV , 60 , 1 , 600 )
122+ retries = _diagnostic_number (RETRIES_ENV , 0 , 0 , 2 , integer = True )
123+ budget = _diagnostic_number (BUDGET_ENV , 180 , 1 , 1800 )
124+ return {
125+ 'http_client' : TabularDiagnosticAsyncClient (budget_seconds = budget , timeout = timeout ),
126+ 'timeout' : timeout ,
127+ 'max_retries' : retries ,
128+ }
129+
130+
131+ async def close_tabular_model_http_client (service ):
132+ """Close only the explicitly owned diagnostic SDK client after a tabular run."""
133+ sdk_client = getattr (service , 'client' , None )
134+ http_client = getattr (sdk_client , '_client' , None )
135+ if isinstance (http_client , TabularDiagnosticAsyncClient ):
136+ try :
137+ await sdk_client .close ()
138+ except Exception as exc :
139+ http_client ._emit ('cleanup_failed' , {'error_type' : type (exc ).__name__ }, level = logging .WARNING )
0 commit comments