core_lib/log/
file_layer.rs1use std::{fs::OpenOptions, io::Write, path::Path};
9
10use chrono::Local;
11use tracing::{
12 Event, Level, Subscriber,
13 field::{Field, Visit},
14};
15use tracing_subscriber::{Layer, registry::LookupSpan};
16
17const RESET: &str = "\x1b[0m";
18const BG_BLUE: &str = "\x1b[44m"; const BG_ORANGE: &str = "\x1b[48;5;208m"; const BG_RED: &str = "\x1b[41m";
21const BG_GREEN: &str = "\x1b[42m"; const BG_MAGENTA: &str = "\x1b[45m"; const FG_BOLD_WHITE: &str = "\x1b[97;1m";
24
25#[derive(Debug, Default)]
27struct LogFields {
28 log_path: Option<String>,
29 message: Option<String>,
30 kind: Option<String>,
31}
32
33impl Visit for LogFields {
34 fn record_str(&mut self, field: &Field, value: &str) {
35 match field.name() {
36 "log_path" => self.log_path = Some(value.to_string()),
37 "message" => self.message = Some(value.to_string()),
38 "kind" => self.kind = Some(value.to_string()),
39 _ => {}
40 }
41 }
42
43 fn record_debug(&mut self, field: &Field, value: &dyn std::fmt::Debug) {
44 let rendered = format!("{value:?}");
45 match field.name() {
46 "log_path" => self.log_path = Some(rendered),
47 "message" => self.message = Some(rendered),
48 "kind" => self.kind = Some(rendered),
49 _ => {}
50 }
51 }
52}
53
54#[derive(Debug)]
56pub struct FileLayer {
57 color_enable: bool,
58}
59
60impl FileLayer {
61 pub fn new() -> Self {
62 let no_color = std::env::var("FLEET_NO_COLOR").ok().as_deref() == Some("1");
63 Self {
64 color_enable: !no_color,
65 }
66 }
67
68 fn label(&self, event: &Event<'_>, fields: &LogFields) -> String {
71 if let Some(kind @ ("JOB START" | "JOB END")) = fields.kind.as_deref() {
72 return if self.color_enable {
73 let bg = if kind == "JOB START" {
74 BG_GREEN
75 } else {
76 BG_MAGENTA
77 };
78 format!("{bg}{FG_BOLD_WHITE} {kind} {RESET}")
79 } else {
80 kind.to_string()
81 };
82 }
83
84 let level = match *event.metadata().level() {
85 Level::ERROR => "ERROR",
86 Level::WARN => "WARNING",
87 Level::INFO => "INFO",
88 Level::DEBUG => "DEBUG",
89 Level::TRACE => "TRACE",
90 };
91
92 if self.color_enable {
93 let bg = match level {
94 "INFO" => BG_BLUE,
95 "WARNING" => BG_ORANGE,
96 "ERROR" => BG_RED,
97 _ => "",
98 };
99 if bg.is_empty() {
100 level.to_string()
101 } else {
102 format!("{bg}{FG_BOLD_WHITE} {level} {RESET}")
103 }
104 } else {
105 level.to_string()
106 }
107 }
108
109 fn append(path: &Path, line: &str) {
112 let Ok(mut file) = OpenOptions::new().append(true).create(true).open(path) else {
113 return;
114 };
115 let _ = file.write_all(line.as_bytes());
116 let _ = file.flush();
117 }
118}
119
120impl Default for FileLayer {
121 fn default() -> Self {
122 Self::new()
123 }
124}
125
126impl<S> Layer<S> for FileLayer
127where
128 S: Subscriber + for<'a> LookupSpan<'a>,
129{
130 fn on_event(&self, event: &Event<'_>, _ctx: tracing_subscriber::layer::Context<'_, S>) {
131 let mut fields = LogFields::default();
132 event.record(&mut fields);
133
134 let Some(log_path) = fields.log_path.as_deref().filter(|p| !p.is_empty()) else {
135 return;
136 };
137
138 let now = Local::now();
139 let line = format!(
140 "[{}] {}: {}\n",
141 now.format("%Y-%m-%d %H:%M:%S"),
142 self.label(event, &fields),
143 fields.message.unwrap_or_default()
144 );
145 Self::append(Path::new(&log_path), &line);
146 }
147}