Skip to content

Commit d238cb6

Browse files
committed
feat(logging): add sensitive diagnostic log preference
1 parent 31690bb commit d238cb6

17 files changed

Lines changed: 689 additions & 54 deletions

File tree

src/apps/desktop/src/logging.rs

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,12 @@ use chrono::Local;
55
use serde::Serialize;
66
use std::path::PathBuf;
77
use std::sync::{
8-
atomic::{AtomicU8, Ordering},
98
OnceLock,
9+
atomic::{AtomicU8, Ordering},
1010
};
1111
use std::thread;
12-
use tauri::{plugin::TauriPlugin, Runtime};
13-
use tauri_plugin_log::{fern, RotationStrategy, Target, TargetKind, TimezoneStrategy};
12+
use tauri::{Runtime, plugin::TauriPlugin};
13+
use tauri_plugin_log::{RotationStrategy, Target, TargetKind, TimezoneStrategy, fern};
1414

1515
const SESSION_DIR_PATTERN: &str = r"^\d{8}T\d{6}$";
1616
const MAX_LOG_SESSIONS: usize = 10;
@@ -309,6 +309,17 @@ pub fn build_log_plugin<R: Runtime>(log_targets: Vec<Target>) -> TauriPlugin<R>
309309
.level_for("opentelemetry_sdk", log::LevelFilter::Off)
310310
.level_for("opentelemetry-otlp", log::LevelFilter::Off)
311311
.level_for("notify", log::LevelFilter::Off)
312+
// These targets can emit hot-path trace diagnostics during event
313+
// routing. Keep debug diagnostics, warnings, and errors, but avoid
314+
// drowning useful app traces in mechanical noise.
315+
.level_for(
316+
"bitfun_core::agentic::events::queue",
317+
log::LevelFilter::Debug,
318+
)
319+
.level_for(
320+
"bitfun_core::agentic::events::router",
321+
log::LevelFilter::Debug,
322+
)
312323
.level_for("hyper_util", log::LevelFilter::Info)
313324
.level_for("h2", log::LevelFilter::Info)
314325
.level_for("portable_pty", log::LevelFilter::Info)
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
use std::sync::atomic::{AtomicBool, Ordering};
2+
3+
static INCLUDE_SENSITIVE_DIAGNOSTICS: AtomicBool = AtomicBool::new(true);
4+
5+
pub fn set_include_sensitive_diagnostics(enabled: bool) {
6+
INCLUDE_SENSITIVE_DIAGNOSTICS.store(enabled, Ordering::Relaxed);
7+
}
8+
9+
pub fn include_sensitive_diagnostics() -> bool {
10+
INCLUDE_SENSITIVE_DIAGNOSTICS.load(Ordering::Relaxed)
11+
}
12+
13+
#[cfg(test)]
14+
mod tests {
15+
use super::{include_sensitive_diagnostics, set_include_sensitive_diagnostics};
16+
17+
#[test]
18+
fn sensitive_diagnostics_can_be_toggled() {
19+
set_include_sensitive_diagnostics(true);
20+
assert!(include_sensitive_diagnostics());
21+
22+
set_include_sensitive_diagnostics(false);
23+
assert!(!include_sensitive_diagnostics());
24+
25+
set_include_sensitive_diagnostics(true);
26+
}
27+
}

src/crates/ai-adapters/src/lib.rs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
#![doc = include_str!("../README.md")]
22

33
pub mod client;
4+
pub mod diagnostics;
45
pub mod providers;
56
pub mod stream;
67
pub mod tool_call_accumulator;
@@ -9,7 +10,7 @@ pub mod types;
910
pub use client::{AIClient, StreamOptions, StreamResponse};
1011
pub use stream::{UnifiedResponse, UnifiedTokenUsage, UnifiedToolCall};
1112
pub use types::{
12-
resolve_request_url, AIConfig, ConnectionTestMessageCode, ConnectionTestResult, GeminiResponse,
13-
GeminiUsage, Message, ProxyConfig, ReasoningMode, RemoteModelInfo, ToolCall, ToolDefinition,
14-
ToolImageAttachment,
13+
AIConfig, ConnectionTestMessageCode, ConnectionTestResult, GeminiResponse, GeminiUsage,
14+
Message, ProxyConfig, ReasoningMode, RemoteModelInfo, ToolCall, ToolDefinition,
15+
ToolImageAttachment, resolve_request_url,
1516
};

