Skip to content

Commit 0fa65b3

Browse files
committed
Logging beautification
1 parent ec56746 commit 0fa65b3

2 files changed

Lines changed: 99 additions & 36 deletions

File tree

src/logging.rs

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,10 @@ use std::sync::atomic::{AtomicUsize, Ordering};
66
use std::sync::{Arc, Once};
77

88
use time::macros::format_description;
9+
use tracing::field::{Field, Visit};
910
use tracing::level_filters::LevelFilter;
1011
use tracing::{Event, Level, Subscriber};
12+
use tracing_log::NormalizeEvent;
1113
use tracing_subscriber::fmt::format::{FormatEvent, FormatFields, Writer};
1214
use tracing_subscriber::fmt::time::{FormatTime, UtcTime};
1315
use tracing_subscriber::fmt::FmtContext;
@@ -121,7 +123,8 @@ where
121123
mut writer: Writer<'_>,
122124
event: &Event<'_>,
123125
) -> fmt::Result {
124-
let meta = event.metadata();
126+
let normalized_meta = event.normalized_metadata();
127+
let meta = normalized_meta.as_ref().unwrap_or_else(|| event.metadata());
125128

126129
self.format_timestamp(&mut writer)?;
127130
write!(
@@ -130,7 +133,11 @@ where
130133
FormattedLevel::new(meta.level(), writer.has_ansi_escapes())
131134
)?;
132135

133-
ctx.format_fields(writer.by_ref(), event)?;
136+
if let Some(message) = multiline_message(event) {
137+
writer.write_str(&message)?;
138+
} else {
139+
ctx.format_fields(writer.by_ref(), event)?;
140+
}
134141
writer.write_char(' ')?;
135142
write_dimmed(&mut writer, meta.target())?;
136143
write_dimmed(&mut writer, ":")?;
@@ -153,6 +160,28 @@ impl TunmuxLogFormat {
153160
}
154161
}
155162

163+
struct MessageVisitor {
164+
message: Option<String>,
165+
}
166+
167+
impl Visit for MessageVisitor {
168+
fn record_debug(&mut self, field: &Field, value: &dyn fmt::Debug) {
169+
if field.name() == "message" {
170+
self.message = Some(format!("{value:?}"));
171+
}
172+
}
173+
}
174+
175+
/// Multi-line messages (e.g. the macOS network overview table) are written
176+
/// verbatim. Reading the message straight off the raw event preserves real
177+
/// newlines and ANSI bytes — `ctx.format_fields` would escape control chars as
178+
/// a log-injection guard, mangling the table's colors.
179+
fn multiline_message(event: &Event<'_>) -> Option<String> {
180+
let mut visitor = MessageVisitor { message: None };
181+
event.record(&mut visitor);
182+
visitor.message.filter(|message| message.contains('\n'))
183+
}
184+
156185
struct FormattedLevel<'a> {
157186
level: &'a Level,
158187
ansi: bool,

src/userspace_helper.rs

Lines changed: 68 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -2063,7 +2063,7 @@ fn log_macos_network_overview(reason: &str, state: &MacosCleanupState) {
20632063
&owned_dns,
20642064
&dns_fingerprint,
20652065
);
2066-
info!(reason, interface = inputs.interface, "\n{table}");
2066+
info!(reason, interface = inputs.interface, "\n{table}\n");
20672067
}
20682068

