use crate::{
display::{
renderers::{ConsoleOutputFeatures, ConsoleRenderer, get_ansi_escape_code_regex},
tracing::{FlattenedTracingField, SuperConsoleLogMessage},
},
errors::LisaError,
input::{InputProvider, TerminalInputEvent},
tasks::{GloballyUniqueTaskId, LisaTaskStatus, TaskEvent},
};
use chrono::{DateTime, Utc};
use fnv::FnvHashMap;
use parking_lot::RwLock;
use regex::Regex;
use serde_json::{Map, Number, Value as JSONValue};
use std::{
borrow::Cow,
env::var as env_var,
sync::atomic::{AtomicBool, Ordering},
};
use valuable_serde::Serializable;
#[derive(Debug)]
pub struct JSONConsoleRenderer {
ansi_escapes: Regex,
force_pause: AtomicBool,
ps1: RwLock<String>,
}
impl JSONConsoleRenderer {
#[must_use]
pub fn new() -> Self {
Self {
ansi_escapes: get_ansi_escape_code_regex(),
force_pause: AtomicBool::new(false),
ps1: RwLock::new("> ".to_owned()),
}
}
}
impl Default for JSONConsoleRenderer {
fn default() -> Self {
Self::new()
}
}
impl ConsoleRenderer for JSONConsoleRenderer {
fn should_use_renderer(
&self,
_features: &dyn ConsoleOutputFeatures,
environment_prefix: &str,
) -> bool {
if let Ok(explicit_renderer) = env_var(format!("{environment_prefix}_LOG_FORMAT")) {
return explicit_renderer.trim().eq_ignore_ascii_case("json");
}
false
}
fn render_message(
&self,
_app_name: &'static str,
log: SuperConsoleLogMessage,
term_width: u16,
) -> Result<String, LisaError> {
let mut map = Map::with_capacity(log.metadata().len() + 1);
{
let mut lisa_map = Map::with_capacity(8);
lisa_map.insert(
"at".to_owned(),
Number::from_i128(i128::from(log.at().timestamp()))
.map_or(JSONValue::Null, JSONValue::Number),
);
lisa_map.insert(
"id".to_owned(),
log.id()
.map_or(JSONValue::Null, |dat| JSONValue::String(dat.to_owned())),
);
lisa_map.insert(
"level".to_owned(),
JSONValue::String(format!("{}", log.level())),
);
lisa_map.insert(
"should_decorate".to_owned(),
JSONValue::Bool(log.should_decorate()),
);
lisa_map.insert(
"subsystem".to_owned(),
log.subsytem()
.map_or(JSONValue::Null, |dat| JSONValue::String(dat.to_owned())),
);
lisa_map.insert(
"towards_stdout".to_owned(),
JSONValue::Bool(log.towards_stdout()),
);
lisa_map.insert(
"term_width".to_owned(),
Number::from_u128(u128::from(term_width))
.map_or(JSONValue::Null, JSONValue::Number),
);
lisa_map.insert(
"color".to_owned(),
log.color()
.map_or(JSONValue::Null, |dat| JSONValue::String(dat.to_owned())),
);
map.insert("lisa".to_owned(), JSONValue::Object(lisa_map));
}
map.insert(
"msg".to_owned(),
log.message().map_or(JSONValue::Null, |str_value| {
JSONValue::String(str_value.to_owned())
}),
);
let mut metadata_map = Map::new();
for (key, val) in log.metadata() {
metadata_map.insert((*key).to_owned(), field_to_json(val));
}
map.insert("metadata".to_owned(), JSONValue::Object(metadata_map));
let mut data = serde_json::to_string(&JSONValue::Object(map))?;
data.push('\n');
Ok(match self.ansi_escapes.replace_all(&data, "") {
Cow::Borrowed(_) => data,
Cow::Owned(owned) => owned,
})
}
fn default_ps1(&self) -> String {
"> ".to_owned()
}
fn update_ps1(&self, new_ps1: String) {
let mut guarded = self.ps1.write();
*guarded = new_ps1;
}
fn supports_ansi(&self) -> bool {
false
}
fn clear_input(&self, _term_width: u16) -> String {
String::with_capacity(0)
}
fn clear_task_list(&self, _task_list_size: usize) -> String {
String::with_capacity(0)
}
fn render_input(
&self,
_app_name: &'static str,
_provider: &dyn InputProvider,
_term_width: u16,
) -> Result<String, LisaError> {
Ok(String::with_capacity(0))
}
fn rerender_tasks(
&self,
new_task_events: &[TaskEvent],
_current_task_states: &FnvHashMap<
GloballyUniqueTaskId,
(DateTime<Utc>, String, LisaTaskStatus),
>,
_running_since: Option<DateTime<Utc>>,
_term_height: u16,
) -> Result<String, LisaError> {
let mut data = String::new();
for event in new_task_events {
data.push_str(&serde_json::to_string(&match event {
TaskEvent::TaskStart(thread_id, task_id, name, status) => {
serde_json::json!({
"lisa": {
"id": "lisa::display::renderers::json::rerender_task::new_event"
},
"task": {
"event": "started",
"id": format!("{thread_id}/{task_id}"),
"name": name,
"status": Serializable::new(status),
}
})
}
TaskEvent::TaskStatusUpdate(thread_id, task_id, new_status) => {
serde_json::json!({
"lisa": {
"id": "lisa::display::renderers::json::rerender_task::new_event"
},
"task": {
"event": "status_update",
"id": format!("{thread_id}/{task_id}"),
"status": Serializable::new(new_status),
}
})
}
TaskEvent::TaskEnd(thread_id, task_id) => {
serde_json::json!({
"lisa": {
"id": "lisa::display::renderers::json::rerender_task::new_event"
},
"task": {
"event": "end",
"id": format!("{thread_id}/{task_id}"),
}
})
}
})?);
data.push('\n');
}
Ok(data)
}
fn on_input(
&self,
event: TerminalInputEvent,
provider: &dyn InputProvider,
) -> Result<String, LisaError> {
match event {
TerminalInputEvent::InputStarted => {
let ps1_read = self.ps1.read();
Ok(ps1_read.clone())
}
TerminalInputEvent::InputFinished => Ok("\n".to_owned()),
TerminalInputEvent::InputAppend(character) => {
let mut new = String::with_capacity(1);
new.push(character);
Ok(new)
}
TerminalInputEvent::InputMassAppend(data) => Ok(data),
TerminalInputEvent::InputChanged(_) => {
let ps1_read = self.ps1.read();
let mut data = String::with_capacity(1 + ps1_read.len());
data.push('\n');
data.push_str(ps1_read.as_str());
data.push_str(&provider.current_input());
Ok(data)
}
TerminalInputEvent::InputCancelled => Ok("<CANCELLED>\n".to_owned()),
TerminalInputEvent::ClearScreen => Ok(String::with_capacity(0)),
TerminalInputEvent::CursorMoveLeft(_) | TerminalInputEvent::CursorMoveRight(_) => {
Ok(String::with_capacity(0))
}
TerminalInputEvent::ToggleOutputPause => {
self.force_pause.fetch_not(Ordering::Release);
Ok(String::with_capacity(0))
}
}
}
fn should_pause_log_events(&self, provider: &dyn InputProvider) -> bool {
provider.input_in_progress() || self.force_pause.load(Ordering::Acquire)
}
}
fn field_to_json(field: &FlattenedTracingField) -> JSONValue {
match field {
FlattenedTracingField::Null => JSONValue::Null,
FlattenedTracingField::Boolean(value) => JSONValue::Bool(*value),
FlattenedTracingField::Bytes(value) => JSONValue::String(format!("{value:02x?}")),
FlattenedTracingField::Float(value) => {
Number::from_f64(*value).map_or(JSONValue::Null, JSONValue::Number)
}
FlattenedTracingField::Int(value) => {
Number::from_i128(i128::from(*value)).map_or(JSONValue::Null, JSONValue::Number)
}
FlattenedTracingField::IntLarge(value) => {
Number::from_i128(*value).map_or(JSONValue::Null, JSONValue::Number)
}
FlattenedTracingField::UnsignedInt(value) => {
Number::from_u128(u128::from(*value)).map_or(JSONValue::Null, JSONValue::Number)
}
FlattenedTracingField::UnsignedIntLarge(value) => {
Number::from_u128(*value).map_or(JSONValue::Null, JSONValue::Number)
}
FlattenedTracingField::Str(value) => JSONValue::String(value.clone()),
FlattenedTracingField::List(value) => {
let mut items = Vec::with_capacity(value.len());
for val in value {
items.push(field_to_json(val));
}
JSONValue::Array(items)
}
FlattenedTracingField::Object(obj) => {
let mut map = Map::new();
for (key, value) in obj {
map.insert((*key).clone(), field_to_json(value));
}
JSONValue::Object(map)
}
}
}