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