pact_broker_cli/cli/
otel.rs

1use 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 log_level is None, disable logs and tracing
63    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    // Stdout log output if log_level is set
73    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    // OTEL trace output
85    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        // Add OTLP exporter as batch if present
113        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            // Add stdout exporter as simple if "stdout" is in the exporters list
121            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    // // OTEL log output
150    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    // create a layered subscriber
174    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}