use std::time::Duration;
use serde_json::Value;
use tokio::runtime::Handle;
use super::EventObject;
use super::redact::{apply_event_redaction, redact_strings, visit_strings};
use super::sanitize::sanitize_event;
use super::settle::{apply, failure_message, settle};
use super::truncate::truncate_event;
use crate::config::{EventRedactor, HookError, StringRedactor};
use crate::event::Event;
use crate::event::assemble::{EventJob, render_backtrace};
use crate::handles::new_event_id;
use crate::hooks::AbortOnDrop;
#[cfg(test)]
mod tests;
const NO_RUNTIME: &str = "no tokio runtime";
#[derive(Clone, Debug, Default)]
pub(crate) struct Redactors {
pub(crate) string: Option<StringRedactor>,
pub(crate) event: Option<EventRedactor>,
}
pub(crate) async fn process(
job: EventJob,
redactors: &Redactors,
hook_budget: Duration,
log: &mut Vec<String>,
) -> Option<EventObject> {
process_with_ids(job, redactors, hook_budget, new_event_id, log).await
}
async fn process_with_ids(
job: EventJob,
redactors: &Redactors,
hook_budget: Duration,
new_id: fn() -> Option<String>,
log: &mut Vec<String>,
) -> Option<EventObject> {
let EventJob {
mut event,
hooks,
hook_mode,
runtime,
string_redaction,
event_redaction,
backtrace,
} = job;
if let Some(backtrace) = &backtrace {
render_backtrace(&mut event, backtrace);
}
let results = settle(hooks, hook_budget, log).await;
if let Err(error) = apply(&mut event, results, hook_mode) {
log.push(format!("Failed to resolve pending hook results: {error}"));
}
if event_redaction && let Some(hook) = &redactors.event {
match redact_event(hook, &event, runtime.as_ref()).await {
Ok(result) => {
let Some(redacted) = apply_event_redaction(&event, result.map(Event::into_object))
else {
log.push("Event dropped by redact_event hook".to_owned());
return None;
};
event = redacted;
}
Err(why) => {
log.push(format!("Failed to redact event (event-level hook): {why}"));
return None;
}
}
}
if string_redaction && let Some(hook) = &redactors.string {
match redact_event_strings(hook, &event, runtime.as_ref()).await {
Ok(redacted) => assign(&mut event, redacted),
Err(why) => {
log.push(format!("Failed to redact event: {why}"));
return None;
}
}
}
let sanitized = sanitize_event(&event);
assign(&mut event, sanitized);
event = match truncate_event(event) {
Ok(truncated) => truncated,
Err(error) => {
log.push(format!("Failed to truncate event: {error}"));
return None;
}
};
let has_id = event
.get("id")
.and_then(Value::as_str)
.is_some_and(|id| !id.is_empty());
if !has_id {
let Some(id) = new_id() else {
log.push(
"Failed to deliver AgentCat event after retries: could not generate an event ID"
.to_owned(),
);
return None;
};
event.insert("id".to_owned(), Value::String(id));
}
Some(event)
}
pub(crate) fn assign(event: &mut EventObject, stage: EventObject) {
event.extend(stage);
}
async fn redact_event(
hook: &EventRedactor,
event: &EventObject,
runtime: Option<&Handle>,
) -> Result<Option<Event>, String> {
let runtime = runtime.ok_or_else(|| NO_RUNTIME.to_owned())?;
match AbortOnDrop::new(runtime.spawn(hook.call(Event::from_object(event)))).await {
Ok(result) => result.map_err(|error| error.to_string()),
Err(error) => Err(failure_message(error)),
}
}
async fn redact_event_strings(
hook: &StringRedactor,
event: &EventObject,
runtime: Option<&Handle>,
) -> Result<EventObject, String> {
let runtime = runtime.ok_or_else(|| NO_RUNTIME.to_owned())?;
let mut strings = Vec::new();
visit_strings(event, |text| strings.push(text.to_owned()));
let hook = hook.clone();
let task = AbortOnDrop::new(runtime.spawn(async move {
let mut redacted = Vec::with_capacity(strings.len());
for text in strings {
redacted.push(hook.call(text).await?);
}
Ok::<_, HookError>(redacted)
}));
let redacted = match task.await {
Ok(result) => result.map_err(|error| error.to_string())?,
Err(error) => return Err(failure_message(error)),
};
let mut redacted = redacted.into_iter();
redact_strings(event, |_| {
redacted
.next()
.ok_or_else(|| "the event changed during redaction".to_owned())
})
}