@@ -25,6 +25,53 @@ pub struct StreamingHttpResponse {
2525 pub error_body : String ,
2626}
2727
28+ /// Information about an HTTP request for metrics collection.
29+ #[ derive( Debug , Clone ) ]
30+ pub struct HttpMetricsRecord {
31+ /// The target URL
32+ pub url : String ,
33+ /// HTTP method (currently only POST is used for LLM calls)
34+ pub method : String ,
35+ /// Response status code
36+ pub status : u16 ,
37+ /// Request duration in milliseconds
38+ pub duration_ms : f64 ,
39+ /// Number of bytes sent (request body size)
40+ pub request_bytes : u64 ,
41+ /// Number of bytes received (response body size)
42+ pub response_bytes : u64 ,
43+ /// Whether this was a streaming request
44+ pub streaming : bool ,
45+ }
46+
47+ /// Callback function type for HTTP metrics collection.
48+ /// The callback is called after each HTTP request completes.
49+ pub type HttpMetricsCallback = Arc < dyn Fn ( HttpMetricsRecord ) + Send + Sync > ;
50+
51+ /// Global HTTP metrics callback registry.
52+ ///
53+ /// Set this to enable HTTP metrics collection for LLM API calls.
54+ /// The callback will be invoked after each HTTP request completes.
55+ static HTTP_METRICS_CALLBACK : std:: sync:: RwLock < Option < HttpMetricsCallback > > =
56+ std:: sync:: RwLock :: new ( None ) ;
57+
58+ /// Register a global HTTP metrics callback.
59+ /// The callback will be invoked after each HTTP request completes.
60+ pub fn set_http_metrics_callback ( callback : HttpMetricsCallback ) {
61+ * HTTP_METRICS_CALLBACK . write ( ) . unwrap ( ) = Some ( callback) ;
62+ }
63+
64+ /// Clear the global HTTP metrics callback.
65+ pub fn clear_http_metrics_callback ( ) {
66+ * HTTP_METRICS_CALLBACK . write ( ) . unwrap ( ) = None ;
67+ }
68+
69+ fn maybe_record_metrics ( record : HttpMetricsRecord ) {
70+ if let Some ( callback) = HTTP_METRICS_CALLBACK . read ( ) . unwrap ( ) . as_ref ( ) {
71+ callback ( record) ;
72+ }
73+ }
74+
2875/// Abstraction over HTTP POST requests for LLM API calls.
2976///
3077/// Enables dependency injection for testing without hitting real HTTP endpoints.
@@ -74,6 +121,10 @@ impl HttpClient for ReqwestHttpClient {
74121 headers : Vec < ( & str , & str ) > ,
75122 body : & serde_json:: Value ,
76123 ) -> Result < HttpResponse > {
124+ let start = std:: time:: Instant :: now ( ) ;
125+ let request_body = serde_json:: to_string ( body) . unwrap_or_default ( ) ;
126+ let request_bytes = request_body. len ( ) as u64 ;
127+
77128 tracing:: debug!(
78129 "HTTP POST to {}: {}" ,
79130 url,
@@ -92,9 +143,24 @@ impl HttpClient for ReqwestHttpClient {
92143 . context ( format ! ( "Failed to send request to {}" , url) ) ?;
93144
94145 let status = response. status ( ) . as_u16 ( ) ;
95- let body = response. text ( ) . await ?;
96-
97- Ok ( HttpResponse { status, body } )
146+ let response_body = response. text ( ) . await ?;
147+ let response_bytes = response_body. len ( ) as u64 ;
148+ let duration_ms = start. elapsed ( ) . as_secs_f64 ( ) * 1000.0 ;
149+
150+ maybe_record_metrics ( HttpMetricsRecord {
151+ url : url. to_string ( ) ,
152+ method : "POST" . to_string ( ) ,
153+ status,
154+ duration_ms,
155+ request_bytes,
156+ response_bytes,
157+ streaming : false ,
158+ } ) ;
159+
160+ Ok ( HttpResponse {
161+ status,
162+ body : response_body,
163+ } )
98164 }
99165
100166 async fn post_streaming (
@@ -103,6 +169,10 @@ impl HttpClient for ReqwestHttpClient {
103169 headers : Vec < ( & str , & str ) > ,
104170 body : & serde_json:: Value ,
105171 ) -> Result < StreamingHttpResponse > {
172+ let start = std:: time:: Instant :: now ( ) ;
173+ let request_body = serde_json:: to_string ( body) . unwrap_or_default ( ) ;
174+ let request_bytes = request_body. len ( ) as u64 ;
175+
106176 let mut request = self . client . post ( url) ;
107177 for ( key, value) in headers {
108178 request = request. header ( key, value) ;
@@ -121,6 +191,19 @@ impl HttpClient for ReqwestHttpClient {
121191 . and_then ( |v| v. to_str ( ) . ok ( ) )
122192 . map ( String :: from) ;
123193
194+ // For streaming, we record metrics after sending but before consuming the stream
195+ // Note: response_bytes is estimated as we can't know the full stream size upfront
196+ let duration_ms = start. elapsed ( ) . as_secs_f64 ( ) * 1000.0 ;
197+ maybe_record_metrics ( HttpMetricsRecord {
198+ url : url. to_string ( ) ,
199+ method : "POST" . to_string ( ) ,
200+ status,
201+ duration_ms,
202+ request_bytes,
203+ response_bytes : 0 , // Unknown for streaming
204+ streaming : true ,
205+ } ) ;
206+
124207 if ( 200 ..300 ) . contains ( & status) {
125208 let byte_stream = response
126209 . bytes_stream ( )
0 commit comments