rm_lisa/display/renderers/
json.rs

1//! A JSON Object per line ideal for strucctured logging where you need to
2//! parse, and slice/dice logs.
3
4use crate::{
5	display::{
6		renderers::{ConsoleOutputFeatures, ConsoleRenderer, get_ansi_escape_code_regex},
7		tracing::{FlattenedTracingField, SuperConsoleLogMessage},
8	},
9	errors::LisaError,
10	input::{InputProvider, TerminalInputEvent},
11	tasks::{GloballyUniqueTaskId, LisaTaskStatus, TaskEvent},
12};
13use chrono::{DateTime, Utc};
14use fnv::FnvHashMap;
15use parking_lot::RwLock;
16use regex::Regex;
17use serde_json::{Map, Number, Value as JSONValue};
18use std::{
19	borrow::Cow,
20	env::var as env_var,
21	sync::atomic::{AtomicBool, Ordering},
22};
23use valuable_serde::Serializable;
24
25/// A simple JSON based console renderer.
26#[derive(Debug)]
27pub struct JSONConsoleRenderer {
28	/// Regex for any ANSI escape codes...
29	ansi_escapes: Regex,
30	/// If a user has manually force requested a pause.
31	force_pause: AtomicBool,
32	/// The PS1 or text to render before a command.
33	ps1: RwLock<String>,
34}
35
36impl JSONConsoleRenderer {
37	/// Create a simple JSON console renderer.
38	#[must_use]
39	pub fn new() -> Self {
40		Self {
41			ansi_escapes: get_ansi_escape_code_regex(),
42			force_pause: AtomicBool::new(false),
43			ps1: RwLock::new("> ".to_owned()),
44		}
45	}
46}
47
48impl Default for JSONConsoleRenderer {
49	fn default() -> Self {
50		Self::new()
51	}
52}
53
54impl ConsoleRenderer for JSONConsoleRenderer {
55	fn should_use_renderer(
56		&self,
57		_features: &dyn ConsoleOutputFeatures,
58		environment_prefix: &str,
59	) -> bool {
60		// If someone has explicitly specificed a log format, ignore all else.
61		if let Ok(explicit_renderer) = env_var(format!("{environment_prefix}_LOG_FORMAT")) {
62			return explicit_renderer.trim().eq_ignore_ascii_case("json");
63		}
64
65		// If we're not explicitly requested, don't enable...
66		false
67	}
68
69	fn render_message(
70		&self,
71		_app_name: &'static str,
72		log: SuperConsoleLogMessage,
73		term_width: u16,
74	) -> Result<String, LisaError> {
75		let mut map = Map::with_capacity(log.metadata().len() + 1);
76
77		// Insert `lisa` map with all our details.
78		{
79			let mut lisa_map = Map::with_capacity(8);
80			lisa_map.insert(
81				"at".to_owned(),
82				Number::from_i128(i128::from(log.at().timestamp()))
83					.map_or(JSONValue::Null, JSONValue::Number),
84			);
85			lisa_map.insert(
86				"id".to_owned(),
87				log.id()
88					.map_or(JSONValue::Null, |dat| JSONValue::String(dat.to_owned())),
89			);
90			lisa_map.insert(
91				"level".to_owned(),
92				JSONValue::String(format!("{}", log.level())),
93			);
94			lisa_map.insert(
95				"should_decorate".to_owned(),
96				JSONValue::Bool(log.should_decorate()),
97			);
98			lisa_map.insert(
99				"subsystem".to_owned(),
100				log.subsytem()
101					.map_or(JSONValue::Null, |dat| JSONValue::String(dat.to_owned())),
102			);
103			lisa_map.insert(
104				"towards_stdout".to_owned(),
105				JSONValue::Bool(log.towards_stdout()),
106			);
107			lisa_map.insert(
108				"term_width".to_owned(),
109				Number::from_u128(u128::from(term_width))
110					.map_or(JSONValue::Null, JSONValue::Number),
111			);
112			lisa_map.insert(
113				"color".to_owned(),
114				log.color()
115					.map_or(JSONValue::Null, |dat| JSONValue::String(dat.to_owned())),
116			);
117			map.insert("lisa".to_owned(), JSONValue::Object(lisa_map));
118		}
119		map.insert(
120			"msg".to_owned(),
121			log.message().map_or(JSONValue::Null, |str_value| {
122				JSONValue::String(str_value.to_owned())
123			}),
124		);
125
126		let mut metadata_map = Map::new();
127		for (key, val) in log.metadata() {
128			metadata_map.insert((*key).to_owned(), field_to_json(val));
129		}
130		map.insert("metadata".to_owned(), JSONValue::Object(metadata_map));
131		let mut data = serde_json::to_string(&JSONValue::Object(map))?;
132		data.push('\n');
133
134		Ok(match self.ansi_escapes.replace_all(&data, "") {
135			Cow::Borrowed(_) => data,
136			Cow::Owned(owned) => owned,
137		})
138	}
139
140	fn default_ps1(&self) -> String {
141		"> ".to_owned()
142	}
143
144	fn update_ps1(&self, new_ps1: String) {
145		let mut guarded = self.ps1.write();
146		*guarded = new_ps1;
147	}
148
149	fn supports_ansi(&self) -> bool {
150		false
151	}
152
153	/// We actually pause rendering so we don't need to do any 'clear'-ing
154	fn clear_input(&self, _term_width: u16) -> String {
155		String::with_capacity(0)
156	}
157
158	/// We don't need to do any 'clear'-ing
159	fn clear_task_list(&self, _task_list_size: usize) -> String {
160		String::with_capacity(0)
161	}
162
163	/// Render a dynamic input line.
164	///
165	/// We actually don't render a dynamic input, as that'd require erasing
166	/// things, and that can't be done in a TEXT mode, or supported in a braille
167	/// display.
168	///
169	/// We actually only render when [`Self::on_input`] is called. As inputs are paused
170	/// anyway clear/render is never called.
171	///
172	/// ## Errors
173	///
174	/// This function will never error.
175	fn render_input(
176		&self,
177		_app_name: &'static str,
178		_provider: &dyn InputProvider,
179		_term_width: u16,
180	) -> Result<String, LisaError> {
181		Ok(String::with_capacity(0))
182	}
183
184	/// Re-render all the tasks as new events come in.
185	///
186	/// For this will just render any new events that come in between the last
187	/// time we called it.
188	fn rerender_tasks(
189		&self,
190		new_task_events: &[TaskEvent],
191		_current_task_states: &FnvHashMap<
192			GloballyUniqueTaskId,
193			(DateTime<Utc>, String, LisaTaskStatus),
194		>,
195		_running_since: Option<DateTime<Utc>>,
196		_term_height: u16,
197	) -> Result<String, LisaError> {
198		let mut data = String::new();
199
200		for event in new_task_events {
201			data.push_str(&serde_json::to_string(&match event {
202				TaskEvent::TaskStart(thread_id, task_id, name, status) => {
203					serde_json::json!({
204						"lisa": {
205							"id": "lisa::display::renderers::json::rerender_task::new_event"
206						},
207						"task": {
208							"event": "started",
209							"id": format!("{thread_id}/{task_id}"),
210							"name": name,
211							"status": Serializable::new(status),
212						}
213					})
214				}
215				TaskEvent::TaskStatusUpdate(thread_id, task_id, new_status) => {
216					serde_json::json!({
217						"lisa": {
218							"id": "lisa::display::renderers::json::rerender_task::new_event"
219						},
220						"task": {
221							"event": "status_update",
222							"id": format!("{thread_id}/{task_id}"),
223							"status": Serializable::new(new_status),
224						}
225					})
226				}
227				TaskEvent::TaskEnd(thread_id, task_id) => {
228					serde_json::json!({
229						"lisa": {
230							"id": "lisa::display::renderers::json::rerender_task::new_event"
231						},
232						"task": {
233							"event": "end",
234							"id": format!("{thread_id}/{task_id}"),
235						}
236					})
237				}
238			})?);
239			data.push('\n');
240		}
241
242		Ok(data)
243	}
244
245	/// Handle a user typing into the terminal.
246	///
247	/// This will simply render the actual input line, and characters as users
248	/// type.
249	///
250	/// ## Errors
251	///
252	/// Never.
253	fn on_input(
254		&self,
255		event: TerminalInputEvent,
256		provider: &dyn InputProvider,
257	) -> Result<String, LisaError> {
258		match event {
259			TerminalInputEvent::InputStarted => {
260				let ps1_read = self.ps1.read();
261				Ok(ps1_read.clone())
262			}
263			TerminalInputEvent::InputFinished => Ok("\n".to_owned()),
264			TerminalInputEvent::InputAppend(character) => {
265				let mut new = String::with_capacity(1);
266				new.push(character);
267				Ok(new)
268			}
269			TerminalInputEvent::InputMassAppend(data) => Ok(data),
270			TerminalInputEvent::InputChanged(_) => {
271				let ps1_read = self.ps1.read();
272				let mut data = String::with_capacity(1 + ps1_read.len());
273				data.push('\n');
274				data.push_str(ps1_read.as_str());
275				data.push_str(&provider.current_input());
276				Ok(data)
277			}
278			TerminalInputEvent::InputCancelled => Ok("<CANCELLED>\n".to_owned()),
279			TerminalInputEvent::ClearScreen => Ok(String::with_capacity(0)),
280			TerminalInputEvent::CursorMoveLeft(_) | TerminalInputEvent::CursorMoveRight(_) => {
281				Ok(String::with_capacity(0))
282			}
283			TerminalInputEvent::ToggleOutputPause => {
284				self.force_pause.fetch_not(Ordering::Release);
285				Ok(String::with_capacity(0))
286			}
287		}
288	}
289
290	fn should_pause_log_events(&self, provider: &dyn InputProvider) -> bool {
291		provider.input_in_progress() || self.force_pause.load(Ordering::Acquire)
292	}
293}
294
295fn field_to_json(field: &FlattenedTracingField) -> JSONValue {
296	match field {
297		FlattenedTracingField::Null => JSONValue::Null,
298		FlattenedTracingField::Boolean(value) => JSONValue::Bool(*value),
299		FlattenedTracingField::Bytes(value) => JSONValue::String(format!("{value:02x?}")),
300		FlattenedTracingField::Float(value) => {
301			Number::from_f64(*value).map_or(JSONValue::Null, JSONValue::Number)
302		}
303		FlattenedTracingField::Int(value) => {
304			Number::from_i128(i128::from(*value)).map_or(JSONValue::Null, JSONValue::Number)
305		}
306		FlattenedTracingField::IntLarge(value) => {
307			Number::from_i128(*value).map_or(JSONValue::Null, JSONValue::Number)
308		}
309		FlattenedTracingField::UnsignedInt(value) => {
310			Number::from_u128(u128::from(*value)).map_or(JSONValue::Null, JSONValue::Number)
311		}
312		FlattenedTracingField::UnsignedIntLarge(value) => {
313			Number::from_u128(*value).map_or(JSONValue::Null, JSONValue::Number)
314		}
315		FlattenedTracingField::Str(value) => JSONValue::String(value.clone()),
316		FlattenedTracingField::List(value) => {
317			let mut items = Vec::with_capacity(value.len());
318			for val in value {
319				items.push(field_to_json(val));
320			}
321			JSONValue::Array(items)
322		}
323		FlattenedTracingField::Object(obj) => {
324			let mut map = Map::new();
325			for (key, value) in obj {
326				map.insert((*key).clone(), field_to_json(value));
327			}
328			JSONValue::Object(map)
329		}
330	}
331}