use std::fmt;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum Outcome {
Served,
Unauthenticated,
NotFound,
Malformed,
Unavailable,
}
impl Outcome {
#[must_use]
pub fn as_str(self) -> &'static str {
match self {
Self::Served => "served",
Self::Unauthenticated => "unauthenticated",
Self::NotFound => "not-found",
Self::Malformed => "malformed",
Self::Unavailable => "unavailable",
}
}
}
#[derive(Debug, Clone)]
#[non_exhaustive]
pub struct AuditEntry {
pub caller: Option<String>,
pub application: Option<String>,
pub profile: Option<String>,
pub endpoint: &'static str,
pub outcome: Outcome,
pub generation: Option<u64>,
}
impl fmt::Display for AuditEntry {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
fn field(value: Option<&String>) -> &str {
value.map_or("-", String::as_str)
}
write!(
f,
"audit caller={} application={} profile={} endpoint={} outcome={} generation={}",
field(self.caller.as_ref()),
field(self.application.as_ref()),
field(self.profile.as_ref()),
self.endpoint,
self.outcome.as_str(),
self.generation
.map_or_else(|| "-".to_owned(), |it| it.to_string()),
)
}
}
pub trait AuditSink: Send + Sync + 'static {
fn record(&self, entry: &AuditEntry);
}
#[derive(Debug, Clone, Copy, Default)]
pub struct StderrAudit;
impl AuditSink for StderrAudit {
fn record(&self, entry: &AuditEntry) {
eprintln!("{entry}");
}
}
#[derive(Debug, Clone, Copy, Default)]
pub struct NoAudit;
impl AuditSink for NoAudit {
fn record(&self, _entry: &AuditEntry) {}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_line_renders_every_field_and_dashes_the_absent_ones() {
let entry = AuditEntry {
caller: Some("billing-pod".to_owned()),
application: Some("billing".to_owned()),
profile: Some("prod".to_owned()),
endpoint: "document",
outcome: Outcome::Served,
generation: Some(3),
};
assert_eq!(
entry.to_string(),
"audit caller=billing-pod application=billing profile=prod endpoint=document \
outcome=served generation=3"
);
let entry = AuditEntry {
caller: None,
application: None,
profile: None,
endpoint: "document",
outcome: Outcome::Unauthenticated,
generation: None,
};
assert_eq!(
entry.to_string(),
"audit caller=- application=- profile=- endpoint=document \
outcome=unauthenticated generation=-"
);
}
}