src/crates/ai-adapters/src/providers/shared.rs

Lines changed: 214 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1+
use crate::client::AIClient;
12
use crate::client::utils::{
23
build_request_body_subset, is_trim_custom_request_body_mode, merge_json_value,
34
};
4-
use crate::client::AIClient;
55
use reqwest::RequestBuilder;
66

77
pub(crate) fn apply_header_policy<F>(
@@ -104,12 +104,175 @@ pub(crate) fn log_extra_body_keys(
104104
);
105105
}
106106

107+
pub(crate) fn summarize_request_body_for_log(
108+
request_body: &serde_json::Value,
109+
) -> serde_json::Value {
110+
let mut summary = serde_json::Map::new();
111+
112+
if let Some(model) = request_body
113+
.get("model")
114+
.and_then(serde_json::Value::as_str)
115+
{
116+
summary.insert(
117+
"model".to_string(),
118+
serde_json::Value::String(model.to_string()),
119+
);
120+
}
121+
if let Some(stream) = request_body
122+
.get("stream")
123+
.and_then(serde_json::Value::as_bool)
124+
{
125+
summary.insert("stream".to_string(), serde_json::Value::Bool(stream));
126+
}
127+
if let Some(max_tokens) = request_body
128+
.get("max_tokens")
129+
.and_then(|value| value.as_u64())
130+
{
131+
summary.insert(
132+
"max_tokens".to_string(),
133+
serde_json::Value::Number(max_tokens.into()),
134+
);
135+
}
136+
if let Some(tool_stream) = request_body
137+
.get("tool_stream")
138+
.and_then(serde_json::Value::as_bool)
139+
{
140+
summary.insert(
141+
"tool_stream".to_string(),
142+
serde_json::Value::Bool(tool_stream),
143+
);
144+
}
145+
if let Some(system) = request_body
146+
.get("system")
147+
.and_then(serde_json::Value::as_str)
148+
{
149+
summary.insert(
150+
"system_chars".to_string(),
151+
serde_json::Value::Number((system.chars().count() as u64).into()),
152+
);
153+
}
154+
if let Some(messages) = request_body
155+
.get("messages")
156+
.and_then(serde_json::Value::as_array)
157+
{
158+
summary.insert(
159+
"message_count".to_string(),
160+
serde_json::Value::Number((messages.len() as u64).into()),
161+
);
162+
summary.insert(
163+
"messages".to_string(),
164+
serde_json::Value::Array(messages.iter().map(summarize_message_for_log).collect()),
165+
);
166+
}
167+
if let Some(tools) = request_body
168+
.get("tools")
169+
.and_then(serde_json::Value::as_array)
170+
{
171+
summary.insert(
172+
"tool_count".to_string(),
173+
serde_json::Value::Number((tools.len() as u64).into()),
174+
);
175+
}
176+
if let Some(object) = request_body.as_object() {
177+
let mut top_level_keys = object.keys().cloned().collect::<Vec<_>>();
178+
top_level_keys.sort();
179+
summary.insert(
180+
"top_level_keys".to_string(),
181+
serde_json::Value::Array(
182+
top_level_keys
183+
.into_iter()
184+
.map(serde_json::Value::String)
185+
.collect(),
186+
),
187+
);
188+
}
189+
190+
serde_json::Value::Object(summary)
191+
}
192+
193+
fn summarize_message_for_log(message: &serde_json::Value) -> serde_json::Value {
194+
let mut summary = serde_json::Map::new();
195+
let content = message.get("content");
196+
197+
if let Some(role) = message.get("role").and_then(serde_json::Value::as_str) {
198+
summary.insert(
199+
"role".to_string(),
200+
serde_json::Value::String(role.to_string()),
201+
);
202+
}
203+
if let Some(content) = content {
204+
summary.insert(
205+
"content_chars".to_string(),
206+
serde_json::Value::Number((content_text_chars(content) as u64).into()),
207+
);
208+
if let Some(items) = content.as_array() {
209+
summary.insert(
210+
"content_items".to_string(),
211+
serde_json::Value::Number((items.len() as u64).into()),
212+
);
213+
let mut content_types = items
214+
.iter()
215+
.filter_map(|item| item.get("type").and_then(serde_json::Value::as_str))
216+
.map(str::to_string)
217+
.collect::<Vec<_>>();
218+
content_types.sort();
219+
content_types.dedup();
220+
if !content_types.is_empty() {
221+
summary.insert(
222+
"content_types".to_string(),
223+
serde_json::Value::Array(
224+
content_types
225+
.into_iter()
226+
.map(serde_json::Value::String)
227+
.collect(),
228+
),
229+
);
230+
}
231+
}
232+
}
233+
234+
serde_json::Value::Object(summary)
235+
}
236+
237+
fn content_text_chars(content: &serde_json::Value) -> usize {
238+
if let Some(text) = content.as_str() {
239+
return text.chars().count();
240+
}
241+
242+
content
243+
.as_array()
244+
.map(|items| {
245+
items
246+
.iter()
247+
.filter_map(|item| item.get("text").and_then(serde_json::Value::as_str))
248+
.map(|text| text.chars().count())
249+
.sum()
250+
})
251+
.unwrap_or(0)
252+
}
253+
254+
fn should_log_full_request_body(include_sensitive_diagnostics: bool) -> bool {
255+
include_sensitive_diagnostics
256+
}
257+
107258
pub(crate) fn log_request_body(target: &str, label: &str, request_body: &serde_json::Value) {
259+
if should_log_full_request_body(crate::diagnostics::include_sensitive_diagnostics()) {
260+
log::debug!(
261+
target: target,
262+
"{}\n{}",
263+
label,
264+
serde_json::to_string_pretty(request_body)
265+
.unwrap_or_else(|_| "serialization failed".to_string())
266+
);
267+
return;
268+
}
269+
270+
let summary_label = label.trim_end_matches(':');
108271
log::debug!(
109272
target: target,
110-
"{}\n{}",
111-
label,
112-
serde_json::to_string_pretty(request_body)
273+
"{} summary:\n{}",
274+
summary_label,
275+
serde_json::to_string_pretty(&summarize_request_body_for_log(request_body))
113276
.unwrap_or_else(|_| "serialization failed".to_string())
114277
);
115278
}
@@ -146,3 +309,50 @@ pub(crate) fn collect_function_declaration_names_or_object_keys(
146309
.collect()
147310
}
148311
}
312+
313+
#[cfg(test)]
314+
mod tests {
315+
use super::should_log_full_request_body;
316+
use super::summarize_request_body_for_log;
317+
318+
#[test]
319+
fn request_body_log_summary_keeps_shape_without_message_contents() {
320+
let request_body = serde_json::json!({
321+
"model": "kimi-k2.6",
322+
"stream": true,
323+
"max_tokens": 32000,
324+
"system": "secret system context",
325+
"messages": [
326+
{ "role": "user", "content": "secret user message" },
327+
{
328+
"role": "assistant",
329+
"content": [
330+
{ "type": "text", "text": "secret assistant message" },
331+
{ "type": "tool_use", "id": "tool-1", "name": "Read" }
332+
]
333+
}
334+
]
335+
});
336+
337+
let summary = summarize_request_body_for_log(&request_body);
338+
let summary_text = serde_json::to_string(&summary).unwrap();
339+
340+
assert!(!summary_text.contains("secret system context"));
341+
assert!(!summary_text.contains("secret user message"));
342+
assert!(!summary_text.contains("secret assistant message"));
343+
assert_eq!(summary["model"], "kimi-k2.6");
344+
assert_eq!(summary["stream"], true);
345+
assert_eq!(summary["max_tokens"], 32000);
346+
assert_eq!(summary["system_chars"], 21);
347+
assert_eq!(summary["message_count"], 2);
348+
assert_eq!(summary["messages"][0]["role"], "user");
349+
assert_eq!(summary["messages"][0]["content_chars"], 19);
350+
assert_eq!(summary["messages"][1]["content_items"], 2);
351+
}
352+
353+
#[test]
354+
fn request_body_logging_keeps_full_payload_when_sensitive_diagnostics_are_enabled() {
355+
assert!(should_log_full_request_body(true));
356+
assert!(!should_log_full_request_body(false));
357+
}
358+
}

0 commit comments

Comments
 (0)