Skip to content

Commit f62634d

Browse files
authored
feat: add deterministic per-package colors for span logs (#2032)
1 parent 5d1ec04 commit f62634d

File tree

5 files changed

+108
-28
lines changed

5 files changed

+108
-28
lines changed

src/build.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,7 @@ pub async fn run_build(
118118
.create_build_dir(cleanup)
119119
.into_diagnostic()?;
120120

121-
let span = tracing::info_span!("Running build for", recipe = output.identifier());
121+
let span = tracing::info_span!("Running build for", recipe = output.identifier(), span_color = output.identifier());
122122
let _enter = span.enter();
123123
output.record_build_start();
124124

src/console_utils.rs

Lines changed: 93 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,17 @@
11
//! This module contains utilities for logging and progress bar handling.
22
use std::{
33
borrow::Cow,
4+
collections::hash_map::DefaultHasher,
45
future::Future,
6+
hash::{Hash, Hasher},
57
io,
68
str::FromStr,
79
sync::{Arc, Mutex},
810
time::{Duration, Instant},
911
};
1012

1113
use clap_verbosity_flag::{InfoLevel, Verbosity};
12-
use console::style;
14+
use console::{Style, style};
1315
use indicatif::{
1416
HumanBytes, HumanDuration, MultiProgress, ProgressBar, ProgressState, ProgressStyle,
1517
};
@@ -29,6 +31,29 @@ use tracing_subscriber::{
2931

3032
use crate::consts;
3133

34+
/// A palette of colors used for different package builds.
35+
/// These are chosen to be visually distinct and readable on both light and dark terminals.
36+
const SPAN_COLOR_PALETTE: &[console::Color] = &[
37+
console::Color::Cyan,
38+
console::Color::Green,
39+
console::Color::Yellow,
40+
console::Color::Blue,
41+
console::Color::Magenta,
42+
console::Color::Color256(208), // Orange
43+
console::Color::Color256(141), // Light purple
44+
console::Color::Color256(43), // Teal
45+
];
46+
47+
/// Returns a deterministic color for a given package identifier.
48+
/// The color is chosen by hashing the identifier and selecting from the palette.
49+
fn get_span_color(identifier: &str) -> Style {
50+
let mut hasher = DefaultHasher::new();
51+
identifier.hash(&mut hasher);
52+
let hash = hasher.finish();
53+
let color_index = (hash as usize) % SPAN_COLOR_PALETTE.len();
54+
Style::new().fg(SPAN_COLOR_PALETTE[color_index])
55+
}
56+
3257
/// A custom formatter for tracing events.
3358
pub struct TracingFormatter;
3459

@@ -56,30 +81,44 @@ where
5681
}
5782
}
5883

59-
#[derive(Debug)]
6084
struct SpanInfo {
6185
id: Id,
6286
start_time: Instant,
6387
header: String,
6488
header_printed: bool,
89+
/// The color style used for this span's tree characters.
90+
/// This is inherited from parent spans or computed from the package identifier.
91+
color: Style,
6592
}
6693

67-
#[derive(Debug, Default)]
94+
#[derive(Default)]
6895
struct SharedState {
6996
span_stack: Vec<SpanInfo>,
7097
warnings: Vec<String>,
7198
}
7299

100+
impl std::fmt::Debug for SharedState {
101+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
102+
f.debug_struct("SharedState")
103+
.field("span_stack_len", &self.span_stack.len())
104+
.field("warnings", &self.warnings)
105+
.finish()
106+
}
107+
}
108+
73109
struct CustomVisitor<'a> {
74110
writer: &'a mut dyn io::Write,
75111
result: io::Result<()>,
112+
/// Captures the span_color field for deterministic color computation.
113+
span_color: Option<String>,
76114
}
77115

78116
impl<'a> CustomVisitor<'a> {
79117
fn new(writer: &'a mut dyn io::Write) -> Self {
80118
Self {
81119
writer,
82120
result: Ok(()),
121+
span_color: None,
83122
}
84123
}
85124
}
@@ -90,6 +129,11 @@ impl field::Visit for CustomVisitor<'_> {
90129
return;
91130
}
92131

132+
// Capture span_color field for deterministic color computation
133+
if field.name() == "span_color" {
134+
self.span_color = Some(value.to_string());
135+
}
136+
93137
self.record_debug(field, &format_args!("{}", value))
94138
}
95139

