use std::future::Future;
use std::sync::Arc;
use std::time::{Duration, Instant};
use serde_json::{Map, Value, json};
use crate::config::Config;
use crate::cost::Cost;
use crate::error::Error;
use crate::message::UsageEntry;
use crate::tokens::Tokens;
pub trait Instrumenter: Send + Sync {
fn instrument(&self, name: &str, payload: &Map<String, Value>, duration: Option<Duration>);
}
impl<F> Instrumenter for F
where
F: Fn(&str, &Map<String, Value>, Option<Duration>) + Send + Sync,
{
fn instrument(&self, name: &str, payload: &Map<String, Value>, duration: Option<Duration>) {
self(name, payload, duration)
}
}
impl std::fmt::Debug for dyn Instrumenter {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str("Instrumenter")
}
}
tokio::task_local! {
static WORKFLOW: Arc<Map<String, Value>>;
}
pub fn current_workflow() -> Option<Map<String, Value>> {
WORKFLOW.try_with(|context| (**context).clone()).ok()
}
pub(crate) async fn with_workflow<F: Future>(context: Map<String, Value>, future: F) -> F::Output {
WORKFLOW.scope(Arc::new(context), future).await
}
pub struct Event {
name: String,
payload: Option<Map<String, Value>>,
config: Arc<Config>,
started: Instant,
span: tracing::Span,
otel: crate::open_telemetry::Span,
}
impl Event {
pub fn start(
config: &Arc<Config>,
name: &str,
payload: impl FnOnce() -> Map<String, Value>,
) -> Event {
let payload = (config.instrumenter.is_some() || crate::open_telemetry::is_traced(name))
.then(|| {
let mut payload = payload();
payload.extend(current_workflow().unwrap_or_default());
payload
});
let otel = crate::open_telemetry::Span::start(name, payload.as_ref());
Event {
name: name.to_string(),
payload,
config: config.clone(),
started: Instant::now(),
span: tracing::debug_span!("rust_llm", event = name),
otel,
}
}
pub fn is_enabled(&self) -> bool {
self.payload.is_some()
}
pub fn set(&mut self, key: &str, value: impl FnOnce() -> Value) {
if let Some(payload) = &mut self.payload {
payload.insert(key.to_string(), value());
}
}
pub fn span(&self) -> tracing::Span {
self.span.clone()
}
pub fn instrument<F: Future>(&self, future: F) -> impl Future<Output = F::Output> + use<F> {
self.otel
.instrument(tracing::Instrument::instrument(future, self.span.clone()))
}
pub fn finish(mut self, error: Option<&Error>) {
if let Some(e) = error {
self.set("exception", || {
json!([format!("{:?}", e.kind()), e.to_string()])
});
}
self.otel.finish(self.payload.as_ref(), error);
let elapsed = self.started.elapsed();
self.emit(Some(elapsed));
}
fn emit(self, duration: Option<Duration>) {
tracing::debug!(parent: &self.span, event = %self.name, duration_ms = duration.map(|d| d.as_secs_f64() * 1000.0), "rust_llm event");
if let (Some(instrumenter), Some(payload)) = (&self.config.instrumenter, &self.payload) {
Instrumenter::instrument(&**instrumenter, &self.name, payload, duration);
}
}
}
pub async fn instrument<T>(
config: &Arc<Config>,
name: &str,
payload: Map<String, Value>,
future: impl Future<Output = crate::Result<T>>,
) -> crate::Result<T> {
let event = Event::start(config, name, || payload);
let result = event.instrument(future).await;
event.finish(result.as_ref().err());
result
}
pub fn instrument_event(config: &Arc<Config>, name: &str, payload: Map<String, Value>) {
Event::start(config, name, || payload).emit(None);
}
pub(crate) fn usage(config: &Arc<Config>, entry: &UsageEntry) {
crate::evaluation::capture_usage(entry);
if config.instrumenter.is_none() {
return;
}
let mut payload = Map::new();
payload.insert("operation".into(), entry.operation.as_str().into());
payload.insert("provider".into(), entry.provider.clone().into());
payload.insert("model".into(), entry.model.clone().into());
payload.insert("status".into(), entry.status.as_str().into());
payload.insert("tokens".into(), tokens_h(&entry.tokens));
payload.insert("cost".into(), cost_h(&entry.cost));
payload.insert(
"owner".into(),
entry
.owner
.as_ref()
.map_or(Value::Null, crate::accounting::UsageOwner::to_value),
);
instrument_event(config, "usage.rust_llm", payload);
}
pub(crate) fn metadata(metadata: &Option<Value>) -> Value {
metadata.clone().unwrap_or(Value::Null)
}
pub(crate) fn tokens_h(tokens: &Tokens) -> Value {
let mut h = Map::new();
for (key, value) in [
("input_tokens", tokens.input),
("output_tokens", tokens.output),
("cache_read_tokens", tokens.cache_read),
("cache_write_tokens", tokens.cache_write),
("thinking_tokens", tokens.thinking),
] {
if let Some(v) = value {
h.insert(key.into(), v.into());
}
}
if let Some(s) = &tokens.server_tool_use {
h.insert("server_tool_use".into(), Value::Object(s.clone()));
}
Value::Object(h)
}
pub(crate) fn cost_h(cost: &Cost) -> Value {
let mut h = Map::new();
for (key, value) in [
("input", cost.input),
("output", cost.output),
("cache_read", cost.cache_read),
("cache_write", cost.cache_write),
("thinking", cost.thinking),
("total", cost.total()),
] {
if let Some(v) = value {
h.insert(key.into(), v.into());
}
}
Value::Object(h)
}
pub(crate) fn payload<const N: usize>(pairs: [(&str, Value); N]) -> Map<String, Value> {
pairs.into_iter().map(|(k, v)| (k.to_string(), v)).collect()
}