Skip to content

Commit 5b7fcab

Browse files
fargitoclaude
andcommitted
feat(circleci): render log groups with indentation and timings
CircleCI collapses output per step and has no in-output section markers, so groups were printed as a single bold line, with nothing marking where they end. Reuse the local logger's formatting instead: a styled header surrounded by blank lines, indented and icon-prefixed records, and a closing checkmark reporting the group name and how long it took. Widen format_group_header, format_elapsed and format_log to pub(crate) so the CircleCI logger can share them. Local output is unchanged. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent 916a1bd commit 5b7fcab

2 files changed

Lines changed: 45 additions & 27 deletions

File tree

src/local_logger/mod.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -168,7 +168,7 @@ impl Log for LocalLogger {
168168
}
169169

170170
/// Format a group header with styled prefix
171-
fn format_group_header(name: &str) -> String {
171+
pub(crate) fn format_group_header(name: &str) -> String {
172172
let prefix = style(Icon::GroupArrow.to_string())
173173
.color256(CODSPEED_U8_COLOR_CODE)
174174
.bold();
@@ -191,7 +191,7 @@ pub(crate) fn format_checkmark(label: &str, dim: bool) -> String {
191191
}
192192

193193
/// Format elapsed duration in a compact human-readable way
194-
fn format_elapsed(duration: Duration) -> String {
194+
pub(crate) fn format_elapsed(duration: Duration) -> String {
195195
let secs = duration.as_secs();
196196
let millis = duration.as_millis();
197197

@@ -234,7 +234,7 @@ fn print_record(record: &log::Record) {
234234
}
235235

236236
/// Format a log entry with the appropriate style for its level.
237-
fn format_log(level: log::Level, message: &str, target: &str) -> String {
237+
pub(crate) fn format_log(level: log::Level, message: &str, target: &str) -> String {
238238
match level {
239239
log::Level::Error => {
240240
let prefix = style(Icon::Error.to_string()).red().bold();

src/run_environment/circleci/logger.rs

Lines changed: 42 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,28 @@
11
use console::style;
22
use log::*;
33
use simplelog::SharedLogger;
4-
use std::{env, io::Write};
4+
use std::{env, io::Write, sync::Mutex, time::Instant};
55

66
use crate::{
7+
local_logger::{format_checkmark, format_elapsed, format_group_header, format_log},
78
logger::{GroupEvent, get_announcement_event, get_group_event, get_json_event},
89
run_environment::logger::should_provider_logger_handle_record,
910
};
1011

12+
/// The group currently open, used to report its name and duration when it ends.
13+
struct OpenGroup {
14+
name: String,
15+
started_at: Instant,
16+
}
17+
1118
/// A logger that prints logs in the format expected by CircleCI
1219
///
13-
/// CircleCI has no collapsible section markers, so groups are printed as plain
14-
/// headers.
20+
/// CircleCI collapses output per step and has no in-output section markers, so
21+
/// groups cannot be folded. They are rendered with the local logger's formatting
22+
/// instead.
1523
pub struct CircleCILogger {
1624
log_level: LevelFilter,
25+
open_group: Mutex<Option<OpenGroup>>,
1726
}
1827

1928
impl CircleCILogger {
@@ -26,7 +35,10 @@ impl CircleCILogger {
2635
.ok()
2736
.and_then(|log_level| log_level.parse::<log::LevelFilter>().ok())
2837
.unwrap_or(log::LevelFilter::Info);
29-
Self { log_level }
38+
Self {
39+
log_level,
40+
open_group: Mutex::new(None),
41+
}
3042
}
3143
}
3244

@@ -45,10 +57,29 @@ impl Log for CircleCILogger {
4557

4658
if let Some(group_event) = get_group_event(record) {
4759
match group_event {
48-
GroupEvent::Start(name) | GroupEvent::StartOpened(name) => {
49-
println!("{}", style(name).cyan().bold());
60+
GroupEvent::Start(ref name) | GroupEvent::StartOpened(ref name) => {
61+
println!();
62+
println!("{}", format_group_header(name));
63+
println!();
64+
65+
// Opened groups are not closed with a checkmark.
66+
if matches!(group_event, GroupEvent::Start(_)) {
67+
*self.open_group.lock().unwrap() = Some(OpenGroup {
68+
name: name.clone(),
69+
started_at: Instant::now(),
70+
});
71+
}
72+
}
73+
GroupEvent::End => {
74+
let open_group = self.open_group.lock().unwrap().take();
75+
if let Some(OpenGroup { name, started_at }) = open_group {
76+
println!(
77+
"{} {}",
78+
format_checkmark(&name, true),
79+
style(format_elapsed(started_at.elapsed())).dim(),
80+
);
81+
}
5082
}
51-
GroupEvent::End => {}
5283
}
5384
return;
5485
}
@@ -66,23 +97,10 @@ impl Log for CircleCILogger {
6697
return;
6798
}
6899

69-
match level {
70-
Level::Error => {
71-
println!("{}", style(message).red());
72-
}
73-
Level::Warn => {
74-
println!("{}", style(message).yellow());
75-
}
76-
Level::Info => {
77-
println!("{message}");
78-
}
79-
Level::Debug => {
80-
println!("{}", style(message).cyan());
81-
}
82-
Level::Trace => {
83-
println!("{}", style(message).magenta());
84-
}
85-
}
100+
println!(
101+
"{}",
102+
format_log(level, &message.to_string(), record.target())
103+
);
86104
}
87105

88106
fn flush(&self) {

0 commit comments

Comments
 (0)