Skip to main content

telemetrylib/
open_telemetry.rs

1use crate::Telemetry;
2use logger_core::log_warn;
3use once_cell::sync::OnceCell;
4use opentelemetry::trace::{
5    Span, SpanContext, SpanId, SpanKind, TraceContextExt, TraceFlags, TraceId, TraceState,
6};
7use opentelemetry::{global, trace::Tracer};
8use opentelemetry_otlp::{MetricExporter, Protocol, WithExportConfig};
9use opentelemetry_sdk::error::OTelSdkError;
10use opentelemetry_sdk::metrics::SdkMeterProvider;
11use opentelemetry_sdk::propagation::TraceContextPropagator;
12use opentelemetry_sdk::runtime::Tokio;
13use opentelemetry_sdk::trace::{
14    BatchConfig, BatchSpanProcessor, SdkTracerProvider, SpanExporter,
15    span_processor_with_async_runtime::BatchSpanProcessor as AsyncBatchSpanProcessor,
16};
17use std::io::{Error, ErrorKind};
18use std::path::PathBuf;
19use std::str::FromStr;
20#[cfg(test)]
21use std::sync::atomic::{AtomicUsize, Ordering};
22use std::sync::{Arc, OnceLock, RwLock};
23use std::time::Duration;
24use thiserror::Error;
25use url::Url;
26
27const SPAN_WRITE_LOCK_ERR: &str = "Failed to acquire span write lock";
28const SPAN_READ_LOCK_ERR: &str = "Failed to acquire span read lock";
29const TRACE_SCOPE: &str = "valkey_glide";
30
31// Metric names
32const TIMEOUT_ERROR_METRIC: &str = "glide.timeout_errors";
33const RETRIES_METRIC: &str = "glide.retry_attempts";
34const MOVED_ERROR_METRIC: &str = "glide.moved_errors";
35const SUBSCRIPTION_OUT_OF_SYNC_METRIC: &str = "glide.subscription_out_of_sync_count";
36const SUBSCRIPTION_LAST_SYNC_TIMESTAMP_METRIC: &str = "glide.subscription_last_sync_timestamp";
37
38/// Custom error type for OpenTelemetry errors in Glide
39#[derive(Debug, Error)]
40pub enum GlideOTELError {
41    #[error("Glide OpenTelemetry exporter build error: {0}")]
42    ExporterBuildError(#[from] opentelemetry_otlp::ExporterBuildError),
43
44    #[error("Glide OpenTelemetry SDK error: {0}")]
45    SdkError(#[from] OTelSdkError),
46
47    #[error("Glide OpenTelemetry trace error: {0}")]
48    TraceError(String),
49
50    #[error("Glide OpenTelemetry error: Failed to acquire read lock")]
51    ReadLockError,
52
53    #[error("Glide OpenTelemetry error: Failed to acquire write lock")]
54    WriteLockError,
55
56    #[error("Other error: {0}")]
57    Other(String),
58}
59
60/// Default interval in milliseconds for flushing open telemetry data to the collector.
61pub const DEFAULT_FLUSH_SIGNAL_INTERVAL_MS: u32 = 5000;
62
63/// Default trace sampling percentage for sending OpenTelemetry data to the collector.
64pub const DEFAULT_TRACE_SAMPLE_PERCENTAGE: u32 = 1;
65
66/// Default filename for the file exporter.
67pub const DEFAULT_SIGNAL_FILENAME: &str = "signals.json";
68
69pub enum GlideSpanStatus {
70    Ok,
71    Error(String),
72}
73
74#[derive(Clone, Debug)]
75/// Defines the method that exporter connects to the collector. It can be:
76/// gRPC or HTTP. The third type (i.e. "File") defines an exporter that does not connect to a collector
77/// instead, it writes the collected signals to files.
78pub enum GlideOpenTelemetrySignalsExporter {
79    /// Collector is listening on grpc
80    Grpc(String),
81    /// Collector is listening on http
82    Http(String),
83    /// No collector. Instead, write the signals collected to a file. The contained value "PathBuf"
84    /// points to the folder where the collected data should be placed.
85    File(PathBuf),
86}
87
88/// Signal types supported when reading protocol configuration from the
89/// environment.
90#[derive(Clone, Copy)]
91enum OtelSignal {
92    Traces,
93    Metrics,
94}
95
96/// Parse the OTLP protocol environment variables for the given signal. Returns
97/// `Some(Protocol)` when the environment specifies a supported protocol,
98/// otherwise `None`.
99fn protocol_from_env(signal: OtelSignal) -> Option<Protocol> {
100    fn parse(value: &str) -> Option<Protocol> {
101        match value.to_ascii_lowercase().as_str() {
102            "grpc" => Some(Protocol::Grpc),
103            "http/protobuf" | "http/binary" | "http" | "http_proto" => Some(Protocol::HttpBinary),
104            "http/json" => Some(Protocol::HttpJson),
105            _ => None,
106        }
107    }
108
109    let specific = match signal {
110        OtelSignal::Traces => "OTEL_EXPORTER_OTLP_TRACES_PROTOCOL",
111        OtelSignal::Metrics => "OTEL_EXPORTER_OTLP_METRICS_PROTOCOL",
112    };
113
114    if let Ok(val) = std::env::var(specific) {
115        parse(&val)
116    } else if let Ok(val) = std::env::var("OTEL_EXPORTER_OTLP_PROTOCOL") {
117        parse(&val)
118    } else {
119        None
120    }
121}
122
123impl std::str::FromStr for GlideOpenTelemetrySignalsExporter {
124    type Err = Error;
125    fn from_str(s: &str) -> Result<Self, Self::Err> {
126        parse_endpoint(s)
127    }
128}
129
130fn parse_endpoint(endpoint: &str) -> Result<GlideOpenTelemetrySignalsExporter, Error> {
131    // Parse the URL using the `url` crate to validate it
132    let url = Url::parse(endpoint)
133        .map_err(|_| Error::new(ErrorKind::InvalidInput, format!("Parse error. {endpoint}")))?;
134
135    match url.scheme() {
136        "http" | "https" => Ok(GlideOpenTelemetrySignalsExporter::Http(
137            endpoint.to_string(),
138        )), // HTTP/HTTPS endpoint
139        "grpc" => Ok(GlideOpenTelemetrySignalsExporter::Grpc(
140            endpoint.to_string(),
141        )), // gRPC endpoint
142        "file" => {
143            // For file, we need to extract the path without the 'file://' prefix
144            let file_prefix = "file://";
145            if !endpoint.starts_with(file_prefix) {
146                return Err(Error::new(
147                    ErrorKind::InvalidInput,
148                    "File path must start with 'file://'",
149                ));
150            }
151
152            // Extract the path by removing the 'file://' prefix
153            let path = endpoint.strip_prefix(file_prefix).ok_or_else(|| {
154                Error::new(
155                    ErrorKind::InvalidInput,
156                    "Failed to extract path from file URL",
157                )
158            })?;
159
160            let path_buf = PathBuf::from(path);
161
162            // Determine if this is a directory path or a file path
163            let final_path = if path_buf.is_dir() || path_buf.extension().is_none() {
164                // If it's a directory or doesn't have an extension, treat it as a directory
165                // and append the default filename
166                path_buf.join(DEFAULT_SIGNAL_FILENAME)
167            } else {
168                path_buf
169            };
170
171            // Check if the parent directory exists and is a directory
172            if let Some(parent_dir) = final_path.parent() {
173                match parent_dir.try_exists() {
174                    Ok(exists) => {
175                        if !exists || !parent_dir.is_dir() {
176                            return Err(Error::new(
177                                ErrorKind::InvalidInput,
178                                format!(
179                                    "The directory does not exist or is not a directory: {}",
180                                    parent_dir.display()
181                                ),
182                            ));
183                        }
184                    }
185                    Err(e) => {
186                        return Err(Error::new(
187                            ErrorKind::InvalidInput,
188                            format!("Error checking if parent directory exists: {}", e),
189                        ));
190                    }
191                }
192            }
193
194            Ok(GlideOpenTelemetrySignalsExporter::File(final_path))
195        } // file endpoint
196        _ => Err(Error::new(ErrorKind::InvalidInput, endpoint)),
197    }
198}
199
200#[derive(Clone, Debug)]
201struct GlideSpanInner {
202    span: Arc<RwLock<opentelemetry::global::BoxedSpan>>,
203    #[cfg(test)]
204    reference_count: Arc<AtomicUsize>,
205}
206
207impl GlideSpanInner {
208    /// Create new span with no parent.
209    pub fn new(name: &str) -> Self {
210        let tracer = global::tracer(TRACE_SCOPE);
211        let span = Arc::new(RwLock::new(
212            tracer
213                .span_builder(name.to_string())
214                .with_kind(SpanKind::Client)
215                .start(&tracer),
216        ));
217
218        GlideSpanInner {
219            span,
220            #[cfg(test)]
221            reference_count: Arc::new(AtomicUsize::new(1)),
222        }
223    }
224
225    /// Create new span as a child of `parent`, returning an error if the parent span lock is poisoned.
226    pub fn new_with_parent(name: &str, parent: &GlideSpanInner) -> Result<Self, GlideOTELError> {
227        let parent_span_ctx = parent
228            .span
229            .read()
230            .map_err(|_| GlideOTELError::TraceError(SPAN_READ_LOCK_ERR.to_string()))?
231            .span_context()
232            .clone();
233
234        let parent_context =
235            opentelemetry::Context::new().with_remote_span_context(parent_span_ctx);
236
237        let tracer = global::tracer(TRACE_SCOPE);
238        let span = Arc::new(RwLock::new(
239            tracer
240                .span_builder(name.to_string())
241                .with_kind(SpanKind::Client)
242                .start_with_context(&tracer, &parent_context),
243        ));
244        Ok(GlideSpanInner {
245            span,
246            #[cfg(test)]
247            reference_count: Arc::new(AtomicUsize::new(1)),
248        })
249    }
250
251    /// Create a new span as a child of a remote span context identified by raw hex IDs.
252    ///
253    /// This is used for cross-process context propagation, e.g. when a Node.js OTel SDK
254    /// passes its active span context to the Rust core.
255    pub fn new_with_remote_context(
256        name: &str,
257        trace_id_hex: &str,
258        span_id_hex: &str,
259        trace_flags: u8,
260        trace_state: Option<&str>,
261    ) -> Result<Self, GlideOTELError> {
262        let trace_id = TraceId::from_hex(trace_id_hex).map_err(|_| {
263            GlideOTELError::TraceError(format!("Invalid trace_id hex: {trace_id_hex}"))
264        })?;
265        let span_id = SpanId::from_hex(span_id_hex).map_err(|_| {
266            GlideOTELError::TraceError(format!("Invalid span_id hex: {span_id_hex}"))
267        })?;
268
269        let trace_state = match trace_state {
270            Some(s) => TraceState::from_str(s)
271                .map_err(|_| GlideOTELError::TraceError(format!("Invalid trace_state: {s}")))?,
272            None => TraceState::default(),
273        };
274
275        let remote_span_context = SpanContext::new(
276            trace_id,
277            span_id,
278            TraceFlags::new(trace_flags),
279            true, // is_remote
280            trace_state,
281        );
282
283        let parent_context =
284            opentelemetry::Context::new().with_remote_span_context(remote_span_context);
285
286        let tracer = global::tracer(TRACE_SCOPE);
287        let span = Arc::new(RwLock::new(
288            tracer
289                .span_builder(name.to_string())
290                .with_kind(SpanKind::Client)
291                .start_with_context(&tracer, &parent_context),
292        ));
293        Ok(GlideSpanInner {
294            span,
295            #[cfg(test)]
296            reference_count: Arc::new(AtomicUsize::new(1)),
297        })
298    }
299
300    /// Attach event with name and list of attributes to this span.
301    pub fn add_event(&self, name: &str, attributes: Option<&Vec<(&str, &str)>>) {
302        let attributes: Vec<opentelemetry::KeyValue> = if let Some(attributes) = attributes {
303            attributes
304                .iter()
305                .map(|(k, v)| opentelemetry::KeyValue::new(k.to_string(), v.to_string()))
306                .collect()
307        } else {
308            Vec::<opentelemetry::KeyValue>::default()
309        };
310        self.span
311            .write()
312            .expect(SPAN_WRITE_LOCK_ERR)
313            .add_event_with_timestamp(name.to_string(), std::time::SystemTime::now(), attributes);
314    }
315
316    pub fn set_status(&self, status: GlideSpanStatus) {
317        match status {
318            GlideSpanStatus::Ok => self
319                .span
320                .write()
321                .expect(SPAN_WRITE_LOCK_ERR)
322                .set_status(opentelemetry::trace::Status::Ok),
323            GlideSpanStatus::Error(error_message) => {
324                self.span.write().expect(SPAN_WRITE_LOCK_ERR).set_status(
325                    opentelemetry::trace::Status::Error {
326                        description: error_message.into(),
327                    },
328                )
329            }
330        }
331    }
332
333    /// Set a string attribute on this span.
334    pub fn set_attribute(&self, key: &str, value: impl Into<opentelemetry::Value>) {
335        self.span
336            .write()
337            .expect(SPAN_WRITE_LOCK_ERR)
338            .set_attribute(opentelemetry::KeyValue::new(key.to_string(), value.into()));
339    }
340
341    /// Set an integer attribute on this span.
342    pub fn set_attribute_i64(&self, key: &str, value: i64) {
343        self.span
344            .write()
345            .expect(SPAN_WRITE_LOCK_ERR)
346            .set_attribute(opentelemetry::KeyValue::new(key.to_string(), value));
347    }
348
349    /// Create new span, add it as a child to this span and return it.
350    /// Returns an error if the child span creation fails.
351    pub fn add_span(&self, name: &str) -> Result<GlideSpanInner, GlideOTELError> {
352        let child = GlideSpanInner::new_with_parent(name, self)?;
353        {
354            let child_span = child
355                .span
356                .read()
357                .map_err(|_| GlideOTELError::TraceError(SPAN_READ_LOCK_ERR.to_string()))?;
358            self.span
359                .write()
360                .expect(SPAN_WRITE_LOCK_ERR)
361                .add_link(child_span.span_context().clone(), Vec::default());
362        }
363        Ok(child)
364    }
365
366    /// Return the span ID
367    pub fn id(&self) -> String {
368        self.span
369            .read()
370            .expect(SPAN_READ_LOCK_ERR)
371            .span_context()
372            .span_id()
373            .to_string()
374    }
375
376    /// Finishes the `Span`.
377    pub fn end(&self) {
378        self.span.write().expect(SPAN_READ_LOCK_ERR).end()
379    }
380
381    #[cfg(test)]
382    pub fn get_reference_count(&self) -> usize {
383        self.reference_count.load(Ordering::SeqCst)
384    }
385
386    #[cfg(test)]
387    pub fn increment_reference_count(&self) {
388        self.reference_count.fetch_add(1, Ordering::SeqCst);
389    }
390
391    #[cfg(test)]
392    pub fn decrement_reference_count(&self) {
393        self.reference_count.fetch_sub(1, Ordering::SeqCst);
394    }
395}
396
397#[cfg(test)]
398impl Drop for GlideSpanInner {
399    fn drop(&mut self) {
400        // Only print debug info if the reference count is non-zero
401        let current_count = self.reference_count.load(Ordering::SeqCst);
402        if current_count > 0 {
403            self.decrement_reference_count();
404        } else {
405            panic!("Span reference count is 0");
406        }
407    }
408}
409
410#[derive(Clone, Debug)]
411pub struct GlideSpan {
412    inner: GlideSpanInner,
413}
414
415impl GlideSpan {
416    pub fn new(name: &str) -> Self {
417        GlideSpan {
418            inner: GlideSpanInner::new(name),
419        }
420    }
421
422    /// Create a new span as a child of a remote span context identified by raw hex IDs.
423    pub fn new_with_remote_context(
424        name: &str,
425        trace_id_hex: &str,
426        span_id_hex: &str,
427        trace_flags: u8,
428        trace_state: Option<&str>,
429    ) -> Result<Self, GlideOTELError> {
430        Ok(GlideSpan {
431            inner: GlideSpanInner::new_with_remote_context(
432                name,
433                trace_id_hex,
434                span_id_hex,
435                trace_flags,
436                trace_state,
437            )?,
438        })
439    }
440
441    /// Attach event with name to this span.
442    pub fn add_event(&self, name: &str) {
443        self.inner.add_event(name, None)
444    }
445
446    /// Attach event with name and attributes to this span.
447    pub fn add_event_with_attributes(&self, name: &str, attributes: &Vec<(&str, &str)>) {
448        self.inner.add_event(name, Some(attributes))
449    }
450
451    pub fn set_status(&self, status: GlideSpanStatus) {
452        self.inner.set_status(status)
453    }
454
455    /// Set a string attribute on this span.
456    pub fn set_attribute(&self, key: &str, value: impl Into<opentelemetry::Value>) {
457        self.inner.set_attribute(key, value)
458    }
459
460    /// Set an integer attribute on this span.
461    pub fn set_attribute_i64(&self, key: &str, value: i64) {
462        self.inner.set_attribute_i64(key, value)
463    }
464
465    /// Add child span to this span and return it
466    pub fn add_span(&self, name: &str) -> Result<GlideSpan, GlideOTELError> {
467        let inner_span = self.inner.add_span(name).map_err(|err| {
468            GlideOTELError::TraceError(format!("Failed to create child span '{}': {}", name, err))
469        })?;
470
471        Ok(GlideSpan { inner: inner_span })
472    }
473
474    pub fn id(&self) -> String {
475        self.inner.id()
476    }
477
478    /// Finishes the `Span`.
479    pub fn end(&self) {
480        self.inner.end()
481    }
482
483    #[cfg(test)]
484    pub fn add_reference(&self) {
485        self.inner.increment_reference_count();
486    }
487
488    #[cfg(test)]
489    pub fn get_reference_count(&self) -> usize {
490        self.inner.get_reference_count()
491    }
492}
493
494/// OpenTelemetry configuration object. Use `GlideOpenTelemetryConfigBuilder` to construct it:
495///
496/// ```text
497/// let config = GlideOpenTelemetryConfigBuilder::default()
498///    .with_flush_interval(std::time::Duration::from_millis(100))
499///    .build();
500/// GlideOpenTelemetry::initialise(config);
501/// ```
502#[derive(Clone, Debug)]
503pub struct GlideOpenTelemetryConfig {
504    /// Default delay interval between two consecutive exports.
505    flush_interval_ms: Duration,
506    traces: Option<GlideOpenTelemetryTracesConfig>,
507    metrics: Option<GlideOpenTelemetryMetricsConfig>,
508}
509
510#[derive(Clone, Debug)]
511pub struct GlideOpenTelemetryTracesConfig {
512    /// Specifies how the exporter sends telemetry data to the collector, and holds the endpoint information.
513    trace_exporter: GlideOpenTelemetrySignalsExporter,
514    /// The percentage of requests to sample and create a span for, used to measure command duration.
515    trace_sample_percentage: u32,
516}
517
518#[derive(Clone, Debug)]
519pub struct GlideOpenTelemetryMetricsConfig {
520    /// Specifies how the exporter sends telemetry data to the collector, and holds the endpoint information.
521    metrics_exporter: GlideOpenTelemetrySignalsExporter,
522}
523
524/// Builder for configuring OpenTelemetry in GLIDE
525///
526/// This struct allows you to configure how telemetry data (traces and metrics) is exported.
527/// - `flush_interval_ms`: Sets the interval between consecutive exports of telemetry data.
528/// - `traces_config`: Optional configuration for exporting trace data. If `None`, trace data will not be exported.
529/// - `metrics_config`: Optional configuration for exporting metrics data. If `None`, metrics data will not be exported.
530///
531/// If both `traces_config` and `metrics_config` are `None`, no telemetry data will be exported.
532#[derive(Clone, Debug)]
533pub struct GlideOpenTelemetryConfigBuilder {
534    /// The interval between consecutive exports of telemetry data.
535    flush_interval_ms: Duration,
536    /// Optional configuration for exporting trace data. If `None`, trace data will not be exported.
537    traces_config: Option<GlideOpenTelemetryTracesConfig>,
538    /// Optional configuration for exporting metrics data. If `None`, metrics data will not be exported.
539    metrics_config: Option<GlideOpenTelemetryMetricsConfig>,
540}
541
542impl Default for GlideOpenTelemetryConfigBuilder {
543    fn default() -> Self {
544        GlideOpenTelemetryConfigBuilder {
545            flush_interval_ms: Duration::from_millis(DEFAULT_FLUSH_SIGNAL_INTERVAL_MS as u64),
546            traces_config: None,
547            metrics_config: None,
548        }
549    }
550}
551
552impl GlideOpenTelemetryConfigBuilder {
553    /// Configure the flush interval in milliseconds
554    ///
555    /// - `duration`: The duration between consecutive exports of telemetry data.
556    pub fn with_flush_interval(mut self, duration: Duration) -> Self {
557        self.flush_interval_ms = duration;
558        self
559    }
560
561    /// Configure the trace exporter
562    ///
563    /// - `exporter`: The exporter endpoint to use for trace data.
564    /// - `sample_percentage`: The percentage of requests to sample and create a span for, used to measure command duration.
565    ///   If `None`, the default value of `DEFAULT_TRACE_SAMPLE_PERCENTAGE` will be used.
566    pub fn with_trace_exporter(
567        mut self,
568        exporter: GlideOpenTelemetrySignalsExporter,
569        sample_percentage: Option<u32>,
570    ) -> Self {
571        self.traces_config = Some(GlideOpenTelemetryTracesConfig {
572            trace_exporter: exporter,
573            trace_sample_percentage: sample_percentage.unwrap_or(DEFAULT_TRACE_SAMPLE_PERCENTAGE),
574        });
575        self
576    }
577
578    /// Configure the metrics exporter
579    ///
580    /// - `exporter`: The exporter endpoint to use for metrics data.
581    pub fn with_metrics_exporter(mut self, exporter: GlideOpenTelemetrySignalsExporter) -> Self {
582        self.metrics_config = Some(GlideOpenTelemetryMetricsConfig {
583            metrics_exporter: exporter,
584        });
585        self
586    }
587
588    pub fn build(self) -> GlideOpenTelemetryConfig {
589        GlideOpenTelemetryConfig {
590            flush_interval_ms: self.flush_interval_ms,
591            traces: self.traces_config,
592            metrics: self.metrics_config,
593        }
594    }
595}
596
597/// Build a dedicated-thread batch span processor. Used for the file exporter,
598/// whose `export` performs only synchronous IO and is therefore safe under the
599/// processor's internal `block_on`.
600fn build_file_span_processor(
601    batch_config: BatchConfig,
602    exporter: impl SpanExporter + 'static,
603) -> BatchSpanProcessor {
604    BatchSpanProcessor::builder(exporter)
605        .with_batch_config(batch_config)
606        .build()
607}
608
609/// Build a Tokio async-runtime batch span processor. Required for the OTLP
610/// exporters, which use an async HTTP client (`reqwest-client`) / tonic and need
611/// a running Tokio reactor to export. The dedicated-thread processor cannot
612/// drive these async transports.
613fn build_otlp_span_processor(
614    batch_config: BatchConfig,
615    exporter: impl SpanExporter + 'static,
616) -> AsyncBatchSpanProcessor<Tokio> {
617    AsyncBatchSpanProcessor::builder(exporter, Tokio)
618        .with_batch_config(batch_config)
619        .build()
620}
621
622#[derive(Clone)]
623pub struct GlideOpenTelemetry {}
624
625static TIMEOUT_COUNTER: OnceLock<opentelemetry::metrics::Counter<u64>> = OnceLock::new();
626static RETRIES_COUNTER: OnceLock<opentelemetry::metrics::Counter<u64>> = OnceLock::new();
627static MOVED_COUNTER: OnceLock<opentelemetry::metrics::Counter<u64>> = OnceLock::new();
628static SUBSCRIPTION_OUT_OF_SYNC_COUNTER: OnceLock<opentelemetry::metrics::Counter<u64>> =
629    OnceLock::new();
630static SUBSCRIPTION_LAST_SYNC_GAUGE: OnceLock<opentelemetry::metrics::Gauge<u64>> = OnceLock::new();
631
632/// Holds the tracer provider so it can be flushed/shut down explicitly.
633/// `opentelemetry::global::shutdown_tracer_provider` was removed in 0.32, so
634/// shutdown now goes through the owned [`SdkTracerProvider`].
635static TRACER_PROVIDER: OnceLock<SdkTracerProvider> = OnceLock::new();
636
637/// Singleton instance of GlideOpenTelemetry. Ensures that telemetry setup happens only once across the application.
638static OTEL: OnceCell<RwLock<GlideOpenTelemetry>> = OnceCell::new();
639
640/// Our interface to OpenTelemetry
641impl GlideOpenTelemetry {
642    /// Validate if a span pointer is valid
643    ///
644    /// # Arguments
645    /// * `span_ptr` - The u64 span pointer to validate
646    ///
647    /// # Returns
648    /// * `true` - If the span pointer is valid (non-zero and within reasonable bounds)
649    /// * `false` - If the span pointer is invalid (zero, out of bounds, or potentially corrupted)
650    ///
651    /// # Safety
652    /// This function performs basic validation but cannot guarantee the pointer points to valid memory.
653    /// It only checks for obvious invalid values like null pointers and unreasonable addresses.
654    pub unsafe fn is_span_pointer_valid(span_ptr: u64) -> bool {
655        // Check for null pointer
656        if span_ptr == 0 {
657            logger_core::log_warn("OpenTelemetry", "Invalid span pointer - null pointer (0)");
658            return false;
659        }
660
661        // Check for obviously invalid pointer values
662        // Pointers should be aligned to at least 8 bytes on 64-bit systems
663        if !span_ptr.is_multiple_of(8) {
664            logger_core::log_warn(
665                "OpenTelemetry",
666                format!(
667                    "Invalid span pointer - misaligned pointer: 0x{:x}",
668                    span_ptr
669                ),
670            );
671            return false;
672        }
673
674        // Check for unreasonably small addresses (likely corrupted)
675        // Valid heap addresses are typically much higher than this
676        const MIN_VALID_ADDRESS: u64 = 0x1000; // 4KB, below this is likely invalid
677        if span_ptr < MIN_VALID_ADDRESS {
678            logger_core::log_warn(
679                "OpenTelemetry",
680                format!("Invalid span pointer - address too low: 0x{:x}", span_ptr),
681            );
682            return false;
683        }
684
685        // Check for unreasonably high addresses (on 64-bit systems, user space is limited)
686        // This is a conservative check for obviously corrupted pointers
687        // On most 64-bit systems, user space is limited to the lower half of the address space
688        const MAX_VALID_ADDRESS: u64 = 0x7FFF_FFFF_FFFF_FFF8; // Max user space on most 64-bit systems
689        if span_ptr > MAX_VALID_ADDRESS {
690            logger_core::log_warn(
691                "OpenTelemetry",
692                format!("Invalid span pointer - address too high: 0x{:x}", span_ptr),
693            );
694            return false;
695        }
696
697        true
698    }
699
700    /// Convert a span pointer to a GlideSpan with validation
701    ///
702    /// # Arguments
703    /// * `span_ptr` - The u64 span pointer to convert
704    ///
705    /// # Returns
706    /// * `Ok(GlideSpan)` - If the pointer is valid and conversion succeeds
707    /// * `Err(TraceError)` - If the pointer is invalid or conversion fails
708    ///
709    /// # Safety
710    /// This function validates the pointer before attempting conversion, but still uses unsafe code
711    pub unsafe fn span_from_pointer(span_ptr: u64) -> Result<GlideSpan, GlideOTELError> {
712        // First validate the pointer
713        if !unsafe { Self::is_span_pointer_valid(span_ptr) } {
714            return Err(GlideOTELError::TraceError(format!(
715                "Invalid span pointer: 0x{:x} failed validation checks",
716                span_ptr
717            )));
718        }
719
720        // Attempt to convert the pointer safely
721        // This follows the same pattern as get_unsafe_span_from_ptr in FFI layer
722        let span = unsafe {
723            // Increment strong count to prevent premature deallocation
724            std::sync::Arc::increment_strong_count(span_ptr as *const GlideSpan);
725
726            // Convert pointer back to Arc and clone the span
727            (*std::sync::Arc::from_raw(span_ptr as *const GlideSpan)).clone()
728        };
729
730        Ok(span)
731    }
732
733    /// Initialise the open telemetry library with a file system exporter
734    ///
735    /// This method should be called once for the given **process**
736    /// If OpenTelemetry is already initialized, this method will return Ok(()) without reinitializing
737    pub fn initialise(config: GlideOpenTelemetryConfig) -> Result<(), GlideOTELError> {
738        OTEL.get_or_try_init(|| {
739            Self::validate_config(config.clone())?;
740
741            if let Some(traces_config) = config.traces.as_ref() {
742                Self::initialise_trace_exporter(
743                    config.flush_interval_ms,
744                    &traces_config.trace_exporter,
745                )?;
746            }
747
748            if let Some(metrics_config) = config.metrics.as_ref() {
749                Self::initialise_metrics_exporter(
750                    config.flush_interval_ms,
751                    &metrics_config.metrics_exporter,
752                )?;
753                Self::init_metrics()?;
754            }
755
756            Ok::<RwLock<GlideOpenTelemetry>, GlideOTELError>(RwLock::new(GlideOpenTelemetry {}))
757        })?;
758
759        Ok(())
760    }
761
762    /// Validate the configuration
763    ///
764    /// - `config`: The OpenTelemetry configuration to validate.
765    ///
766    /// Returns an error if the configuration is invalid:
767    /// - `flush_interval_ms` cannot be zero
768    /// - `trace_sample_percentage` must be between 0 and 100
769    fn validate_config(config: GlideOpenTelemetryConfig) -> Result<(), GlideOTELError> {
770        // Validate flush_interval_ms
771        if config.flush_interval_ms.is_zero() {
772            return Err(GlideOTELError::Other(
773                "InvalidInput: flushIntervalMs cannot be zero".to_string(),
774            ));
775        }
776
777        // Validate trace_sample_percentage
778        if let Some(traces_config) = config.traces.as_ref()
779            && traces_config.trace_sample_percentage > 100
780        {
781            return Err(GlideOTELError::Other(
782                "Trace sample percentage must be between 0 and 100".into(),
783            ));
784        }
785        Ok(())
786    }
787
788    /// Initialize the trace exporter based on the configuration
789    fn initialise_trace_exporter(
790        flush_interval_ms: Duration,
791        trace_exporter: &GlideOpenTelemetrySignalsExporter,
792    ) -> Result<(), GlideOTELError> {
793        let batch_config = opentelemetry_sdk::trace::BatchConfigBuilder::default()
794            .with_scheduled_delay(flush_interval_ms)
795            .build();
796
797        let env_protocol = protocol_from_env(OtelSignal::Traces);
798        // The file exporter uses the dedicated-thread span processor while the
799        // OTLP exporters use the Tokio async-runtime processor (their async
800        // transports require a reactor). Those processors are different concrete
801        // types, so the `SdkTracerProvider` is built inside each arm.
802        let provider = match trace_exporter {
803            GlideOpenTelemetrySignalsExporter::File(p) => {
804                let exporter = crate::SpanExporterFile::new(p.clone()).map_err(|e| {
805                    GlideOTELError::Other(format!("Failed to create traces exporter: {}", e))
806                })?;
807                SdkTracerProvider::builder()
808                    .with_span_processor(build_file_span_processor(batch_config, exporter))
809                    .build()
810            }
811            GlideOpenTelemetrySignalsExporter::Http(url) => {
812                let processor = match env_protocol.unwrap_or(Protocol::HttpBinary) {
813                    Protocol::Grpc => {
814                        let exporter = opentelemetry_otlp::SpanExporter::builder()
815                            .with_tonic()
816                            .with_endpoint(url)
817                            .with_protocol(Protocol::Grpc)
818                            .build()?;
819                        build_otlp_span_processor(batch_config, exporter)
820                    }
821                    protocol => {
822                        let exporter = opentelemetry_otlp::SpanExporter::builder()
823                            .with_http()
824                            .with_endpoint(url)
825                            .with_protocol(protocol)
826                            .build()?;
827                        build_otlp_span_processor(batch_config, exporter)
828                    }
829                };
830                SdkTracerProvider::builder()
831                    .with_span_processor(processor)
832                    .build()
833            }
834            GlideOpenTelemetrySignalsExporter::Grpc(url) => {
835                let protocol = env_protocol.unwrap_or(Protocol::Grpc);
836                if protocol != Protocol::Grpc {
837                    log_warn(
838                        "opentelemetry",
839                        format!(
840                            "Inconsistent configuration: The endpoint URL '{url}' suggests gRPC, but the protocol is set to '{protocol:?}' via environment variables. The environment variable setting will be used."
841                        ),
842                    );
843                }
844                let processor = match protocol {
845                    Protocol::Grpc => {
846                        let exporter = opentelemetry_otlp::SpanExporter::builder()
847                            .with_tonic()
848                            .with_endpoint(url)
849                            .with_protocol(Protocol::Grpc)
850                            .build()?;
851                        build_otlp_span_processor(batch_config, exporter)
852                    }
853                    protocol => {
854                        let exporter = opentelemetry_otlp::SpanExporter::builder()
855                            .with_http()
856                            .with_endpoint(url)
857                            .with_protocol(protocol)
858                            .build()?;
859                        build_otlp_span_processor(batch_config, exporter)
860                    }
861                };
862                SdkTracerProvider::builder()
863                    .with_span_processor(processor)
864                    .build()
865            }
866        };
867
868        global::set_text_map_propagator(TraceContextPropagator::new());
869        // Keep a clone so the provider can be flushed/shut down explicitly later;
870        // `global::shutdown_tracer_provider` was removed in opentelemetry 0.32.
871        let _ = TRACER_PROVIDER.set(provider.clone());
872        global::set_tracer_provider(provider);
873
874        Ok(())
875    }
876
877    /// Initialize the metrics exporter based on the configuration
878    fn initialise_metrics_exporter(
879        flush_interval_ms: Duration,
880        metrics_exporter: &GlideOpenTelemetrySignalsExporter,
881    ) -> Result<(), GlideOTELError> {
882        let env_protocol = protocol_from_env(OtelSignal::Metrics);
883        // The async-runtime `PeriodicReader<E>` is generic over the exporter
884        // type, so the file and OTLP readers are distinct concrete types. Build
885        // the `SdkMeterProvider` inside each arm to keep the reader type local.
886        let meter_provider = match metrics_exporter {
887            GlideOpenTelemetrySignalsExporter::File(p) => {
888                let exporter = crate::FileMetricExporter::new(p.clone()).map_err(|e| {
889                    GlideOTELError::Other(format!("Failed to create metrics exporter: {}", e))
890                })?;
891                // The file exporter performs synchronous IO, so the
892                // dedicated-thread `PeriodicReader` (the 0.32 default) drives it
893                // reliably on the configured interval without needing the async
894                // runtime.
895                let reader = opentelemetry_sdk::metrics::PeriodicReader::builder(exporter)
896                    .with_interval(flush_interval_ms)
897                    .build();
898                SdkMeterProvider::builder().with_reader(reader).build()
899            }
900            GlideOpenTelemetrySignalsExporter::Http(url) => {
901                let protocol = env_protocol.unwrap_or(Protocol::HttpBinary);
902                let exporter = match protocol {
903                    Protocol::Grpc => MetricExporter::builder()
904                        .with_tonic()
905                        .with_endpoint(url)
906                        .with_protocol(Protocol::Grpc)
907                        .build()?,
908                    p => MetricExporter::builder()
909                        .with_http()
910                        .with_endpoint(url)
911                        .with_protocol(p)
912                        .build()?,
913                };
914                let reader = opentelemetry_sdk::metrics::periodic_reader_with_async_runtime::PeriodicReader::builder(exporter, Tokio)
915                    .with_interval(flush_interval_ms)
916                    .build();
917                SdkMeterProvider::builder().with_reader(reader).build()
918            }
919            GlideOpenTelemetrySignalsExporter::Grpc(url) => {
920                let protocol = env_protocol.unwrap_or(Protocol::Grpc);
921                if protocol != Protocol::Grpc {
922                    log_warn(
923                        "opentelemetry",
924                        format!(
925                            "Inconsistent configuration: The endpoint URL '{url}' suggests gRPC, but the protocol is set to '{protocol:?}' via environment variables. The environment variable setting will be used."
926                        ),
927                    );
928                }
929                let exporter = match protocol {
930                    Protocol::Grpc => MetricExporter::builder()
931                        .with_tonic()
932                        .with_endpoint(url)
933                        .with_protocol(Protocol::Grpc)
934                        .build()?,
935                    p => MetricExporter::builder()
936                        .with_http()
937                        .with_endpoint(url)
938                        .with_protocol(p)
939                        .build()?,
940                };
941                let reader = opentelemetry_sdk::metrics::periodic_reader_with_async_runtime::PeriodicReader::builder(exporter, Tokio)
942                    .with_interval(flush_interval_ms)
943                    .build();
944                SdkMeterProvider::builder().with_reader(reader).build()
945            }
946        };
947
948        global::set_meter_provider(meter_provider);
949
950        Ok(())
951    }
952
953    /// Initialize metrics counters
954    fn init_metrics() -> Result<(), GlideOTELError> {
955        let meter = global::meter(TRACE_SCOPE);
956
957        // Create timeout error counter
958        TIMEOUT_COUNTER
959            .set(
960                meter
961                    .u64_counter(TIMEOUT_ERROR_METRIC)
962                    .with_description("Number of timeout errors encountered")
963                    .with_unit("1")
964                    .build(),
965            )
966            .map_err(|_| {
967                GlideOTELError::Other(
968                    "OpenTelemetry error: Failed to initialize timeout counter".to_owned(),
969                )
970            })?;
971
972        // Create retries counter
973        RETRIES_COUNTER
974            .set(
975                meter
976                    .u64_counter(RETRIES_METRIC)
977                    .with_description("Number of retry attempts made")
978                    .with_unit("1")
979                    .build(),
980            )
981            .map_err(|_| {
982                GlideOTELError::Other(
983                    "OpenTelemetry error: Failed to initialize retries counter".to_owned(),
984                )
985            })?;
986
987        // Create moved counter
988        MOVED_COUNTER
989            .set(
990                meter
991                    .u64_counter(MOVED_ERROR_METRIC)
992                    .with_description("Number of moved errors encountered")
993                    .with_unit("1")
994                    .build(),
995            )
996            .map_err(|_| {
997                GlideOTELError::Other(
998                    "OpenTelemetry error: Failed to initialize moved counter".to_owned(),
999                )
1000            })?;
1001        // Create subscription out of sync counter
1002        SUBSCRIPTION_OUT_OF_SYNC_COUNTER
1003            .set(
1004                meter
1005                    .u64_counter(SUBSCRIPTION_OUT_OF_SYNC_METRIC)
1006                    .with_description("Number of times subscriptions were detected as out of sync")
1007                    .with_unit("1")
1008                    .build(),
1009            )
1010            .map_err(|_| {
1011                GlideOTELError::Other(
1012                    "OpenTelemetry error: Failed to initialize subscription out of sync counter"
1013                        .to_owned(),
1014                )
1015            })?;
1016
1017        // Create subscription last sync timestamp gauge
1018        SUBSCRIPTION_LAST_SYNC_GAUGE
1019            .set(
1020                meter
1021                    .u64_gauge(SUBSCRIPTION_LAST_SYNC_TIMESTAMP_METRIC)
1022                    .with_description("Unix timestamp (in milliseconds) of the last time subscriptions were in sync")
1023                    .with_unit("ms")
1024                    .build(),
1025            )
1026            .map_err(|_| {
1027                GlideOTELError::Other(
1028                    "OpenTelemetry error: Failed to initialize subscription last sync gauge".to_owned(),
1029                )
1030            })?;
1031
1032        Ok(())
1033    }
1034
1035    /// Record a timeout error
1036    ///
1037    /// If OpenTelemetry is not initialized, this method will do nothing.
1038    pub fn record_timeout_error() -> Result<(), GlideOTELError> {
1039        if GlideOpenTelemetry::is_initialized() {
1040            TIMEOUT_COUNTER
1041                .get()
1042                .ok_or_else(|| {
1043                    GlideOTELError::Other(
1044                        "OpenTelemetry error: Timeout counter not initialized".to_owned(),
1045                    )
1046                })?
1047                .add(1, &[]);
1048        }
1049        Ok(())
1050    }
1051
1052    /// Record a retry attempt
1053    ///
1054    /// If OpenTelemetry is not initialized, this method will do nothing.
1055    pub fn record_retry_attempt() -> Result<(), GlideOTELError> {
1056        if GlideOpenTelemetry::is_initialized() {
1057            RETRIES_COUNTER
1058                .get()
1059                .ok_or_else(|| {
1060                    GlideOTELError::Other(
1061                        "OpenTelemetry error: Retries counter not initialized".to_string(),
1062                    )
1063                })?
1064                .add(1, &[]);
1065        }
1066        Ok(())
1067    }
1068
1069    /// Record a moved error
1070    ///
1071    /// If OpenTelemetry is not initialized, this method will do nothing.
1072    pub fn record_moved_error() -> Result<(), GlideOTELError> {
1073        if GlideOpenTelemetry::is_initialized() {
1074            MOVED_COUNTER
1075                .get()
1076                .ok_or_else(|| {
1077                    GlideOTELError::Other(
1078                        "OpenTelemetry error: Moved counter not initialized".to_string(),
1079                    )
1080                })?
1081                .add(1, &[]);
1082        }
1083        Ok(())
1084    }
1085
1086    /// Record that subscriptions are out of sync
1087    ///
1088    /// If OpenTelemetry is not initialized, this method will do nothing.
1089    pub fn record_subscription_out_of_sync() -> Result<(), GlideOTELError> {
1090        Telemetry::incr_subscription_out_of_sync();
1091        if GlideOpenTelemetry::is_initialized() {
1092            SUBSCRIPTION_OUT_OF_SYNC_COUNTER
1093                .get()
1094                .ok_or_else(|| {
1095                    GlideOTELError::Other(
1096                        "OpenTelemetry error: Subscription out of sync counter not initialized"
1097                            .to_string(),
1098                    )
1099                })?
1100                .add(1, &[]);
1101        }
1102        Ok(())
1103    }
1104
1105    /// Update the timestamp of when subscriptions were last in sync
1106    ///
1107    /// Records the current system time as a Unix timestamp in milliseconds.
1108    ///
1109    /// If OpenTelemetry is not initialized, this method will do nothing.
1110    pub fn update_subscription_last_sync_timestamp() -> Result<(), GlideOTELError> {
1111        let timestamp = std::time::SystemTime::now()
1112            .duration_since(std::time::UNIX_EPOCH)
1113            .map_err(|e| GlideOTELError::Other(format!("Failed to get system time: {}", e)))?
1114            .as_millis() as u64;
1115
1116        Telemetry::update_subscription_last_sync_timestamp(timestamp);
1117
1118        if GlideOpenTelemetry::is_initialized() {
1119            SUBSCRIPTION_LAST_SYNC_GAUGE
1120                .get()
1121                .ok_or_else(|| {
1122                    GlideOTELError::Other(
1123                        "OpenTelemetry error: Subscription last sync gauge not initialized"
1124                            .to_string(),
1125                    )
1126                })?
1127                .record(timestamp, &[]);
1128        }
1129        Ok(())
1130    }
1131
1132    /// Get the flush interval milliseconds
1133    pub fn get_flush_interval_ms(config: GlideOpenTelemetryConfig) -> Duration {
1134        config.flush_interval_ms
1135    }
1136
1137    /// Create new span
1138    pub fn new_span(name: &str) -> GlideSpan {
1139        GlideSpan::new(name)
1140    }
1141
1142    /// Trigger a shutdown procedure flushing all remaining traces
1143    pub fn shutdown() {
1144        if let Some(provider) = TRACER_PROVIDER.get()
1145            && let Err(e) = provider.shutdown()
1146        {
1147            log_warn(
1148                "opentelemetry",
1149                format!("Failed to shut down tracer provider: {e}"),
1150            );
1151        }
1152    }
1153
1154    /// Check if OpenTelemetry is initialized
1155    pub fn is_initialized() -> bool {
1156        OTEL.get().is_some()
1157    }
1158}
1159
1160#[cfg(test)]
1161mod tests {
1162    use super::*;
1163    use std::panic;
1164    use std::sync::OnceLock;
1165    use tokio::runtime::Runtime;
1166    use tokio::time::sleep;
1167
1168    const SPANS_JSON: &str = "/tmp/spans.json";
1169    const METRICS_JSON: &str = "/tmp/metrics.json";
1170
1171    fn shared_runtime() -> &'static Runtime {
1172        static RUNTIME: OnceLock<Runtime> = OnceLock::new();
1173        RUNTIME.get_or_init(|| Runtime::new().expect("Failed to create runtime"))
1174    }
1175
1176    fn string_property_to_u64(json: &serde_json::Value, prop: &str) -> u64 {
1177        let s = json[prop].to_string().replace('"', "");
1178        s.parse::<u64>().unwrap()
1179    }
1180
1181    /// Helper function to find a metric by name in the metrics array
1182    fn find_metric_by_name<'a>(
1183        metric_json: &'a serde_json::Value,
1184        metric_name: &str,
1185    ) -> Option<&'a serde_json::Value> {
1186        metric_json["scope_metrics"][0]["metrics"]
1187            .as_array()?
1188            .iter()
1189            .find(|m| m["name"] == metric_name)
1190    }
1191
1192    async fn init_otel() -> Result<(), GlideOTELError> {
1193        let config = GlideOpenTelemetryConfigBuilder::default()
1194            .with_flush_interval(Duration::from_millis(2000))
1195            .with_trace_exporter(
1196                GlideOpenTelemetrySignalsExporter::File(PathBuf::from(SPANS_JSON)),
1197                Some(100),
1198            )
1199            .with_metrics_exporter(GlideOpenTelemetrySignalsExporter::File(PathBuf::from(
1200                METRICS_JSON,
1201            )))
1202            .build();
1203        if let Err(e) = GlideOpenTelemetry::initialise(config) {
1204            panic!("Failed to initialize OpenTelemetry: {}", e);
1205        }
1206        Ok(())
1207    }
1208
1209    async fn create_test_spans() {
1210        // Clear the file
1211        let _ = std::fs::remove_file(SPANS_JSON);
1212
1213        let span = GlideOpenTelemetry::new_span("Root_Span_1");
1214        span.add_event("Event1");
1215        span.set_status(GlideSpanStatus::Ok);
1216
1217        let child1 = span.add_span("Network_Span").unwrap();
1218
1219        // Simulate some work
1220        sleep(Duration::from_millis(100)).await;
1221        child1.end();
1222
1223        // Simulate that the parent span is still doing some work
1224        sleep(Duration::from_millis(100)).await;
1225        span.end();
1226
1227        let span = GlideOpenTelemetry::new_span("Root_Span_2");
1228        span.add_event("Event1");
1229        span.add_event("Event2");
1230        span.set_status(GlideSpanStatus::Ok);
1231        drop(span); // writes the span
1232
1233        sleep(Duration::from_millis(2100)).await;
1234    }
1235
1236    #[test]
1237    fn test_span_json_exporter() {
1238        let rt = shared_runtime();
1239        rt.block_on(async {
1240            let _ = std::fs::remove_file(SPANS_JSON);
1241
1242            init_otel().await.unwrap();
1243            create_test_spans().await;
1244
1245            let file_content = std::fs::read_to_string(SPANS_JSON).unwrap();
1246            let lines: Vec<&str> = file_content
1247                .split('\n')
1248                .filter(|l| !l.trim().is_empty())
1249                .collect();
1250
1251            // Parse all spans and find the ones we created by name
1252            let mut network_span: Option<serde_json::Value> = None;
1253            let mut root_span_1: Option<serde_json::Value> = None;
1254            let mut root_span_2: Option<serde_json::Value> = None;
1255
1256            for line in lines {
1257                if let Ok(span) = serde_json::from_str::<serde_json::Value>(line) {
1258                    match span["name"].as_str() {
1259                        Some("Network_Span") if network_span.is_none() => network_span = Some(span),
1260                        Some("Root_Span_1") if span["status"] == "Ok" && root_span_1.is_none() => {
1261                            root_span_1 = Some(span)
1262                        }
1263                        Some("Root_Span_2") if root_span_2.is_none() => root_span_2 = Some(span),
1264                        _ => {}
1265                    }
1266                }
1267            }
1268
1269            let network_span = network_span.expect("Network_Span not found");
1270            let root_span_1 = root_span_1.expect("Root_Span_1 not found");
1271            let root_span_2 = root_span_2.expect("Root_Span_2 not found");
1272
1273            // Verify Network_Span timing
1274            let network_span_id = network_span["span_id"].to_string();
1275            let network_span_start_time = string_property_to_u64(&network_span, "start_time");
1276            let network_span_end_time = string_property_to_u64(&network_span, "end_time");
1277
1278            // Because of the sleep above, the network span should be at least 100ms (units are microseconds)
1279            assert!(network_span_end_time - network_span_start_time >= 100_000);
1280
1281            // Verify Root_Span_1 has the network span as a child
1282            assert_eq!(root_span_1["links"].as_array().unwrap().len(), 1); // we expect 1 child
1283            let root_1_span_start_time = string_property_to_u64(&root_span_1, "start_time");
1284            let root_1_span_end_time = string_property_to_u64(&root_span_1, "end_time");
1285
1286            // The network span started *after* its parent
1287            assert!(network_span_start_time >= root_1_span_start_time);
1288
1289            // The parent span ends *after* the child span (by at least 100ms)
1290            assert!(root_1_span_end_time - network_span_end_time >= 100_000);
1291
1292            let child_span_id = root_span_1["links"][0]["span_id"].to_string();
1293            assert_eq!(child_span_id, network_span_id);
1294
1295            // Verify Root_Span_2 has 2 events
1296            assert_eq!(root_span_2["events"].as_array().unwrap().len(), 2); // we expect 2 events
1297        });
1298    }
1299
1300    #[test]
1301    fn test_span_reference_count() {
1302        let rt = shared_runtime();
1303        rt.block_on(async {
1304            let _ = std::fs::remove_file(SPANS_JSON);
1305            init_otel().await.unwrap();
1306            let span = GlideOpenTelemetry::new_span("Root_Span_1");
1307            span.add_reference();
1308            assert_eq!(span.get_reference_count(), 2);
1309            drop(span);
1310        });
1311    }
1312
1313    #[test]
1314    fn test_record_timeout_error() {
1315        let rt = shared_runtime();
1316        rt.block_on(async {
1317            let _ = std::fs::remove_file(METRICS_JSON);
1318            init_otel().await.unwrap();
1319            GlideOpenTelemetry::record_timeout_error().unwrap();
1320            sleep(Duration::from_millis(2100)).await;
1321            GlideOpenTelemetry::record_timeout_error().unwrap();
1322            GlideOpenTelemetry::record_timeout_error().unwrap();
1323
1324            // Add a sleep to wait for the metrics to be flushed
1325            sleep(Duration::from_millis(2100)).await;
1326
1327            let file_content = std::fs::read_to_string(METRICS_JSON).unwrap();
1328            let lines: Vec<&str> = file_content
1329                .split('\n')
1330                .filter(|l| !l.trim().is_empty())
1331                .collect();
1332
1333            let metric_json: serde_json::Value = serde_json::from_str(lines[0]).unwrap();
1334            assert_eq!(
1335                metric_json["scope_metrics"][0]["metrics"][0]["name"],
1336                "glide.timeout_errors"
1337            );
1338            assert_eq!(
1339                metric_json["scope_metrics"][0]["metrics"][0]["data_points"][0]["value"],
1340                1
1341            );
1342            let metric_json: serde_json::Value =
1343                serde_json::from_str(lines[lines.len() - 1]).unwrap();
1344            assert_eq!(
1345                metric_json["scope_metrics"][0]["metrics"][0]["data_points"][0]["value"],
1346                3
1347            );
1348        });
1349    }
1350
1351    #[test]
1352    fn test_record_retry_attempts() {
1353        let rt = shared_runtime();
1354        rt.block_on(async {
1355            let _ = std::fs::remove_file(METRICS_JSON);
1356            init_otel().await.unwrap();
1357            GlideOpenTelemetry::record_retry_attempt().unwrap();
1358            sleep(Duration::from_millis(2100)).await;
1359            GlideOpenTelemetry::record_retry_attempt().unwrap();
1360            GlideOpenTelemetry::record_retry_attempt().unwrap();
1361
1362            // Add a sleep to wait for the metrics to be flushed
1363            sleep(Duration::from_millis(2100)).await;
1364
1365            let file_content = std::fs::read_to_string(METRICS_JSON).unwrap();
1366            let lines: Vec<&str> = file_content
1367                .split('\n')
1368                .filter(|l| !l.trim().is_empty())
1369                .collect();
1370
1371            let metric_json: serde_json::Value = serde_json::from_str(lines[0]).unwrap();
1372            let retry_metric = find_metric_by_name(&metric_json, "glide.retry_attempts")
1373                .expect("glide.retry_attempts metric not found");
1374            assert_eq!(retry_metric["data_points"][0]["value"], 1);
1375
1376            let metric_json: serde_json::Value =
1377                serde_json::from_str(lines[lines.len() - 1]).unwrap();
1378            let retry_metric = find_metric_by_name(&metric_json, "glide.retry_attempts")
1379                .expect("glide.retry_attempts metric not found");
1380            assert_eq!(retry_metric["data_points"][0]["value"], 3);
1381        });
1382    }
1383
1384    #[test]
1385    fn test_record_moved_error() {
1386        let rt = shared_runtime();
1387        rt.block_on(async {
1388            let _ = std::fs::remove_file(METRICS_JSON);
1389            init_otel().await.unwrap();
1390            GlideOpenTelemetry::record_moved_error().unwrap();
1391            sleep(Duration::from_millis(2100)).await;
1392            GlideOpenTelemetry::record_moved_error().unwrap();
1393            GlideOpenTelemetry::record_moved_error().unwrap();
1394
1395            // Add a sleep to wait for the metrics to be flushed
1396            sleep(Duration::from_millis(2100)).await;
1397
1398            let file_content = std::fs::read_to_string(METRICS_JSON).unwrap();
1399            let lines: Vec<&str> = file_content
1400                .split('\n')
1401                .filter(|l| !l.trim().is_empty())
1402                .collect();
1403
1404            let metric_json: serde_json::Value = serde_json::from_str(lines[0]).unwrap();
1405            let moved_metric = find_metric_by_name(&metric_json, "glide.moved_errors")
1406                .expect("glide.moved_errors metric not found");
1407            assert_eq!(moved_metric["data_points"][0]["value"], 1);
1408
1409            let metric_json: serde_json::Value =
1410                serde_json::from_str(lines[lines.len() - 1]).unwrap();
1411            let moved_metric = find_metric_by_name(&metric_json, "glide.moved_errors")
1412                .expect("glide.moved_errors metric not found");
1413            assert_eq!(moved_metric["data_points"][0]["value"], 3);
1414        });
1415    }
1416
1417    #[test]
1418    fn test_set_status_ok() {
1419        let rt = shared_runtime();
1420        rt.block_on(async {
1421            // Clear the file
1422            let _ = std::fs::remove_file(SPANS_JSON);
1423
1424            init_otel().await.unwrap();
1425            let span = GlideOpenTelemetry::new_span("Root_Span_1");
1426            span.add_event("Event1");
1427            span.set_status(GlideSpanStatus::Ok);
1428
1429            let child1 = span.add_span("Network_Span").unwrap();
1430
1431            // Simulate some work
1432            sleep(Duration::from_millis(100)).await;
1433            child1.end();
1434
1435            // Simulate that the parent span is still doing some work
1436            sleep(Duration::from_millis(100)).await;
1437            span.end();
1438
1439            let span = GlideOpenTelemetry::new_span("Root_Span_2");
1440            span.add_event("Event1");
1441            span.add_event("Event2");
1442            span.set_status(GlideSpanStatus::Error("simple error".to_string())); // Fixed typo in "simple"
1443            drop(span); // writes the span
1444
1445            sleep(Duration::from_millis(2100)).await;
1446
1447            let file_content = std::fs::read_to_string(SPANS_JSON).unwrap();
1448            let lines: Vec<&str> = file_content
1449                .split('\n')
1450                .filter(|l| !l.trim().is_empty())
1451                .collect();
1452
1453            // Find the specific spans by name and status
1454            let mut ok_span: Option<serde_json::Value> = None;
1455            let mut error_span: Option<serde_json::Value> = None;
1456
1457            for line in lines {
1458                if let Ok(span) = serde_json::from_str::<serde_json::Value>(line) {
1459                    if span["name"] == "Root_Span_1" && span["status"] == "Ok" {
1460                        ok_span = Some(span);
1461                    } else if span["name"] == "Root_Span_2" {
1462                        let status = span["status"].as_str().unwrap_or("");
1463                        if status.starts_with("Error") {
1464                            error_span = Some(span);
1465                        }
1466                    }
1467                }
1468            }
1469
1470            let ok_span = ok_span.expect("Root_Span_1 with Ok status not found");
1471            assert_eq!(ok_span["status"], "Ok");
1472
1473            let error_span = error_span.expect("Root_Span_2 with Error status not found");
1474            let status = error_span["status"].as_str().unwrap_or("");
1475            assert!(status.starts_with("Error"));
1476            assert!(status.contains("simple error"));
1477        });
1478    }
1479
1480    #[test]
1481    fn test_protocol_from_env() {
1482        unsafe {
1483            std::env::remove_var("OTEL_EXPORTER_OTLP_PROTOCOL");
1484            std::env::remove_var("OTEL_EXPORTER_OTLP_TRACES_PROTOCOL");
1485            std::env::remove_var("OTEL_EXPORTER_OTLP_METRICS_PROTOCOL");
1486        }
1487
1488        // default: None
1489        assert!(protocol_from_env(OtelSignal::Traces).is_none());
1490
1491        unsafe { std::env::set_var("OTEL_EXPORTER_OTLP_PROTOCOL", "grpc") };
1492        assert_eq!(protocol_from_env(OtelSignal::Traces), Some(Protocol::Grpc));
1493
1494        unsafe { std::env::set_var("OTEL_EXPORTER_OTLP_METRICS_PROTOCOL", "http/protobuf") };
1495        assert_eq!(
1496            protocol_from_env(OtelSignal::Metrics),
1497            Some(Protocol::HttpBinary)
1498        );
1499
1500        unsafe {
1501            std::env::remove_var("OTEL_EXPORTER_OTLP_PROTOCOL");
1502            std::env::set_var("OTEL_EXPORTER_OTLP_TRACES_PROTOCOL", "http/json");
1503        }
1504        assert_eq!(
1505            protocol_from_env(OtelSignal::Traces),
1506            Some(Protocol::HttpJson)
1507        );
1508        unsafe {
1509            std::env::remove_var("OTEL_EXPORTER_OTLP_TRACES_PROTOCOL");
1510            std::env::remove_var("OTEL_EXPORTER_OTLP_METRICS_PROTOCOL");
1511        }
1512    }
1513
1514    #[test]
1515    fn test_span_pointer_validation() {
1516        // Test null pointer validation
1517        assert!(unsafe { !GlideOpenTelemetry::is_span_pointer_valid(0) });
1518
1519        // Test misaligned pointer validation
1520        assert!(unsafe { !GlideOpenTelemetry::is_span_pointer_valid(0x1001) }); // Not 8-byte aligned
1521        assert!(unsafe { !GlideOpenTelemetry::is_span_pointer_valid(0x1002) }); // Not 8-byte aligned
1522        assert!(unsafe { !GlideOpenTelemetry::is_span_pointer_valid(0x1007) }); // Not 8-byte aligned
1523
1524        // Test address too low validation
1525        assert!(unsafe { !GlideOpenTelemetry::is_span_pointer_valid(0x800) }); // Below MIN_VALID_ADDRESS
1526        assert!(unsafe { !GlideOpenTelemetry::is_span_pointer_valid(0x100) }); // Way too low
1527
1528        // Test address too high validation
1529        assert!(unsafe { !GlideOpenTelemetry::is_span_pointer_valid(0x8000_0000_0000_0000) }); // Above MAX_VALID_ADDRESS
1530        assert!(unsafe { !GlideOpenTelemetry::is_span_pointer_valid(0xFFFF_FFFF_FFFF_FFFF) }); // Maximum u64
1531
1532        // Test valid pointer ranges
1533        assert!(unsafe { GlideOpenTelemetry::is_span_pointer_valid(0x1000) }); // Minimum valid
1534        assert!(unsafe { GlideOpenTelemetry::is_span_pointer_valid(0x10000) }); // Reasonable heap address
1535        assert!(unsafe { GlideOpenTelemetry::is_span_pointer_valid(0x7FFF_FFFF_FFFF_FFF8) }); // Near maximum valid
1536    }
1537
1538    #[test]
1539    fn test_span_from_pointer_validation() {
1540        let rt = shared_runtime();
1541        rt.block_on(async {
1542            init_otel().await.unwrap();
1543
1544            // Test with null pointer
1545            let result = unsafe { GlideOpenTelemetry::span_from_pointer(0) };
1546            assert!(result.is_err());
1547            assert!(
1548                result
1549                    .unwrap_err()
1550                    .to_string()
1551                    .contains("Invalid span pointer")
1552            );
1553
1554            // Test with misaligned pointer
1555            let result = unsafe { GlideOpenTelemetry::span_from_pointer(0x1001) };
1556            assert!(result.is_err());
1557            assert!(
1558                result
1559                    .unwrap_err()
1560                    .to_string()
1561                    .contains("failed validation checks")
1562            );
1563
1564            // Test with address too low
1565            let result = unsafe { GlideOpenTelemetry::span_from_pointer(0x800) };
1566            assert!(result.is_err());
1567            assert!(
1568                result
1569                    .unwrap_err()
1570                    .to_string()
1571                    .contains("failed validation checks")
1572            );
1573
1574            // Note: We don't test with very high addresses that would cause segfaults
1575            // The validation function will catch these, but we can't safely test Arc conversion
1576        });
1577    }
1578
1579    #[test]
1580    fn test_new_with_parent_creates_child_span() {
1581        let rt = shared_runtime();
1582        rt.block_on(async {
1583            let _ = std::fs::remove_file(SPANS_JSON);
1584            init_otel().await.unwrap();
1585
1586            // Create parent span
1587            let parent_span = GlideOpenTelemetry::new_span("parent_span");
1588
1589            // Create child span using new_with_parent
1590            let child_span_result =
1591                GlideSpanInner::new_with_parent("child_span", &parent_span.inner);
1592            assert!(
1593                child_span_result.is_ok(),
1594                "Failed to create child span with parent"
1595            );
1596
1597            let child_span = child_span_result.unwrap();
1598
1599            // Verify child span has different ID from parent
1600            let parent_id = parent_span.id();
1601            let child_id = child_span.id();
1602            assert_ne!(
1603                parent_id, child_id,
1604                "Child span should have different ID from parent"
1605            );
1606
1607            // End spans to trigger export
1608            child_span.end();
1609            parent_span.end();
1610
1611            // Wait for export
1612            sleep(Duration::from_millis(2100)).await;
1613
1614            // Verify spans were exported
1615            let file_content = std::fs::read_to_string(SPANS_JSON).unwrap();
1616            let lines: Vec<&str> = file_content
1617                .split('\n')
1618                .filter(|l| !l.trim().is_empty())
1619                .collect();
1620
1621            assert!(lines.len() >= 2, "Expected at least 2 spans to be exported");
1622
1623            // Find child and parent spans in export
1624            let mut child_found = false;
1625            let mut parent_found = false;
1626
1627            for line in lines {
1628                let span_json: serde_json::Value = serde_json::from_str(line).unwrap();
1629                let span_name = span_json["name"].as_str().unwrap();
1630
1631                if span_name == "child_span" {
1632                    child_found = true;
1633                    // Verify child span has parent context
1634                    assert!(
1635                        span_json["parent_span_id"].is_string(),
1636                        "Child span should have parent_span_id"
1637                    );
1638                } else if span_name == "parent_span" {
1639                    parent_found = true;
1640                }
1641            }
1642
1643            assert!(child_found, "Child span should be found in export");
1644            assert!(parent_found, "Parent span should be found in export");
1645        });
1646    }
1647
1648    #[test]
1649    fn test_new_with_parent_error_handling() {
1650        let rt = shared_runtime();
1651        rt.block_on(async {
1652            init_otel().await.unwrap();
1653
1654            // Create a parent span
1655            let parent_span = GlideOpenTelemetry::new_span("error_test_parent");
1656
1657            // Test creating child with empty name (should still work)
1658            let child_result = GlideSpanInner::new_with_parent("", &parent_span.inner);
1659            assert!(
1660                child_result.is_ok(),
1661                "Should be able to create child span with empty name"
1662            );
1663
1664            // Test creating child with very long name (should still work)
1665            let long_name = "a".repeat(1000);
1666            let child_result = GlideSpanInner::new_with_parent(&long_name, &parent_span.inner);
1667            assert!(
1668                child_result.is_ok(),
1669                "Should be able to create child span with long name"
1670            );
1671
1672            // Clean up
1673            if let Ok(child) = child_result {
1674                child.end();
1675            }
1676            parent_span.end();
1677        });
1678    }
1679
1680    #[test]
1681    fn test_span_from_pointer_error_messages() {
1682        let rt = shared_runtime();
1683        rt.block_on(async {
1684            init_otel().await.unwrap();
1685
1686            // Test null pointer error message
1687            let result = unsafe { GlideOpenTelemetry::span_from_pointer(0) };
1688            assert!(result.is_err());
1689            let error_msg = result.unwrap_err().to_string();
1690            assert!(
1691                error_msg.contains("Invalid span pointer"),
1692                "Error message should mention invalid span pointer"
1693            );
1694            assert!(
1695                error_msg.contains("0x0"),
1696                "Error message should include the pointer value"
1697            );
1698
1699            // Test misaligned pointer error message
1700            let result = unsafe { GlideOpenTelemetry::span_from_pointer(0x1001) };
1701            assert!(result.is_err());
1702            let error_msg = result.unwrap_err().to_string();
1703            assert!(
1704                error_msg.contains("failed validation checks"),
1705                "Error message should mention validation failure"
1706            );
1707            assert!(
1708                error_msg.contains("0x1001"),
1709                "Error message should include the pointer value"
1710            );
1711
1712            // Test address too low error message
1713            let result = unsafe { GlideOpenTelemetry::span_from_pointer(0x800) };
1714            assert!(result.is_err());
1715            let error_msg = result.unwrap_err().to_string();
1716            assert!(
1717                error_msg.contains("failed validation checks"),
1718                "Error message should mention validation failure"
1719            );
1720        });
1721    }
1722
1723    #[test]
1724    fn test_new_with_remote_context_valid_inputs() {
1725        let rt = shared_runtime();
1726        rt.block_on(async {
1727            init_otel().await.unwrap();
1728
1729            let result = GlideSpanInner::new_with_remote_context(
1730                "remote_child",
1731                "0af7651916cd43dd8448eb211c80319c", // valid 32-char hex trace ID
1732                "b7ad6b7169203331",                 // valid 16-char hex span ID
1733                1,                                  // trace flags
1734                None,
1735            );
1736            assert!(result.is_ok(), "Valid inputs should return Ok");
1737
1738            let span = result.unwrap();
1739            span.end();
1740        });
1741    }
1742
1743    #[test]
1744    fn test_new_with_remote_context_invalid_trace_id() {
1745        let rt = shared_runtime();
1746        rt.block_on(async {
1747            init_otel().await.unwrap();
1748
1749            let result = GlideSpanInner::new_with_remote_context(
1750                "remote_child",
1751                "not_valid_hex",
1752                "b7ad6b7169203331",
1753                1,
1754                None,
1755            );
1756            assert!(result.is_err(), "Invalid trace_id should return Err");
1757        });
1758    }
1759
1760    #[test]
1761    fn test_new_with_remote_context_invalid_span_id() {
1762        let rt = shared_runtime();
1763        rt.block_on(async {
1764            init_otel().await.unwrap();
1765
1766            let result = GlideSpanInner::new_with_remote_context(
1767                "remote_child",
1768                "0af7651916cd43dd8448eb211c80319c",
1769                "zzzzzzzzzzzzzzzz", // 16 chars but not valid hex
1770                1,
1771                None,
1772            );
1773            assert!(result.is_err(), "Invalid span_id should return Err");
1774        });
1775    }
1776
1777    #[test]
1778    fn test_validation_functions_fallback_behavior() {
1779        let rt = shared_runtime();
1780        rt.block_on(async {
1781            init_otel().await.unwrap();
1782
1783            // Test that validation functions provide proper fallback behavior
1784            // when given invalid inputs
1785
1786            let invalid_pointers = vec![
1787                0,                     // null
1788                0x1001,                // misaligned
1789                0x800,                 // too low
1790                0x8000_0000_0000_0000, // too high
1791            ];
1792
1793            for &invalid_ptr in &invalid_pointers {
1794                // Validation should return false
1795                assert!(
1796                    unsafe { !GlideOpenTelemetry::is_span_pointer_valid(invalid_ptr) },
1797                    "Pointer 0x{:x} should be invalid",
1798                    invalid_ptr
1799                );
1800
1801                // Safe conversion should return error
1802                let result = unsafe { GlideOpenTelemetry::span_from_pointer(invalid_ptr) };
1803                assert!(
1804                    result.is_err(),
1805                    "Conversion of invalid pointer 0x{:x} should fail",
1806                    invalid_ptr
1807                );
1808
1809                // Error should contain meaningful information
1810                let error_msg = result.unwrap_err().to_string();
1811                assert!(
1812                    error_msg.contains("Invalid span pointer")
1813                        || error_msg.contains("failed validation"),
1814                    "Error message should be meaningful for pointer 0x{:x}: {}",
1815                    invalid_ptr,
1816                    error_msg
1817                );
1818            }
1819        });
1820    }
1821}