nautilus_network/
logging.rs1use std::fmt::Debug;
17
18use nautilus_core::string::secret::{REDACTED, SecretString};
19use url::Url;
20
21#[derive(Clone, PartialEq, Eq)]
27pub struct ConnectionUrl(SecretString);
28
29impl ConnectionUrl {
30 #[must_use]
34 pub fn expose(&self) -> &str {
35 self.0.expose_secret()
36 }
37
38 #[must_use]
40 pub fn origin(&self) -> String {
41 url_origin(self.expose())
42 }
43}
44
45impl From<String> for ConnectionUrl {
46 fn from(value: String) -> Self {
47 Self(value.into())
48 }
49}
50
51impl From<&str> for ConnectionUrl {
52 fn from(value: &str) -> Self {
53 Self(value.into())
54 }
55}
56
57impl Debug for ConnectionUrl {
58 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
59 self.origin().fmt(f)
60 }
61}
62
63#[must_use]
68pub fn url_origin(value: &str) -> String {
69 match Url::parse(value) {
70 Ok(url) if matches!(url.scheme(), "http" | "https" | "ws" | "wss") => {
71 url.origin().ascii_serialization()
72 }
73 _ => REDACTED.to_string(),
74 }
75}
76
77pub(crate) fn log_task_started(task_name: &str) {
79 log::debug!("Started task '{task_name}'");
80}
81
82pub(crate) fn log_task_stopped(task_name: &str) {
84 log::debug!("Stopped task '{task_name}'");
85}
86
87pub(crate) fn log_task_aborted(task_name: &str) {
89 log::debug!("Aborted task '{task_name}'");
90}
91
92#[cfg(test)]
93mod url_origin_tests {
94 use rstest::rstest;
95
96 use super::*;
97
98 #[rstest]
99 #[case("wss://stream.example.com/feed", "wss://stream.example.com")]
100 #[case("https://api.example.com:443/v1", "https://api.example.com")]
101 #[case(
102 "ws://user:password@example.com:8080/path-key?token=query-key#fragment-key",
103 "ws://example.com:8080"
104 )]
105 #[case(
106 "https://user%40secret:password%3Asecret@example.com/key%2Fsecret?api_key=secret",
107 "https://example.com"
108 )]
109 #[case(
110 "http://user:secret@[::1]:8123/path-secret?key=secret",
111 "http://[::1]:8123"
112 )]
113 #[case("https://bücher.example/path-secret", "https://xn--bcher-kva.example")]
114 #[case("wss://example.com:invalid/secret", REDACTED)]
115 #[case("invalid-secret", REDACTED)]
116 #[case("file:///secret", REDACTED)]
117 #[case("blob:https://example.com/secret", REDACTED)]
118 fn url_origin_preserves_endpoint_without_credentials(
119 #[case] input: &str,
120 #[case] expected: &str,
121 ) {
122 let url = ConnectionUrl::from(input);
123
124 assert_eq!(url_origin(input), expected);
125 assert_eq!(url.origin(), expected);
126 assert_eq!(format!("{url:?}"), format!("{expected:?}"));
127 assert_eq!(ConnectionUrl::from(input.to_owned()), url);
128 let cloned = url.clone();
129 assert_eq!(url.expose(), input);
130 drop(url);
131 assert_eq!(cloned.expose(), input);
132 }
133}
134
135#[cfg(test)]
136#[cfg(not(all(feature = "simulation", madsim)))]
137#[cfg(target_os = "linux")]
138pub(crate) mod tests {
139 use std::sync::Once;
140
141 use log::{Level, LevelFilter, Log, Metadata, Record};
142 use parking_lot::Mutex;
143
144 struct CaptureState {
145 targets: &'static [&'static str],
146 messages: Vec<(Level, String)>,
147 }
148
149 struct CapturingLogger {
150 state: Mutex<CaptureState>,
151 }
152
153 impl CapturingLogger {
154 fn clear(&self, targets: &'static [&'static str]) {
155 let mut state = self.state.lock();
156 state.targets = targets;
157 state.messages.clear();
158 }
159
160 fn messages(&self) -> Vec<(Level, String)> {
161 self.state.lock().messages.clone()
162 }
163 }
164
165 impl Log for CapturingLogger {
166 fn enabled(&self, metadata: &Metadata<'_>) -> bool {
167 metadata.level() <= Level::Trace
168 }
169
170 fn log(&self, record: &Record<'_>) {
171 if self.enabled(record.metadata()) {
172 let mut state = self.state.lock();
173 if state.targets.is_empty() || state.targets.contains(&record.target()) {
174 state
175 .messages
176 .push((record.level(), record.args().to_string()));
177 }
178 }
179 }
180
181 fn flush(&self) {}
182 }
183
184 static CAPTURING_LOGGER: CapturingLogger = CapturingLogger {
185 state: Mutex::new(CaptureState {
186 targets: &[],
187 messages: Vec::new(),
188 }),
189 };
190 static CAPTURE_LOCK: tokio::sync::Mutex<()> = tokio::sync::Mutex::const_new(());
191 static INSTALL_LOGGER: Once = Once::new();
192
193 pub(crate) struct LogCapture {
194 logger: &'static CapturingLogger,
195 _guard: tokio::sync::MutexGuard<'static, ()>,
196 }
197
198 impl LogCapture {
199 pub(crate) fn messages(&self) -> Vec<(Level, String)> {
200 self.logger.messages()
201 }
202 }
203
204 pub(crate) async fn capture_logs() -> LogCapture {
205 capture_logs_for(&[]).await
206 }
207
208 pub(crate) async fn capture_logs_for(targets: &'static [&'static str]) -> LogCapture {
209 let guard = CAPTURE_LOCK.lock().await;
210 INSTALL_LOGGER.call_once(|| {
211 log::set_logger(&CAPTURING_LOGGER).expect("test logger already installed");
212 });
213 log::set_max_level(LevelFilter::Trace);
214 CAPTURING_LOGGER.clear(targets);
215 LogCapture {
216 logger: &CAPTURING_LOGGER,
217 _guard: guard,
218 }
219 }
220}