@@ -146,12 +190,13 @@ fn chunk_string_without_ansi(input: &str, max_chunk_length: usize) -> Vec<String
146190
chunks
147191
}
148192

149-
fn indent_levels(indent: usize) -> String {
193+
/// Creates an indentation string with vertical bars colored according to each span's color.
194+
fn indent_levels_colored(span_stack: &[SpanInfo]) -> String {
150195
let mut s = String::new();
151-
for _ in 0..indent {
152-
s.push_str(" │");
196+
for span_info in span_stack {
197+
s.push_str(&format!(" {}", span_info.color.apply_to("│")));
153198
}
154-
format!("{}", style(s).cyan())
199+
s
155200
}
156201

157202
impl<S> Layer<S> for LoggingOutputHandler
@@ -168,24 +213,45 @@ where
168213

169214
if let Some(span) = ctx.span(id) {
170215
let mut s = Vec::new();
171-
let mut w = io::Cursor::new(&mut s);
172-
attrs.record(&mut CustomVisitor::new(&mut w));
173-
let s = String::from_utf8_lossy(w.get_ref());
216+
let color_key = {
217+
let mut w = io::Cursor::new(&mut s);
218+
let mut visitor = CustomVisitor::new(&mut w);
219+
attrs.record(&mut visitor);
220+
visitor.span_color
221+
};
222+
let s = String::from_utf8_lossy(&s);
174223

175224
let name = if s.is_empty() {
176225
span.name().to_string()
177226
} else {
178227
format!("{}{}", span.name(), s)
179228
};
180229

181-
let indent = indent_levels(state.span_stack.len());
182-
let header = format!("{indent}\n{indent} {} {}", style("╭─").cyan(), name);
230+
// Determine the color for this span:
231+
// - If there's a span_color field, compute color from it
232+
// - Otherwise, inherit from parent span
233+
// - If no parent, use gray (for initial/setup output)
234+
let span_color = if let Some(ref key) = color_key {
235+
get_span_color(key)
236+
} else if let Some(parent) = state.span_stack.last() {
237+
parent.color.clone()
238+
} else {
239+
Style::new().dim()
240+
};
241+
242+
let indent = indent_levels_colored(&state.span_stack);
243+
let header = format!(
244+
"{indent}\n{indent} {} {}",
245+
span_color.apply_to("╭─"),
246+
name
247+
);
183248

184249
state.span_stack.push(SpanInfo {
185250
id: id.clone(),
186251
start_time: Instant::now(),
187252
header,
188253
header_printed: false,
254+
color: span_color,
189255
});
190256
}
191257
}
@@ -196,26 +262,34 @@ where
196262
if let Some(pos) = state.span_stack.iter().position(|info| &info.id == id) {
197263
let elapsed = state.span_stack[pos].start_time.elapsed();
198264
let header_printed = state.span_stack[pos].header_printed;
265+
let span_color = state.span_stack[pos].color.clone();
266+
267+
// Get the indent before truncating (parent spans only)
268+
let indent = indent_levels_colored(&state.span_stack[..pos]);
269+
// For indent_plus_one, we need to include this span's color too
270+
let indent_plus_one = format!(
271+
"{} {}",
272+
indent,
273+
span_color.apply_to("│")
274+
);
275+
199276
state.span_stack.truncate(pos);
200277

201278
if !header_printed {
202279
return;
203280
}
204281

205-
let indent = indent_levels(pos);
206-
let indent_plus_one = indent_levels(pos + 1);
207-
208282
eprintln!(
209283
"{indent_plus_one}\n{indent} {} (took {})",
210-
style("╰───────────────────").cyan(),
284+
span_color.apply_to("╰───────────────────"),
211285
HumanDuration(elapsed)
212286
);
213287
}
214288
}
215289

