Skip to content

Commit 012e6b6

Browse files
authored
Custom OTLP File Exporter + opentelemetry updates (#909)
* Implement FileExporter * Update opentelemetry and add resource to BuildpackTrace * Make opentelemetry-proto optional * Correct Cargo.toml syntax * Serialize direct to writer; skip the string * Conditional compilation for trace dependencies * Update to opentelemetry 0.28 * Use the batch exporter * Simplify FileExporter matches * Ensure serde_json is also in dev-dependencies for testing * Add changelog entry for OTLP File Exporter * Update changelog with crate in question
1 parent 693a064 commit 012e6b6

File tree

3 files changed

+115
-43
lines changed

3 files changed

+115
-43
lines changed

CHANGELOG.md

+4
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
99

1010
## [Unreleased]
1111

12+
### Changed
13+
14+
- `libcnb`:
15+
- Implemented custom OTLP File Exporter instead of `opentelemetry-stdout` and updated `opentelemetry` libraries to `0.28`. ([#909](https://github.com/heroku/libcnb.rs/pull/909/))
1216

1317
## [0.26.1] - 2024-12-10
1418

libcnb/Cargo.toml

+14-7
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,16 @@ workspace = true
1616

1717
[features]
1818
trace = [
19+
"dep:futures-core",
1920
"dep:opentelemetry",
21+
"opentelemetry/trace",
2022
"dep:opentelemetry_sdk",
21-
"dep:opentelemetry-stdout",
23+
"opentelemetry_sdk/trace",
24+
"dep:opentelemetry-proto",
25+
"opentelemetry-proto/trace",
26+
"opentelemetry-proto/gen-tonic-messages",
27+
"opentelemetry-proto/with-serde",
28+
"dep:serde_json",
2229
]
2330

2431
[dependencies]
@@ -27,15 +34,15 @@ cyclonedx-bom = { version = "0.8.0", optional = true }
2734
libcnb-common.workspace = true
2835
libcnb-data.workspace = true
2936
libcnb-proc-macros.workspace = true
30-
opentelemetry = { version = "0.24", optional = true }
31-
opentelemetry_sdk = { version = "0.24", optional = true }
32-
opentelemetry-stdout = { version = "0.5", optional = true, features = [
33-
"trace",
34-
] }
37+
futures-core = { version = "0.3", optional = true }
38+
opentelemetry = { version = "0.28.0", optional = true, default-features = false }
39+
opentelemetry_sdk = { version = "0.28.0", optional = true, default-features = false }
40+
opentelemetry-proto = { version = "0.28.0", optional = true, default-features = false }
3541
serde = { version = "1.0.215", features = ["derive"] }
42+
serde_json = { version = "1.0.133", optional = true }
3643
thiserror = "2.0.6"
3744
toml.workspace = true
3845

3946
[dev-dependencies]
40-
serde_json = "1.0.133"
4147
tempfile = "3.14.0"
48+
serde_json = "1.0.133"

libcnb/src/tracing.rs

+97-36
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,24 @@
1+
use futures_core::future::BoxFuture;
12
use libcnb_data::buildpack::Buildpack;
23
use opentelemetry::{
3-
global,
4+
global::{self, BoxedSpan},
45
trace::{Span as SpanTrait, Status, Tracer, TracerProvider as TracerProviderTrait},
5-
KeyValue,
6+
InstrumentationScope, KeyValue,
67
};
8+
use opentelemetry_proto::transform::common::tonic::ResourceAttributesWithSchema;
9+
use opentelemetry_proto::transform::trace::tonic::group_spans_by_resource_and_scope;
710
use opentelemetry_sdk::{
8-
trace::{Config, Span, TracerProvider},
11+
error::{OTelSdkError, OTelSdkResult},
12+
trace::SdkTracerProvider,
13+
trace::SpanExporter,
914
Resource,
1015
};
11-
use std::{io::BufWriter, path::Path};
16+
use std::{
17+
fmt::Debug,
18+
io::{LineWriter, Write},
19+
path::Path,
20+
sync::{Arc, Mutex},
21+
};
1222

1323
// This is the directory in which `BuildpackTrace` stores OpenTelemetry File
1424
// Exports. Services which intend to export the tracing data from libcnb.rs
@@ -22,8 +32,8 @@ const TELEMETRY_EXPORT_ROOT: &str = "/tmp/libcnb-telemetry";
2232
/// Represents an OpenTelemetry tracer provider and single span tracing
2333
/// a single CNB build or detect phase.
2434
pub(crate) struct BuildpackTrace {
25-
provider: TracerProvider,
26-
span: Span,
35+
provider: SdkTracerProvider,
36+
span: BoxedSpan,
2737
}
2838

2939
/// Start an OpenTelemetry trace and span that exports to an
@@ -40,45 +50,44 @@ pub(crate) fn start_trace(buildpack: &Buildpack, phase_name: &'static str) -> Bu
4050
if let Some(parent_dir) = tracing_file_path.parent() {
4151
let _ = std::fs::create_dir_all(parent_dir);
4252
}
43-
let exporter = match std::fs::File::options()
44-
.create(true)
45-
.append(true)
46-
.open(&tracing_file_path)
47-
{
48-
// Write tracing data to a file, which may be read by other
49-
// services. Wrap with a BufWriter to prevent serde from sending each
50-
// JSON token to IO, and instead send entire JSON objects to IO.
51-
Ok(file) => opentelemetry_stdout::SpanExporter::builder()
52-
.with_writer(BufWriter::new(file))
53-
.build(),
54-
// Failed tracing shouldn't fail a build, and any logging here would
55-
// likely confuse the user, so send telemetry to /dev/null on errors.
56-
Err(_) => opentelemetry_stdout::SpanExporter::builder()
57-
.with_writer(std::io::sink())
58-
.build(),
59-
};
6053

61-
let provider = TracerProvider::builder()
62-
.with_simple_exporter(exporter)
63-
.with_config(Config::default().with_resource(Resource::new([
64-
// Associate the tracer provider with service attributes. The buildpack
65-
// name/version seems to map well to the suggestion here
66-
// https://opentelemetry.io/docs/specs/semconv/resource/#service.
54+
let resource = Resource::builder()
55+
// Define a resource that defines the trace provider.
56+
// The buildpack name/version seems to map well to the suggestion here
57+
// https://opentelemetry.io/docs/specs/semconv/resource/#service.
58+
.with_attributes([
6759
KeyValue::new("service.name", buildpack.id.to_string()),
6860
KeyValue::new("service.version", buildpack.version.to_string()),
69-
])))
61+
])
7062
.build();
7163

64+
let provider_builder = SdkTracerProvider::builder().with_resource(resource.clone());
65+
66+
let provider = match std::fs::File::options()
67+
.create(true)
68+
.append(true)
69+
.open(&tracing_file_path)
70+
.map(|file| FileExporter::new(file, resource))
71+
{
72+
// Write tracing data to a file, which may be read by other services
73+
Ok(exporter) => provider_builder.with_batch_exporter(exporter),
74+
// Failed tracing shouldn't fail a build, and any export logging here
75+
// would likely confuse the user; don't export when the file has IO errors
76+
Err(_) => provider_builder,
77+
}
78+
.build();
79+
7280
// Set the global tracer provider so that buildpacks may use it.
7381
global::set_tracer_provider(provider.clone());
7482

7583
// Get a tracer identified by the instrumentation scope/library. The libcnb
7684
// crate name/version seems to map well to the suggestion here:
7785
// https://opentelemetry.io/docs/specs/otel/trace/api/#get-a-tracer.
78-
let tracer = provider
79-
.tracer_builder(env!("CARGO_PKG_NAME"))
80-
.with_version(env!("CARGO_PKG_VERSION"))
81-
.build();
86+
let tracer = global::tracer_provider().tracer_with_scope(
87+
InstrumentationScope::builder(env!("CARGO_PKG_NAME"))
88+
.with_version(env!("CARGO_PKG_VERSION"))
89+
.build(),
90+
);
8291

8392
let mut span = tracer.start(trace_name);
8493
span.set_attributes([
@@ -109,8 +118,60 @@ impl BuildpackTrace {
109118
impl Drop for BuildpackTrace {
110119
fn drop(&mut self) {
111120
self.span.end();
112-
self.provider.force_flush();
113-
global::shutdown_tracer_provider();
121+
self.provider.force_flush().ok();
122+
self.provider.shutdown().ok();
123+
}
124+
}
125+
126+
#[derive(Debug)]
127+
struct FileExporter<W: Write + Send + Debug> {
128+
writer: Arc<Mutex<LineWriter<W>>>,
129+
resource: Resource,
130+
}
131+
132+
impl<W: Write + Send + Debug> FileExporter<W> {
133+
fn new(writer: W, resource: Resource) -> Self {
134+
Self {
135+
writer: Arc::new(Mutex::new(LineWriter::new(writer))),
136+
resource,
137+
}
138+
}
139+
}
140+
141+
impl<W: Write + Send + Debug> SpanExporter for FileExporter<W> {
142+
fn export(
143+
&mut self,
144+
batch: Vec<opentelemetry_sdk::trace::SpanData>,
145+
) -> BoxFuture<'static, OTelSdkResult> {
146+
let resource = ResourceAttributesWithSchema::from(&self.resource);
147+
let data = group_spans_by_resource_and_scope(batch, &resource);
148+
let mut writer = match self.writer.lock() {
149+
Ok(f) => f,
150+
Err(e) => {
151+
return Box::pin(std::future::ready(Err(OTelSdkError::InternalFailure(
152+
e.to_string(),
153+
))));
154+
}
155+
};
156+
Box::pin(std::future::ready(
157+
serde_json::to_writer(writer.get_mut(), &data)
158+
.map_err(|e| OTelSdkError::InternalFailure(e.to_string())),
159+
))
160+
}
161+
162+
fn force_flush(&mut self) -> OTelSdkResult {
163+
let mut writer = self
164+
.writer
165+
.lock()
166+
.map_err(|e| OTelSdkError::InternalFailure(e.to_string()))?;
167+
168+
writer
169+
.flush()
170+
.map_err(|e| OTelSdkError::InternalFailure(e.to_string()))
171+
}
172+
173+
fn set_resource(&mut self, res: &opentelemetry_sdk::Resource) {
174+
self.resource = res.clone();
114175
}
115176
}
116177

0 commit comments

Comments
 (0)