1
+ use futures_core:: future:: BoxFuture ;
1
2
use libcnb_data:: buildpack:: Buildpack ;
2
3
use opentelemetry:: {
3
- global,
4
+ global:: { self , BoxedSpan } ,
4
5
trace:: { Span as SpanTrait , Status , Tracer , TracerProvider as TracerProviderTrait } ,
5
- KeyValue ,
6
+ InstrumentationScope , KeyValue ,
6
7
} ;
8
+ use opentelemetry_proto:: transform:: common:: tonic:: ResourceAttributesWithSchema ;
9
+ use opentelemetry_proto:: transform:: trace:: tonic:: group_spans_by_resource_and_scope;
7
10
use opentelemetry_sdk:: {
8
- trace:: { Config , Span , TracerProvider } ,
11
+ error:: { OTelSdkError , OTelSdkResult } ,
12
+ trace:: SdkTracerProvider ,
13
+ trace:: SpanExporter ,
9
14
Resource ,
10
15
} ;
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
+ } ;
12
22
13
23
// This is the directory in which `BuildpackTrace` stores OpenTelemetry File
14
24
// Exports. Services which intend to export the tracing data from libcnb.rs
@@ -22,8 +32,8 @@ const TELEMETRY_EXPORT_ROOT: &str = "/tmp/libcnb-telemetry";
22
32
/// Represents an OpenTelemetry tracer provider and single span tracing
23
33
/// a single CNB build or detect phase.
24
34
pub ( crate ) struct BuildpackTrace {
25
- provider : TracerProvider ,
26
- span : Span ,
35
+ provider : SdkTracerProvider ,
36
+ span : BoxedSpan ,
27
37
}
28
38
29
39
/// 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
40
50
if let Some ( parent_dir) = tracing_file_path. parent ( ) {
41
51
let _ = std:: fs:: create_dir_all ( parent_dir) ;
42
52
}
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
- } ;
60
53
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 ( [
67
59
KeyValue :: new ( "service.name" , buildpack. id . to_string ( ) ) ,
68
60
KeyValue :: new ( "service.version" , buildpack. version . to_string ( ) ) ,
69
- ] ) ) )
61
+ ] )
70
62
. build ( ) ;
71
63
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
+
72
80
// Set the global tracer provider so that buildpacks may use it.
73
81
global:: set_tracer_provider ( provider. clone ( ) ) ;
74
82
75
83
// Get a tracer identified by the instrumentation scope/library. The libcnb
76
84
// crate name/version seems to map well to the suggestion here:
77
85
// 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
+ ) ;
82
91
83
92
let mut span = tracer. start ( trace_name) ;
84
93
span. set_attributes ( [
@@ -109,8 +118,60 @@ impl BuildpackTrace {
109
118
impl Drop for BuildpackTrace {
110
119
fn drop ( & mut self ) {
111
120
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 ( ) ;
114
175
}
115
176
}
116
177
0 commit comments