216290
fn on_event(&self, event: &Event<'_>, _ctx: Context<'_, S>) {
217291
let mut state = self.state.lock().unwrap();
218-
let indent = indent_levels(state.span_stack.len());
292+
let indent = indent_levels_colored(&state.span_stack);
219293

220294
// Print pending headers
221295
for span_info in &mut state.span_stack {
@@ -299,10 +373,10 @@ impl Default for LoggingOutputHandler {
299373

300374
impl LoggingOutputHandler {
301375
/// Return a string with the current indentation level (bars added to the
302-
/// front of the string).
376+
/// front of the string), colored according to each span's color.
303377
pub fn with_indent_levels(&self, template: &str) -> String {
304378
let state = self.state.lock().unwrap();
305-
let indent_str = indent_levels(state.span_stack.len());
379+
let indent_str = indent_levels_colored(&state.span_stack);
306380
format!("{} {}", indent_str, template)
307381
}
308382

src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -798,7 +798,7 @@ pub async fn run_test(
798798
.to_string_lossy()
799799
.to_string();
800800

801-
let span = tracing::info_span!("Running tests for", package = %package_name);
801+
let span = tracing::info_span!("Running tests for", package = %package_name, span_color = package_name);
802802
let _enter = span.enter();
803803
package_test::run_test(&package_file, &test_options, None)
804804
.await

src/package_test/run_test.rs

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -536,7 +536,8 @@ impl PythonTest {
536536
prefix: &Path,
537537
config: &TestConfiguration,
538538
) -> Result<(), TestError> {
539-
let span = tracing::info_span!("Running python test(s)");
539+
let pkg_id = format!("{}-{}-{}", pkg.name, pkg.version, pkg.build_string);
540+
let span = tracing::info_span!("Running python test(s)", span_color = pkg_id);
540541
let _guard = span.enter();
541542

542543
// The version spec of the package being built
@@ -695,7 +696,8 @@ impl PerlTest {
695696
prefix: &Path,
696697
config: &TestConfiguration,
697698
) -> Result<(), TestError> {
698-
let span = tracing::info_span!("Running perl test");
699+
let pkg_id = format!("{}-{}-{}", pkg.name, pkg.version, pkg.build_string);
700+
let span = tracing::info_span!("Running perl test", span_color = pkg_id);
699701
let _guard = span.enter();
700702

701703
let match_spec = MatchSpec::from_str(
@@ -770,7 +772,8 @@ impl CommandsTest {
770772
) -> Result<(), TestError> {
771773
let deps = self.requirements.clone();
772774

773-
let span = tracing::info_span!("Running script test for", recipe = pkg.to_string());
775+
let pkg_str = pkg.to_string();
776+
let span = tracing::info_span!("Running script test for", recipe = %pkg_str, span_color = pkg_str);
774777
let _guard = span.enter();
775778

776779
let build_prefix = if !deps.build.is_empty() {
@@ -880,8 +883,9 @@ impl DownstreamTest {
880883
config: &TestConfiguration,
881884
) -> Result<(), TestError> {
882885
let downstream_spec = self.downstream.clone();
886+
let pkg_id = format!("{}-{}-{}", pkg.name, pkg.version, pkg.build_string);
883887

884-
let span = tracing::info_span!("Running downstream test for", package = downstream_spec);
888+
let span = tracing::info_span!("Running downstream test for", package = downstream_spec, span_color = pkg_id);
885889
let _guard = span.enter();
886890

887891
// first try to resolve an environment with the downstream spec and our
@@ -971,7 +975,8 @@ impl RTest {
971975
prefix: &Path,
972976
config: &TestConfiguration,
973977
) -> Result<(), TestError> {
974-
let span = tracing::info_span!("Running R test");
978+
let pkg_id = format!("{}-{}-{}", pkg.name, pkg.version, pkg.build_string);
979+
let span = tracing::info_span!("Running R test", span_color = pkg_id);
975980
let _guard = span.enter();
976981

977982
let match_spec = MatchSpec::from_str(
@@ -1042,7 +1047,8 @@ impl RubyTest {
10421047
prefix: &Path,
10431048
config: &TestConfiguration,
10441049
) -> Result<(), TestError> {
1045-
let span = tracing::info_span!("Running Ruby test");
1050+
let pkg_id = format!("{}-{}-{}", pkg.name, pkg.version, pkg.build_string);
1051+
let span = tracing::info_span!("Running Ruby test", span_color = pkg_id);
10461052
let _guard = span.enter();
10471053

10481054
let match_spec = MatchSpec::from_str(

src/types/build_output.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -170,7 +170,7 @@ impl BuildOutput {
170170
pub fn log_build_summary(&self) -> Result<(), std::io::Error> {
171171
let summary = self.build_summary.lock().unwrap();
172172
let identifier = self.identifier();
173-
let span = tracing::info_span!("Build summary for", recipe = identifier);
173+
let span = tracing::info_span!("Build summary for", recipe = identifier, span_color = identifier);
174174
let _enter = span.enter();
175175

176176
tracing::info!("{}", self);

0 commit comments

Comments
 (0)