Skip to content

Commit e292cdb

Browse files
committed
feat(logs): capture request/response payloads and redesign log viewer
Backend: - Extend request_logs schema with method, path, request_headers, request_body, response_headers, response_body (SQLite + Postgres migrations via ensure_request_log_column / ALTER TABLE IF NOT EXISTS) - Capture ingress method/path/headers/body across universal, gemini and embeddings proxy entrypoints; aggregate final JSON for streaming responses and persist as response_body - Emit logs on all early-exit error paths (decode failure, no route, auth failure, upstream error, cache miss fallbacks) with full context - Cache-hit paths now include complete request/response bodies - Embeddings proxy: log every success/error branch and parse usage.prompt_tokens into input_tokens - Split API: query_logs list strips heavy fields (NULL bodies/headers); new get_log(id) endpoint fetches full payload on demand - Settings: DEFAULT_RETENTION_DAYS 30 -> 7, batch size 64 -> 32, cleanup interval 60s -> 600s; new log_record_payloads toggle (default true) to disable payload persistence Frontend: - Compact 7-column log list (Time / Status / Model / Protocol / Latency / Token / Type), left-aligned, row click opens detail - New LogDetailDialog with meta header and four copy-enabled payload blocks (request headers/body, response headers/body) using lazy get_log fetch and pretty-printed JSON - Token display with IN/OUT labels and K/M formatting (<1000 raw, <1M 1-decimal K, >=1M 2-decimal M) - Type badge: green SSE / sky JSON replaces boolean stream column - Settings: split Log Configuration into its own half-width card next to Proxy Configuration; rename to "Retention Period" + "Record Payloads" with HelpCircle tooltips; tighten Config Backup layout
1 parent d141afa commit e292cdb

File tree

17 files changed

+1308
-275
lines changed

17 files changed

+1308
-275
lines changed

crates/nyro-core/src/admin/mod.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -751,6 +751,10 @@ impl AdminService {
751751
self.gw.storage.logs().query(q).await
752752
}
753753

754+
pub async fn get_log(&self, id: &str) -> anyhow::Result<Option<RequestLog>> {
755+
self.gw.storage.logs().find_by_id(id).await
756+
}
757+
754758
// ── Stats ──
755759

