use crate::display::{SuperConsole, renderers::ConsoleOutputFeatures};
use chrono::prelude::*;
use fnv::FnvHashMap;
use serde_json::{Value as JSONValue, json};
use std::{
error::Error as StdError,
fmt::{Debug, Display, Formatter, Result as FmtResult},
hash::BuildHasherDefault,
io::Write as IoWrite,
sync::Arc,
};
use tokio::runtime::Handle;
use tracing::{
Event, Level, Metadata, Subscriber,
field::{Field, Visit},
span::{Attributes as SpanAttributes, Id as SpanId},
subscriber::Interest,
};
use tracing_subscriber::{Layer, layer::Context, registry::LookupSpan};
use valuable::{
Fields, NamedField, NamedValues, StructDef, Structable, Valuable, Value, Visit as ValuableVisit,
};
use valuable_serde::Serializable;
pub struct TracableSuperConsole<
StdoutTy: IoWrite + ConsoleOutputFeatures + Send + 'static,
StderrTy: IoWrite + ConsoleOutputFeatures + Send + 'static,
> {
console: Arc<SuperConsole<StdoutTy, StderrTy>>,
}
impl<
StdoutTy: IoWrite + ConsoleOutputFeatures + Send + 'static,
StderrTy: IoWrite + ConsoleOutputFeatures + Send + 'static,
> TracableSuperConsole<StdoutTy, StderrTy>
{
#[must_use]
pub fn new(console: Arc<SuperConsole<StdoutTy, StderrTy>>) -> Self {
Self { console }
}
}
impl<
StdoutTy: IoWrite + ConsoleOutputFeatures + Send + 'static,
StderrTy: IoWrite + ConsoleOutputFeatures + Send + 'static,
SubscriberTy,
> Layer<SubscriberTy> for TracableSuperConsole<StdoutTy, StderrTy>
where
SubscriberTy: Subscriber + for<'lookup> LookupSpan<'lookup>,
{
fn register_callsite(&self, _metadata: &'static Metadata<'static>) -> Interest {
Interest::always()
}
fn on_new_span(&self, attrs: &SpanAttributes<'_>, id: &SpanId, ctx: Context<'_, SubscriberTy>) {
let mut span_data = SuperConsoleSpanData::new();
span_data.reserve(attrs.fields().len());
attrs.record(&mut span_data);
if let Some(span) = ctx.span(id) {
span.extensions_mut().insert(span_data);
}
}
fn on_event(&self, event: &Event<'_>, ctxt: Context<'_, SubscriberTy>) {
let mut visitor = SuperConsoleLogMessage::new();
let mut optional_span = ctxt.event_span(event);
while let Some(current_span) = optional_span {
if let Some(ext) = current_span.extensions().get::<SuperConsoleSpanData>() {
visitor.visit_span(ext);
}
optional_span = current_span.parent();
}
visitor.set_level(*event.metadata().level());
visitor.reserve_metadata_space(event.metadata().fields().len());
event.record(&mut visitor);
if let Ok(h) = Handle::try_current() {
tokio::task::block_in_place(move || {
h.block_on(async { self.console.log(visitor).await });
});
} else {
_ = self.console.log_sync(visitor);
}
}
}
#[allow(
// In this case booleans are the best way to track state through
// tracing.
clippy::struct_excessive_bools,
)]
#[derive(Clone, Debug, PartialEq)]
pub struct SuperConsoleLogMessage {
at: DateTime<Utc>,
color: Option<String>,
force_combine: bool,
hide_fields_for_humans: bool,
id: Option<String>,
level: Level,
subsystem: Option<String>,
should_decorate: bool,
target_stdout: bool,
message: Option<String>,
metadata: FnvHashMap<&'static str, FlattenedTracingField>,
}
impl SuperConsoleLogMessage {
#[must_use]
pub(self) fn new() -> Self {
Self {
at: Utc::now(),
color: None,
id: None,
force_combine: false,
hide_fields_for_humans: false,
level: Level::INFO,
subsystem: None,
should_decorate: true,
target_stdout: false,
message: None,
metadata: FnvHashMap::with_capacity_and_hasher(0, BuildHasherDefault::default()),
}
}
pub(self) fn reserve_metadata_space(&mut self, additional: usize) {
self.metadata.reserve(additional);
}
#[must_use]
pub const fn at(&self) -> &DateTime<Utc> {
&self.at
}
#[must_use]
pub fn color(&self) -> Option<&str> {
self.color.as_deref()
}
#[must_use]
pub const fn should_hide_fields_for_humans(&self) -> bool {
self.hide_fields_for_humans
}
#[must_use]
pub fn id(&self) -> Option<&str> {
self.id.as_deref()
}
#[must_use]
pub const fn force_combine(&self) -> bool {
self.force_combine
}
#[must_use]
pub fn subsytem(&self) -> Option<&str> {
self.subsystem.as_deref()
}
#[must_use]
pub fn message(&self) -> Option<&str> {
self.message.as_deref()
}
#[must_use]
pub const fn level(&self) -> &Level {
&self.level
}
#[must_use]
pub const fn should_decorate(&self) -> bool {
self.should_decorate
}
#[must_use]
pub const fn towards_stdout(&self) -> bool {
self.target_stdout
}
#[must_use]
pub const fn metadata(&self) -> &FnvHashMap<&'static str, FlattenedTracingField> {
&self.metadata
}
pub(self) fn set_level(&mut self, level: Level) {
self.level = level;
}
pub(self) fn visit_span(&mut self, ext: &SuperConsoleSpanData) {
for (key, value) in ext.fields() {
match *key {
"id" => {
if self.id.is_none() {
_ = self.id.insert(Self::get_field_as_string(value));
}
}
"lisa.force_combine_fields" => {
self.force_combine = Self::get_field_as_bool(value);
}
"lisa.hide_fields_for_humans" => {
self.hide_fields_for_humans = Self::get_field_as_bool(value);
}
"lisa.subsystem" => {
if self.subsystem.is_none() {
_ = self.subsystem.insert(Self::get_field_as_string(value));
}
}
"lisa.stdout" => {
self.target_stdout = Self::get_field_as_bool(value);
}
"lisa.decorate" => {
self.should_decorate = Self::get_field_as_bool(value);
}
key_value => {
if !self.metadata.contains_key(key_value) {
self.metadata.insert(key_value, value.clone());
}
}
}
}
}
fn get_field_as_bool(value: &FlattenedTracingField) -> bool {
match value {
FlattenedTracingField::Null => false,
FlattenedTracingField::Boolean(val) => *val,
FlattenedTracingField::Bytes(val) => !val.is_empty() && val[0] > 0,
FlattenedTracingField::Float(val) => *val > 0.0,
FlattenedTracingField::Int(val) => *val > 0,
FlattenedTracingField::IntLarge(val) => *val > 0,
FlattenedTracingField::Str(val) => val.eq_ignore_ascii_case("true") || val == "1",
FlattenedTracingField::UnsignedInt(val) => *val > 0,
FlattenedTracingField::UnsignedIntLarge(val) => *val > 0,
FlattenedTracingField::List(val) => !val.is_empty(),
FlattenedTracingField::Object(val) => !val.is_empty(),
}
}
fn get_field_as_string(value: &FlattenedTracingField) -> String {
match value {
FlattenedTracingField::Null => String::with_capacity(0),
FlattenedTracingField::Boolean(val) => format!("{val}"),
FlattenedTracingField::Float(val) => format!("{val}"),
FlattenedTracingField::Int(val) => format!("{val}"),
FlattenedTracingField::IntLarge(val) => format!("{val}"),
FlattenedTracingField::UnsignedInt(val) => format!("{val}"),
FlattenedTracingField::UnsignedIntLarge(val) => format!("{val}"),
FlattenedTracingField::Str(val) => val.clone(),
FlattenedTracingField::Bytes(val) => format!("{val:02x?}"),
FlattenedTracingField::List(_) | FlattenedTracingField::Object(_) => {
format!("{value}")
}
}
}
fn do_field_visit(&mut self, field: &Field, value: FlattenedTracingField) {
match field.name() {
"id" => {
_ = self.id.insert(Self::get_field_as_string(&value));
}
"lisa.subsystem" => {
_ = self.subsystem.insert(Self::get_field_as_string(&value));
}
"lisa.stdout" => {
self.target_stdout = Self::get_field_as_bool(&value);
}
"lisa.decorate" => {
self.should_decorate = Self::get_field_as_bool(&value);
}
"lisa.force_combine_fields" => {
self.force_combine = Self::get_field_as_bool(&value);
}
"lisa.hide_fields_for_humans" => {
self.hide_fields_for_humans = Self::get_field_as_bool(&value);
}
"message" => {
_ = self.message.insert(Self::get_field_as_string(&value));
}
key_value => {
self.metadata.insert(key_value, value);
}
}
}
}
impl Visit for SuperConsoleLogMessage {
fn record_f64(&mut self, field: &Field, value: f64) {
self.do_field_visit(field, FlattenedTracingField::Float(value));
}
fn record_i64(&mut self, field: &Field, value: i64) {
self.do_field_visit(field, FlattenedTracingField::Int(value));
}
fn record_u64(&mut self, field: &Field, value: u64) {
self.do_field_visit(field, FlattenedTracingField::UnsignedInt(value));
}
fn record_i128(&mut self, field: &Field, value: i128) {
self.do_field_visit(field, FlattenedTracingField::IntLarge(value));
}
fn record_u128(&mut self, field: &Field, value: u128) {
self.do_field_visit(field, FlattenedTracingField::UnsignedIntLarge(value));
}
fn record_bool(&mut self, field: &Field, value: bool) {
self.do_field_visit(field, FlattenedTracingField::Boolean(value));
}
fn record_str(&mut self, field: &Field, value: &str) {
self.do_field_visit(field, FlattenedTracingField::Str(value.to_owned()));
}
fn record_bytes(&mut self, field: &Field, value: &[u8]) {
self.do_field_visit(field, FlattenedTracingField::Bytes(Vec::from(value)));
}
fn record_error(&mut self, field: &Field, value: &(dyn StdError + 'static)) {
self.do_field_visit(field, FlattenedTracingField::Str(format!("{value}")));
}
fn record_debug(&mut self, field: &Field, value: &dyn Debug) {
self.do_field_visit(field, FlattenedTracingField::Str(format!("{value:?}")));
}
fn record_value(&mut self, field: &Field, value: Value<'_>) {
self.do_field_visit(field, valuable_to_flattened(value));
}
}
const SUPER_CONSOLE_LOG_MESSAGE_FIELDS: &[NamedField<'static>] = &[
NamedField::new("id"),
NamedField::new("level"),
NamedField::new("subsystem"),
NamedField::new("should_decorate"),
NamedField::new("target_stdout"),
NamedField::new("metadata"),
];
impl Structable for SuperConsoleLogMessage {
fn definition(&self) -> StructDef<'_> {
StructDef::new_static(
"SuperConsoleLogMessage",
Fields::Named(SUPER_CONSOLE_LOG_MESSAGE_FIELDS),
)
}
}
impl Valuable for SuperConsoleLogMessage {
fn as_value(&self) -> Value<'_> {
Value::Structable(self)
}
fn visit(&self, visitor: &mut dyn ValuableVisit) {
visitor.visit_named_fields(&NamedValues::new(
SUPER_CONSOLE_LOG_MESSAGE_FIELDS,
&[
Valuable::as_value(&self.id),
Valuable::as_value(&format!("{}", self.level())),
Valuable::as_value(&self.subsystem),
Valuable::as_value(&self.should_decorate),
Valuable::as_value(&self.target_stdout),
Valuable::as_value(&self.metadata),
],
));
}
}
#[derive(Clone, PartialEq, Valuable)]
pub enum FlattenedTracingField {
Null,
Float(f64),
Int(i64),
UnsignedInt(u64),
IntLarge(i128),
UnsignedIntLarge(u128),
Boolean(bool),
Str(String),
Bytes(Vec<u8>),
List(Vec<FlattenedTracingField>),
Object(FnvHashMap<String, FlattenedTracingField>),
}
impl FlattenedTracingField {
#[must_use]
pub fn field_count(&self) -> usize {
match self {
FlattenedTracingField::Boolean(_)
| FlattenedTracingField::Bytes(_)
| FlattenedTracingField::Float(_)
| FlattenedTracingField::Int(_)
| FlattenedTracingField::IntLarge(_)
| FlattenedTracingField::Null
| FlattenedTracingField::Str(_)
| FlattenedTracingField::UnsignedInt(_)
| FlattenedTracingField::UnsignedIntLarge(_) => 1,
FlattenedTracingField::List(list) => {
list.iter().map(FlattenedTracingField::field_count).sum()
}
FlattenedTracingField::Object(obj) => {
obj.values().map(FlattenedTracingField::field_count).sum()
}
}
}
}
impl Display for FlattenedTracingField {
fn fmt(&self, fmt: &mut Formatter<'_>) -> FmtResult {
match self {
Self::Null => write!(fmt, ""),
Self::Boolean(value) => write!(fmt, "{value}"),
Self::Bytes(value) => write!(fmt, "{value:02x?}"),
Self::Float(value) => write!(fmt, "{value}"),
Self::Int(value) => write!(fmt, "{value}"),
Self::IntLarge(value) => write!(fmt, "{value}"),
Self::Str(value) => write!(fmt, "{value}"),
Self::UnsignedInt(value) => write!(fmt, "{value}"),
Self::UnsignedIntLarge(value) => write!(fmt, "{value}"),
Self::List(value) => {
for (idx, val) in value.iter().enumerate() {
if idx != 0 {
write!(fmt, ",")?;
}
write!(fmt, "{val}")?;
}
Ok(())
}
Self::Object(value) => {
write!(fmt, "{{")?;
let mut written_once = false;
for (key, val) in value {
if written_once {
write!(fmt, ",")?;
}
written_once = true;
write!(fmt, "{key}={val}")?;
}
write!(fmt, "}}")
}
}
}
}
impl Debug for FlattenedTracingField {
fn fmt(&self, fmt: &mut Formatter<'_>) -> FmtResult {
match self {
Self::Null => write!(fmt, ""),
Self::Boolean(value) => write!(fmt, "{value:?}"),
Self::Bytes(value) => write!(fmt, "{value:02x?}"),
Self::Float(value) => write!(fmt, "{value:?}"),
Self::Int(value) => write!(fmt, "{value:?}"),
Self::IntLarge(value) => write!(fmt, "{value:?}"),
Self::Str(value) => write!(fmt, "{value:?}"),
Self::UnsignedInt(value) => write!(fmt, "{value:?}"),
Self::UnsignedIntLarge(value) => write!(fmt, "{value:?}"),
Self::List(value) => {
let mut has_written = false;
for val in value {
if has_written {
write!(fmt, ",")?;
} else {
has_written = true;
}
write!(fmt, "{val:?}")?;
}
Ok(())
}
Self::Object(value) => {
write!(fmt, "{{")?;
let mut written_once = false;
for (key, val) in value {
if written_once {
write!(fmt, ",")?;
}
written_once = true;
write!(fmt, "{key}={val:?}")?;
}
write!(fmt, "}}")
}
}
}
}
#[derive(Clone, Debug, PartialEq, Valuable)]
struct SuperConsoleSpanData {
fields: FnvHashMap<&'static str, FlattenedTracingField>,
}
impl SuperConsoleSpanData {
#[must_use]
pub fn new() -> Self {
Self {
fields: FnvHashMap::with_capacity_and_hasher(0, BuildHasherDefault::default()),
}
}
pub fn reserve(&mut self, additional: usize) {
self.fields.reserve(additional);
}
#[must_use]
pub fn fields(&self) -> &FnvHashMap<&'static str, FlattenedTracingField> {
&self.fields
}
}
impl Visit for SuperConsoleSpanData {
fn record_f64(&mut self, field: &Field, value: f64) {
self.fields
.insert(field.name(), FlattenedTracingField::Float(value));
}
fn record_i64(&mut self, field: &Field, value: i64) {
self.fields
.insert(field.name(), FlattenedTracingField::Int(value));
}
fn record_u64(&mut self, field: &Field, value: u64) {
self.fields
.insert(field.name(), FlattenedTracingField::UnsignedInt(value));
}
fn record_i128(&mut self, field: &Field, value: i128) {
self.fields
.insert(field.name(), FlattenedTracingField::IntLarge(value));
}
fn record_u128(&mut self, field: &Field, value: u128) {
self.fields
.insert(field.name(), FlattenedTracingField::UnsignedIntLarge(value));
}
fn record_bool(&mut self, field: &Field, value: bool) {
self.fields
.insert(field.name(), FlattenedTracingField::Boolean(value));
}
fn record_str(&mut self, field: &Field, value: &str) {
self.fields
.insert(field.name(), FlattenedTracingField::Str(value.to_owned()));
}
fn record_bytes(&mut self, field: &Field, value: &[u8]) {
self.fields
.insert(field.name(), FlattenedTracingField::Bytes(Vec::from(value)));
}
fn record_error(&mut self, field: &Field, value: &(dyn StdError + 'static)) {
self.fields
.insert(field.name(), FlattenedTracingField::Str(format!("{value}")));
}
fn record_debug(&mut self, field: &Field, value: &dyn Debug) {
self.fields.insert(
field.name(),
FlattenedTracingField::Str(format!("{value:?}")),
);
}
fn record_value(&mut self, field: &Field, value: Value<'_>) {
self.fields
.insert(field.name(), valuable_to_flattened(value));
}
}
#[must_use]
fn valuable_to_flattened(value: Value<'_>) -> FlattenedTracingField {
json_value_to_flattened(json!(Serializable::new(value)))
}
#[must_use]
fn json_value_to_flattened(as_json_value: JSONValue) -> FlattenedTracingField {
match as_json_value {
JSONValue::Null => FlattenedTracingField::Null,
JSONValue::Bool(val) => FlattenedTracingField::Boolean(val),
JSONValue::Number(val) => {
if let Some(float) = val.as_f64() {
FlattenedTracingField::Float(float)
} else if let Some(uint) = val.as_u64() {
FlattenedTracingField::UnsignedInt(uint)
} else if let Some(int) = val.as_i64() {
FlattenedTracingField::Int(int)
} else if let Some(uint) = val.as_u128() {
FlattenedTracingField::UnsignedIntLarge(uint)
} else if let Some(int) = val.as_i128() {
FlattenedTracingField::IntLarge(int)
} else {
FlattenedTracingField::Null
}
}
JSONValue::String(val) => FlattenedTracingField::Str(val.clone()),
JSONValue::Array(val) => {
let mut list = Vec::with_capacity(val.len());
for item in val {
list.push(json_value_to_flattened(item));
}
FlattenedTracingField::List(list)
}
JSONValue::Object(val) => {
let mut obj =
FnvHashMap::with_capacity_and_hasher(val.len(), BuildHasherDefault::default());
for (key, value) in val {
obj.insert(key, json_value_to_flattened(value));
}
FlattenedTracingField::Object(obj)
}
}
}
#[cfg(test)]
mod unit_tests {
use super::*;
use serde_json::{Value as JSONValue, json};
#[test]
pub fn super_console_span_data_reservation() {
let mut span = SuperConsoleSpanData::new();
assert_eq!(
span.fields().capacity(),
0,
"SuperConsoleSpanData new should start with a 0 capacity item!",
);
span.reserve(10);
assert!(
span.fields().capacity() >= 10,
"SuperConsoleSpanData did not our first 10 items: {}",
span.fields.capacity(),
);
}
#[test]
pub fn valuable_to_flattened_simple_conversion() {
#[derive(Valuable)]
struct InnerObject {
array: Vec<u64>,
}
#[derive(Valuable)]
struct SimpleObject {
null: Option<bool>,
bool: bool,
real: u64,
float: f64,
str: String,
array: Vec<String>,
inner_obj: InnerObject,
}
let serializable = SimpleObject {
null: None,
bool: true,
real: 10,
float: 10.5,
str: "hello".to_owned(),
array: vec!["oh".to_owned(), "hello".to_owned(), "there".to_owned()],
inner_obj: InnerObject {
array: vec![10, 11, 12],
},
};
let mut global_object = FnvHashMap::default();
global_object.insert("null".to_owned(), FlattenedTracingField::Null);
global_object.insert("bool".to_owned(), FlattenedTracingField::Boolean(true));
global_object.insert("real".to_owned(), FlattenedTracingField::Float(10.0));
global_object.insert("float".to_owned(), FlattenedTracingField::Float(10.5));
global_object.insert(
"str".to_owned(),
FlattenedTracingField::Str("hello".to_owned()),
);
global_object.insert(
"array".to_owned(),
FlattenedTracingField::List(vec![
FlattenedTracingField::Str("oh".to_owned()),
FlattenedTracingField::Str("hello".to_owned()),
FlattenedTracingField::Str("there".to_owned()),
]),
);
let mut inner_object = FnvHashMap::default();
inner_object.insert(
"array".to_owned(),
FlattenedTracingField::List(vec![
FlattenedTracingField::Float(10.0),
FlattenedTracingField::Float(11.0),
FlattenedTracingField::Float(12.0),
]),
);
global_object.insert(
"inner_obj".to_owned(),
FlattenedTracingField::Object(inner_object),
);
assert_eq!(
valuable_to_flattened(serializable.as_value()),
FlattenedTracingField::Object(global_object),
"Valuable to flattened differed: {:?}",
valuable_to_flattened(serializable.as_value()),
);
}
#[test]
pub fn json_to_flattened_simple_conversion() {
let json_nul = JSONValue::Null;
let json_bool = json!(true);
let json_real = json!(100);
let json_flot = json!(10.5);
let json_strg = json!("hello world!");
let json_arry = json!(["oh no", "why did", "we do this"]);
let json_objc = json!({
"a": false,
"b": 10,
"c": 10.2,
"d": "hello world",
"e": ["a", "b", "c"],
"f": {
"g": [10, 11, 12],
},
});
assert_eq!(
json_value_to_flattened(json_nul.clone()),
FlattenedTracingField::Null,
"Converting JSON NULL did not produce FTF Null: {}",
json_value_to_flattened(json_nul),
);
assert_eq!(
json_value_to_flattened(json_bool.clone()),
FlattenedTracingField::Boolean(true),
"Converting JSON BOOL did not produce FTF Bool: {}",
json_value_to_flattened(json_bool),
);
assert_eq!(
json_value_to_flattened(json_real.clone()),
FlattenedTracingField::Float(100.0),
"Converting JSON REAL did not produce FTF UnsignedInt: {}",
json_value_to_flattened(json_real),
);
assert_eq!(
json_value_to_flattened(json_flot.clone()),
FlattenedTracingField::Float(10.5),
"Converting JSON Float did not produce FTF Float: {}",
json_value_to_flattened(json_flot),
);
assert_eq!(
json_value_to_flattened(json_strg.clone()),
FlattenedTracingField::Str("hello world!".to_owned()),
"Converting JSON String did not produce FTF String: {}",
json_value_to_flattened(json_strg),
);
assert_eq!(
json_value_to_flattened(json_arry.clone()),
FlattenedTracingField::List(vec![
FlattenedTracingField::Str("oh no".to_owned()),
FlattenedTracingField::Str("why did".to_owned()),
FlattenedTracingField::Str("we do this".to_owned()),
]),
"Converting JSON List did not produce FTF List: {}",
json_value_to_flattened(json_arry),
);
let mut f_object = FnvHashMap::default();
f_object.insert(
"g".to_owned(),
FlattenedTracingField::List(vec![
FlattenedTracingField::Float(10.0),
FlattenedTracingField::Float(11.0),
FlattenedTracingField::Float(12.0),
]),
);
let mut global_object = FnvHashMap::default();
global_object.insert("a".to_owned(), FlattenedTracingField::Boolean(false));
global_object.insert("b".to_owned(), FlattenedTracingField::Float(10.0));
global_object.insert("c".to_owned(), FlattenedTracingField::Float(10.2));
global_object.insert(
"d".to_owned(),
FlattenedTracingField::Str("hello world".to_owned()),
);
global_object.insert(
"e".to_owned(),
FlattenedTracingField::List(vec![
FlattenedTracingField::Str("a".to_owned()),
FlattenedTracingField::Str("b".to_owned()),
FlattenedTracingField::Str("c".to_owned()),
]),
);
global_object.insert("f".to_owned(), FlattenedTracingField::Object(f_object));
assert_eq!(
json_value_to_flattened(json_objc.clone()),
FlattenedTracingField::Object(global_object),
"Converting JSON List did not produce FTF Object: {}",
json_value_to_flattened(json_objc),
);
}
}