1#[cfg(test)]
57mod tests;
58
59pub mod baggage;
60pub mod metrics;
61
62pub use baggage::Baggage;
63
64use std::cell::{Cell, RefCell};
65use std::sync::atomic::{AtomicBool, Ordering};
66use std::sync::{Mutex, OnceLock};
67use std::time::{SystemTime, UNIX_EPOCH};
68
69use crate::application::Application;
70use crate::core::json::escape as json_escape;
71use crate::middleware::Middleware;
72use crate::request::Request;
73use crate::response::Response;
74use crate::server::ConnectionInfo;
75
76fn random_u64() -> u64 {
83 thread_local! {
84 static STATE: Cell<u64> = Cell::new({
85 use std::hash::{BuildHasher, Hasher};
86 let mut hasher = std::collections::hash_map::RandomState::new().build_hasher();
87 hasher.write_u128(SystemTime::now().duration_since(UNIX_EPOCH).unwrap_or_default().as_nanos());
88 hasher.finish()
89 });
90 }
91 STATE.with(|state| {
92 let x = state.get().wrapping_add(0x9E37_79B9_7F4A_7C15);
93 state.set(x);
94 let mut z = x;
95 z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
96 z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
97 z ^ (z >> 31)
98 })
99}
100
101fn sampled(roll: u64, rate: f64) -> bool {
104 if rate >= 1.0 {
105 return true;
106 }
107 if rate <= 0.0 {
108 return false;
109 }
110 roll % 10_000 < (rate * 10_000.0) as u64
111}
112
113pub fn new_trace_id() -> [u8; 16] {
115 let mut id = [0u8; 16];
116 id[..8].copy_from_slice(&random_u64().to_be_bytes());
117 id[8..].copy_from_slice(&(random_u64() | 1).to_be_bytes());
118 id
119}
120
121pub fn new_span_id() -> [u8; 8] {
123 (random_u64() | 1).to_be_bytes()
124}
125
126fn hex16(b: &[u8; 16]) -> String {
127 b.iter().map(|x| format!("{:02x}", x)).collect()
128}
129
130fn hex8(b: &[u8; 8]) -> String {
131 b.iter().map(|x| format!("{:02x}", x)).collect()
132}
133
134#[derive(Copy, Clone, Debug, PartialEq, Eq)]
140pub struct TraceContext {
141 pub trace_id: [u8; 16],
142 pub parent_span_id: [u8; 8],
143 pub sampled: bool,
144}
145
146impl TraceContext {
147 pub fn parse(header: &str) -> Option<Self> {
149 let parts: Vec<&str> = header.trim().splitn(4, '-').collect();
150 if parts.len() != 4 || parts[0] != "00" {
151 return None;
152 }
153 let trace_id = parse_hex16(parts[1])?;
154 let parent_span_id = parse_hex8(parts[2])?;
155 let flags = u8::from_str_radix(parts[3], 16).ok()?;
156 Some(TraceContext { trace_id, parent_span_id, sampled: flags & 0x01 != 0 })
157 }
158
159 pub fn as_header(&self, span_id: &[u8; 8]) -> String {
161 format!("00-{}-{}-{:02x}", hex16(&self.trace_id), hex8(span_id), self.sampled as u8)
162 }
163}
164
165fn parse_hex16(s: &str) -> Option<[u8; 16]> {
166 if s.len() != 32 { return None; }
167 let mut out = [0u8; 16];
168 for (i, chunk) in s.as_bytes().chunks(2).enumerate() {
169 out[i] = u8::from_str_radix(std::str::from_utf8(chunk).ok()?, 16).ok()?;
170 }
171 Some(out)
172}
173
174fn parse_hex8(s: &str) -> Option<[u8; 8]> {
175 if s.len() != 16 { return None; }
176 let mut out = [0u8; 8];
177 for (i, chunk) in s.as_bytes().chunks(2).enumerate() {
178 out[i] = u8::from_str_radix(std::str::from_utf8(chunk).ok()?, 16).ok()?;
179 }
180 Some(out)
181}
182
183#[derive(Copy, Clone, Debug, PartialEq, Eq, Default)]
190pub enum SpanKind {
191 #[default]
194 Internal = 1,
195 Server = 2,
197 Client = 3,
199}
200
201#[derive(Clone, Debug, PartialEq)]
203pub enum AttributeValue {
204 String(String),
205 Int(i64),
206 Float(f64),
207 Bool(bool),
208}
209
210impl From<&str> for AttributeValue {
211 fn from(s: &str) -> Self { AttributeValue::String(s.to_string()) }
212}
213impl From<String> for AttributeValue {
214 fn from(s: String) -> Self { AttributeValue::String(s) }
215}
216impl From<i64> for AttributeValue {
217 fn from(v: i64) -> Self { AttributeValue::Int(v) }
218}
219impl From<i32> for AttributeValue {
220 fn from(v: i32) -> Self { AttributeValue::Int(v as i64) }
221}
222impl From<u32> for AttributeValue {
223 fn from(v: u32) -> Self { AttributeValue::Int(v as i64) }
224}
225impl From<f64> for AttributeValue {
226 fn from(v: f64) -> Self { AttributeValue::Float(v) }
227}
228impl From<bool> for AttributeValue {
229 fn from(v: bool) -> Self { AttributeValue::Bool(v) }
230}
231
232#[derive(Debug, Clone, Default)]
236pub struct SpanData {
237 pub trace_id: [u8; 16],
238 pub span_id: [u8; 8],
239 pub parent_span_id: Option<[u8; 8]>,
240 pub name: String,
242 pub start_ns: u64,
243 pub end_ns: u64,
244 pub http_method: String,
247 pub http_target: String,
248 pub http_status: i16,
249 pub status_code: u8,
251 pub kind: SpanKind,
252 pub attributes: Vec<(String, AttributeValue)>,
256}
257
258impl SpanData {
259 fn duration_ms(&self) -> f64 {
260 (self.end_ns.saturating_sub(self.start_ns)) as f64 / 1_000_000.0
261 }
262}
263
264fn now_ns() -> u64 {
265 SystemTime::now()
266 .duration_since(UNIX_EPOCH)
267 .unwrap_or_default()
268 .as_nanos() as u64
269}
270
271fn strip_query(uri: &str) -> &str {
272 match uri.find('?') {
273 Some(i) => &uri[..i],
274 None => uri,
275 }
276}
277
278#[derive(Clone)]
283struct ActiveSpanCtx {
284 trace_id: [u8; 16],
285 span_id: [u8; 8],
286 sampled: bool,
287 baggage: Option<std::sync::Arc<Baggage>>,
289}
290
291thread_local! {
292 static ACTIVE_STACK: RefCell<Vec<ActiveSpanCtx>> = const { RefCell::new(Vec::new()) };
297}
298
299pub fn current_traceparent() -> Option<String> {
306 ACTIVE_STACK.with(|stack| {
307 stack.borrow().last().map(|s| {
308 format!(
309 "00-{}-{}-{:02x}",
310 hex16(&s.trace_id),
311 hex8(&s.span_id),
312 s.sampled as u8,
313 )
314 })
315 })
316}
317
318pub struct Span {
346 trace_id: [u8; 16],
347 span_id: [u8; 8],
348 parent_span_id: Option<[u8; 8]>,
349 sampled: bool,
350 kind: SpanKind,
351 name: String,
352 start_ns: u64,
353 attributes: RefCell<Vec<(String, AttributeValue)>>,
354 status_code: Cell<u8>,
355 http_method: RefCell<Option<String>>,
356 http_target: RefCell<Option<String>>,
357 http_status: Cell<Option<i16>>,
358 _not_send: std::marker::PhantomData<std::rc::Rc<()>>,
361}
362
363fn new_span(name: &str, kind: SpanKind, incoming: Option<TraceContext>) -> Span {
364 new_span_with_baggage(name, kind, incoming, None)
365}
366
367fn new_span_with_baggage(name: &str, kind: SpanKind, incoming: Option<TraceContext>, incoming_baggage: Option<Baggage>) -> Span {
368 let mut baggage = incoming_baggage.filter(|b| !b.is_empty()).map(std::sync::Arc::new);
369 let (trace_id, parent_span_id, sampled) = ACTIVE_STACK.with(|stack| {
370 let stack = stack.borrow();
371 if let Some(top) = stack.last() {
372 if baggage.is_none() {
377 baggage = top.baggage.clone();
378 }
379 (top.trace_id, Some(top.span_id), top.sampled)
380 } else {
381 let sampled = tracer().map(|t| t.should_sample()).unwrap_or(false);
384 match incoming {
385 Some(ctx) => (ctx.trace_id, Some(ctx.parent_span_id), sampled),
386 None => (new_trace_id(), None, sampled),
387 }
388 }
389 });
390 let span_id = new_span_id();
391 ACTIVE_STACK.with(|stack| stack.borrow_mut().push(ActiveSpanCtx { trace_id, span_id, sampled, baggage }));
392
393 Span {
394 trace_id,
395 span_id,
396 parent_span_id,
397 sampled,
398 kind,
399 name: name.to_string(),
400 start_ns: now_ns(),
401 attributes: RefCell::new(Vec::new()),
402 status_code: Cell::new(0),
403 http_method: RefCell::new(None),
404 http_target: RefCell::new(None),
405 http_status: Cell::new(None),
406 _not_send: std::marker::PhantomData,
407 }
408}
409
410impl Span {
411 pub fn new(name: &str, kind: SpanKind) -> Span {
414 new_span(name, kind, None)
415 }
416
417 #[doc(hidden)]
420 pub fn start_root(name: &str, kind: SpanKind, incoming: Option<TraceContext>, baggage: Option<Baggage>) -> Span {
421 new_span_with_baggage(name, kind, incoming, baggage)
422 }
423
424 pub fn set_attribute(&self, key: &str, value: impl Into<AttributeValue>) {
427 self.attributes.borrow_mut().push((key.to_string(), value.into()));
428 }
429
430 pub fn set_error(&self) {
432 self.status_code.set(2);
433 }
434
435 pub fn record_error(&self, message: &str) {
437 self.set_error();
438 self.set_attribute("error.message", message);
439 }
440
441 pub fn trace_id(&self) -> [u8; 16] {
442 self.trace_id
443 }
444
445 pub fn span_id(&self) -> [u8; 8] {
446 self.span_id
447 }
448
449 pub fn parent_span_id(&self) -> Option<[u8; 8]> {
450 self.parent_span_id
451 }
452
453 pub fn end(self) {}
458
459 #[doc(hidden)]
460 pub fn set_http(&self, method: &str, target: &str) {
461 *self.http_method.borrow_mut() = Some(method.to_string());
462 *self.http_target.borrow_mut() = Some(target.to_string());
463 }
464
465 #[doc(hidden)]
467 pub fn set_http_status(&self, status: i16) {
468 self.http_status.set(Some(status));
469 if status >= 500 {
470 self.set_error();
471 }
472 }
473
474 fn finish(&mut self, end_ns: u64) -> SpanData {
475 SpanData {
476 trace_id: self.trace_id,
477 span_id: self.span_id,
478 parent_span_id: self.parent_span_id,
479 name: std::mem::take(&mut self.name),
480 start_ns: self.start_ns,
481 end_ns,
482 http_method: self.http_method.get_mut().take().unwrap_or_default(),
483 http_target: self.http_target.get_mut().take().unwrap_or_default(),
484 http_status: self.http_status.get().unwrap_or(0),
485 status_code: self.status_code.get(),
486 kind: self.kind,
487 attributes: std::mem::take(self.attributes.get_mut()),
488 }
489 }
490}
491
492impl Drop for Span {
493 fn drop(&mut self) {
494 ACTIVE_STACK.with(|stack| {
495 let mut stack = stack.borrow_mut();
496 if let Some(pos) = stack.iter().rposition(|c| c.span_id == self.span_id) {
499 stack.truncate(pos);
500 }
501 });
502
503 if !self.sampled {
504 return;
505 }
506 let Some(t) = tracer() else { return };
507 let end_ns = now_ns();
508 t.record(self.finish(end_ns));
509 }
510}
511
512pub fn span(name: &str) -> Span {
524 Span::new(name, SpanKind::Internal)
525}
526
527pub fn client_span(name: &str) -> Span {
530 Span::new(name, SpanKind::Client)
531}
532
533pub fn is_tracing() -> bool {
535 tracer().is_some() && ACTIVE_STACK.with(|stack| !stack.borrow().is_empty())
536}
537
538#[doc(hidden)]
541pub fn outbound_span(name: &str) -> Option<Span> {
542 is_tracing().then(|| client_span(name))
543}
544
545#[doc(hidden)]
549pub fn http_client_attributes(method: &str, url: &str) -> Vec<(&'static str, AttributeValue)> {
550 let (scheme, rest) = url.split_once("://").unwrap_or(("http", url));
551 let rest = rest.split(['?', '#']).next().unwrap_or("");
552 let rest = match rest.find('@') {
553 Some(at) if rest.find('/').is_none_or(|slash| at < slash) => &rest[at + 1..],
554 _ => rest,
555 };
556 let authority = rest.split('/').next().unwrap_or("");
557 let default_port = if scheme.eq_ignore_ascii_case("https") { 443 } else { 80 };
558 let (host, port) = match authority.rsplit_once(':') {
559 Some((host, port)) if !host.ends_with(']') || authority.starts_with('[') => match port.parse::<i64>() {
560 Ok(port) => (host, port),
561 Err(_) => (authority, default_port),
562 },
563 _ => (authority, default_port),
564 };
565 vec![
566 ("http.request.method", AttributeValue::from(method)),
567 ("url.full", AttributeValue::from(format!("{scheme}://{rest}"))),
568 ("server.address", AttributeValue::from(host.trim_start_matches('[').trim_end_matches(']'))),
569 ("server.port", AttributeValue::Int(port)),
570 ]
571}
572
573#[doc(hidden)]
576pub fn http_client_outcome(status: Result<u16, &str>) -> (Option<i64>, Option<String>) {
577 match status {
578 Ok(status) => (Some(status as i64), (status >= 400).then(|| status.to_string())),
579 Err(message) => (None, Some(message.to_string())),
580 }
581}
582
583impl Span {
584 #[doc(hidden)]
586 pub fn finish_http_client(&self, status: Result<u16, &str>) {
587 let (status, error) = http_client_outcome(status);
588 if let Some(status) = status {
589 self.set_attribute("http.response.status_code", status);
590 }
591 if let Some(error) = error {
592 self.set_attribute("error.type", error.as_str());
593 self.set_error();
594 }
595 }
596}
597
598pub fn current_baggage() -> Baggage {
602 ACTIVE_STACK.with(|stack| stack.borrow().last().and_then(|top| top.baggage.as_deref().cloned()).unwrap_or_default())
603}
604
605pub fn set_baggage(key: &str, value: &str) -> bool {
609 ACTIVE_STACK.with(|stack| {
610 let mut stack = stack.borrow_mut();
611 let Some(top) = stack.last_mut() else { return false };
612 let mut baggage = top.baggage.as_deref().cloned().unwrap_or_default();
613 if !baggage.set(key, value) {
614 return false;
615 }
616 top.baggage = Some(std::sync::Arc::new(baggage));
617 true
618 })
619}
620
621pub fn propagation_headers() -> Option<(String, Option<String>)> {
624 ACTIVE_STACK.with(|stack| {
625 let stack = stack.borrow();
626 let top = stack.last()?;
627 let traceparent = format!("00-{}-{}-{:02x}", hex16(&top.trace_id), hex8(&top.span_id), top.sampled as u8);
628 let baggage = top.baggage.as_deref().filter(|b| !b.is_empty()).map(Baggage::to_header);
629 Some((traceparent, baggage))
630 })
631}
632
633#[doc(hidden)]
636pub fn inject(headers: &mut Vec<(String, String)>) {
637 let Some((traceparent, baggage)) = propagation_headers() else { return };
638 if !headers.iter().any(|(name, _)| name.eq_ignore_ascii_case("traceparent")) {
639 headers.push(("traceparent".to_string(), traceparent));
640 }
641 if let Some(baggage) = baggage {
642 if !headers.iter().any(|(name, _)| name.eq_ignore_ascii_case("baggage")) {
643 headers.push(("baggage".to_string(), baggage));
644 }
645 }
646}
647
648#[derive(Clone, Default)]
653pub struct Context {
654 active: Option<ActiveSpanCtx>,
655}
656
657impl Context {
658 pub fn current() -> Context {
660 Context { active: ACTIVE_STACK.with(|stack| stack.borrow().last().cloned()) }
661 }
662
663 pub fn is_empty(&self) -> bool {
665 self.active.is_none()
666 }
667
668 pub fn attach(&self) -> ContextGuard {
670 let span_id = self.active.as_ref().map(|active| {
671 ACTIVE_STACK.with(|stack| stack.borrow_mut().push(active.clone()));
672 active.span_id
673 });
674 ContextGuard { span_id, _not_send: std::marker::PhantomData }
675 }
676}
677
678pub struct ContextGuard {
681 span_id: Option<[u8; 8]>,
682 _not_send: std::marker::PhantomData<std::rc::Rc<()>>,
683}
684
685impl Drop for ContextGuard {
686 fn drop(&mut self) {
687 let Some(span_id) = self.span_id else { return };
688 ACTIVE_STACK.with(|stack| {
689 let mut stack = stack.borrow_mut();
690 if let Some(pos) = stack.iter().rposition(|c| c.span_id == span_id) {
691 stack.truncate(pos);
692 }
693 });
694 }
695}
696
697pub struct DetachedSpan {
705 data: SpanData,
706 sampled: bool,
707 baggage: Option<std::sync::Arc<Baggage>>,
708}
709
710pub fn detached_span(name: &str, kind: SpanKind) -> Option<DetachedSpan> {
713 tracer()?;
714 let parent = ACTIVE_STACK.with(|stack| stack.borrow().last().cloned())?;
715 Some(DetachedSpan {
716 data: SpanData {
717 trace_id: parent.trace_id,
718 span_id: new_span_id(),
719 parent_span_id: Some(parent.span_id),
720 name: name.to_string(),
721 start_ns: now_ns(),
722 kind,
723 ..SpanData::default()
724 },
725 sampled: parent.sampled,
726 baggage: parent.baggage,
727 })
728}
729
730impl DetachedSpan {
731 pub fn set_attribute(&mut self, key: &str, value: impl Into<AttributeValue>) {
732 self.data.attributes.push((key.to_string(), value.into()));
733 }
734
735 pub fn record_error(&mut self, message: &str) {
737 self.data.status_code = 2;
738 self.set_attribute("error.message", message);
739 }
740
741 pub fn enter<T>(&self, work: impl FnOnce() -> T) -> T {
744 let context = Context {
745 active: Some(ActiveSpanCtx {
746 trace_id: self.data.trace_id,
747 span_id: self.data.span_id,
748 sampled: self.sampled,
749 baggage: self.baggage.clone(),
750 }),
751 };
752 let _guard = context.attach();
753 work()
754 }
755
756 #[doc(hidden)]
758 pub fn finish_http_client(&mut self, status: Result<u16, &str>) {
759 let (status, error) = http_client_outcome(status);
760 if let Some(status) = status {
761 self.set_attribute("http.response.status_code", status);
762 }
763 if let Some(error) = error {
764 self.set_attribute("error.type", error.as_str());
765 self.data.status_code = 2;
766 }
767 }
768
769 pub fn end(self) {}
770}
771
772impl Drop for DetachedSpan {
773 fn drop(&mut self) {
774 if !self.sampled {
775 return;
776 }
777 if let Some(tracer) = tracer() {
778 let mut data = std::mem::take(&mut self.data);
779 data.end_ns = now_ns();
780 tracer.record(data);
781 }
782 }
783}
784
785pub trait Exporter: Send + Sync {
789 fn export(&self, spans: &[SpanData]);
790 fn shutdown(&self) {}
791}
792
793fn attr_value_json(v: &AttributeValue) -> String {
796 match v {
797 AttributeValue::String(s) => format!("{{\"stringValue\":\"{}\"}}", json_escape(s)),
798 AttributeValue::Int(i) => format!("{{\"intValue\":\"{i}\"}}"),
799 AttributeValue::Float(f) => format!("{{\"doubleValue\":{f}}}"),
800 AttributeValue::Bool(b) => format!("{{\"boolValue\":{b}}}"),
801 }
802}
803
804fn attr_json(key: &str, value: &AttributeValue) -> String {
806 format!("{{\"key\":\"{}\",\"value\":{}}}", json_escape(key), attr_value_json(value))
807}
808
809pub struct StdoutExporter;
812
813impl StdoutExporter {
814 fn format_span(span: &SpanData) -> String {
815 let http_attrs = if span.http_method.is_empty() {
816 String::new()
817 } else {
818 format!(
819 ",\"httpMethod\":\"{}\",\"httpTarget\":\"{}\",\"httpStatus\":{}",
820 span.http_method, span.http_target, span.http_status,
821 )
822 };
823 let extra_attrs: String = span.attributes.iter()
824 .map(|(k, v)| format!(",\"{k}\":{}", attr_value_json(v)))
825 .collect();
826 format!(
827 "{{\"traceId\":\"{}\",\"spanId\":\"{}\",\"parentSpanId\":{},\
828 \"name\":\"{}\",\"kind\":{},\"startNs\":{},\"durationMs\":{:.3}{http_attrs}{extra_attrs}}}",
829 hex16(&span.trace_id),
830 hex8(&span.span_id),
831 span.parent_span_id
832 .as_ref()
833 .map(|p| format!("\"{}\"", hex8(p)))
834 .unwrap_or_else(|| "null".to_string()),
835 span.name,
836 span.kind as i32,
837 span.start_ns,
838 span.duration_ms(),
839 )
840 }
841}
842
843impl Exporter for StdoutExporter {
844 fn export(&self, spans: &[SpanData]) {
845 for span in spans {
846 println!("{}", Self::format_span(span));
847 }
848 }
849}
850
851pub struct OtlpHttpExporter {
856 url: String,
857 headers: Vec<(String, String)>,
858 service_name: String,
859 service_version: String,
860}
861
862impl OtlpHttpExporter {
863 pub fn new(endpoint: &str, service_name: &str, service_version: &str) -> Self {
867 let endpoint = endpoint.trim_end_matches('/');
868 let url = if endpoint.ends_with("/v1/traces") {
869 endpoint.to_string()
870 } else if endpoint.contains("://") {
871 format!("{endpoint}/v1/traces")
872 } else {
873 format!("http://{endpoint}/v1/traces")
874 };
875 OtlpHttpExporter {
876 url,
877 headers: Vec::new(),
878 service_name: service_name.to_string(),
879 service_version: service_version.to_string(),
880 }
881 }
882
883 pub fn with_headers(mut self, headers: Vec<(String, String)>) -> Self {
885 self.headers = headers;
886 self
887 }
888
889 pub fn url(&self) -> &str {
891 &self.url
892 }
893
894 pub fn build_body(&self, spans: &[SpanData]) -> String {
895 let span_jsons: Vec<String> = spans.iter().map(|s| {
896 let parent = s.parent_span_id
897 .as_ref()
898 .map(|p| format!(",\"parentSpanId\":\"{}\"", hex8(p)))
899 .unwrap_or_default();
900 let status_msg = if s.status_code == 2 { "Error" } else { "Unset" };
901
902 let mut attrs: Vec<String> = Vec::new();
903 if !s.http_method.is_empty() {
904 attrs.push(format!("{{\"key\":\"http.method\",\"value\":{{\"stringValue\":\"{}\"}} }}", json_escape(&s.http_method)));
905 attrs.push(format!("{{\"key\":\"http.target\",\"value\":{{\"stringValue\":\"{}\"}} }}", json_escape(&s.http_target)));
906 attrs.push(format!("{{\"key\":\"http.status_code\",\"value\":{{\"intValue\":\"{}\"}} }}", s.http_status));
907 }
908 attrs.extend(s.attributes.iter().map(|(k, v)| attr_json(k, v)));
909
910 format!(
911 "{{\"traceId\":\"{trace}\",\"spanId\":\"{span}\"{parent},\
912 \"name\":\"{name}\",\"kind\":{kind},\
913 \"startTimeUnixNano\":\"{start}\",\"endTimeUnixNano\":\"{end}\",\
914 \"attributes\":[{attrs}],\
915 \"status\":{{\"code\":{scode},\"message\":\"{smsg}\"}} }}",
916 trace = hex16(&s.trace_id),
917 span = hex8(&s.span_id),
918 name = json_escape(&s.name),
919 kind = s.kind as i32,
920 start = s.start_ns,
921 end = s.end_ns,
922 attrs = attrs.join(","),
923 scode = s.status_code,
924 smsg = status_msg,
925 )
926 }).collect();
927
928 format!(
929 "{{\"resourceSpans\":[{{\"resource\":{{\"attributes\":[\
930 {{\"key\":\"service.name\",\"value\":{{\"stringValue\":\"{svc}\"}} }},\
931 {{\"key\":\"service.version\",\"value\":{{\"stringValue\":\"{ver}\"}} }}\
932 ]}},\"scopeSpans\":[{{\"scope\":{{\"name\":\"rws\"}},\"spans\":[{spans}]}}]}}]}}",
933 svc = json_escape(&self.service_name),
934 ver = json_escape(&self.service_version),
935 spans = span_jsons.join(","),
936 )
937 }
938
939 fn post(&self, body: &str) {
940 let client = crate::http_client::Client::new().timeout_ms(10_000);
941 let mut request = client.post(&self.url).body_json(body);
942 for (name, value) in &self.headers {
943 request = request.header(name, value);
944 }
945 match request.send() {
946 Ok(response) if response.is_success() => {}
947 Ok(response) => eprintln!("OTLP trace export to {}: HTTP {}", self.url, response.status()),
948 Err(e) => eprintln!("OTLP trace export to {}: {}", self.url, e),
949 }
950 }
951}
952
953impl Exporter for OtlpHttpExporter {
954 fn export(&self, spans: &[SpanData]) {
955 if spans.is_empty() { return; }
956 let body = self.build_body(spans);
957 self.post(&body);
958 }
959}
960
961struct GlobalTracer {
964 exporter: std::sync::Arc<dyn Exporter>,
965 full_batches: Mutex<Option<std::sync::mpsc::SyncSender<Vec<SpanData>>>>,
968 export_thread: Mutex<Option<std::thread::JoinHandle<()>>>,
969 batch: Mutex<Vec<SpanData>>,
970 batch_size: usize,
971 sample_rate: f64,
972 shutdown_flag: AtomicBool,
973}
974
975impl GlobalTracer {
976 fn should_sample(&self) -> bool {
977 sampled(random_u64(), self.sample_rate)
978 }
979
980 fn record(&self, span: SpanData) {
981 let mut guard = self.batch.lock().unwrap();
982 guard.push(span);
983 if guard.len() >= self.batch_size {
984 let spans = std::mem::take(&mut *guard);
985 drop(guard);
986 let sender = self.full_batches.lock().unwrap_or_else(|e| e.into_inner()).clone();
987 match sender {
988 Some(sender) => {
990 if sender.try_send(spans).is_err() {
991 eprintln!("OTLP trace export is behind: a batch of spans was dropped");
992 }
993 }
994 None => self.exporter.export(&spans),
995 }
996 }
997 }
998
999 fn flush(&self) {
1000 let spans = std::mem::take(&mut *self.batch.lock().unwrap());
1001 if !spans.is_empty() {
1002 self.exporter.export(&spans);
1003 }
1004 }
1005}
1006
1007static TRACER: OnceLock<GlobalTracer> = OnceLock::new();
1008
1009const SCHEDULE_DELAY: std::time::Duration = std::time::Duration::from_secs(5);
1012
1013fn tracer() -> Option<&'static GlobalTracer> {
1014 TRACER.get()
1015}
1016
1017#[derive(Clone, Debug)]
1021pub enum ExporterConfig {
1022 Stdout,
1024 Otlp { endpoint: String },
1029 Discard,
1031}
1032
1033#[derive(Clone, Debug)]
1035pub struct TracingConfig {
1036 pub service_name: String,
1038 pub service_version: String,
1040 pub exporter: ExporterConfig,
1042 pub sample_rate: f64,
1044 pub batch_size: usize,
1046}
1047
1048impl Default for TracingConfig {
1049 fn default() -> Self {
1050 TracingConfig {
1051 service_name: "rws".to_string(),
1052 service_version: env!("CARGO_PKG_VERSION").to_string(),
1053 exporter: ExporterConfig::Stdout,
1054 sample_rate: 1.0,
1055 batch_size: 128,
1056 }
1057 }
1058}
1059
1060pub fn setup(config: TracingConfig) {
1065 let exporter: Box<dyn Exporter> = match &config.exporter {
1066 ExporterConfig::Stdout => Box::new(StdoutExporter),
1067 ExporterConfig::Otlp { endpoint } => Box::new(OtlpHttpExporter::new(
1068 endpoint,
1069 &config.service_name,
1070 &config.service_version,
1071 )),
1072 ExporterConfig::Discard => Box::new(DiscardExporter),
1073 };
1074 setup_with_exporter(config, exporter);
1075}
1076
1077pub fn setup_with_exporter(config: TracingConfig, exporter: Box<dyn Exporter>) {
1084 let exporter: std::sync::Arc<dyn Exporter> = exporter.into();
1085 let (sender, batches) = std::sync::mpsc::sync_channel::<Vec<SpanData>>(8);
1086 let background = std::sync::Arc::clone(&exporter);
1087 let spawned = std::thread::Builder::new().name("rws-otel-export".to_string()).spawn(move || loop {
1088 match batches.recv_timeout(SCHEDULE_DELAY) {
1089 Ok(spans) => background.export(&spans),
1090 Err(std::sync::mpsc::RecvTimeoutError::Timeout) => {
1092 if let Some(tracer) = tracer() {
1093 let spans = std::mem::take(&mut *tracer.batch.lock().unwrap_or_else(|e| e.into_inner()));
1094 if !spans.is_empty() {
1095 background.export(&spans);
1096 }
1097 }
1098 }
1099 Err(std::sync::mpsc::RecvTimeoutError::Disconnected) => return,
1100 }
1101 });
1102 let _ = TRACER.set(GlobalTracer {
1103 exporter,
1104 full_batches: Mutex::new(spawned.as_ref().ok().map(|_| sender)),
1105 export_thread: Mutex::new(spawned.ok()),
1106 batch: Mutex::new(Vec::new()),
1107 batch_size: config.batch_size.max(1),
1108 sample_rate: config.sample_rate.clamp(0.0, 1.0),
1109 shutdown_flag: AtomicBool::new(false),
1110 });
1111}
1112
1113pub fn setup_from_env() {
1125 let var = |name: &str| std::env::var(name).ok().map(|v| v.trim().to_string()).filter(|v| !v.is_empty());
1126 let service_name = var("OTEL_SERVICE_NAME").unwrap_or_else(|| "rws".to_string());
1127 let sample_rate: f64 = var("OTEL_TRACES_SAMPLER_ARG").and_then(|v| v.parse().ok()).unwrap_or(1.0);
1128 let config = TracingConfig {
1129 service_name,
1130 service_version: env!("CARGO_PKG_VERSION").to_string(),
1131 exporter: ExporterConfig::Stdout,
1132 sample_rate,
1133 batch_size: 128,
1134 };
1135 let endpoint = var("OTEL_EXPORTER_OTLP_TRACES_ENDPOINT")
1136 .map(|url| if url.trim_end_matches('/').ends_with("/v1/traces") { url } else { format!("{}/v1/traces", url.trim_end_matches('/')) })
1137 .or_else(|| var("OTEL_EXPORTER_OTLP_ENDPOINT"));
1138 match endpoint {
1139 Some(endpoint) => {
1140 let headers = var("OTEL_EXPORTER_OTLP_TRACES_HEADERS").or_else(|| var("OTEL_EXPORTER_OTLP_HEADERS")).map(|h| parse_headers(&h)).unwrap_or_default();
1141 let exporter = OtlpHttpExporter::new(&endpoint, &config.service_name, &config.service_version).with_headers(headers);
1142 setup_with_exporter(config, Box::new(exporter));
1143 }
1144 None => setup(config),
1145 }
1146}
1147
1148pub fn traces_configured() -> bool {
1152 let var = |name: &str| std::env::var(name).ok().filter(|v| !v.trim().is_empty());
1153 !var("OTEL_TRACES_EXPORTER").is_some_and(|v| v.trim().eq_ignore_ascii_case("none"))
1154 && (var("OTEL_EXPORTER_OTLP_TRACES_ENDPOINT").is_some() || var("OTEL_EXPORTER_OTLP_ENDPOINT").is_some())
1155}
1156
1157pub fn parse_headers(value: &str) -> Vec<(String, String)> {
1160 value
1161 .split(',')
1162 .filter_map(|pair| {
1163 let (name, value) = pair.split_once('=')?;
1164 let name = name.trim();
1165 if name.is_empty() {
1166 return None;
1167 }
1168 let value = crate::url::URL::percent_decode(value.trim());
1169 Some((name.to_string(), value))
1170 })
1171 .collect()
1172}
1173
1174pub fn shutdown() {
1177 metrics::shutdown();
1178 if let Some(t) = tracer() {
1179 t.shutdown_flag.store(true, Ordering::Relaxed);
1180 t.full_batches.lock().unwrap_or_else(|e| e.into_inner()).take();
1182 if let Some(thread) = t.export_thread.lock().unwrap_or_else(|e| e.into_inner()).take() {
1183 let _ = thread.join();
1184 }
1185 t.flush();
1186 t.exporter.shutdown();
1187 }
1188}
1189
1190pub fn flush() {
1192 if let Some(t) = tracer() {
1193 t.flush();
1194 }
1195}
1196
1197pub struct OtelLayer;
1211
1212impl Middleware for OtelLayer {
1213 fn handle(
1214 &self,
1215 request: &Request,
1216 connection: &ConnectionInfo,
1217 next: &dyn Application,
1218 ) -> Result<Response, String> {
1219 if tracer().is_none() {
1220 return next.execute(request, connection);
1221 }
1222
1223 let incoming = request.headers.iter()
1225 .find(|h| h.name.eq_ignore_ascii_case("traceparent"))
1226 .and_then(|h| TraceContext::parse(&h.value));
1227
1228 let baggage = request.header("baggage").map(|h| Baggage::parse(&h.value));
1229 let path = strip_query(&request.request_uri).to_string();
1230 let root = Span::start_root(&format!("{} {}", request.method, path), SpanKind::Server, incoming, baggage);
1231 root.set_http(&request.method, &request.request_uri);
1232
1233 let result = next.execute(request, connection);
1234
1235 let status = match &result {
1236 Ok(r) => r.status_code,
1237 Err(_) => 500,
1238 };
1239 root.set_http_status(status);
1240
1241 result
1242 }
1245 fn runs_inline(&self, _request: &Request, _connection: &ConnectionInfo) -> bool {
1248 true
1249 }
1250
1251 fn accepts_streamed_body(&self, _request: &Request, _connection: &ConnectionInfo) -> bool {
1253 true
1254 }
1255}
1256
1257struct DiscardExporter;
1260impl Exporter for DiscardExporter {
1261 fn export(&self, _: &[SpanData]) {}
1262}
1263
1264pub struct CapturingExporter {
1286 pub spans: Mutex<Vec<SpanData>>,
1287}
1288
1289impl CapturingExporter {
1290 pub fn new() -> Self {
1291 CapturingExporter { spans: Mutex::new(Vec::new()) }
1292 }
1293
1294 pub fn take(&self) -> Vec<SpanData> {
1295 std::mem::take(&mut *self.spans.lock().unwrap())
1296 }
1297}
1298
1299impl Default for CapturingExporter {
1300 fn default() -> Self { Self::new() }
1301}
1302
1303impl Exporter for CapturingExporter {
1304 fn export(&self, spans: &[SpanData]) {
1305 self.spans.lock().unwrap().extend_from_slice(spans);
1306 }
1307}
1308
1309impl Exporter for std::sync::Arc<CapturingExporter> {
1314 fn export(&self, spans: &[SpanData]) {
1315 self.spans.lock().unwrap().extend_from_slice(spans);
1316 }
1317}