Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Custom OTLP File Exporter + opentelemetry updates #909

Merged
merged 12 commits into from
Feb 19, 2025
14 changes: 7 additions & 7 deletions libcnb/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@ workspace = true
trace = [
"dep:opentelemetry",
"dep:opentelemetry_sdk",
"dep:opentelemetry-stdout",
"dep:opentelemetry-proto",
"dep:serde_json",
]

[dependencies]
Expand All @@ -27,15 +28,14 @@ cyclonedx-bom = { version = "0.8.0", optional = true }
libcnb-common.workspace = true
libcnb-data.workspace = true
libcnb-proc-macros.workspace = true
opentelemetry = { version = "0.24", optional = true }
opentelemetry_sdk = { version = "0.24", optional = true }
opentelemetry-stdout = { version = "0.5", optional = true, features = [
"trace",
] }
futures-core = "0.3"
opentelemetry = { version = "0.27.0", optional = true }
opentelemetry_sdk = { version = "0.27.0", optional = true }
opentelemetry-proto = { version = "0.27.0", optional = true }
serde = { version = "1.0.215", features = ["derive"] }
serde_json = { version = "1.0.133", optional = true }
thiserror = "2.0.6"
toml.workspace = true

[dev-dependencies]
serde_json = "1.0.133"
tempfile = "3.14.0"
114 changes: 82 additions & 32 deletions libcnb/src/tracing.rs
Original file line number Diff line number Diff line change
@@ -1,14 +1,23 @@
use futures_core::future::BoxFuture;
use libcnb_data::buildpack::Buildpack;
use opentelemetry::{
global,
trace::{Span as SpanTrait, Status, Tracer, TracerProvider as TracerProviderTrait},
KeyValue,
global::{self, BoxedSpan},
trace::{Span as SpanTrait, Status, TraceError, Tracer, TracerProvider as TracerProviderTrait},
InstrumentationScope, KeyValue,
};
use opentelemetry_proto::transform::common::tonic::ResourceAttributesWithSchema;
use opentelemetry_proto::transform::trace::tonic::group_spans_by_resource_and_scope;
use opentelemetry_sdk::{
trace::{Config, Span, TracerProvider},
export::trace::SpanExporter,
trace::TracerProvider,
Resource,
};
use std::{io::BufWriter, path::Path};
use std::{
fmt::Debug,
io::Write,
path::Path,
sync::{Arc, Mutex},
};

// This is the directory in which `BuildpackTrace` stores OpenTelemetry File
// Exports. Services which intend to export the tracing data from libcnb.rs
Expand All @@ -23,7 +32,7 @@ const TELEMETRY_EXPORT_ROOT: &str = "/tmp/libcnb-telemetry";
/// a single CNB build or detect phase.
pub(crate) struct BuildpackTrace {
provider: TracerProvider,
span: Span,
span: BoxedSpan,
}

/// Start an OpenTelemetry trace and span that exports to an
Expand All @@ -40,45 +49,44 @@ pub(crate) fn start_trace(buildpack: &Buildpack, phase_name: &'static str) -> Bu
if let Some(parent_dir) = tracing_file_path.parent() {
let _ = std::fs::create_dir_all(parent_dir);
}
let exporter = match std::fs::File::options()

let resource = Resource::new([
// Define a resource that defines the trace provider.
// The buildpac name/version seems to map well to the suggestion here
// https://opentelemetry.io/docs/specs/semconv/resource/#service.
KeyValue::new("service.name", buildpack.id.to_string()),
KeyValue::new("service.version", buildpack.version.to_string()),
]);

let provider_builder = TracerProvider::builder().with_resource(resource.clone());

let provider = match std::fs::File::options()
.create(true)
.append(true)
.open(&tracing_file_path)
.map(|file| FileExporter::new(file, resource))
{
// Write tracing data to a file, which may be read by other
// services. Wrap with a BufWriter to prevent serde from sending each
// JSON token to IO, and instead send entire JSON objects to IO.
Ok(file) => opentelemetry_stdout::SpanExporter::builder()
.with_writer(BufWriter::new(file))
.build(),
// Failed tracing shouldn't fail a build, and any logging here would
// likely confuse the user, so send telemetry to /dev/null on errors.
Err(_) => opentelemetry_stdout::SpanExporter::builder()
.with_writer(std::io::sink())
.build(),
};

let provider = TracerProvider::builder()
.with_simple_exporter(exporter)
.with_config(Config::default().with_resource(Resource::new([
// Associate the tracer provider with service attributes. The buildpack
// name/version seems to map well to the suggestion here
// https://opentelemetry.io/docs/specs/semconv/resource/#service.
KeyValue::new("service.name", buildpack.id.to_string()),
KeyValue::new("service.version", buildpack.version.to_string()),
])))
.build();
Ok(exporter) => provider_builder.with_simple_exporter(exporter),
// Failed tracing shouldn't fail a build, and any export logging here
// would likely confuse the user, so we won't export when the file has IO errors
Err(_) => provider_builder,
}
.build();

// Set the global tracer provider so that buildpacks may use it.
global::set_tracer_provider(provider.clone());

// Get a tracer identified by the instrumentation scope/library. The libcnb
// crate name/version seems to map well to the suggestion here:
// https://opentelemetry.io/docs/specs/otel/trace/api/#get-a-tracer.
let tracer = provider
.tracer_builder(env!("CARGO_PKG_NAME"))
.with_version(env!("CARGO_PKG_VERSION"))
.build();
let tracer = global::tracer_provider().tracer_with_scope(
InstrumentationScope::builder(env!("CARGO_PKG_NAME"))
.with_version(env!("CARGO_PKG_VERSION"))
.build(),
);

let mut span = tracer.start(trace_name);
span.set_attributes([
Expand Down Expand Up @@ -110,7 +118,49 @@ impl Drop for BuildpackTrace {
fn drop(&mut self) {
self.span.end();
self.provider.force_flush();
global::shutdown_tracer_provider();
self.provider.shutdown().ok();
}
}

#[derive(Debug)]
struct FileExporter<W: Write + Send + Debug> {
resource: Resource,
writer: Arc<Mutex<W>>,
}

impl<W: Write + Send + Debug> FileExporter<W> {
fn new(w: W, r: Resource) -> Self {
Self {
resource: r,
writer: Arc::new(Mutex::new(w)),
}
}
}

impl<W: Write + Send + Debug> SpanExporter for FileExporter<W> {
fn export(
&mut self,
batch: Vec<opentelemetry_sdk::export::trace::SpanData>,
) -> BoxFuture<'static, opentelemetry_sdk::export::trace::ExportResult> {
let resource = ResourceAttributesWithSchema::from(&self.resource);
let data = group_spans_by_resource_and_scope(batch, &resource);
let json = serde_json::to_string(&data);
let line = match json {
Ok(line) => line,
Err(e) => {
return Box::pin(std::future::ready(Err(TraceError::from(e.to_string()))));
}
};
let mut file = match self.writer.lock() {
Ok(f) => f,
Err(e) => {
return Box::pin(std::future::ready(Err(TraceError::from(e.to_string()))));
}
};
match file.write_all(line.as_bytes()) {
Ok(()) => Box::pin(std::future::ready(Ok(()))),
Err(e) => Box::pin(std::future::ready(Err(TraceError::from(e.to_string())))),
}
}
}

Expand Down
Loading