mirror of
https://github.com/hanzoai/dev.git
synced 2026-10-25 03:32:32 +00:00
feat(core/debug): capture headers and order usage logs
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
use std::collections::BTreeMap;
|
||||
use std::time::Duration;
|
||||
|
||||
use bytes::Bytes;
|
||||
@@ -7,6 +8,7 @@ use futures::Stream;
|
||||
use futures::StreamExt;
|
||||
use futures::TryStreamExt;
|
||||
use reqwest::StatusCode;
|
||||
use reqwest::header::HeaderMap;
|
||||
use serde_json::json;
|
||||
use std::pin::Pin;
|
||||
use std::task::Context;
|
||||
@@ -344,17 +346,9 @@ pub(crate) async fn stream_chat_completions(
|
||||
serde_json::to_string_pretty(&payload).unwrap_or_default()
|
||||
);
|
||||
|
||||
// Start logging the request and get a request_id to track the response
|
||||
let request_id = if let Ok(logger) = debug_logger.lock() {
|
||||
logger
|
||||
.start_request_log(&endpoint, &payload, log_tag)
|
||||
.unwrap_or_default()
|
||||
} else {
|
||||
String::new()
|
||||
};
|
||||
|
||||
let mut attempt = 0;
|
||||
let max_retries = provider.request_max_retries();
|
||||
let mut request_id = String::new();
|
||||
loop {
|
||||
attempt += 1;
|
||||
|
||||
@@ -369,11 +363,30 @@ pub(crate) async fn stream_chat_completions(
|
||||
}
|
||||
}
|
||||
|
||||
let res = req_builder
|
||||
req_builder = req_builder
|
||||
.header(reqwest::header::ACCEPT, "text/event-stream")
|
||||
.json(&payload)
|
||||
.send()
|
||||
.await;
|
||||
.json(&payload);
|
||||
|
||||
if request_id.is_empty() {
|
||||
let endpoint_for_log = provider.get_full_url(&auth);
|
||||
let header_snapshot = req_builder
|
||||
.try_clone()
|
||||
.and_then(|builder| builder.build().ok())
|
||||
.map(|req| header_map_to_json(req.headers()));
|
||||
|
||||
if let Ok(logger) = debug_logger.lock() {
|
||||
request_id = logger
|
||||
.start_request_log(
|
||||
&endpoint_for_log,
|
||||
&payload,
|
||||
header_snapshot.as_ref(),
|
||||
log_tag,
|
||||
)
|
||||
.unwrap_or_default();
|
||||
}
|
||||
}
|
||||
|
||||
let res = req_builder.send().await;
|
||||
|
||||
match res {
|
||||
Ok(resp) if resp.status().is_success() => {
|
||||
@@ -1097,3 +1110,13 @@ impl<S> AggregatedChatStream<S> {
|
||||
Self::new(inner, AggregateMode::Streaming)
|
||||
}
|
||||
}
|
||||
|
||||
fn header_map_to_json(headers: &HeaderMap) -> serde_json::Value {
|
||||
let mut ordered: BTreeMap<String, Vec<String>> = BTreeMap::new();
|
||||
for (name, value) in headers.iter() {
|
||||
let entry = ordered.entry(name.as_str().to_string()).or_default();
|
||||
entry.push(value.to_str().unwrap_or_default().to_string());
|
||||
}
|
||||
|
||||
serde_json::to_value(ordered).unwrap_or(serde_json::Value::Null)
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use std::collections::BTreeMap;
|
||||
use std::io::BufRead;
|
||||
use std::path::Path;
|
||||
// use std::sync::OnceLock;
|
||||
@@ -464,6 +465,7 @@ impl ModelClient {
|
||||
|
||||
let mut attempt = 0;
|
||||
let max_retries = self.provider.request_max_retries();
|
||||
let mut request_id = String::new();
|
||||
|
||||
// Compute endpoint with the latest available auth (may be None at this point).
|
||||
let endpoint = self
|
||||
@@ -475,15 +477,6 @@ impl ModelClient {
|
||||
serde_json::to_string(&payload_json)?
|
||||
);
|
||||
|
||||
// Start logging the request and get a request_id to track the response
|
||||
let request_id = if let Ok(logger) = self.debug_logger.lock() {
|
||||
logger
|
||||
.start_request_log(&endpoint, &payload_json, log_tag)
|
||||
.unwrap_or_default()
|
||||
} else {
|
||||
String::new()
|
||||
};
|
||||
|
||||
loop {
|
||||
attempt += 1;
|
||||
|
||||
@@ -521,6 +514,25 @@ impl ModelClient {
|
||||
req_builder = req_builder.header("chatgpt-account-id", account_id);
|
||||
}
|
||||
|
||||
if request_id.is_empty() {
|
||||
let endpoint_for_log = self.provider.get_full_url(&auth);
|
||||
let header_snapshot = req_builder
|
||||
.try_clone()
|
||||
.and_then(|builder| builder.build().ok())
|
||||
.map(|req| header_map_to_json(req.headers()));
|
||||
|
||||
if let Ok(logger) = self.debug_logger.lock() {
|
||||
request_id = logger
|
||||
.start_request_log(
|
||||
&endpoint_for_log,
|
||||
&payload_json,
|
||||
header_snapshot.as_ref(),
|
||||
log_tag,
|
||||
)
|
||||
.unwrap_or_default();
|
||||
}
|
||||
}
|
||||
|
||||
let res = if let Some(otel) = self.otel_event_manager.as_ref() {
|
||||
otel.log_request(attempt, || req_builder.send()).await
|
||||
} else {
|
||||
@@ -910,6 +922,16 @@ fn parse_header_str<'a>(headers: &'a HeaderMap, name: &str) -> Option<&'a str> {
|
||||
headers.get(name)?.to_str().ok()
|
||||
}
|
||||
|
||||
fn header_map_to_json(headers: &HeaderMap) -> Value {
|
||||
let mut ordered: BTreeMap<String, Vec<String>> = BTreeMap::new();
|
||||
for (name, value) in headers.iter() {
|
||||
let entry = ordered.entry(name.as_str().to_string()).or_default();
|
||||
entry.push(value.to_str().unwrap_or_default().to_string());
|
||||
}
|
||||
|
||||
serde_json::to_value(ordered).unwrap_or(Value::Null)
|
||||
}
|
||||
|
||||
async fn process_sse<S>(
|
||||
stream: S,
|
||||
tx_event: mpsc::Sender<Result<ResponseEvent>>,
|
||||
|
||||
@@ -89,6 +89,7 @@ impl DebugLogger {
|
||||
&self,
|
||||
endpoint: &str,
|
||||
payload: &Value,
|
||||
headers: Option<&Value>,
|
||||
tag: Option<&str>,
|
||||
) -> Result<String, std::io::Error> {
|
||||
if !self.enabled {
|
||||
@@ -109,13 +110,17 @@ impl DebugLogger {
|
||||
let request_file_path = log_dir.join(request_filename);
|
||||
|
||||
// Create request object with metadata
|
||||
let request_entry = serde_json::json!({
|
||||
let mut request_entry = serde_json::json!({
|
||||
"timestamp": timestamp.to_rfc3339(),
|
||||
"request_id": request_id,
|
||||
"endpoint": endpoint,
|
||||
"payload": payload
|
||||
});
|
||||
|
||||
if let (Some(headers), Some(obj)) = (headers, request_entry.as_object_mut()) {
|
||||
obj.insert("headers".to_string(), headers.clone());
|
||||
}
|
||||
|
||||
// Write pretty-printed JSON to request file
|
||||
let formatted_request = serde_json::to_string_pretty(&request_entry)?;
|
||||
fs::write(&request_file_path, formatted_request)?;
|
||||
@@ -234,9 +239,15 @@ impl DebugLogger {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let path = self
|
||||
.usage_dir
|
||||
.join(format!("{}_usage.json", session_id));
|
||||
let session_id_str = session_id.to_string();
|
||||
let path = if let Some(existing) = self.find_existing_usage_log(&session_id_str) {
|
||||
existing
|
||||
} else {
|
||||
let timestamp = Local::now().format("%Y%m%d_%H%M%S%.3f");
|
||||
self
|
||||
.usage_dir
|
||||
.join(format!("{}_{}_usage.json", timestamp, session_id_str))
|
||||
};
|
||||
|
||||
if let Some(parent) = path.parent() {
|
||||
fs::create_dir_all(parent)?;
|
||||
@@ -255,6 +266,25 @@ impl DebugLogger {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn find_existing_usage_log(&self, session_id: &str) -> Option<PathBuf> {
|
||||
if let Ok(entries) = fs::read_dir(&self.usage_dir) {
|
||||
for entry in entries.flatten() {
|
||||
let path = entry.path();
|
||||
if !path.is_file() {
|
||||
continue;
|
||||
}
|
||||
if let Some(name) = path.file_name().and_then(|n| n.to_str()) {
|
||||
if name == format!("{}_usage.json", session_id)
|
||||
|| name.ends_with(&format!("_{}_usage.json", session_id))
|
||||
{
|
||||
return Some(path);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
// Legacy methods for backward compatibility - they now create standalone files
|
||||
pub fn log_request(
|
||||
&self,
|
||||
|
||||
Reference in New Issue
Block a user