1use crate::layout::{fit_segments, OverflowPolicy};
3use rich::{Console, ConsoleOptions, Renderable, Segment, Style, Text};
4#[derive(Clone, Debug, PartialEq)]
5pub enum Value {
6 Null,
7 Bool(bool),
8 Integer(i64),
9 Unsigned(u64),
10 Integer128(i128),
11 Unsigned128(u128),
12 Float(f64),
13 String(String),
14 List(Vec<Value>),
15 Map(Vec<(String, Value)>),
16}
17impl Value {
18 pub(crate) fn format(&self, expanded: bool, depth: usize) -> String {
19 match self {
20 Self::Null => "null".into(),
21 Self::Bool(v) => v.to_string(),
22 Self::Integer(v) => v.to_string(),
23 Self::Unsigned(v) => v.to_string(),
24 Self::Integer128(v) => v.to_string(),
25 Self::Unsigned128(v) => v.to_string(),
26 Self::Float(v) => v.to_string(),
27 Self::String(v) => v.clone(),
28 Self::List(values) => {
29 let items: Vec<_> = values
30 .iter()
31 .map(|v| v.format(expanded, depth + 1))
32 .collect();
33 collection("[", "]", items, expanded, depth)
34 }
35 Self::Map(values) => {
36 let items: Vec<_> = values
37 .iter()
38 .map(|(k, v)| format!("{k}: {}", v.format(expanded, depth + 1)))
39 .collect();
40 collection("{", "}", items, expanded, depth)
41 }
42 }
43 }
44}
45fn collection(open: &str, close: &str, items: Vec<String>, expanded: bool, depth: usize) -> String {
46 if !expanded || items.is_empty() {
47 return format!("{open}{}{close}", items.join(", "));
48 }
49 let indent = " ".repeat(depth.saturating_add(1));
50 format!(
51 "{open}\n{indent}{}\n{}{close}",
52 items.join(&format!(",\n{indent}")),
53 " ".repeat(depth)
54 )
55}
56#[derive(Clone, Debug, PartialEq, Eq)]
57pub enum Message {
58 Literal(String),
59 Markup(String),
60}
61#[derive(Clone, Copy, Debug, PartialEq, Eq)]
62pub enum Severity {
63 Trace,
64 Debug,
65 Info,
66 Warn,
67 Error,
68 Fatal,
69}
70impl Severity {
71 pub fn as_str(self) -> &'static str {
72 match self {
73 Self::Trace => "trace",
74 Self::Debug => "debug",
75 Self::Info => "info",
76 Self::Warn => "warn",
77 Self::Error => "error",
78 Self::Fatal => "fatal",
79 }
80 }
81}
82#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
83pub enum EventView {
84 #[default]
85 Compact,
86 Expanded,
87}
88#[derive(Clone, Debug, PartialEq, Eq)]
89pub struct SourceLocation {
90 pub path: String,
91 pub line: usize,
92 pub column: Option<usize>,
93}
94#[derive(Clone, Debug, Default, PartialEq, Eq)]
95pub struct EventContext {
96 pub timestamp: Option<String>,
97 pub severity: Option<Severity>,
98 pub target: Option<String>,
99 pub module: Option<String>,
100 pub source: Option<SourceLocation>,
101 pub thread: Option<String>,
102 pub task: Option<String>,
103 pub correlation_id: Option<String>,
104}
105#[derive(Clone, Debug, PartialEq)]
107pub struct SpanContext {
108 pub name: String,
109 pub fields: Vec<(String, Value)>,
110}
111impl SpanContext {
112 pub fn new(name: impl Into<String>) -> Self {
113 Self {
114 name: name.into(),
115 fields: Vec::new(),
116 }
117 }
118 pub fn field(mut self, key: impl Into<String>, value: Value) -> Self {
119 self.fields.push((key.into(), value));
120 self
121 }
122}
123#[derive(Clone, Copy, Debug, PartialEq, Eq)]
126pub enum SpanEvent {
127 Open,
128 Close {
130 elapsed: std::time::Duration,
131 },
132}
133#[derive(Clone, Debug)]
134pub struct StructuredEvent {
135 diagnostics: Vec<crate::diagnostic::Diagnostic>,
136 spans: Vec<SpanContext>,
137 span_event: Option<SpanEvent>,
138 pub message: Message,
139 pub fields: Vec<(String, Value)>,
140 pub context: EventContext,
141 order: Vec<String>,
142 hidden: Vec<String>,
143 view: EventView,
144 overflow: OverflowPolicy,
145}
146impl StructuredEvent {
147 pub fn new(message: Message) -> Self {
148 Self {
149 diagnostics: Vec::new(),
150 spans: Vec::new(),
151 span_event: None,
152 message,
153 fields: Vec::new(),
154 context: EventContext::default(),
155 order: Vec::new(),
156 hidden: Vec::new(),
157 view: EventView::Compact,
158 overflow: OverflowPolicy::Fold,
159 }
160 }
161 pub fn diagnostic(mut self, diagnostic: crate::diagnostic::Diagnostic) -> Self {
162 self.diagnostics.push(diagnostic);
163 self
164 }
165 pub fn field(mut self, key: impl Into<String>, value: Value) -> Self {
166 let key = key.into();
167 if let Some((_, old)) = self.fields.iter_mut().find(|(name, _)| name == &key) {
168 *old = value;
169 } else {
170 self.fields.push((key, value));
171 }
172 self
173 }
174 pub fn field_order(mut self, keys: Vec<String>) -> Self {
175 self.order = keys;
176 self
177 }
178 pub fn hide_fields(mut self, keys: Vec<String>) -> Self {
179 self.hidden = keys;
180 self
181 }
182 pub fn view(mut self, view: EventView) -> Self {
183 self.view = view;
184 self
185 }
186 pub fn context(mut self, context: EventContext) -> Self {
187 self.context = context;
188 self
189 }
190 pub fn overflow(mut self, overflow: OverflowPolicy) -> Self {
191 self.overflow = overflow;
192 self
193 }
194 pub fn spans(mut self, spans: Vec<SpanContext>) -> Self {
196 self.spans = spans;
197 self
198 }
199 pub fn span_event(mut self, event: SpanEvent) -> Self {
202 self.span_event = Some(event);
203 self
204 }
205 pub fn span_context(&self) -> &[SpanContext] {
207 &self.spans
208 }
209 pub fn span_marker(&self) -> Option<SpanEvent> {
211 self.span_event
212 }
213}
214pub(crate) fn theme_style(console: &Console, key: &str, fallback: &str) -> Style {
215 console
216 .theme()
217 .get(key)
218 .cloned()
219 .unwrap_or_else(|| Style::parse(fallback).unwrap_or_default())
220}
221pub(crate) fn flatten(lines: Vec<Vec<Segment>>) -> Vec<Segment> {
222 let count = lines.len();
223 lines
224 .into_iter()
225 .enumerate()
226 .flat_map(|(i, mut row)| {
227 if i + 1 < count {
228 row.push(Segment::line());
229 }
230 row
231 })
232 .collect()
233}
234impl Renderable for StructuredEvent {
235 fn rich_render(&self, console: &Console, options: &ConsoleOptions) -> Vec<Segment> {
236 if options.max_width == 0 || options.height == Some(0) {
237 return Vec::new();
238 }
239 let mut text = Text::new("");
240 if let Some(timestamp) = &self.context.timestamp {
241 text.append(&format!("{timestamp} "), None);
242 }
243 if let Some(severity) = self.context.severity {
244 let fallback = match severity {
245 Severity::Error | Severity::Fatal => "bold red",
246 Severity::Warn => "yellow",
247 Severity::Info => "green",
248 _ => "dim",
249 };
250 text.append(
251 &format!("{} ", severity.as_str()),
252 Some(
253 theme_style(
254 console,
255 &format!("event.severity.{}", severity.as_str()),
256 fallback,
257 )
258 .into(),
259 ),
260 );
261 }
262 for (label, value) in [
263 ("target", &self.context.target),
264 ("module", &self.context.module),
265 ("thread", &self.context.thread),
266 ("task", &self.context.task),
267 ("correlation", &self.context.correlation_id),
268 ] {
269 if let Some(value) = value {
270 text.append(&format!("{label}={value} "), None);
271 }
272 }
273 if let Some(source) = &self.context.source {
274 text.append(
275 &format!(
276 "{}:{}{} ",
277 source.path,
278 source.line,
279 source.column.map(|c| format!(":{c}")).unwrap_or_default()
280 ),
281 None,
282 );
283 }
284 let mut message = match &self.message {
285 Message::Literal(s) => Text::new(s),
286 Message::Markup(s) => Text::from_markup(s).unwrap_or_else(|_| Text::new(s)),
287 };
288 message.set_base_style(theme_style(console, "event.message", ""));
289 text = text.append_text(&message);
290 let mut selected = Vec::new();
291 for name in &self.order {
292 if let Some(index) = self.fields.iter().position(|(k, _)| k == name) {
293 if !selected.contains(&index) {
294 selected.push(index);
295 }
296 }
297 }
298 for index in 0..self.fields.len() {
299 if !selected.contains(&index) {
300 selected.push(index);
301 }
302 }
303 for index in selected {
304 let (name, value) = &self.fields[index];
305 if self.hidden.contains(name) {
306 continue;
307 }
308 text.append(
309 if self.view == EventView::Compact {
310 " "
311 } else {
312 "\n"
313 },
314 None,
315 );
316 text.append(
317 &format!("{name}="),
318 Some(theme_style(console, "event.field", "cyan").into()),
319 );
320 text.append(
321 &value.format(self.view == EventView::Expanded, 0),
322 Some(theme_style(console, "event.value", "").into()),
323 );
324 }
325 let mut lines = fit_segments(
326 &text.render(console.theme(), &Style::new()),
327 options.max_width,
328 self.overflow,
329 );
330 for diagnostic in &self.diagnostics {
331 let mut diagnostic_options = options.clone();
332 diagnostic_options.height = None;
333 lines.extend(Segment::split_lines(
334 &diagnostic.rich_render(console, &diagnostic_options),
335 ));
336 }
337 if let Some(height) = options.height {
338 lines.truncate(height);
339 }
340 flatten(lines)
341 }
342}