use tracing_subscriber::fmt::format::Writer;
#[derive(Debug)]
pub struct SecurityJsonFormat {
inner: tracing_subscriber::fmt::format::Format<tracing_subscriber::fmt::format::Json>,
security_target_prefix: &'static str,
}
impl SecurityJsonFormat {
#[must_use]
pub fn new(security_target_prefix: &'static str) -> Self {
Self {
inner: tracing_subscriber::fmt::format::Format::default()
.json()
.with_target(true),
security_target_prefix,
}
}
#[must_use]
pub fn default_gasket() -> Self {
Self::new("rusty_gasket_auth")
}
#[must_use]
pub const fn security_target_prefix(&self) -> &'static str {
self.security_target_prefix
}
pub fn init(self) {
use tracing_subscriber::EnvFilter;
use tracing_subscriber::prelude::*;
let filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info"));
tracing_subscriber::registry()
.with(
tracing_subscriber::fmt::layer()
.fmt_fields(tracing_subscriber::fmt::format::JsonFields::new())
.event_format(self)
.with_filter(filter),
)
.init();
tracing::info!("Initialized SecurityJsonFormat logging");
}
}
impl Default for SecurityJsonFormat {
fn default() -> Self {
Self::default_gasket()
}
}
impl<S, N> tracing_subscriber::fmt::FormatEvent<S, N> for SecurityJsonFormat
where
S: tracing::Subscriber + for<'lookup> tracing_subscriber::registry::LookupSpan<'lookup>,
N: for<'writer> tracing_subscriber::fmt::FormatFields<'writer> + 'static,
{
fn format_event(
&self,
context: &tracing_subscriber::fmt::FmtContext<'_, S, N>,
mut writer: Writer<'_>,
event: &tracing::Event<'_>,
) -> std::fmt::Result {
let target = event.metadata().target();
let is_security_event = target.starts_with(self.security_target_prefix);
if !is_security_event {
return self.inner.format_event(context, writer, event);
}
let mut buffer = String::new();
self.inner
.format_event(context, Writer::new(&mut buffer), event)?;
let trimmed = buffer.trim_end();
if let Some(rest) = trimmed.strip_prefix('{') {
write!(writer, "{{\"tags\":[\"security\"],{rest}")?;
writeln!(writer)
} else {
write!(writer, "{buffer}")
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn default_prefix_targets_rusty_gasket_auth() {
let formatter = SecurityJsonFormat::default();
assert_eq!(formatter.security_target_prefix(), "rusty_gasket_auth");
}
#[test]
fn custom_prefix_is_preserved() {
let formatter = SecurityJsonFormat::new("my_app::auth");
assert_eq!(formatter.security_target_prefix(), "my_app::auth");
}
#[test]
fn debug_names_the_formatter() {
let formatter = SecurityJsonFormat::default();
let debug = format!("{formatter:?}");
assert!(debug.contains("SecurityJsonFormat"));
}
}