20692069
#[cfg(target_os = "macos")]
@@ -2104,7 +2104,7 @@ fn format_macos_network_overview_table(
21042104
format_list(&inputs.dns_servers),
21052105
dns_fingerprint.primary_service.as_deref().unwrap_or("none")
21062106
));
2107-
lines.extend(format_table(
2107+
lines.extend(format_table_with_dimmed_rows(
21082108
&[
21092109
"SERVICE",
21102110
"ROLE",
@@ -2114,6 +2114,7 @@ fn format_macos_network_overview_table(
21142114
"STATUS",
21152115
],
21162116
&macos_dns_overview_rows(inputs, owned_dns, dns_fingerprint),
2117+
macos_log_table_ansi_enabled(),
21172118
));
21182119
lines.join("\n")
21192120
}
@@ -2150,16 +2151,19 @@ fn macos_dns_overview_rows(
21502151
inputs: &MacosReconcileInputs,
21512152
owned_dns: &[MacosDnsServiceState],
21522153
dns_fingerprint: &MacosDnsFingerprint,
2153-
) -> Vec<Vec<String>> {
2154+
) -> Vec<(Vec<String>, bool)> {
21542155
if dns_fingerprint.services.is_empty() {
2155-
return vec![vec![
2156-
"none".to_string(),
2157-
"-".to_string(),
2158-
"-".to_string(),
2159-
"no".to_string(),
2160-
"-".to_string(),
2161-
"no services observed".to_string(),
2162-
]];
2156+
return vec![(
2157+
vec![
2158+
"none".to_string(),
2159+
"-".to_string(),
2160+
"-".to_string(),
2161+
"no".to_string(),
2162+
"-".to_string(),
2163+
"no services observed".to_string(),
2164+
],
2165+
true,
2166+
)];
21632167
}
21642168

21652169
let targets = dns_target_services(DNS_POLICY, dns_fingerprint);
@@ -2176,33 +2180,63 @@ fn macos_dns_overview_rows(
21762180
let targeted = targets.iter().any(|target| target == service);
21772181
let live_matches_tunnel =
21782182
live_dns.map(Vec::as_slice) == Some(inputs.dns_servers.as_slice());
2179-
vec![
2180-
service.clone(),
2181-
if dns_fingerprint.primary_service.as_ref() == Some(service) {
2182-
"primary"
2183-
} else {
2184-
"secondary"
2185-
}
2186-
.to_string(),
2187-
live_dns.map_or_else(|| "empty".to_string(), |servers| format_list(servers)),
2188-
if owned.is_some() { "yes" } else { "no" }.to_string(),
2189-
owned
2190-
.and_then(|state| state.dns_servers.as_ref())
2191-
.map_or_else(|| "empty".to_string(), |servers| format_list(servers)),
2192-
match (targeted, owned.is_some(), live_matches_tunnel) {
2193-
(true, true, true) => "tunnel DNS active",
2194-
(true, true, false) => "owned; pending reapply",
2195-
(true, false, true) => "already tunnel DNS",
2196-
(true, false, false) => "target; not owned",
2197-
(false, true, _) => "owned; pending restore",
2198-
(false, false, _) => "not targeted",
2199-
}
2200-
.to_string(),
2201-
]
2183+
let status = match (targeted, owned.is_some(), live_matches_tunnel) {
2184+
(true, true, true) => "tunnel DNS active",
2185+
(true, true, false) => "owned; pending reapply",
2186+
(true, false, true) => "already tunnel DNS",
2187+
(true, false, false) => "target; not owned",
2188+
(false, true, _) => "owned; pending restore",
2189+
(false, false, _) => "not targeted",
2190+
};
2191+
(
2192+
vec![
2193+
service.clone(),
2194+
if dns_fingerprint.primary_service.as_ref() == Some(service) {
2195+
"primary"
2196+
} else {
2197+
"secondary"
2198+
}
2199+
.to_string(),
2200+
live_dns.map_or_else(|| "empty".to_string(), |servers| format_list(servers)),
2201+
if owned.is_some() { "yes" } else { "no" }.to_string(),
2202+
owned
2203+
.and_then(|state| state.dns_servers.as_ref())
2204+
.map_or_else(|| "empty".to_string(), |servers| format_list(servers)),
2205+
status.to_string(),
2206+
],
2207+
status == "not targeted",
2208+
)
22022209
})
22032210
.collect()
22042211
}
22052212

2213+
#[cfg(target_os = "macos")]
2214+
fn macos_log_table_ansi_enabled() -> bool {
2215+
let Some(value) = std::env::var_os(crate::logging::COLOR_ENV) else {
2216+
return false;
2217+
};
2218+
matches!(
2219+
value.to_string_lossy().to_ascii_lowercase().as_str(),
2220+
"always" | "1" | "true" | "yes" | "on"
2221+
)
2222+
}
2223+
2224+
#[cfg(target_os = "macos")]
2225+
fn format_table_with_dimmed_rows(
2226+
headers: &[&str],
2227+
rows: &[(Vec<String>, bool)],
2228+
ansi: bool,
2229+
) -> Vec<String> {
2230+
let plain_rows = rows.iter().map(|(row, _)| row.clone()).collect::<Vec<_>>();
2231+
let mut lines = format_table(headers, &plain_rows);
2232+
for (line, (_, dimmed)) in lines.iter_mut().skip(2).zip(rows.iter()) {
2233+
if ansi && *dimmed {
2234+
*line = format!("\x1b[2m{line}\x1b[0m");
2235+
}
2236+
}
2237+
lines
2238+
}
2239+
22062240
#[cfg(target_os = "macos")]
22072241
fn format_table(headers: &[&str], rows: &[Vec<String>]) -> Vec<String> {
22082242
let mut widths: Vec<usize> = headers.iter().map(|header| header.len()).collect();

0 commit comments

Comments
 (0)