Skip to main content

openapi_rs/observability/
mod.rs

1/*
2 * Licensed to the Apache Software Foundation (ASF) under one or more
3 * contributor license agreements.  See the NOTICE file distributed with
4 * this work for additional information regarding copyright ownership.
5 * The ASF licenses this file to You under the Apache License, Version 2.0
6 * (the "License"); you may not use this file except in compliance with
7 * the License.  You may obtain a copy of the License at
8 *
9 *     http://www.apache.org/licenses/LICENSE-2.0
10 *
11 * Unless required by applicable law or agreed to in writing, software
12 * distributed under the License is distributed on an "AS IS" BASIS,
13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 * See the License for the specific language governing permissions and
15 * limitations under the License.
16 */
17
18use 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/// Log configuration structure
81#[derive(Debug, Clone)]
82pub struct LogConfig {
83    /// Log level (trace, debug, info, warn, error)
84    pub level: String,
85    /// Log file path (optional)
86    pub log_file: Option<String>,
87    /// Enable console output
88    pub console_output: bool,
89    /// Show timestamp
90    pub show_timestamp: bool,
91    /// Show code location information
92    pub show_target: bool,
93    /// Show thread information
94    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    /// Create new log configuration
112    pub fn new() -> Self {
113        Self::default()
114    }
115
116    /// Set log level
117    pub fn with_level(mut self, level: &str) -> Self {
118        self.level = level.to_string();
119        self
120    }
121
122    /// Set log file path
123    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    /// Enable/disable console output
129    pub fn with_console_output(mut self, enabled: bool) -> Self {
130        self.console_output = enabled;
131        self
132    }
133
134    /// Enable/disable timestamp display
135    pub fn with_timestamp(mut self, enabled: bool) -> Self {
136        self.show_timestamp = enabled;
137        self
138    }
139
140    /// Enable/disable target information display
141    pub fn with_target(mut self, enabled: bool) -> Self {
142        self.show_target = enabled;
143        self
144    }
145
146    /// Enable/disable thread information display
147    pub fn with_thread(mut self, enabled: bool) -> Self {
148        self.show_thread = enabled;
149        self
150    }
151}
152
153/// Initialize logger with default configuration
154pub fn init_logger() {
155    init_logger_with_config(LogConfig::default());
156}
157
158/// Initialize logger with specified configuration
159pub 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    // Console output
200    if config.console_output {
201        dispatch = dispatch.chain(std::io::stdout());
202    }
203
204    // File output
205    if let Some(log_file) = &config.log_file {
206        // Ensure log file directory exists
207        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    // Apply configuration
226    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}