From f2bcec547e3e87621c39a5f92adf6bbf5d0b7bd2 Mon Sep 17 00:00:00 2001 From: Emily Albini Date: Tue, 14 Apr 2026 21:43:30 +0200 Subject: [PATCH] use an enum to define job streams --- Cargo.toml | 1 + agent/src/exec.rs | 146 ++++++++++++++--------------- agent/src/main.rs | 36 +++---- bin/src/main.rs | 44 ++++----- common/src/job_streams.rs | 87 +++++++++++++++++ common/src/lib.rs | 4 + github/server/src/variety/basic.rs | 133 +++++++++++++------------- jobsh/src/lib.rs | 37 ++++---- 8 files changed, 288 insertions(+), 200 deletions(-) create mode 100644 common/src/job_streams.rs diff --git a/Cargo.toml b/Cargo.toml index e46049a2..35bd8401 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -36,6 +36,7 @@ version = "0.0.0" [workspace.lints.clippy] identity_op = "allow" many_single_char_names = "allow" +should_implement_trait = "allow" too_many_arguments = "allow" type_complexity = "allow" vec_init_then_push = "allow" diff --git a/agent/src/exec.rs b/agent/src/exec.rs index bcdf320c..282426cd 100644 --- a/agent/src/exec.rs +++ b/agent/src/exec.rs @@ -11,13 +11,14 @@ use std::time::{Duration, Instant}; use tokio::sync::mpsc::{channel, Receiver, Sender}; use anyhow::{anyhow, bail, Result}; +use buildomat_common::JobStream; use chrono::prelude::*; use super::OutputRecord; fn spawn_reader( tx: Sender, - name: String, + name: JobStream, stream: Option, ) -> Option> where @@ -47,7 +48,10 @@ where let s = String::from_utf8_lossy(&buf); if tx - .blocking_send(Activity::msg(&name, s.trim_end())) + .blocking_send(Activity::msg( + name.clone(), + s.trim_end(), + )) .is_err() { /* @@ -63,7 +67,7 @@ where * server, but don't panic if we cannot. */ tx.blocking_send(Activity::msg( - "error", + JobStream::Error, &format!("failed to read {name}: {e:?}"), )) .ok(); @@ -76,7 +80,7 @@ where #[derive(Debug)] pub struct ExitDetails { - stream: String, + stream: JobStream, duration_ms: u64, when: DateTime, code: i32, @@ -85,7 +89,7 @@ pub struct ExitDetails { impl ExitDetails { pub(crate) fn to_record(&self) -> OutputRecord { OutputRecord { - stream: self.stream.to_string(), + stream: self.stream.clone(), msg: format!( "process exited: duration {} ms, exit code {}", self.duration_ms, self.code @@ -101,7 +105,7 @@ impl ExitDetails { #[derive(Clone, Debug)] pub struct OutputDetails { - stream: String, + stream: JobStream, msg: String, time: DateTime, } @@ -109,7 +113,7 @@ pub struct OutputDetails { impl OutputDetails { pub(crate) fn to_record(&self) -> OutputRecord { OutputRecord { - stream: self.stream.to_string(), + stream: self.stream.clone(), msg: self.msg.to_string(), time: self.time, } @@ -123,51 +127,76 @@ pub enum Activity { Complete, } -struct ActivityBuilder { - error_stream: String, - exit_stream: String, - bgproc: Option, +#[derive(Clone)] +pub enum ActivityBuilder { + Task, + Diag(String), + Bg(String), } impl ActivityBuilder { - fn exit(&self, start: &Instant, end: &Instant, code: i32) -> Activity { - Activity::Exit(ExitDetails { - stream: self.exit_stream.to_string(), - duration_ms: end.duration_since(*start).as_millis() as u64, - when: Utc::now(), - code, - }) + fn stdout_stream(&self) -> JobStream { + match self.clone() { + ActivityBuilder::Task => JobStream::Stdout, + ActivityBuilder::Diag(_) => JobStream::Stdout, + ActivityBuilder::Bg(name) => JobStream::BgStdout { name }, + } } - fn stdout_stream(&self) -> String { - if let Some(n) = &self.bgproc { - format!("bg.{n}.stdout") - } else { - "stdout".to_string() + fn stderr_stream(&self) -> JobStream { + match self.clone() { + ActivityBuilder::Task => JobStream::Stderr, + ActivityBuilder::Diag(_) => JobStream::Stderr, + ActivityBuilder::Bg(name) => JobStream::BgStderr { name }, } } - fn stderr_stream(&self) -> String { - if let Some(n) = &self.bgproc { - format!("bg.{n}.stderr") - } else { - "stderr".to_string() + fn exit_stream(&self) -> JobStream { + match self.clone() { + ActivityBuilder::Task => JobStream::Task, + ActivityBuilder::Diag(name) => JobStream::Diag { name }, + ActivityBuilder::Bg(name) => JobStream::Bg { name }, } } + fn error_stream(&self) -> JobStream { + match self.clone() { + ActivityBuilder::Task => JobStream::Worker, + ActivityBuilder::Diag(name) => JobStream::Diag { name }, + ActivityBuilder::Bg(name) => JobStream::Bg { name }, + } + } + + fn is_bg(&self) -> bool { + matches!(self, ActivityBuilder::Bg(_)) + } + fn errmsg(&self, pfx: &str, msg: &str) -> String { let mut s = format!("{pfx}: "); - if let Some(bg) = &self.bgproc { - s += &format!("background process {bg:?}: "); + match self { + ActivityBuilder::Task => {} + ActivityBuilder::Diag(_) => {} + ActivityBuilder::Bg(name) => { + s += &format!("background process {name:?}: ") + } } s += ": "; s += msg; s } + fn exit(&self, start: &Instant, end: &Instant, code: i32) -> Activity { + Activity::Exit(ExitDetails { + stream: self.exit_stream(), + duration_ms: end.duration_since(*start).as_millis() as u64, + when: Utc::now(), + code, + }) + } + fn err(&self, msg: &str) -> Activity { Activity::Output(OutputDetails { - stream: self.error_stream.to_string(), + stream: self.error_stream(), msg: self.errmsg("exec error", msg), time: Utc::now(), }) @@ -175,7 +204,7 @@ impl ActivityBuilder { fn warn(&self, msg: &str) -> Activity { Activity::Output(OutputDetails { - stream: self.error_stream.to_string(), + stream: self.error_stream(), msg: self.errmsg("exec warning", msg), time: Utc::now(), }) @@ -183,9 +212,9 @@ impl ActivityBuilder { } impl Activity { - fn msg(stream: &str, msg: &str) -> Activity { + fn msg(stream: JobStream, msg: &str) -> Activity { Activity::Output(OutputDetails { - stream: stream.to_string(), + stream, msg: msg.to_string(), time: Utc::now(), }) @@ -229,39 +258,13 @@ pub fn thread_done( } } -pub fn run_diagnostic(cmd: Command, name: &str) -> Result> { - let (tx, rx) = channel::(100); - - run_common( - cmd, - ActivityBuilder { - error_stream: format!("diag.{name}"), - exit_stream: format!("diag.{name}"), - bgproc: None, - }, - tx, - )?; - - Ok(rx) -} - -pub fn run(cmd: Command) -> Result> { +pub fn run(cmd: Command, ab: ActivityBuilder) -> Result> { let (tx, rx) = channel::(100); - - run_common( - cmd, - ActivityBuilder { - error_stream: "worker".to_string(), - exit_stream: "task".to_string(), - bgproc: None, - }, - tx, - )?; - + run_inner(cmd, ab, tx)?; Ok(rx) } -fn run_common( +fn run_inner( mut cmd: Command, ab: ActivityBuilder, tx: Sender, @@ -289,7 +292,7 @@ fn run_common( ) .unwrap(); - if ab.bgproc.is_some() { + if ab.is_bg() { /* * No further notifications are required for background * processes. @@ -313,7 +316,7 @@ fn run_common( let stdio_warning = !thread_done(&mut readout, "stdout", until) | !thread_done(&mut readerr, "stderr", until); - if ab.bgproc.is_some() { + if ab.is_bg() { /* * No further notifications are required for background * processes. @@ -333,7 +336,7 @@ fn run_common( } }; - assert!(ab.bgproc.is_none()); + assert!(!ab.is_bg()); if stdio_warning { tx.blocking_send(ab.warn( @@ -449,13 +452,9 @@ impl BackgroundProcesses { c.uid(uid); c.gid(gid); - let pid = run_common( + let pid = run_inner( c, - ActivityBuilder { - error_stream: format!("bg.{name}"), - exit_stream: format!("bg.{name}"), - bgproc: Some(name.to_string()), - }, + ActivityBuilder::Bg(name.to_string()), self.tx.clone(), ) .map_err(|e| anyhow!("starting background process {name:?}: {e}"))?; @@ -522,8 +521,7 @@ impl BackgroundProcesses { self.rx.close(); while let Some(a) = self.rx.recv().await { if let Activity::Output(o) = &a { - if o.stream.ends_with("stdout") || o.stream.ends_with("stderr") - { + if o.stream.is_output() { lastwords.push(a); } } diff --git a/agent/src/main.rs b/agent/src/main.rs index 3d677fc7..d4e617f5 100644 --- a/agent/src/main.rs +++ b/agent/src/main.rs @@ -36,6 +36,7 @@ mod shadow; mod upload; use control::protocol::{FactoryInfo, PayloadReq, PayloadRes}; +use exec::ActivityBuilder; struct Agent { log: Logger, @@ -92,18 +93,14 @@ impl ConfigFile { } struct OutputRecord { - stream: String, + stream: JobStream, time: DateTime, msg: String, } impl OutputRecord { - fn new(stream: &str, msg: &str) -> OutputRecord { - OutputRecord { - stream: stream.to_string(), - time: Utc::now(), - msg: msg.to_string(), - } + fn new(stream: JobStream, msg: &str) -> OutputRecord { + OutputRecord { stream, time: Utc::now(), msg: msg.to_string() } } } @@ -154,13 +151,13 @@ async fn append_job_worker( events.push(match ae { AppendJobEntry::JobEvent(rec) => WorkerAppendJobOrTask { task: None, - stream: rec.stream, + stream: rec.stream.to_string(), payload: rec.msg, time: rec.time, }, AppendJobEntry::TaskEvent(task, rec) => WorkerAppendJobOrTask { task: Some(task), - stream: rec.stream, + stream: rec.stream.to_string(), payload: rec.msg, time: rec.time, }, @@ -242,7 +239,7 @@ async fn append_worker_worker( events.push(match ae { AppendWorkerEntry::WorkerEvent(rec) => WorkerAppend { - stream: rec.stream, + stream: rec.stream.to_string(), payload: rec.msg, time: rec.time, }, @@ -305,8 +302,11 @@ impl ClientWrap { } async fn append_worker_msg(&self, name: &str, msg: &str) { - self.append_worker(OutputRecord::new(&format!("diag.{name}"), msg)) - .await + self.append_worker(OutputRecord::new( + JobStream::Diag { name: name.into() }, + msg, + )) + .await } async fn append_worker(&self, rec: OutputRecord) { @@ -331,7 +331,7 @@ impl ClientWrap { } async fn append_msg(&self, msg: &str) { - self.append(OutputRecord::new("worker", msg)).await; + self.append(OutputRecord::new(JobStream::Worker, msg)).await; } async fn append_task(&self, task: &WorkerPingTask, rec: OutputRecord) { @@ -344,7 +344,7 @@ impl ClientWrap { } async fn append_task_msg(&self, task: &WorkerPingTask, msg: &str) { - self.append_task(task, OutputRecord::new("task", msg)).await; + self.append_task(task, OutputRecord::new(JobStream::Task, msg)).await; } async fn flush_job_barrier(&self) { @@ -656,7 +656,7 @@ impl ClientWrap { cmd.uid(0); cmd.gid(0); - match exec::run_diagnostic(cmd, name) { + match exec::run(cmd, ActivityBuilder::Diag(name.into())) { Ok(c) => Ok(c), Err(e) => { /* @@ -666,7 +666,7 @@ impl ClientWrap { self.client .worker_append() .body(vec![WorkerAppend { - stream: "agent".into(), + stream: JobStream::Agent.to_string(), time: Utc::now(), payload: format!("ERROR: diag.{name} exec: {e:?}"), }]) @@ -1633,7 +1633,7 @@ async fn cmd_run(mut l: Level) -> Result<()> { cmd.uid(t.uid); cmd.gid(t.gid); - match exec::run(cmd) { + match exec::run(cmd, ActivityBuilder::Task) { Ok(c) => { stage = Stage::Child(c, t, None); } @@ -1649,7 +1649,7 @@ async fn cmd_run(mut l: Level) -> Result<()> { .job(cw.job_id().unwrap()) .body(vec![WorkerAppendJobOrTask { task: None, - stream: "agent".into(), + stream: JobStream::Agent.to_string(), time: Utc::now(), payload: format!("ERROR: exec: {:?}", e), }]) diff --git a/bin/src/main.rs b/bin/src/main.rs index 96f0fd49..be716a1e 100644 --- a/bin/src/main.rs +++ b/bin/src/main.rs @@ -644,33 +644,23 @@ async fn poll_job(l: &Level, id: &str, json: bool) -> Result<()> { loop { match wat.recv().await { Some(Ok(buildomat_client::EventOrState::Event(e))) => { - if json { - println!("{}", serde_json::to_string(&e)?); - } else if e.stream == "stdout" || e.stream == "stderr" { - println!("{}", e.payload); - } else if e.stream == "control" { - println!("|=| {}", e.payload); - } else if e.stream == "worker" { - println!("|W| {}", e.payload); - } else if e.stream == "task" { - println!("|T| {}", e.payload); - } else if e.stream == "console" { - println!("|C| {}", e.payload); - } else if e.stream == "panic" { - println!("|!| {}", e.payload); - } else if e.stream.starts_with("bg.") { - let t = e.stream.split('.').collect::>(); - if t.len() == 3 { - if t[2] == "stdout" || t[2] == "stderr" { - println!("[{}] {}", t[1], e.payload); - } else { - println!("{:?}", e); - } - } else { - println!("{:?}", e); - } - } else { - println!("{:?}", e); + let stream = JobStream::from_str(&e.stream); + let p = &e.payload; + match stream { + _ if json => println!("{}", serde_json::to_string(&e)?), + JobStream::BgStderr { name } => println!("[{name}] {p}"), + JobStream::BgStdout { name } => println!("[{name}] {p}"), + JobStream::Console => println!("|C| {p}"), + JobStream::Control => println!("|=| {p}"), + JobStream::Panic => println!("|!| {p}"), + JobStream::Task => println!("|T| {p}"), + JobStream::Worker => println!("|W| {p}"), + JobStream::Stderr | JobStream::Stdout => println!("{p}"), + JobStream::Agent + | JobStream::Bg { .. } + | JobStream::Diag { .. } + | JobStream::Error + | JobStream::Unknown(_) => println!("{e:?}"), } } Some(Ok(buildomat_client::EventOrState::State(st))) => { diff --git a/common/src/job_streams.rs b/common/src/job_streams.rs new file mode 100644 index 00000000..b9640e68 --- /dev/null +++ b/common/src/job_streams.rs @@ -0,0 +1,87 @@ +/* + * Copyright 2026 Oxide Computer Company + */ + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum JobStream { + Agent, + Bg { name: String }, + BgStderr { name: String }, + BgStdout { name: String }, + Console, + Control, + Diag { name: String }, + Error, + Panic, + Stderr, + Stdout, + Task, + Worker, + Unknown(String), +} + +impl JobStream { + pub fn from_str(stream: &str) -> JobStream { + let parts = stream.split('.').collect::>(); + match parts.as_slice() { + ["agent"] => JobStream::Agent, + ["bg", name] => JobStream::Bg { name: name.to_string() }, + ["bg", name, "stdout"] => { + JobStream::BgStdout { name: name.to_string() } + } + ["bg", name, "stderr"] => { + JobStream::BgStderr { name: name.to_string() } + } + ["console"] => JobStream::Console, + ["control"] => JobStream::Control, + ["diag", name] => JobStream::Diag { name: name.to_string() }, + ["error"] => JobStream::Error, + ["panic"] => JobStream::Panic, + ["stderr"] => JobStream::Stderr, + ["stdout"] => JobStream::Stdout, + ["task"] => JobStream::Task, + ["worker"] => JobStream::Worker, + _ => JobStream::Unknown(stream.to_string()), + } + } + + pub fn is_output(&self) -> bool { + match self { + JobStream::BgStderr { .. } + | JobStream::BgStdout { .. } + | JobStream::Stderr + | JobStream::Stdout => true, + JobStream::Agent + | JobStream::Bg { .. } + | JobStream::Console + | JobStream::Control + | JobStream::Diag { .. } + | JobStream::Error + | JobStream::Panic + | JobStream::Task + | JobStream::Worker + | JobStream::Unknown(_) => false, + } + } +} + +impl std::fmt::Display for JobStream { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + JobStream::Agent => write!(f, "agent"), + JobStream::Bg { name } => write!(f, "bg.{name}"), + JobStream::BgStderr { name } => write!(f, "bg.{name}.stderr"), + JobStream::BgStdout { name } => write!(f, "bg.{name}.stdout"), + JobStream::Console => f.write_str("console"), + JobStream::Control => f.write_str("control"), + JobStream::Diag { name } => write!(f, "diag.{name}"), + JobStream::Error => f.write_str("error"), + JobStream::Panic => f.write_str("panic"), + JobStream::Stderr => f.write_str("stderr"), + JobStream::Stdout => f.write_str("stdout"), + JobStream::Task => f.write_str("task"), + JobStream::Worker => f.write_str("worker"), + JobStream::Unknown(s) => f.write_str(s), + } + } +} diff --git a/common/src/lib.rs b/common/src/lib.rs index 88ea9656..dda5f806 100644 --- a/common/src/lib.rs +++ b/common/src/lib.rs @@ -2,6 +2,8 @@ * Copyright 2025 Oxide Computer Company */ +mod job_streams; + use std::io::{IsTerminal, Read}; use std::path::Path; use std::sync::{Mutex, OnceLock}; @@ -16,6 +18,8 @@ use rusty_ulid::Ulid; use serde::{Deserialize, Serialize}; use slog::{o, Drain, Logger}; +pub use job_streams::JobStream; + pub fn read_toml, T>(n: P) -> Result where for<'de> T: Deserialize<'de>, diff --git a/github/server/src/variety/basic.rs b/github/server/src/variety/basic.rs index 1a24a35c..2db4731b 100644 --- a/github/server/src/variety/basic.rs +++ b/github/server/src/variety/basic.rs @@ -456,74 +456,43 @@ pub(crate) async fn run( p.event_minseq = ev.seq + 1; } - let stdio = ev.stream == "stdout" || ev.stream == "stderr"; - let console = ev.stream == "console"; - let panic = ev.stream == "panic"; - let worker = ev.stream == "worker"; - let bgproc = ev.stream.starts_with("bg."); - - if stdio || console || panic { - /* - * Some commands, like "cargo build --verbose", generate - * exceptionally long output lines, running into the - * thousands of characters. The long lines present two - * challenges: they are not readily visible without - * horizontal scrolling in the GitHub UI; the maximum status - * message length GitHub will accept is 64KB, and even a - * small number of long lines means our status update will - * not be accepted. - * - * If a line is longer than 90 characters, truncate it. - * Users will still be able to see the full output in our - * detailed view where we get to render the whole page. - */ - let mut line = if console { - "|C| " - } else if panic { - "|!|" - } else { - "| " + match JobStream::from_str(&ev.stream) { + JobStream::Stderr | JobStream::Stdout => { + let l = format!("| {}", truncate_line(&ev.payload)); + p.events_tail.push_back((None, l)); } - .to_string(); - - /* - * We support ANSI escapes in the log renderer, which means - * that tools will generate ANSI sequences. That doesn't - * work in the GitHub renderer, so we need to strip them out - * entirely. - */ - let payload = strip_ansi_escapes::strip_str(&ev.payload); - let mut chars = payload.chars(); - - for _ in 0..MAX_LINE_LENGTH { - if let Some(c) = chars.next() { - line.push(c); - } else { - break; - } + JobStream::Console => { + let l = format!("|C| {}", truncate_line(&ev.payload)); + p.events_tail.push_back((None, l)); } - if chars.next().is_some() { + JobStream::Panic => { + let l = format!("|!| {}", truncate_line(&ev.payload)); + p.events_tail.push_back((None, l)); + } + JobStream::Worker => { /* - * If any characters remain, the string was truncated. + * A job may produce a large number of files. We must + * not treat worker output (which is mostly about file + * uploads) as headers. They must be regular records + * that are discarded as they scroll off the top. */ - line.push_str(" [...]"); + let line = format!("|W| {}", ev.payload); + p.events_tail.push_back((None, line)); } - - p.events_tail.push_back((None, line)); - } else if worker { - /* - * A job may produce a large number of files. We must not - * treat worker output (which is mostly about file uploads - * and so on) as headers. They must be regular records that - * are discarded as they scroll off the top. - */ - let line = format!("|W| {}", ev.payload); - p.events_tail.push_back((None, line)); - } else if !bgproc { - p.events_tail.push_back(( - Some(format!("{}/{:?}", ev.stream, ev.task)), - format!("{}: {}", ev.stream, ev.payload), - )); + JobStream::Agent + | JobStream::Control + | JobStream::Diag { .. } + | JobStream::Error + | JobStream::Task + | JobStream::Unknown(_) => { + p.events_tail.push_back(( + Some(format!("{}/{:?}", ev.stream, ev.task)), + format!("{}: {}", ev.stream, ev.payload), + )); + } + JobStream::Bg { .. } + | JobStream::BgStderr { .. } + | JobStream::BgStdout { .. } => {} } } @@ -837,6 +806,44 @@ pub(crate) async fn run( Ok(true) } +/* + * Some commands, like "cargo build --verbose", generate exceptionally long + * output lines, running into the thousands of characters. The long lines + * present two challenges: they are not readily visible without horizontal + * scrolling in the GitHub UI; the maximum status message length GitHub will + * accept is 64KB, and even a small number of long lines means our status + * update will not be accepted. + * + * If a line is longer than 90 characters, truncate it. Users will still be + * able to see the full output in our detailed view where we get to render the + * whole page. + */ +fn truncate_line(payload: &str) -> String { + /* + * We support ANSI escapes in the log renderer, which means that tools will + * generate ANSI sequences. That doesn't work in the GitHub renderer, so + * we need to strip them out entirely. + */ + let payload = strip_ansi_escapes::strip_str(payload); + let mut chars = payload.chars(); + + let mut line = String::new(); + for _ in 0..MAX_LINE_LENGTH { + if let Some(c) = chars.next() { + line.push(c); + } else { + break; + } + } + if chars.next().is_some() { + /* + * If any characters remain, the string was truncated. + */ + line.push_str(" [...]"); + } + line +} + async fn bunyan_to_html( f: &mut tokio::fs::File, dec: &mut buildomat_bunyan::BunyanDecoder, diff --git a/jobsh/src/lib.rs b/jobsh/src/lib.rs index 3989764a..204ae7a8 100644 --- a/jobsh/src/lib.rs +++ b/jobsh/src/lib.rs @@ -5,25 +5,19 @@ use std::borrow::Cow; use buildomat_client::types::JobEvent; +use buildomat_common::JobStream; use chrono::SecondsFormat; use serde::Serialize; pub mod jobfile; pub mod variety; -/* - * Classes for these streams are defined in the "variety/basic/www/style.css", - * which we send along with the generated HTML output. - */ -const CSS_STREAM_CLASSES: &[&str] = - &["stdout", "stderr", "task", "worker", "control", "console", "panic"]; - pub trait JobEventEx { /** * Choose a colour (CSS class name) for the stream to which this event * belongs. */ - fn css_class(&self) -> String; + fn css_class(&self) -> &'static str; /** * Turn a job event into a somewhat abstract object with pre-formatted HTML @@ -34,15 +28,22 @@ pub trait JobEventEx { } impl JobEventEx for JobEvent { - fn css_class(&self) -> String { - let s = self.stream.as_str(); - - if CSS_STREAM_CLASSES.contains(&s) { - format!("s_{s}") - } else if s.starts_with("bg.") { - "s_bgtask".into() - } else { - "s_default".into() + fn css_class(&self) -> &'static str { + match JobStream::from_str(&self.stream) { + JobStream::Console => "s_console", + JobStream::Control => "s_control", + JobStream::Panic => "s_panic", + JobStream::Stderr => "s_stderr", + JobStream::Stdout => "s_stdout", + JobStream::Task => "s_task", + JobStream::Worker => "s_worker", + JobStream::Bg { .. } + | JobStream::BgStderr { .. } + | JobStream::BgStdout { .. } => "s_bgtask", + JobStream::Agent + | JobStream::Error + | JobStream::Diag { .. } + | JobStream::Unknown(_) => "s_default", } } @@ -137,7 +138,7 @@ fn encode_payload(payload: &str) -> Cow<'_, str> { #[derive(Debug, Serialize)] pub struct EventRow { task: Option, - css_class: String, + css_class: &'static str, fields: Vec, }