Skip to main content

systemprompt_logging/services/
json_format.rs

1//! Console event formatters: JSON lines, and the switch between text and JSON.
2//!
3//! [`JsonEventFormat`] renders one JSON object per event — `timestamp`,
4//! `level`, `target`, `fields`, `spans` and `instance` — and routes every field
5//! through the same [`is_redacted`] rule as the text formatter, so switching
6//! `observability.log_format` never widens what reaches stderr. Span fields are
7//! the text the span's own field formatter already redacted.
8//!
9//! [`ConsoleEventFormat`] is the formatter the global subscriber installs. It
10//! reads the process-wide choice made by [`set_console_format`], because the
11//! subscriber is installed before the profile that names the format is loaded.
12//!
13//! Copyright (c) systemprompt.io — Business Source License 1.1.
14//! See <https://systemprompt.io> for licensing details.
15
16use std::fmt;
17use std::sync::atomic::{AtomicBool, Ordering};
18
19use serde_json::{Map, Value};
20use systemprompt_manifest::profile::LogFormat;
21use tracing::field::{Field, Visit};
22use tracing::{Event, Subscriber};
23use tracing_subscriber::fmt::format::{Format, Full, Writer};
24use tracing_subscriber::fmt::time::SystemTime;
25use tracing_subscriber::fmt::{FmtContext, FormatEvent, FormatFields, FormattedFields};
26use tracing_subscriber::registry::LookupSpan;
27
28use crate::sanitize::{REDACTION_PLACEHOLDER, is_redacted, is_system_sentinel};
29
30static JSON_CONSOLE: AtomicBool = AtomicBool::new(false);
31
32pub fn set_console_format(format: LogFormat) {
33    JSON_CONSOLE.store(matches!(format, LogFormat::Json), Ordering::Relaxed);
34}
35
36#[must_use]
37pub fn console_format() -> LogFormat {
38    if JSON_CONSOLE.load(Ordering::Relaxed) {
39        LogFormat::Json
40    } else {
41        LogFormat::Text
42    }
43}
44
45/// One JSON object per event, with sensitive field values redacted.
46#[derive(Debug, Clone, Copy, Default)]
47pub struct JsonEventFormat;
48
49/// The installed console formatter: text or JSON per [`set_console_format`].
50#[derive(Debug, Clone, Default)]
51pub struct ConsoleEventFormat {
52    text: Format<Full, SystemTime>,
53    json: JsonEventFormat,
54}
55
56impl<S, N> FormatEvent<S, N> for ConsoleEventFormat
57where
58    S: Subscriber + for<'a> LookupSpan<'a>,
59    N: for<'a> FormatFields<'a> + 'static,
60{
61    fn format_event(
62        &self,
63        ctx: &FmtContext<'_, S, N>,
64        writer: Writer<'_>,
65        event: &Event<'_>,
66    ) -> fmt::Result {
67        if JSON_CONSOLE.load(Ordering::Relaxed) {
68            self.json.format_event(ctx, writer, event)
69        } else {
70            self.text.format_event(ctx, writer, event)
71        }
72    }
73}
74
75impl<S, N> FormatEvent<S, N> for JsonEventFormat
76where
77    S: Subscriber + for<'a> LookupSpan<'a>,
78    N: for<'a> FormatFields<'a> + 'static,
79{
80    fn format_event(
81        &self,
82        ctx: &FmtContext<'_, S, N>,
83        mut writer: Writer<'_>,
84        event: &Event<'_>,
85    ) -> fmt::Result {
86        let meta = event.metadata();
87        let mut fields = JsonFieldVisitor::default();
88        event.record(&mut fields);
89
90        let mut object = Map::new();
91        object.insert(
92            "timestamp".to_owned(),
93            Value::String(chrono::Utc::now().to_rfc3339()),
94        );
95        object.insert("level".to_owned(), Value::String(meta.level().to_string()));
96        object.insert("target".to_owned(), Value::String(meta.target().to_owned()));
97        object.insert("fields".to_owned(), Value::Object(fields.fields));
98
99        let spans: Vec<Value> = ctx
100            .event_scope()
101            .into_iter()
102            .flat_map(tracing_subscriber::registry::Scope::from_root)
103            .map(|span| {
104                let mut entry = Map::new();
105                entry.insert("name".to_owned(), Value::String(span.name().to_owned()));
106                if let Some(rendered) = span.extensions().get::<FormattedFields<N>>()
107                    && !rendered.is_empty()
108                {
109                    entry.insert("fields".to_owned(), Value::String(rendered.to_string()));
110                }
111                Value::Object(entry)
112            })
113            .collect();
114        if !spans.is_empty() {
115            object.insert("spans".to_owned(), Value::Array(spans));
116        }
117        if let Some(instance_id) = crate::instance_id() {
118            object.insert(
119                "instance".to_owned(),
120                Value::String(instance_id.as_str().to_owned()),
121            );
122        }
123
124        let line = serde_json::to_string(&Value::Object(object)).map_err(|_encode| fmt::Error)?;
125        writeln!(writer, "{line}")
126    }
127}
128
129#[derive(Debug, Default)]
130struct JsonFieldVisitor {
131    // JSON: a log event's fields are open-ended by definition; this is the
132    // outgoing line being built, never parsed.
133    fields: Map<String, Value>,
134}
135
136impl JsonFieldVisitor {
137    // JSON: see `fields`.
138    fn insert(&mut self, field: &Field, value: Value) {
139        let value = if is_redacted(field.name()) {
140            Value::String(REDACTION_PLACEHOLDER.to_owned())
141        } else {
142            value
143        };
144        self.fields.insert(field.name().to_owned(), value);
145    }
146}
147
148impl Visit for JsonFieldVisitor {
149    fn record_debug(&mut self, field: &Field, value: &dyn fmt::Debug) {
150        let rendered = format!("{value:?}");
151        if is_system_sentinel(&rendered) {
152            return;
153        }
154        self.insert(field, Value::String(rendered));
155    }
156
157    fn record_str(&mut self, field: &Field, value: &str) {
158        if is_system_sentinel(value) {
159            return;
160        }
161        self.insert(field, Value::String(value.to_owned()));
162    }
163
164    fn record_i64(&mut self, field: &Field, value: i64) {
165        self.insert(field, Value::from(value));
166    }
167
168    fn record_u64(&mut self, field: &Field, value: u64) {
169        self.insert(field, Value::from(value));
170    }
171
172    fn record_f64(&mut self, field: &Field, value: f64) {
173        self.insert(field, Value::from(value));
174    }
175
176    fn record_bool(&mut self, field: &Field, value: bool) {
177        self.insert(field, Value::from(value));
178    }
179}