openapi_rs/observability/
mod.rs1use std::path::Path;
19use std::time::Instant;
20
21#[derive(Debug, Clone)]
22pub struct RequestContext {
23 pub method: String,
24 pub path: String,
25}
26
27impl RequestContext {
28 pub fn new(method: String, path: String) -> Self {
29 Self { method, path }
30 }
31}
32
33pub struct ValidationMetrics {
34 start_time: Instant,
35 method: String,
36 path: String,
37}
38
39impl ValidationMetrics {
40 pub fn new(method: &str, path: &str) -> Self {
41 Self {
42 start_time: Instant::now(),
43 method: method.to_string(),
44 path: path.to_string(),
45 }
46 }
47
48 pub fn from_context(context: &RequestContext) -> Self {
49 Self::new(&context.method, &context.path)
50 }
51
52 pub fn record_success(self) {
53 let duration_ms = self.start_time.elapsed().as_millis();
54 let timestamp = chrono::Utc::now().timestamp_millis();
55
56 log::info!(
57 "openapi_validation method=\"{}\" path=\"{}\" success=true duration_ms={} timestamp={}",
58 self.method,
59 self.path,
60 duration_ms,
61 timestamp
62 );
63 }
64
65 pub fn record_failure(self, error: String) {
66 let duration_ms = self.start_time.elapsed().as_millis();
67 let timestamp = chrono::Utc::now().timestamp_millis();
68
69 log::warn!(
70 "openapi_validation method=\"{}\" path=\"{}\" success=false duration_ms={} error=\"{}\" timestamp={}",
71 self.method,
72 self.path,
73 duration_ms,
74 error,
75 timestamp
76 );
77 }
78}
79
80#[derive(Debug, Clone)]
82pub struct LogConfig {
83 pub level: String,
85 pub log_file: Option<String>,
87 pub console_output: bool,
89 pub show_timestamp: bool,
91 pub show_target: bool,
93 pub show_thread: bool,
95}
96
97impl Default for LogConfig {
98 fn default() -> Self {
99 Self {
100 level: "info".to_string(),
101 log_file: None,
102 console_output: true,
103 show_timestamp: true,
104 show_target: false,
105 show_thread: false,
106 }
107 }
108}
109
110impl LogConfig {
111 pub fn new() -> Self {
113 Self::default()
114 }
115
116 pub fn with_level(mut self, level: &str) -> Self {
118 self.level = level.to_string();
119 self
120 }
121
122 pub fn with_log_file<P: AsRef<Path>>(mut self, file: P) -> Self {
124 self.log_file = Some(file.as_ref().to_string_lossy().to_string());
125 self
126 }
127
128 pub fn with_console_output(mut self, enabled: bool) -> Self {
130 self.console_output = enabled;
131 self
132 }
133
134 pub fn with_timestamp(mut self, enabled: bool) -> Self {
136 self.show_timestamp = enabled;
137 self
138 }
139
140 pub fn with_target(mut self, enabled: bool) -> Self {
142 self.show_target = enabled;
143 self
144 }
145
146 pub fn with_thread(mut self, enabled: bool) -> Self {
148 self.show_thread = enabled;
149 self
150 }
151}
152
153pub fn init_logger() {
155 init_logger_with_config(LogConfig::default());
156}
157
158pub fn init_logger_with_config(config: LogConfig) {
160 let log_level = match config.level.as_str() {
161 "trace" => log::LevelFilter::Trace,
162 "debug" => log::LevelFilter::Debug,
163 "info" => log::LevelFilter::Info,
164 "warn" => log::LevelFilter::Warn,
165 "error" => log::LevelFilter::Error,
166 _ => log::LevelFilter::Info,
167 };
168
169 let mut dispatch = fern::Dispatch::new()
170 .format(move |out, message, record| {
171 let mut format_str = String::new();
172
173 if config.show_timestamp {
174 format_str.push_str(&format!(
175 "{} ",
176 chrono::Utc::now().format("%Y-%m-%d %H:%M:%S%.3f")
177 ));
178 }
179
180 format_str.push_str(&format!("[{}]", record.level()));
181
182 if config.show_thread {
183 format_str.push_str(&format!(
184 " [{}]",
185 std::thread::current().name().unwrap_or("main")
186 ));
187 }
188
189 if config.show_target {
190 format_str.push_str(&format!(" {}", record.target()));
191 }
192
193 format_str.push_str(&format!(" - {message}"));
194
195 out.finish(format_args!("{format_str}"))
196 })
197 .level(log_level);
198
199 if config.console_output {
201 dispatch = dispatch.chain(std::io::stdout());
202 }
203
204 if let Some(log_file) = &config.log_file {
206 if let Some(parent) = Path::new(log_file).parent() {
208 if let Err(e) = std::fs::create_dir_all(parent) {
209 eprintln!("Failed to create log directory {parent:?}: {e}");
210 return;
211 }
212 }
213
214 match fern::log_file(log_file) {
215 Ok(file) => {
216 dispatch = dispatch.chain(file);
217 }
218 Err(e) => {
219 eprintln!("Failed to create log file {log_file}: {e}");
220 return;
221 }
222 }
223 }
224
225 if let Err(e) = dispatch.apply() {
227 eprintln!("Failed to initialize logger: {e}");
228 } else {
229 log::info!("Logger initialized with config: {config:?}");
230 }
231}