pact_broker_cli/cli/
otel.rs1use opentelemetry::KeyValue;
2use opentelemetry::global;
3use opentelemetry::trace::TraceContextExt;
4use opentelemetry_appender_tracing::layer::OpenTelemetryTracingBridge;
5use opentelemetry_otlp::Protocol;
6use opentelemetry_otlp::WithExportConfig;
7use opentelemetry_sdk::Resource;
8use opentelemetry_sdk::{
9 logs::SdkLoggerProvider, propagation::TraceContextPropagator, trace::SdkTracerProvider,
10};
11use std::sync::OnceLock;
12use tracing::Level;
13use tracing::info;
14use tracing_opentelemetry::OpenTelemetrySpanExt;
15use tracing_subscriber::Layer;
16use tracing_subscriber::Registry;
17use tracing_subscriber::layer::SubscriberExt;
18
19#[derive(Debug)]
20pub struct OtelConfig {
21 pub exporter: Option<Vec<String>>,
22 pub endpoint: Option<String>,
23 pub protocol: Option<String>,
24 pub enable_otel: Option<bool>,
25 pub enable_traces: Option<bool>,
26 pub enable_logs: Option<bool>,
27 pub log_level: Option<Level>,
28}
29
30pub struct TracerProviderDropper(pub opentelemetry_sdk::trace::SdkTracerProvider);
31
32impl Drop for TracerProviderDropper {
33 fn drop(&mut self) {
34 match self.0.force_flush() {
35 Ok(_) => (),
36 Err(e) => eprintln!("Failed to flush OpenTelemetry tracing: {e}"),
37 }
38 }
39}
40
41fn get_resource() -> Resource {
42 static RESOURCE: OnceLock<Resource> = OnceLock::new();
43 RESOURCE
44 .get_or_init(|| {
45 Resource::builder()
46 .with_service_name("pact-broker-cli")
47 .with_attributes(vec![
48 KeyValue::new("service.name", env!("CARGO_CRATE_NAME")),
49 KeyValue::new("service.version", env!("CARGO_PKG_VERSION")),
50 KeyValue::new(
51 "service.instance.id",
52 std::env::var("HOSTNAME").unwrap_or_default(),
53 ),
54 KeyValue::new("service.auto.version", env!("CARGO_PKG_VERSION")),
55 ])
56 .build()
57 })
58 .clone()
59}
60
61pub fn init_logging(otel_config: OtelConfig) -> Option<SdkTracerProvider> {
62 if otel_config.log_level.is_none() {
64 info!("Log level not set, skipping logging and tracing initialization.");
65 return None;
66 }
67 global::set_text_map_propagator(TraceContextPropagator::new());
68 let resource = get_resource();
69
70 let mut layers: Vec<Box<dyn Layer<Registry> + Send + Sync>> = Vec::new();
71
72 layers.push(
74 tracing_subscriber::fmt::layer()
75 .with_level(true)
76 .with_filter(tracing_subscriber::filter::LevelFilter::from_level(
77 otel_config.log_level.unwrap(),
78 ))
79 .boxed(),
80 );
81
82 let mut tracer_provider: Option<SdkTracerProvider> = None;
83
84 if otel_config.enable_traces.unwrap_or(false) {
86 let otlp_exporter = if let Some(exporters) = &otel_config.exporter {
87 if exporters.iter().any(|e| e == "otlp") {
88 let endpoint = otel_config
89 .endpoint
90 .unwrap_or_else(|| "http://localhost:4318".to_string());
91 let protocol = otel_config.protocol.unwrap_or_else(|| "http".to_string());
92 let exporter = match protocol.as_str() {
93 "grpc" => opentelemetry_otlp::SpanExporter::builder()
94 .with_tonic()
95 .with_endpoint(endpoint.to_string())
96 .build()
97 .expect("Failed to configure grpc exporter"),
98 _ => opentelemetry_otlp::SpanExporter::builder()
99 .with_http()
100 .with_protocol(Protocol::HttpBinary)
101 .build()
102 .expect("Failed to configure http exporter"),
103 };
104 Some(exporter)
105 } else {
106 None
107 }
108 } else {
109 None
110 };
111
112 tracer_provider = if let Some(exporters_list) = &otel_config.exporter {
114 let mut builder = SdkTracerProvider::builder().with_resource(resource.clone());
115
116 if let Some(exporter) = otlp_exporter {
117 builder = builder.with_batch_exporter(exporter);
118 }
119
120 if exporters_list
122 .iter()
123 .any(|e| e == "stdout" || e == "console")
124 {
125 println!("Adding stdout exporter for tracing");
126 let stdout_exporter = opentelemetry_stdout::SpanExporter::default();
127 builder = builder.with_simple_exporter(stdout_exporter);
128 }
129
130 Some(builder.build())
131 } else {
132 Some(
133 SdkTracerProvider::builder()
134 .with_resource(resource.clone())
135 .build(),
136 )
137 };
138
139 if let Some(ref provider) = tracer_provider {
140 global::set_tracer_provider(provider.clone());
141 }
142
143 let tracer = global::tracer("pact-broker-cli");
144
145 let telemetry = tracing_opentelemetry::layer().with_tracer(tracer);
146 layers.push(Box::new(telemetry));
147 }
148
149 if otel_config.enable_logs.unwrap_or(false) {
151 let otel_log_stdout_exporter = opentelemetry_stdout::LogExporter::default();
152
153 let otel_logger_provider = if otel_config.enable_logs.unwrap_or(false) {
154 let otel_otlp_stdout_exporter = opentelemetry_otlp::LogExporter::builder()
155 .with_http()
156 .with_protocol(Protocol::HttpBinary)
157 .build()
158 .expect("Failed to create log exporter");
159 SdkLoggerProvider::builder()
160 .with_resource(get_resource())
161 .with_simple_exporter(otel_log_stdout_exporter)
162 .with_batch_exporter(otel_otlp_stdout_exporter)
163 .build()
164 } else {
165 SdkLoggerProvider::builder()
166 .with_resource(get_resource())
167 .with_simple_exporter(otel_log_stdout_exporter)
168 .build()
169 };
170 let otel_layer = OpenTelemetryTracingBridge::new(&otel_logger_provider);
171 layers.push(Box::new(otel_layer));
172 }
173 let subscriber = tracing_subscriber::registry().with(layers);
175
176 if tracing::subscriber::set_global_default(subscriber).is_err() {
177 info!(
178 "Global tracing subscriber already set, attaching layers is not supported at runtime."
179 );
180 }
181 tracer_provider
182}
183
184pub fn capture_telemetry(args: &[String], exit_code: i32, error_message: Option<&str>) {
185 let span = tracing::Span::current();
186 let _enter = span.enter();
187 let span_context = span.context();
188 let otel_span = span_context.span();
189
190 if let Some(binary) = args.get(0) {
191 otel_span.set_attribute(KeyValue::new("binary", binary.clone()));
192 }
193 if let Some(command) = args.get(1) {
194 otel_span.set_attribute(KeyValue::new("command", command.clone()));
195 }
196 if let Some(subcommand) = args.get(2) {
197 otel_span.set_attribute(KeyValue::new("subcommand", subcommand.clone()));
198 }
199 if args.len() > 3 {
200 otel_span.set_attribute(KeyValue::new("args", format!("{:?}", &args[3..])));
201 }
202 otel_span.set_attribute(KeyValue::new("exit_code", exit_code.to_string()));
203 if let Some(message) = error_message {
204 otel_span.set_attribute(KeyValue::new("error_message", message.to_string()));
205 }
206}