#![deny(missing_docs, unreachable_pub, missing_debug_implementations)]
mod cmd;
mod context;
mod id;
mod pipeline;
mod status;
mod timestamp;
use id::{BuildID, StepID};
use opentelemetry::{
metrics::Meter,
trace::{FutureExt, Span, SpanKind, StatusCode, TraceContextExt, Tracer},
Context, Key, KeyValue, Unit,
};
use status::Status;
use std::borrow::Cow;
use structopt::StructOpt;
use timestamp::Timestamp;
fn record_event_duration(meter: &Meter, name: &str, start_time: Timestamp, labels: &[KeyValue]) {
let duration = start_time.system_time().elapsed().unwrap_or_default();
match meter
.f64_value_recorder(name)
.with_unit(Unit::new("seconds"))
.try_init()
{
Ok(value_recorder) => value_recorder.record(duration.as_secs_f64(), labels),
Err(err) => eprintln!("Failed to record duration {}: {}", name, err),
}
}
#[derive(StructOpt)]
enum Args {
ID,
Now,
Cmd {
#[structopt(long = "build", env = "TRACEBUILD_BUILD_ID")]
build: BuildID,
#[structopt(long = "step", env = "TRACEBUILD_STEP_ID")]
step: Option<StepID>,
#[structopt(long = "name")]
name: Option<String>,
#[structopt(long = "build-name", env = "TRACEBUILD_BUILD_NAME")]
build_name: Option<String>,
#[structopt(name = "CMD")]
cmd: String,
#[structopt(name = "ARGS")]
args: Vec<String>,
},
Step {
#[structopt(long = "build", env = "TRACEBUILD_BUILD_ID")]
build: BuildID,
#[structopt(long = "step")]
step: Option<StepID>,
#[structopt(long = "id", env = "TRACEBUILD_STEP_ID")]
id: StepID,
#[structopt(long = "start-time", env = "TRACEBUILD_STEP_START")]
start_time: Timestamp,
#[structopt(long = "name")]
name: Option<String>,
#[structopt(long = "build-name", env = "TRACEBUILD_BUILD_NAME")]
build_name: Option<String>,
#[structopt(long = "status")]
status: Option<Status>,
},
Build {
#[structopt(long = "id", env = "TRACEBUILD_BUILD_ID")]
id: BuildID,
#[structopt(long = "start-time", env = "TRACEBUILD_BUILD_START")]
start_time: Timestamp,
#[structopt(long = "name", env = "TRACEBUILD_BUILD_NAME")]
name: Option<String>,
#[structopt(long = "branch")]
branch: Option<String>,
#[structopt(long = "commit")]
commit: Option<String>,
#[structopt(long = "status")]
status: Option<Status>,
},
}
async fn async_main() -> i32 {
let args = Args::from_args();
match args {
Args::ID => {
let id = BuildID::generate();
println!("{}", id);
0
}
Args::Now => {
let now = Timestamp::now();
println!("{}", now);
0
}
Args::Cmd {
build,
step,
name,
build_name,
cmd,
args,
} => {
let pipeline = pipeline::install_pipeline();
let name = name.unwrap_or_else(|| format!("{} {}", cmd, args.join(" ")));
let span = pipeline
.tracer
.span_builder(&format!("cmd - {}", name))
.with_parent_context(context::get_parent_context(build, step))
.with_kind(SpanKind::Client)
.with_attributes(vec![
Key::new("tracebuild.cmd.command").string(cmd.clone()),
Key::new("tracebuild.cmd.arguments").array(
args.iter()
.map(|arg| Cow::from(arg.clone()))
.collect::<Vec<_>>(),
),
])
.start(&pipeline.tracer);
let cx = Context::current_with_span(span);
let start_time = Timestamp::now();
let exit_code = match cmd::fork_with_sigterm(cmd, args)
.with_context(cx.clone())
.await
{
Ok(exit_status) => {
let exit_code = exit_status.code().unwrap_or(1);
cx.span()
.set_attribute(Key::new("tracebuild.cmd.exit_code").i64(exit_code.into()));
exit_code
}
Err(err) => {
eprintln!("{}", err);
cx.span().record_exception(&err);
cx.span().set_status(StatusCode::Error, err.to_string());
err.suggested_exit_code()
}
};
let mut labels = Vec::new();
labels.push(Key::new("name").string(name));
if let Some(build_name) = build_name {
labels.push(Key::new("build_name").string(build_name));
}
labels.push(Key::new("exit_code").i64(exit_code.into()));
record_event_duration(
&pipeline.meter,
"tracebuild.cmd.duration",
start_time,
&labels,
);
exit_code
}
Args::Step {
build,
step,
id,
start_time,
name,
build_name,
status,
} => {
let pipeline = pipeline::install_pipeline();
let span_name: Cow<'static, str> = if let Some(name) = name.clone() {
format!("step - {}", name).into()
} else {
"step".into()
};
let span = pipeline
.tracer
.span_builder(&span_name)
.with_parent_context(context::get_parent_context(build, step))
.with_start_time(start_time.system_time())
.with_span_id(id.span_id())
.with_kind(SpanKind::Internal)
.start(&pipeline.tracer);
if let Some(status) = &status {
span.set_status(status.into(), "".into());
}
let mut labels = Vec::new();
if let Some(name) = name {
labels.push(Key::new("name").string(name));
}
if let Some(build_name) = build_name {
labels.push(Key::new("build_name").string(build_name));
}
if let Some(status) = status {
labels.push(Key::new("status").string(status.to_string()));
}
record_event_duration(
&pipeline.meter,
"tracebuild.step.duration",
start_time,
&labels,
);
0
}
Args::Build {
id,
start_time,
name,
branch,
commit,
status,
} => {
let pipeline = pipeline::install_pipeline();
let span_name: Cow<'static, str> = if let Some(name) = name.clone() {
format!("build - {}", name).into()
} else {
"build".into()
};
let span = pipeline
.tracer
.span_builder(&span_name)
.with_start_time(start_time.system_time())
.with_trace_id(id.trace_id())
.with_span_id(id.span_id())
.with_kind(SpanKind::Internal)
.start(&pipeline.tracer);
if let Some(branch) = branch.clone() {
span.set_attribute(Key::new("tracebuild.build.branch").string(branch));
}
if let Some(commit) = commit {
span.set_attribute(Key::new("tracebuild.build.commit").string(commit));
}
if let Some(status) = &status {
span.set_status(status.into(), "".into());
}
let mut labels = Vec::new();
if let Some(name) = name {
labels.push(Key::new("name").string(name));
}
if let Some(branch) = branch {
labels.push(Key::new("branch").string(branch));
}
if let Some(status) = status {
labels.push(Key::new("status").string(status.to_string()));
}
record_event_duration(
&pipeline.meter,
"tracebuild.build.duration",
start_time,
&labels,
);
0
}
}
}
fn main() {
let exit_code = tokio::runtime::Builder::new_multi_thread()
.enable_all()
.build()
.unwrap()
.block_on(async_main());
std::process::exit(exit_code);
}