mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-07 02:59:05 +00:00
* feat(logger): add shared Rust diagnostics and Python logging bridge * feat(logger): dispatch diagnostic processing through Rust * chore: regenerate Cargo.lock after rebase Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * test: allowlist bounded logging tree walkers in recursive detector Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * perf(logger): skip decoding plain access arguments * test(logger): skip embedded-python logger test when litellm deps are absent Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * style: cargo fmt Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * test: expect NativeDiagnosticProcessor in the native public surface Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * fix(stub): export NativeDiagnosticProcessor via __new__ in _native.pyi Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * refactor(tracing): rename logger crate and document host sink contract * test(logger): cover exc, stack, and nested extras in the diagnostic filter Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * fix(logger): keep rendered redacted line when template scan flags a key pattern The blanket REDACTED for a changed msg/color template discarded lines whose rendered form was already redacted by the same pipeline, e.g. 'password=%s' became 'REDACTED' instead of 'password=REDACTED'. Only fall back to REDACTED when the rendered form did not change either, which is where interpolation can mangle the key pattern the scrub would otherwise see. Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * ci(rust): install python deps so the logger bridge test runs The end-to-end bridge test skipped silently when litellm's Python deps were absent. uv sync --no-install-project installs them without a maturin build, and PYTHONPATH makes them visible to the embedded interpreter Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --------- Co-authored-by: Yujong Lee <yujong@berri.ai> Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
122 lines
3.5 KiB
Rust
122 lines
3.5 KiB
Rust
use std::sync::{
|
|
Arc,
|
|
atomic::{AtomicBool, Ordering},
|
|
mpsc,
|
|
};
|
|
|
|
use litellm_tracing::{Level, Logger, Metadata, Record, Sink, info, warn};
|
|
use serde_json::{Value, json};
|
|
|
|
struct Output {
|
|
enabled: Arc<AtomicBool>,
|
|
sender: mpsc::Sender<(String, Value, Level, &'static str, Option<u32>)>,
|
|
}
|
|
|
|
impl Sink for Output {
|
|
fn enabled(&self, _: &Metadata<'_>) -> bool {
|
|
self.enabled.load(Ordering::Relaxed)
|
|
}
|
|
|
|
fn emit(&self, record: &Record) {
|
|
self.sender
|
|
.send((
|
|
record.message.clone(),
|
|
Value::Object(record.fields.clone()),
|
|
*record.metadata.level(),
|
|
record.metadata.target(),
|
|
record.metadata.line(),
|
|
))
|
|
.unwrap();
|
|
Logger::default().scope(|| warn!("a sink must not recursively emit"));
|
|
}
|
|
}
|
|
|
|
fn emit() {
|
|
warn!(
|
|
attempt = 3_u64,
|
|
elapsed = 1.5,
|
|
retry = true,
|
|
reason = "timeout",
|
|
"retry {}",
|
|
3
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn records_preserve_fields_metadata_and_dynamic_filtering_without_recursion() {
|
|
let (sender, receiver) = mpsc::channel();
|
|
let enabled = Arc::new(AtomicBool::new(false));
|
|
let logger = Logger::new(Output {
|
|
enabled: enabled.clone(),
|
|
sender,
|
|
});
|
|
logger.scope(emit);
|
|
assert!(receiver.try_recv().is_err());
|
|
enabled.store(true, Ordering::Relaxed);
|
|
logger.scope(emit);
|
|
let (message, fields, level, target, line) = receiver.try_recv().unwrap();
|
|
assert_eq!(message, "retry 3");
|
|
assert_eq!(
|
|
fields,
|
|
json!({"attempt": 3, "elapsed": 1.5, "retry": true, "reason": "timeout"})
|
|
);
|
|
assert_eq!(level, Level::WARN);
|
|
assert_eq!(target, module_path!());
|
|
assert!(line.is_some());
|
|
enabled.store(false, Ordering::Relaxed);
|
|
logger.scope(emit);
|
|
assert!(receiver.try_recv().is_err());
|
|
}
|
|
|
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
async fn concurrent_futures_keep_their_sinks_across_suspension_and_spawn() {
|
|
let tasks = (0..2)
|
|
.map(|id| {
|
|
let (sender, receiver) = mpsc::channel();
|
|
let logger = Logger::new(Output {
|
|
enabled: Arc::new(AtomicBool::new(true)),
|
|
sender,
|
|
});
|
|
let task = tokio::spawn(logger.instrument(async move {
|
|
tokio::task::yield_now().await;
|
|
info!(id, "worker");
|
|
}));
|
|
(id, task, receiver)
|
|
})
|
|
.collect::<Vec<_>>();
|
|
for (id, task, receiver) in tasks {
|
|
task.await.unwrap();
|
|
let (message, fields, level, _, _) = receiver.try_recv().unwrap();
|
|
assert_eq!(message, "worker");
|
|
assert_eq!(fields, json!({"id": id}));
|
|
assert_eq!(level, Level::INFO);
|
|
assert!(receiver.try_recv().is_err());
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn nested_scopes_restore_the_previous_sink() {
|
|
let (outer_sender, outer) = mpsc::channel();
|
|
let (inner_sender, inner) = mpsc::channel();
|
|
let logger = |sender| {
|
|
Logger::new(Output {
|
|
enabled: Arc::new(AtomicBool::new(true)),
|
|
sender,
|
|
})
|
|
};
|
|
let outside = logger(outer_sender);
|
|
let inside = logger(inner_sender);
|
|
outside.scope(|| {
|
|
info!("before");
|
|
inside.scope(|| info!("inside"));
|
|
info!("after");
|
|
});
|
|
assert_eq!(
|
|
outer.try_iter().map(|event| event.0).collect::<Vec<_>>(),
|
|
["before", "after"]
|
|
);
|
|
assert_eq!(
|
|
inner.try_iter().map(|event| event.0).collect::<Vec<_>>(),
|
|
["inside"]
|
|
);
|
|
}
|