//! Traces, for ipx and the directory both (#213): the exporter, and W3C `traceparent` in from a //! caller and out to a callee, so one trace follows a request from the browser through ipx-web //! and ipx into the directory. use axum::http::HeaderMap; /// Traces over OTLP, to Tempo for one, when OTEL_EXPORTER_OTLP_ENDPOINT names a collector /// (`http://host:4318`: the exporter speaks OTLP over HTTP and adds `/v1/traces`). The exporter /// reads that and the other `OTEL_` variables itself. pub fn provider(service: &'static str) -> anyhow::Result> { if std::env::var("OTEL_EXPORTER_OTLP_ENDPOINT").unwrap_or_default().is_empty() { return Ok(None); } let exporter = opentelemetry_otlp::SpanExporter::builder().with_http().build()?; let resource = opentelemetry_sdk::Resource::builder().with_service_name(service).build(); Ok(Some( opentelemetry_sdk::trace::SdkTracerProvider::builder() .with_batch_exporter(exporter) .with_resource(resource) .build(), )) } /// The trace a request from the page belongs to, from its `traceparent` (#190), so the requests /// one action sets off are one trace. None for a missing or malformed one. In the query where no /// header can be set: a position saved by sendBeacon, and the playlist Safari's player fetches /// itself (#192). #[must_use] pub fn remote_parent(headers: &HeaderMap, query: Option<&str>) -> Option { use opentelemetry::trace::{SpanContext, SpanId, TraceContextExt, TraceFlags, TraceId, TraceState}; let v = match headers.get("traceparent") { Some(h) => h.to_str().ok()?, None => query?.split('&').find_map(|kv| kv.strip_prefix("traceparent="))?, }; let mut parts = v.split('-'); let (version, trace, span) = (parts.next()?, parts.next()?, parts.next()?); if version != "00" || trace.len() != 32 || span.len() != 16 { return None; } let sc = SpanContext::new( TraceId::from_hex(trace).ok()?, SpanId::from_hex(span).ok()?, TraceFlags::SAMPLED, true, TraceState::default(), ); sc.is_valid().then(|| opentelemetry::Context::new().with_remote_span_context(sc)) } /// The current span as a `traceparent`, for a request to another service, so its spans join /// this trace. None outside a traced span. #[must_use] pub fn traceparent() -> Option { use opentelemetry::trace::TraceContextExt; use tracing_opentelemetry::OpenTelemetrySpanExt; let cx = tracing::Span::current().context(); let sc = cx.span().span_context().clone(); sc.is_valid().then(|| format!("00-{}-{}-{:02x}", sc.trace_id(), sc.span_id(), sc.trace_flags().to_u8())) } #[cfg(test)] mod tests { use super::*; #[test] fn a_requests_trace_is_the_pages_from_its_traceparent() { use opentelemetry::trace::TraceContextExt; let with = |v: &str| { let mut h = axum::http::HeaderMap::new(); h.insert("traceparent", v.parse().unwrap()); remote_parent(&h, None) }; let cx = with("00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01").expect("parsed"); assert_eq!(cx.span().span_context().trace_id().to_string(), "4bf92f3577b34da6a3ce929d0e0e4736"); assert!(cx.span().span_context().is_remote()); // Anything else from the browser starts a trace of its own. for bad in [ "", "00-xyz-00f067aa0ba902b7-01", "01-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01", "00-00000000000000000000000000000000-00f067aa0ba902b7-01", "00-4bf92f3577b34da6a3ce929d0e0e4736", ] { assert!(with(bad).is_none(), "{bad}"); } assert!(remote_parent(&axum::http::HeaderMap::new(), None).is_none()); // From the query, where a beacon or Safari's player cannot send a header (#192). let q = remote_parent( &axum::http::HeaderMap::new(), Some("x=1&traceparent=00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01"), ); assert_eq!(q.unwrap().span().span_context().trace_id().to_string(), "4bf92f3577b34da6a3ce929d0e0e4736"); assert!(remote_parent(&axum::http::HeaderMap::new(), Some("traceparent=nonsense")).is_none()); } }