1#[cfg(test)]
55mod tests;
56
57pub mod baggage;
58pub mod metrics;
59
60pub use baggage::Baggage;
61
62use std::cell::{Cell, RefCell};
63use std::sync::atomic::{AtomicBool, Ordering};
64use std::sync::{Mutex, OnceLock};
65use std::time::{SystemTime, UNIX_EPOCH};
66
67use crate::application::Application;
68use crate::mcp::json_escape;
69use crate::middleware::Middleware;
70use crate::request::Request;
71use crate::response::Response;
72use crate::server::ConnectionInfo;
73
74fn random_u64() -> u64 {
81 thread_local! {
82 static STATE: Cell<u64> = Cell::new({
83 use std::hash::{BuildHasher, Hasher};
84 let mut hasher = std::collections::hash_map::RandomState::new().build_hasher();
85 hasher.write_u128(SystemTime::now().duration_since(UNIX_EPOCH).unwrap_or_default().as_nanos());
86 hasher.finish()
87 });
88 }
89 STATE.with(|state| {
90 let x = state.get().wrapping_add(0x9E37_79B9_7F4A_7C15);
91 state.set(x);
92 let mut z = x;
93 z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
94 z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
95 z ^ (z >> 31)
96 })
97}
98
99fn sampled(roll: u64, rate: f64) -> bool {
102 if rate >= 1.0 {
103 return true;
104 }
105 if rate <= 0.0 {
106 return false;
107 }
108 roll % 10_000 < (rate * 10_000.0) as u64
109}
110
111pub fn new_trace_id() -> [u8; 16] {
113 let mut id = [0u8; 16];
114 id[..8].copy_from_slice(&random_u64().to_be_bytes());
115 id[8..].copy_from_slice(&(random_u64() | 1).to_be_bytes());
116 id
117}
118
119pub fn new_span_id() -> [u8; 8] {
121 (random_u64() | 1).to_be_bytes()
122}
123
124fn hex16(b: &[u8; 16]) -> String {
125 b.iter().map(|x| format!("{:02x}", x)).collect()
126}
127
128fn hex8(b: &[u8; 8]) -> String {
129 b.iter().map(|x| format!("{:02x}", x)).collect()
130}
131
132#[derive(Copy, Clone, Debug, PartialEq, Eq)]
138pub struct TraceContext {
139 pub trace_id: [u8; 16],
140 pub parent_span_id: [u8; 8],
141 pub sampled: bool,
142}
143
144impl TraceContext {
145 pub fn parse(header: &str) -> Option<Self> {
147 let parts: Vec<&str> = header.trim().splitn(4, '-').collect();
148 if parts.len() != 4 || parts[0] != "00" {
149 return None;
150 }
151 let trace_id = parse_hex16(parts[1])?;
152 let parent_span_id = parse_hex8(parts[2])?;
153 let flags = u8::from_str_radix(parts[3], 16).ok()?;
154 Some(TraceContext { trace_id, parent_span_id, sampled: flags & 0x01 != 0 })
155 }
156
157 pub fn as_header(&self, span_id: &[u8; 8]) -> String {
159 format!("00-{}-{}-{:02x}", hex16(&self.trace_id), hex8(span_id), self.sampled as u8)
160 }
161}
162
163fn parse_hex16(s: &str) -> Option<[u8; 16]> {
164 if s.len() != 32 { return None; }
165 let mut out = [0u8; 16];
166 for (i, chunk) in s.as_bytes().chunks(2).enumerate() {
167 out[i] = u8::from_str_radix(std::str::from_utf8(chunk).ok()?, 16).ok()?;
168 }
169 Some(out)
170}
171
172fn parse_hex8(s: &str) -> Option<[u8; 8]> {
173 if s.len() != 16 { return None; }
174 let mut out = [0u8; 8];
175 for (i, chunk) in s.as_bytes().chunks(2).enumerate() {
176 out[i] = u8::from_str_radix(std::str::from_utf8(chunk).ok()?, 16).ok()?;
177 }
178 Some(out)
179}
180
181#[derive(Copy, Clone, Debug, PartialEq, Eq, Default)]
188pub enum SpanKind {
189 #[default]
192 Internal = 1,
193 Server = 2,
195 Client = 3,
197}
198
199#[derive(Clone, Debug, PartialEq)]
201pub enum AttributeValue {
202 String(String),
203 Int(i64),
204 Float(f64),
205 Bool(bool),
206}
207
208impl From<&str> for AttributeValue {
209 fn from(s: &str) -> Self { AttributeValue::String(s.to_string()) }
210}
211impl From<String> for AttributeValue {
212 fn from(s: String) -> Self { AttributeValue::String(s) }
213}
214impl From<i64> for AttributeValue {
215 fn from(v: i64) -> Self { AttributeValue::Int(v) }
216}
217impl From<i32> for AttributeValue {
218 fn from(v: i32) -> Self { AttributeValue::Int(v as i64) }
219}
220impl From<u32> for AttributeValue {
221 fn from(v: u32) -> Self { AttributeValue::Int(v as i64) }
222}
223impl From<f64> for AttributeValue {
224 fn from(v: f64) -> Self { AttributeValue::Float(v) }
225}
226impl From<bool> for AttributeValue {
227 fn from(v: bool) -> Self { AttributeValue::Bool(v) }
228}
229
230#[derive(Debug, Clone, Default)]
234pub struct SpanData {
235 pub trace_id: [u8; 16],
236 pub span_id: [u8; 8],
237 pub parent_span_id: Option<[u8; 8]>,
238 pub name: String,
240 pub start_ns: u64,
241 pub end_ns: u64,
242 pub http_method: String,
245 pub http_target: String,
246 pub http_status: i16,
247 pub status_code: u8,
249 pub kind: SpanKind,
250 pub attributes: Vec<(String, AttributeValue)>,
254}
255
256impl SpanData {
257 fn duration_ms(&self) -> f64 {
258 (self.end_ns.saturating_sub(self.start_ns)) as f64 / 1_000_000.0
259 }
260}
261
262fn now_ns() -> u64 {
263 SystemTime::now()
264 .duration_since(UNIX_EPOCH)
265 .unwrap_or_default()
266 .as_nanos() as u64
267}
268
269fn strip_query(uri: &str) -> &str {
270 match uri.find('?') {
271 Some(i) => &uri[..i],
272 None => uri,
273 }
274}
275
276#[derive(Clone)]
281struct ActiveSpanCtx {
282 trace_id: [u8; 16],
283 span_id: [u8; 8],
284 sampled: bool,
285 baggage: Option<std::sync::Arc<Baggage>>,
287}
288
289thread_local! {
290 static ACTIVE_STACK: RefCell<Vec<ActiveSpanCtx>> = const { RefCell::new(Vec::new()) };
295}
296
297pub fn current_traceparent() -> Option<String> {
304 ACTIVE_STACK.with(|stack| {
305 stack.borrow().last().map(|s| {
306 format!(
307 "00-{}-{}-{:02x}",
308 hex16(&s.trace_id),
309 hex8(&s.span_id),
310 s.sampled as u8,
311 )
312 })
313 })
314}
315
316pub struct Span {
343 trace_id: [u8; 16],
344 span_id: [u8; 8],
345 parent_span_id: Option<[u8; 8]>,
346 sampled: bool,
347 kind: SpanKind,
348 name: String,
349 start_ns: u64,
350 attributes: RefCell<Vec<(String, AttributeValue)>>,
351 status_code: Cell<u8>,
352 http_method: RefCell<Option<String>>,
353 http_target: RefCell<Option<String>>,
354 http_status: Cell<Option<i16>>,
355 _not_send: std::marker::PhantomData<std::rc::Rc<()>>,
358}
359
360fn new_span(name: &str, kind: SpanKind, incoming: Option<TraceContext>) -> Span {
361 new_span_with_baggage(name, kind, incoming, None)
362}
363
364fn new_span_with_baggage(name: &str, kind: SpanKind, incoming: Option<TraceContext>, incoming_baggage: Option<Baggage>) -> Span {
365 let mut baggage = incoming_baggage.filter(|b| !b.is_empty()).map(std::sync::Arc::new);
366 let (trace_id, parent_span_id, sampled) = ACTIVE_STACK.with(|stack| {
367 let stack = stack.borrow();
368 if let Some(top) = stack.last() {
369 if baggage.is_none() {
374 baggage = top.baggage.clone();
375 }
376 (top.trace_id, Some(top.span_id), top.sampled)
377 } else {
378 let sampled = tracer().map(|t| t.should_sample()).unwrap_or(false);
381 match incoming {
382 Some(ctx) => (ctx.trace_id, Some(ctx.parent_span_id), sampled),
383 None => (new_trace_id(), None, sampled),
384 }
385 }
386 });
387 let span_id = new_span_id();
388 ACTIVE_STACK.with(|stack| stack.borrow_mut().push(ActiveSpanCtx { trace_id, span_id, sampled, baggage }));
389
390 Span {
391 trace_id,
392 span_id,
393 parent_span_id,
394 sampled,
395 kind,
396 name: name.to_string(),
397 start_ns: now_ns(),
398 attributes: RefCell::new(Vec::new()),
399 status_code: Cell::new(0),
400 http_method: RefCell::new(None),
401 http_target: RefCell::new(None),
402 http_status: Cell::new(None),
403 _not_send: std::marker::PhantomData,
404 }
405}
406
407impl Span {
408 pub fn new(name: &str, kind: SpanKind) -> Span {
411 new_span(name, kind, None)
412 }
413
414 pub(crate) fn start_root(name: &str, kind: SpanKind, incoming: Option<TraceContext>, baggage: Option<Baggage>) -> Span {
417 new_span_with_baggage(name, kind, incoming, baggage)
418 }
419
420 pub fn set_attribute(&self, key: &str, value: impl Into<AttributeValue>) {
423 self.attributes.borrow_mut().push((key.to_string(), value.into()));
424 }
425
426 pub fn set_error(&self) {
428 self.status_code.set(2);
429 }
430
431 pub fn record_error(&self, message: &str) {
433 self.set_error();
434 self.set_attribute("error.message", message);
435 }
436
437 pub fn trace_id(&self) -> [u8; 16] {
438 self.trace_id
439 }
440
441 pub fn span_id(&self) -> [u8; 8] {
442 self.span_id
443 }
444
445 pub fn parent_span_id(&self) -> Option<[u8; 8]> {
446 self.parent_span_id
447 }
448
449 pub fn end(self) {}
454
455 pub(crate) fn set_http(&self, method: &str, target: &str) {
456 *self.http_method.borrow_mut() = Some(method.to_string());
457 *self.http_target.borrow_mut() = Some(target.to_string());
458 }
459
460 pub(crate) fn set_http_status(&self, status: i16) {
462 self.http_status.set(Some(status));
463 if status >= 500 {
464 self.set_error();
465 }
466 }
467
468 fn finish(&mut self, end_ns: u64) -> SpanData {
469 SpanData {
470 trace_id: self.trace_id,
471 span_id: self.span_id,
472 parent_span_id: self.parent_span_id,
473 name: std::mem::take(&mut self.name),
474 start_ns: self.start_ns,
475 end_ns,
476 http_method: self.http_method.get_mut().take().unwrap_or_default(),
477 http_target: self.http_target.get_mut().take().unwrap_or_default(),
478 http_status: self.http_status.get().unwrap_or(0),
479 status_code: self.status_code.get(),
480 kind: self.kind,
481 attributes: std::mem::take(self.attributes.get_mut()),
482 }
483 }
484}
485
486impl Drop for Span {
487 fn drop(&mut self) {
488 ACTIVE_STACK.with(|stack| {
489 let mut stack = stack.borrow_mut();
490 if let Some(pos) = stack.iter().rposition(|c| c.span_id == self.span_id) {
493 stack.truncate(pos);
494 }
495 });
496
497 if !self.sampled {
498 return;
499 }
500 let Some(t) = tracer() else { return };
501 let end_ns = now_ns();
502 t.record(self.finish(end_ns));
503 }
504}
505
506pub fn span(name: &str) -> Span {
517 Span::new(name, SpanKind::Internal)
518}
519
520pub fn client_span(name: &str) -> Span {
523 Span::new(name, SpanKind::Client)
524}
525
526pub fn is_tracing() -> bool {
528 tracer().is_some() && ACTIVE_STACK.with(|stack| !stack.borrow().is_empty())
529}
530
531pub(crate) fn outbound_span(name: &str) -> Option<Span> {
534 is_tracing().then(|| client_span(name))
535}
536
537pub(crate) fn http_client_attributes(method: &str, url: &str) -> Vec<(&'static str, AttributeValue)> {
541 let (scheme, rest) = url.split_once("://").unwrap_or(("http", url));
542 let rest = rest.split(['?', '#']).next().unwrap_or("");
543 let rest = match rest.find('@') {
544 Some(at) if rest.find('/').is_none_or(|slash| at < slash) => &rest[at + 1..],
545 _ => rest,
546 };
547 let authority = rest.split('/').next().unwrap_or("");
548 let default_port = if scheme.eq_ignore_ascii_case("https") { 443 } else { 80 };
549 let (host, port) = match authority.rsplit_once(':') {
550 Some((host, port)) if !host.ends_with(']') || authority.starts_with('[') => match port.parse::<i64>() {
551 Ok(port) => (host, port),
552 Err(_) => (authority, default_port),
553 },
554 _ => (authority, default_port),
555 };
556 vec![
557 ("http.request.method", AttributeValue::from(method)),
558 ("url.full", AttributeValue::from(format!("{scheme}://{rest}"))),
559 ("server.address", AttributeValue::from(host.trim_start_matches('[').trim_end_matches(']'))),
560 ("server.port", AttributeValue::Int(port)),
561 ]
562}
563
564pub(crate) fn http_client_outcome(status: Result<u16, &str>) -> (Option<i64>, Option<String>) {
567 match status {
568 Ok(status) => (Some(status as i64), (status >= 400).then(|| status.to_string())),
569 Err(message) => (None, Some(message.to_string())),
570 }
571}
572
573impl Span {
574 pub(crate) fn finish_http_client(&self, status: Result<u16, &str>) {
576 let (status, error) = http_client_outcome(status);
577 if let Some(status) = status {
578 self.set_attribute("http.response.status_code", status);
579 }
580 if let Some(error) = error {
581 self.set_attribute("error.type", error.as_str());
582 self.set_error();
583 }
584 }
585}
586
587pub fn current_baggage() -> Baggage {
591 ACTIVE_STACK.with(|stack| stack.borrow().last().and_then(|top| top.baggage.as_deref().cloned()).unwrap_or_default())
592}
593
594pub fn set_baggage(key: &str, value: &str) -> bool {
598 ACTIVE_STACK.with(|stack| {
599 let mut stack = stack.borrow_mut();
600 let Some(top) = stack.last_mut() else { return false };
601 let mut baggage = top.baggage.as_deref().cloned().unwrap_or_default();
602 if !baggage.set(key, value) {
603 return false;
604 }
605 top.baggage = Some(std::sync::Arc::new(baggage));
606 true
607 })
608}
609
610pub fn propagation_headers() -> Option<(String, Option<String>)> {
613 ACTIVE_STACK.with(|stack| {
614 let stack = stack.borrow();
615 let top = stack.last()?;
616 let traceparent = format!("00-{}-{}-{:02x}", hex16(&top.trace_id), hex8(&top.span_id), top.sampled as u8);
617 let baggage = top.baggage.as_deref().filter(|b| !b.is_empty()).map(Baggage::to_header);
618 Some((traceparent, baggage))
619 })
620}
621
622pub(crate) fn inject(headers: &mut Vec<(String, String)>) {
625 let Some((traceparent, baggage)) = propagation_headers() else { return };
626 if !headers.iter().any(|(name, _)| name.eq_ignore_ascii_case("traceparent")) {
627 headers.push(("traceparent".to_string(), traceparent));
628 }
629 if let Some(baggage) = baggage {
630 if !headers.iter().any(|(name, _)| name.eq_ignore_ascii_case("baggage")) {
631 headers.push(("baggage".to_string(), baggage));
632 }
633 }
634}
635
636#[derive(Clone, Default)]
641pub struct Context {
642 active: Option<ActiveSpanCtx>,
643}
644
645impl Context {
646 pub fn current() -> Context {
648 Context { active: ACTIVE_STACK.with(|stack| stack.borrow().last().cloned()) }
649 }
650
651 pub fn is_empty(&self) -> bool {
653 self.active.is_none()
654 }
655
656 pub fn attach(&self) -> ContextGuard {
658 let span_id = self.active.as_ref().map(|active| {
659 ACTIVE_STACK.with(|stack| stack.borrow_mut().push(active.clone()));
660 active.span_id
661 });
662 ContextGuard { span_id, _not_send: std::marker::PhantomData }
663 }
664}
665
666pub struct ContextGuard {
669 span_id: Option<[u8; 8]>,
670 _not_send: std::marker::PhantomData<std::rc::Rc<()>>,
671}
672
673impl Drop for ContextGuard {
674 fn drop(&mut self) {
675 let Some(span_id) = self.span_id else { return };
676 ACTIVE_STACK.with(|stack| {
677 let mut stack = stack.borrow_mut();
678 if let Some(pos) = stack.iter().rposition(|c| c.span_id == span_id) {
679 stack.truncate(pos);
680 }
681 });
682 }
683}
684
685pub struct DetachedSpan {
693 data: SpanData,
694 sampled: bool,
695 baggage: Option<std::sync::Arc<Baggage>>,
696}
697
698pub fn detached_span(name: &str, kind: SpanKind) -> Option<DetachedSpan> {
701 tracer()?;
702 let parent = ACTIVE_STACK.with(|stack| stack.borrow().last().cloned())?;
703 Some(DetachedSpan {
704 data: SpanData {
705 trace_id: parent.trace_id,
706 span_id: new_span_id(),
707 parent_span_id: Some(parent.span_id),
708 name: name.to_string(),
709 start_ns: now_ns(),
710 kind,
711 ..SpanData::default()
712 },
713 sampled: parent.sampled,
714 baggage: parent.baggage,
715 })
716}
717
718impl DetachedSpan {
719 pub fn set_attribute(&mut self, key: &str, value: impl Into<AttributeValue>) {
720 self.data.attributes.push((key.to_string(), value.into()));
721 }
722
723 pub fn record_error(&mut self, message: &str) {
725 self.data.status_code = 2;
726 self.set_attribute("error.message", message);
727 }
728
729 pub fn enter<T>(&self, work: impl FnOnce() -> T) -> T {
732 let context = Context {
733 active: Some(ActiveSpanCtx {
734 trace_id: self.data.trace_id,
735 span_id: self.data.span_id,
736 sampled: self.sampled,
737 baggage: self.baggage.clone(),
738 }),
739 };
740 let _guard = context.attach();
741 work()
742 }
743
744 pub(crate) fn finish_http_client(&mut self, status: Result<u16, &str>) {
746 let (status, error) = http_client_outcome(status);
747 if let Some(status) = status {
748 self.set_attribute("http.response.status_code", status);
749 }
750 if let Some(error) = error {
751 self.set_attribute("error.type", error.as_str());
752 self.data.status_code = 2;
753 }
754 }
755
756 pub fn end(self) {}
757}
758
759impl Drop for DetachedSpan {
760 fn drop(&mut self) {
761 if !self.sampled {
762 return;
763 }
764 if let Some(tracer) = tracer() {
765 let mut data = std::mem::take(&mut self.data);
766 data.end_ns = now_ns();
767 tracer.record(data);
768 }
769 }
770}
771
772pub trait Exporter: Send + Sync {
776 fn export(&self, spans: &[SpanData]);
777 fn shutdown(&self) {}
778}
779
780fn attr_value_json(v: &AttributeValue) -> String {
783 match v {
784 AttributeValue::String(s) => format!("{{\"stringValue\":\"{}\"}}", json_escape(s)),
785 AttributeValue::Int(i) => format!("{{\"intValue\":\"{i}\"}}"),
786 AttributeValue::Float(f) => format!("{{\"doubleValue\":{f}}}"),
787 AttributeValue::Bool(b) => format!("{{\"boolValue\":{b}}}"),
788 }
789}
790
791fn attr_json(key: &str, value: &AttributeValue) -> String {
793 format!("{{\"key\":\"{}\",\"value\":{}}}", json_escape(key), attr_value_json(value))
794}
795
796pub struct StdoutExporter;
799
800impl StdoutExporter {
801 fn format_span(span: &SpanData) -> String {
802 let http_attrs = if span.http_method.is_empty() {
803 String::new()
804 } else {
805 format!(
806 ",\"httpMethod\":\"{}\",\"httpTarget\":\"{}\",\"httpStatus\":{}",
807 span.http_method, span.http_target, span.http_status,
808 )
809 };
810 let extra_attrs: String = span.attributes.iter()
811 .map(|(k, v)| format!(",\"{k}\":{}", attr_value_json(v)))
812 .collect();
813 format!(
814 "{{\"traceId\":\"{}\",\"spanId\":\"{}\",\"parentSpanId\":{},\
815 \"name\":\"{}\",\"kind\":{},\"startNs\":{},\"durationMs\":{:.3}{http_attrs}{extra_attrs}}}",
816 hex16(&span.trace_id),
817 hex8(&span.span_id),
818 span.parent_span_id
819 .as_ref()
820 .map(|p| format!("\"{}\"", hex8(p)))
821 .unwrap_or_else(|| "null".to_string()),
822 span.name,
823 span.kind as i32,
824 span.start_ns,
825 span.duration_ms(),
826 )
827 }
828}
829
830impl Exporter for StdoutExporter {
831 fn export(&self, spans: &[SpanData]) {
832 for span in spans {
833 println!("{}", Self::format_span(span));
834 }
835 }
836}
837
838pub struct OtlpHttpExporter {
843 url: String,
844 headers: Vec<(String, String)>,
845 service_name: String,
846 service_version: String,
847}
848
849impl OtlpHttpExporter {
850 pub fn new(endpoint: &str, service_name: &str, service_version: &str) -> Self {
854 let endpoint = endpoint.trim_end_matches('/');
855 let url = if endpoint.ends_with("/v1/traces") {
856 endpoint.to_string()
857 } else if endpoint.contains("://") {
858 format!("{endpoint}/v1/traces")
859 } else {
860 format!("http://{endpoint}/v1/traces")
861 };
862 OtlpHttpExporter {
863 url,
864 headers: Vec::new(),
865 service_name: service_name.to_string(),
866 service_version: service_version.to_string(),
867 }
868 }
869
870 pub fn with_headers(mut self, headers: Vec<(String, String)>) -> Self {
872 self.headers = headers;
873 self
874 }
875
876 pub fn url(&self) -> &str {
878 &self.url
879 }
880
881 pub fn build_body(&self, spans: &[SpanData]) -> String {
882 let span_jsons: Vec<String> = spans.iter().map(|s| {
883 let parent = s.parent_span_id
884 .as_ref()
885 .map(|p| format!(",\"parentSpanId\":\"{}\"", hex8(p)))
886 .unwrap_or_default();
887 let status_msg = if s.status_code == 2 { "Error" } else { "Unset" };
888
889 let mut attrs: Vec<String> = Vec::new();
890 if !s.http_method.is_empty() {
891 attrs.push(format!("{{\"key\":\"http.method\",\"value\":{{\"stringValue\":\"{}\"}} }}", json_escape(&s.http_method)));
892 attrs.push(format!("{{\"key\":\"http.target\",\"value\":{{\"stringValue\":\"{}\"}} }}", json_escape(&s.http_target)));
893 attrs.push(format!("{{\"key\":\"http.status_code\",\"value\":{{\"intValue\":\"{}\"}} }}", s.http_status));
894 }
895 attrs.extend(s.attributes.iter().map(|(k, v)| attr_json(k, v)));
896
897 format!(
898 "{{\"traceId\":\"{trace}\",\"spanId\":\"{span}\"{parent},\
899 \"name\":\"{name}\",\"kind\":{kind},\
900 \"startTimeUnixNano\":\"{start}\",\"endTimeUnixNano\":\"{end}\",\
901 \"attributes\":[{attrs}],\
902 \"status\":{{\"code\":{scode},\"message\":\"{smsg}\"}} }}",
903 trace = hex16(&s.trace_id),
904 span = hex8(&s.span_id),
905 name = json_escape(&s.name),
906 kind = s.kind as i32,
907 start = s.start_ns,
908 end = s.end_ns,
909 attrs = attrs.join(","),
910 scode = s.status_code,
911 smsg = status_msg,
912 )
913 }).collect();
914
915 format!(
916 "{{\"resourceSpans\":[{{\"resource\":{{\"attributes\":[\
917 {{\"key\":\"service.name\",\"value\":{{\"stringValue\":\"{svc}\"}} }},\
918 {{\"key\":\"service.version\",\"value\":{{\"stringValue\":\"{ver}\"}} }}\
919 ]}},\"scopeSpans\":[{{\"scope\":{{\"name\":\"rws\"}},\"spans\":[{spans}]}}]}}]}}",
920 svc = json_escape(&self.service_name),
921 ver = json_escape(&self.service_version),
922 spans = span_jsons.join(","),
923 )
924 }
925
926 fn post(&self, body: &str) {
927 let client = crate::http_client::Client::new().timeout_ms(10_000);
928 let mut request = client.post(&self.url).body_json(body);
929 for (name, value) in &self.headers {
930 request = request.header(name, value);
931 }
932 match request.send() {
933 Ok(response) if response.is_success() => {}
934 Ok(response) => eprintln!("OTLP trace export to {}: HTTP {}", self.url, response.status()),
935 Err(e) => eprintln!("OTLP trace export to {}: {}", self.url, e),
936 }
937 }
938}
939
940impl Exporter for OtlpHttpExporter {
941 fn export(&self, spans: &[SpanData]) {
942 if spans.is_empty() { return; }
943 let body = self.build_body(spans);
944 self.post(&body);
945 }
946}
947
948struct GlobalTracer {
951 exporter: std::sync::Arc<dyn Exporter>,
952 full_batches: Mutex<Option<std::sync::mpsc::SyncSender<Vec<SpanData>>>>,
955 export_thread: Mutex<Option<std::thread::JoinHandle<()>>>,
956 batch: Mutex<Vec<SpanData>>,
957 batch_size: usize,
958 sample_rate: f64,
959 shutdown_flag: AtomicBool,
960}
961
962impl GlobalTracer {
963 fn should_sample(&self) -> bool {
964 sampled(random_u64(), self.sample_rate)
965 }
966
967 fn record(&self, span: SpanData) {
968 let mut guard = self.batch.lock().unwrap();
969 guard.push(span);
970 if guard.len() >= self.batch_size {
971 let spans = std::mem::take(&mut *guard);
972 drop(guard);
973 let sender = self.full_batches.lock().unwrap_or_else(|e| e.into_inner()).clone();
974 match sender {
975 Some(sender) => {
977 if sender.try_send(spans).is_err() {
978 eprintln!("OTLP trace export is behind: a batch of spans was dropped");
979 }
980 }
981 None => self.exporter.export(&spans),
982 }
983 }
984 }
985
986 fn flush(&self) {
987 let spans = std::mem::take(&mut *self.batch.lock().unwrap());
988 if !spans.is_empty() {
989 self.exporter.export(&spans);
990 }
991 }
992}
993
994static TRACER: OnceLock<GlobalTracer> = OnceLock::new();
995
996const SCHEDULE_DELAY: std::time::Duration = std::time::Duration::from_secs(5);
999
1000fn tracer() -> Option<&'static GlobalTracer> {
1001 TRACER.get()
1002}
1003
1004#[derive(Clone, Debug)]
1008pub enum ExporterConfig {
1009 Stdout,
1011 Otlp { endpoint: String },
1016 Discard,
1018}
1019
1020#[derive(Clone, Debug)]
1022pub struct TracingConfig {
1023 pub service_name: String,
1025 pub service_version: String,
1027 pub exporter: ExporterConfig,
1029 pub sample_rate: f64,
1031 pub batch_size: usize,
1033}
1034
1035impl Default for TracingConfig {
1036 fn default() -> Self {
1037 TracingConfig {
1038 service_name: "rws".to_string(),
1039 service_version: env!("CARGO_PKG_VERSION").to_string(),
1040 exporter: ExporterConfig::Stdout,
1041 sample_rate: 1.0,
1042 batch_size: 128,
1043 }
1044 }
1045}
1046
1047pub fn setup(config: TracingConfig) {
1052 let exporter: Box<dyn Exporter> = match &config.exporter {
1053 ExporterConfig::Stdout => Box::new(StdoutExporter),
1054 ExporterConfig::Otlp { endpoint } => Box::new(OtlpHttpExporter::new(
1055 endpoint,
1056 &config.service_name,
1057 &config.service_version,
1058 )),
1059 ExporterConfig::Discard => Box::new(DiscardExporter),
1060 };
1061 setup_with_exporter(config, exporter);
1062}
1063
1064pub fn setup_with_exporter(config: TracingConfig, exporter: Box<dyn Exporter>) {
1071 let exporter: std::sync::Arc<dyn Exporter> = exporter.into();
1072 let (sender, batches) = std::sync::mpsc::sync_channel::<Vec<SpanData>>(8);
1073 let background = std::sync::Arc::clone(&exporter);
1074 let spawned = std::thread::Builder::new().name("rws-otel-export".to_string()).spawn(move || loop {
1075 match batches.recv_timeout(SCHEDULE_DELAY) {
1076 Ok(spans) => background.export(&spans),
1077 Err(std::sync::mpsc::RecvTimeoutError::Timeout) => {
1079 if let Some(tracer) = tracer() {
1080 let spans = std::mem::take(&mut *tracer.batch.lock().unwrap_or_else(|e| e.into_inner()));
1081 if !spans.is_empty() {
1082 background.export(&spans);
1083 }
1084 }
1085 }
1086 Err(std::sync::mpsc::RecvTimeoutError::Disconnected) => return,
1087 }
1088 });
1089 let _ = TRACER.set(GlobalTracer {
1090 exporter,
1091 full_batches: Mutex::new(spawned.as_ref().ok().map(|_| sender)),
1092 export_thread: Mutex::new(spawned.ok()),
1093 batch: Mutex::new(Vec::new()),
1094 batch_size: config.batch_size.max(1),
1095 sample_rate: config.sample_rate.clamp(0.0, 1.0),
1096 shutdown_flag: AtomicBool::new(false),
1097 });
1098}
1099
1100pub fn setup_from_env() {
1112 let var = |name: &str| std::env::var(name).ok().map(|v| v.trim().to_string()).filter(|v| !v.is_empty());
1113 let service_name = var("OTEL_SERVICE_NAME").unwrap_or_else(|| "rws".to_string());
1114 let sample_rate: f64 = var("OTEL_TRACES_SAMPLER_ARG").and_then(|v| v.parse().ok()).unwrap_or(1.0);
1115 let config = TracingConfig {
1116 service_name,
1117 service_version: env!("CARGO_PKG_VERSION").to_string(),
1118 exporter: ExporterConfig::Stdout,
1119 sample_rate,
1120 batch_size: 128,
1121 };
1122 let endpoint = var("OTEL_EXPORTER_OTLP_TRACES_ENDPOINT")
1123 .map(|url| if url.trim_end_matches('/').ends_with("/v1/traces") { url } else { format!("{}/v1/traces", url.trim_end_matches('/')) })
1124 .or_else(|| var("OTEL_EXPORTER_OTLP_ENDPOINT"));
1125 match endpoint {
1126 Some(endpoint) => {
1127 let headers = var("OTEL_EXPORTER_OTLP_TRACES_HEADERS").or_else(|| var("OTEL_EXPORTER_OTLP_HEADERS")).map(|h| parse_headers(&h)).unwrap_or_default();
1128 let exporter = OtlpHttpExporter::new(&endpoint, &config.service_name, &config.service_version).with_headers(headers);
1129 setup_with_exporter(config, Box::new(exporter));
1130 }
1131 None => setup(config),
1132 }
1133}
1134
1135pub fn traces_configured() -> bool {
1139 let var = |name: &str| std::env::var(name).ok().filter(|v| !v.trim().is_empty());
1140 !var("OTEL_TRACES_EXPORTER").is_some_and(|v| v.trim().eq_ignore_ascii_case("none"))
1141 && (var("OTEL_EXPORTER_OTLP_TRACES_ENDPOINT").is_some() || var("OTEL_EXPORTER_OTLP_ENDPOINT").is_some())
1142}
1143
1144pub fn parse_headers(value: &str) -> Vec<(String, String)> {
1147 value
1148 .split(',')
1149 .filter_map(|pair| {
1150 let (name, value) = pair.split_once('=')?;
1151 let name = name.trim();
1152 if name.is_empty() {
1153 return None;
1154 }
1155 let value = crate::url::URL::percent_decode(value.trim());
1156 Some((name.to_string(), value))
1157 })
1158 .collect()
1159}
1160
1161pub fn shutdown() {
1164 metrics::shutdown();
1165 if let Some(t) = tracer() {
1166 t.shutdown_flag.store(true, Ordering::Relaxed);
1167 t.full_batches.lock().unwrap_or_else(|e| e.into_inner()).take();
1169 if let Some(thread) = t.export_thread.lock().unwrap_or_else(|e| e.into_inner()).take() {
1170 let _ = thread.join();
1171 }
1172 t.flush();
1173 t.exporter.shutdown();
1174 }
1175}
1176
1177pub fn flush() {
1179 if let Some(t) = tracer() {
1180 t.flush();
1181 }
1182}
1183
1184pub struct OtelLayer;
1198
1199impl Middleware for OtelLayer {
1200 fn handle(
1201 &self,
1202 request: &Request,
1203 connection: &ConnectionInfo,
1204 next: &dyn Application,
1205 ) -> Result<Response, String> {
1206 if tracer().is_none() {
1207 return next.execute(request, connection);
1208 }
1209
1210 let incoming = request.headers.iter()
1212 .find(|h| h.name.eq_ignore_ascii_case("traceparent"))
1213 .and_then(|h| TraceContext::parse(&h.value));
1214
1215 let baggage = request.header("baggage").map(|h| Baggage::parse(&h.value));
1216 let path = strip_query(&request.request_uri).to_string();
1217 let root = Span::start_root(&format!("{} {}", request.method, path), SpanKind::Server, incoming, baggage);
1218 root.set_http(&request.method, &request.request_uri);
1219
1220 let result = next.execute(request, connection);
1221
1222 let status = match &result {
1223 Ok(r) => r.status_code,
1224 Err(_) => 500,
1225 };
1226 root.set_http_status(status);
1227
1228 result
1229 }
1232 fn runs_inline(&self, _request: &Request, _connection: &ConnectionInfo) -> bool {
1235 true
1236 }
1237
1238 fn accepts_streamed_body(&self, _request: &Request, _connection: &ConnectionInfo) -> bool {
1240 true
1241 }
1242}
1243
1244struct DiscardExporter;
1247impl Exporter for DiscardExporter {
1248 fn export(&self, _: &[SpanData]) {}
1249}
1250
1251pub struct CapturingExporter {
1272 pub spans: Mutex<Vec<SpanData>>,
1273}
1274
1275impl CapturingExporter {
1276 pub fn new() -> Self {
1277 CapturingExporter { spans: Mutex::new(Vec::new()) }
1278 }
1279
1280 pub fn take(&self) -> Vec<SpanData> {
1281 std::mem::take(&mut *self.spans.lock().unwrap())
1282 }
1283}
1284
1285impl Default for CapturingExporter {
1286 fn default() -> Self { Self::new() }
1287}
1288
1289impl Exporter for CapturingExporter {
1290 fn export(&self, spans: &[SpanData]) {
1291 self.spans.lock().unwrap().extend_from_slice(spans);
1292 }
1293}
1294
1295impl Exporter for std::sync::Arc<CapturingExporter> {
1300 fn export(&self, spans: &[SpanData]) {
1301 self.spans.lock().unwrap().extend_from_slice(spans);
1302 }
1303}