756760
fn normalize_hours(hours: Option<i32>) -> Option<i32> {

crates/nyro-core/src/db/mod.rs

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,12 @@ pub async fn migrate(pool: &SqlitePool, vector_dimensions: usize) -> anyhow::Res
5858
ensure_route_column(pool, "cache_semantic_ttl", "INTEGER").await?;
5959
ensure_route_column(pool, "cache_semantic_threshold", "REAL").await?;
6060
ensure_request_log_column(pool, "api_key_id", "TEXT").await?;
61+
ensure_request_log_column(pool, "method", "TEXT").await?;
62+
ensure_request_log_column(pool, "path", "TEXT").await?;
63+
ensure_request_log_column(pool, "request_headers", "TEXT").await?;
64+
ensure_request_log_column(pool, "request_body", "TEXT").await?;
65+
ensure_request_log_column(pool, "response_headers", "TEXT").await?;
66+
ensure_request_log_column(pool, "response_body", "TEXT").await?;
6167
ensure_api_key_tables(pool).await?;
6268
ensure_api_key_column(pool, "rpd", "INTEGER").await?;
6369
// Migrate: providers/routes is_active -> is_enabled
@@ -478,7 +484,13 @@ CREATE TABLE IF NOT EXISTS request_logs (
478484
is_stream INTEGER DEFAULT 0,
479485
is_tool_call INTEGER DEFAULT 0,
480486
error_message TEXT,
481-
response_preview TEXT
487+
response_preview TEXT,
488+
method TEXT,
489+
path TEXT,
490+
request_headers TEXT,
491+
request_body TEXT,
492+
response_headers TEXT,
493+
response_body TEXT
482494
);
483495
484496
CREATE INDEX IF NOT EXISTS idx_logs_created_at ON request_logs(created_at);

crates/nyro-core/src/db/models.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,16 @@ pub struct RequestLog {
160160
pub is_tool_call: bool,
161161
pub error_message: Option<String>,
162162
pub response_preview: Option<String>,
163+
pub method: Option<String>,
164+
pub path: Option<String>,
165+
#[serde(skip_serializing_if = "Option::is_none")]
166+
pub request_headers: Option<String>,
167+
#[serde(skip_serializing_if = "Option::is_none")]
168+
pub request_body: Option<String>,
169+
#[serde(skip_serializing_if = "Option::is_none")]
170+
pub response_headers: Option<String>,
171+
#[serde(skip_serializing_if = "Option::is_none")]
172+
pub response_body: Option<String>,
163173
}
164174

165175
#[derive(Debug, Clone, Serialize, Deserialize)]

crates/nyro-core/src/logging/mod.rs

Lines changed: 36 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,10 @@ use tokio::sync::mpsc;
33
use crate::protocol::types::TokenUsage;
44
use crate::storage::DynStorage;
55

6-
const DEFAULT_RETENTION_DAYS: i64 = 30;
6+
const DEFAULT_RETENTION_DAYS: i64 = 7;
7+
const DEFAULT_RECORD_PAYLOADS: bool = true;
8+
pub const LOG_RECORD_PAYLOADS_KEY: &str = "log_record_payloads";
9+
pub const LOG_RETENTION_DAYS_KEY: &str = "log_retention_days";
710

811
#[derive(Debug, Clone)]
912
pub struct LogEntry {
@@ -20,18 +23,24 @@ pub struct LogEntry {
2023
pub is_tool_call: bool,
2124
pub error_message: Option<String>,
2225
pub response_preview: Option<String>,
26+
pub method: Option<String>,
27+
pub path: Option<String>,
28+
pub request_headers: Option<String>,
29+
pub request_body: Option<String>,
30+
pub response_headers: Option<String>,
31+
pub response_body: Option<String>,
2332
}
2433

2534
pub async fn run_collector(mut rx: mpsc::Receiver<LogEntry>, storage: DynStorage) {
26-
let mut buffer: Vec<LogEntry> = Vec::with_capacity(64);
35+
let mut buffer: Vec<LogEntry> = Vec::with_capacity(32);
2736
let mut flush_interval = tokio::time::interval(std::time::Duration::from_secs(2));
28-
let mut cleanup_interval = tokio::time::interval(std::time::Duration::from_secs(3600));
37+
let mut cleanup_interval = tokio::time::interval(std::time::Duration::from_secs(600));
2938

3039
loop {
3140
tokio::select! {
3241
Some(entry) = rx.recv() => {
3342
buffer.push(entry);
34-
if buffer.len() >= 64 {
43+
if buffer.len() >= 32 {
3544
flush(storage.clone(), &mut buffer).await;
3645
}
3746
}
@@ -50,7 +59,7 @@ pub async fn run_collector(mut rx: mpsc::Receiver<LogEntry>, storage: DynStorage
5059
async fn cleanup_old_logs(storage: DynStorage) {
5160
let days = storage
5261
.settings()
53-
.get("log_retention_days")
62+
.get(LOG_RETENTION_DAYS_KEY)
5463
.await
5564
.ok()
5665
.flatten()
@@ -65,7 +74,27 @@ async fn cleanup_old_logs(storage: DynStorage) {
6574
}
6675
}
6776

77+
async fn read_record_payloads(storage: &DynStorage) -> bool {
78+
storage
79+
.settings()
80+
.get(LOG_RECORD_PAYLOADS_KEY)
81+
.await
82+
.ok()
83+
.flatten()
84+
.map(|v| !matches!(v.to_ascii_lowercase().as_str(), "false" | "0" | "off" | "no"))
85+
.unwrap_or(DEFAULT_RECORD_PAYLOADS)
86+
}
87+
6888
async fn flush(storage: DynStorage, buffer: &mut Vec<LogEntry>) {
69-
let entries = std::mem::take(buffer);
70-
let _ = storage.logs().append_batch(entries).await;
89+
let mut entries = std::mem::take(buffer);
90+
let record_payloads = read_record_payloads(&storage).await;
91+
if !record_payloads {
92+
for entry in entries.iter_mut() {
93+
entry.request_headers = None;
94+
entry.request_body = None;
95+
entry.response_headers = None;
96+
entry.response_body = None;
97+
}
7198
}
99+
let _ = storage.logs().append_batch(entries).await;
100+
}

0 commit comments

Comments
 (0)