1use serde::{Deserialize, Serialize};
2use serde_with::DisplayFromStr;
3use tracing::Level;
4use tracing::level_filters::LevelFilter;
5use tracing_subscriber::EnvFilter;
6use tracing_subscriber::Layer;
7use tracing_subscriber::layer::SubscriberExt;
8use tracing_subscriber::util::SubscriberInitExt;
9use url::Url;
10
11#[serde_with::serde_as]
13#[derive(Clone, clap::Parser, Serialize, Deserialize, Debug)]
14#[serde(deny_unknown_fields, default)]
15#[non_exhaustive]
16pub struct Log {
17 #[serde_as(as = "DisplayFromStr")]
19 #[arg(id = "log-level", long = "log-level", default_value = "info", env = "MOQ_LOG_LEVEL")]
20 pub level: Level,
21}
22
23impl Default for Log {
24 fn default() -> Self {
25 Self { level: Level::INFO }
26 }
27}
28
29impl Log {
30 pub fn new(level: Level) -> Self {
32 Self { level }
33 }
34
35 pub fn level(&self) -> LevelFilter {
37 LevelFilter::from_level(self.level)
38 }
39
40 pub fn init(&self) -> crate::Result<()> {
46 let filter = EnvFilter::builder()
47 .with_default_directive(self.level().into()) .from_env_lossy() .add_directive("h2=warn".parse()?)
50 .add_directive("quinn=info".parse()?)
51 .add_directive("noq=info".parse()?)
52 .add_directive("tungstenite=info".parse()?)
53 .add_directive("rustls=info".parse()?)
54 .add_directive("tracing::span=off".parse()?)
55 .add_directive("tracing::span::active=off".parse()?)
56 .add_directive("tokio=info".parse()?)
57 .add_directive("runtime=info".parse()?);
58
59 let registry = tracing_subscriber::registry();
60
61 #[cfg(all(target_os = "android", feature = "android-logcat"))]
64 let registry = {
65 let logcat_layer = tracing_android::layer("MoQNative")
66 .map_err(|e| crate::Error::Logcat(std::sync::Arc::new(e)))?
67 .with_filter(filter);
68 registry.with(logcat_layer)
69 };
70
71 #[cfg(not(all(target_os = "android", feature = "android-logcat")))]
72 let registry = {
73 let fmt_layer = tracing_subscriber::fmt::layer()
74 .with_writer(std::io::stderr)
75 .with_filter(filter);
76 registry.with(fmt_layer)
77 };
78
79 registry
80 .try_init()
81 .map_err(|e| crate::Error::SetSubscriber(std::sync::Arc::new(e)))?;
82
83 Ok(())
84 }
85}
86
87#[derive(Clone, Copy)]
101pub struct RedactedUrl<'a>(&'a Url);
102
103impl<'a> RedactedUrl<'a> {
104 pub fn new(url: &'a Url) -> Self {
106 Self(url)
107 }
108}
109
110impl std::fmt::Debug for RedactedUrl<'_> {
113 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
114 write!(f, "{self}")
115 }
116}
117
118impl std::fmt::Display for RedactedUrl<'_> {
119 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
120 write!(f, "{}://", self.0.scheme())?;
121
122 if let Some(host) = self.0.host_str() {
125 f.write_str(host)?;
126 if let Some(port) = self.0.port() {
127 write!(f, ":{port}")?;
128 }
129 }
130
131 f.write_str(self.0.path())
132 }
133}
134
135#[cfg(test)]
136mod tests {
137 use super::RedactedUrl;
138 use url::Url;
139
140 fn redact(url: &str) -> String {
141 RedactedUrl::new(&Url::parse(url).unwrap()).to_string()
142 }
143
144 #[test]
145 fn drops_query_and_userinfo() {
146 let rendered = redact("https://user:pass@relay.example.com/anon/demo?jwt=secret#frag");
147 assert_eq!(rendered, "https://relay.example.com/anon/demo");
148 for secret in ["jwt", "secret", "user", "pass", "frag"] {
149 assert!(!rendered.contains(secret), "{rendered} leaked {secret}");
150 }
151 }
152
153 #[test]
154 fn debug_matches_display() {
155 let url = Url::parse("https://user:pass@relay.example.com/anon/demo?jwt=secret").unwrap();
156 let redacted = RedactedUrl::new(&url);
157 assert_eq!(format!("{redacted:?}"), redacted.to_string());
158 }
159
160 #[test]
161 fn keeps_the_dial_target() {
162 assert_eq!(redact("https://relay.example.com/"), "https://relay.example.com/");
163 assert_eq!(
164 redact("tcp://relay.example.com:4443/anon"),
165 "tcp://relay.example.com:4443/anon"
166 );
167 assert_eq!(redact("https://[::1]:8443/anon"), "https://[::1]:8443/anon");
168 assert_eq!(redact("unix:///run/moq/internal.sock"), "unix:///run/moq/internal.sock");
169 }
170}