Skip to content

Commit

Permalink
Add tracing for build phase and tests for with_tracing
Browse files Browse the repository at this point in the history
  • Loading branch information
joshwlewis committed Oct 27, 2023
1 parent e2fa9b8 commit f8b7056
Show file tree
Hide file tree
Showing 2 changed files with 157 additions and 63 deletions.
100 changes: 50 additions & 50 deletions libcnb/src/runtime.rs
Original file line number Diff line number Diff line change
Expand Up @@ -142,22 +142,13 @@ pub fn libcnb_runtime_detect<B: Buildpack>(
buildpack_descriptor: read_buildpack_descriptor()?,
};

with_tracing(
"detect",
detect_context.buildpack_descriptor.buildpack.id.clone(),
with_tracing::<B>(
&"detect",
&detect_context.buildpack_descriptor.buildpack.id.clone(),
|trace_ctx| {
let span = trace_ctx.span();
set_buildpack_span_attributes::<B>(&span, &detect_context.buildpack_descriptor);
let detect_result = buildpack.detect(detect_context);
if let Err(err) = detect_result {
span.record_error(&err);
span.set_status(opentelemetry::trace::Status::Error {
description: std::borrow::Cow::Borrowed("detect error"),
});
return Err(err);
}

match detect_result.unwrap().0 {
match buildpack.detect(detect_context)?.0 {
InnerDetectResult::Fail => {
span.add_event("detect-fail", vec![]);
Ok(exit_code::DETECT_DETECTION_FAILED)
Expand Down Expand Up @@ -203,7 +194,7 @@ pub fn libcnb_runtime_build<B: Buildpack>(
}
.map_err(Error::CannotReadStore)?;

let build_result = buildpack.build(BuildContext {
let build_context = BuildContext {
layers_dir: layers_dir.clone(),
app_dir,
stack_id,
Expand All @@ -212,44 +203,53 @@ pub fn libcnb_runtime_build<B: Buildpack>(
buildpack_dir: read_buildpack_dir()?,
buildpack_descriptor: read_buildpack_descriptor()?,
store,
})?;

match build_result.0 {
InnerBuildResult::Pass {
launch,
store,
build_sboms,
launch_sboms,
} => {
if let Some(launch) = launch {
write_toml_file(&launch, layers_dir.join("launch.toml"))
.map_err(Error::CannotWriteLaunch)?;
};

if let Some(store) = store {
write_toml_file(&store, layers_dir.join("store.toml"))
.map_err(Error::CannotWriteStore)?;
};

for build_sbom in build_sboms {
fs::write(
cnb_sbom_path(&build_sbom.format, &layers_dir, "build"),
&build_sbom.data,
)
.map_err(Error::CannotWriteBuildSbom)?;
}
};

for launch_sbom in launch_sboms {
fs::write(
cnb_sbom_path(&launch_sbom.format, &layers_dir, "launch"),
&launch_sbom.data,
)
.map_err(Error::CannotWriteLaunchSbom)?;
}
with_tracing::<B>(
"build",
&build_context.buildpack_descriptor.buildpack.id.clone(),
|trace_ctx| {
let span = trace_ctx.span();
set_buildpack_span_attributes::<B>(&span, &build_context.buildpack_descriptor);
match buildpack.build(build_context)?.0 {
InnerBuildResult::Pass {
launch,
store,
build_sboms,
launch_sboms,
} => {
if let Some(launch) = launch {
write_toml_file(&launch, layers_dir.join("launch.toml"))
.map_err(Error::CannotWriteLaunch)?;
};

if let Some(store) = store {
write_toml_file(&store, layers_dir.join("store.toml"))
.map_err(Error::CannotWriteStore)?;
};

for build_sbom in build_sboms {
fs::write(
cnb_sbom_path(&build_sbom.format, &layers_dir, "build"),
&build_sbom.data,
)
.map_err(Error::CannotWriteBuildSbom)?;
}

Ok(exit_code::GENERIC_SUCCESS)
}
}
for launch_sbom in launch_sboms {
fs::write(
cnb_sbom_path(&launch_sbom.format, &layers_dir, "launch"),
&launch_sbom.data,
)
.map_err(Error::CannotWriteLaunchSbom)?;
}

span.add_event("build-success", vec![]);
Ok(exit_code::GENERIC_SUCCESS)
}
}
},
)
}

// A partial representation of buildpack.toml that contains only the Buildpack API version,
Expand Down
120 changes: 107 additions & 13 deletions libcnb/src/tracing.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,27 @@ use crate::Buildpack;
use libcnb_data::buildpack::{BuildpackId, ComponentBuildpackDescriptor};
use opentelemetry::{
global::{self},
sdk::trace::TracerProvider,
trace::{SpanRef, Tracer, TracerProvider as TracerProviderImpl},
sdk::trace::{Span, TracerProvider},
trace::{
Span as SpanImpl, SpanRef, TraceContextExt, Tracer, TracerProvider as TracerProviderImpl,
},
Context, KeyValue,
};
use opentelemetry_stdout::SpanExporter;
use std::{
fmt::Display,
fmt::{Debug, Display},
fs::{create_dir_all, File},
io::sink,
path::Path,
};

pub fn with_tracing<F, R, S>(step: S, bp_id: BuildpackId, f: F) -> R
pub fn with_tracing<B>(
phase: impl Display,
bp_id: &BuildpackId,
f: impl FnOnce(&Context) -> crate::Result<i32, B::Error>,
) -> crate::Result<i32, B::Error>
where
F: FnOnce(Context) -> R,
S: Display,
B: Buildpack,
{
let bp_slug = bp_id.replace(['/', '.'], "_");
let provider = init_tracing(&bp_slug);
Expand All @@ -28,10 +33,23 @@ where
None as Option<&str>,
None,
);
let result = tracer.in_span(format!("libcnb-{step}-{bp_slug}"), |trace_ctx| f(trace_ctx));
let outer_result = tracer.in_span(format!("libcnb-{bp_slug}-{phase}"), |trace_ctx| {
let inner_result = f(&trace_ctx);
if let Err(err) = &inner_result {
let span = trace_ctx.span();
// span.record_error(err) would make more sense than an event here,
// but Buildpack::Error doesn't implement std::error::Error.
// Should it?
span.add_event(format!("{phase}-error"), vec![]);
span.set_status(opentelemetry::trace::Status::Error {
description: std::borrow::Cow::Owned(format!("{err:?}")),
});
};
inner_result
});
provider.force_flush();
global::shutdown_tracer_provider();
result
outer_result
}

fn init_tracing(bp_id: &str) -> TracerProvider {
Expand Down Expand Up @@ -69,12 +87,88 @@ pub fn set_buildpack_span_attributes<B: Buildpack + ?Sized>(
),
KeyValue::new(
"buildpack_name",
bp_descriptor
.buildpack
.name
.clone()
.unwrap_or_else(String::new),
bp_descriptor.buildpack.name.clone().unwrap_or_default(),
),
KeyValue::new("buildpack_api", bp_descriptor.api.to_string()),
]);
}

#[cfg(test)]
mod tests {
use super::with_tracing;
use crate::{
build::{BuildContext, BuildResult, BuildResultBuilder},
detect::{DetectContext, DetectResult, DetectResultBuilder},
generic::GenericPlatform,
Buildpack, Error,
};
use libcnb_data::{buildpack::BuildpackId, generic::GenericMetadata};
use opentelemetry::trace::TraceContextExt;
use std::fs;

struct TestBuildpack;

impl Buildpack for TestBuildpack {
type Platform = GenericPlatform;
type Metadata = GenericMetadata;
type Error = TestBuildpackError;

fn detect(
&self,
_context: DetectContext<Self>,
) -> crate::Result<DetectResult, Self::Error> {
DetectResultBuilder::pass().build()
}

fn build(&self, _context: BuildContext<Self>) -> crate::Result<BuildResult, Self::Error> {
BuildResultBuilder::new().build()
}
}

#[derive(Debug)]
struct TestBuildpackError;

#[test]
fn with_tracing_ok() {
let buildpack_id: BuildpackId = "heroku/foo-engine"
.parse()
.expect("Expected to parse this buildpack id");
let telemetry_path = "/tmp/cnb-telemetry/heroku_foo-engine.jsonl";

with_tracing::<TestBuildpack>("detect", &buildpack_id, |trace_ctx| {
trace_ctx.span().add_event("realigning-splines", vec![]);
Ok(0)
})
.expect("Expected tracing result to be Ok, but was an Err");

let tracing_contents = fs::read_to_string(telemetry_path)
.expect("Expected telemetry file to exist, but couldn't read it");
_ = fs::remove_file(telemetry_path);

println!("tracing_contents: {tracing_contents}");
assert!(tracing_contents.contains("libcnb-heroku_foo-engine-detect"));
assert!(tracing_contents.contains("realigning-splines"));
}

#[test]
fn with_tracing_err() {
let buildpack_id: BuildpackId = "heroku/bar-engine"
.parse()
.expect("Expected to parse this buildpack id");
let telemetry_path = "/tmp/cnb-telemetry/heroku_bar-engine.jsonl";

with_tracing::<TestBuildpack>("build", &buildpack_id, |_| {
Err(Error::BuildpackError(TestBuildpackError))
})
.expect_err("Expected tracing result to be an Err, but was Ok");

let tracing_contents = fs::read_to_string(telemetry_path)
.expect("Expected telemetry file to exist, but couldn't read it");
_ = fs::remove_file(telemetry_path);

println!("tracing_contents: {tracing_contents}");
assert!(tracing_contents.contains("build-error"));
assert!(tracing_contents.contains("TestBuildpackError"));
assert!(tracing_contents.contains("libcnb-heroku_bar-engine-build"));
}
}

0 comments on commit f8b7056

Please sign in to comment.