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