1use std::collections::{BTreeMap, HashMap};
30use std::sync::Once;
31
32use figment::{
33 Figment,
34 providers::{Format, Serialized, Toml},
35};
36use serde::{Deserialize, Serialize};
37use tracing::level_filters::LevelFilter;
38use tracing::{Event, Subscriber};
39use tracing_subscriber::EnvFilter;
40use tracing_subscriber::filter::Targets;
41use tracing_subscriber::fmt::time::FormatTime;
42use tracing_subscriber::fmt::time::LocalTime;
43use tracing_subscriber::fmt::time::SystemTime;
44use tracing_subscriber::fmt::time::UtcTime;
45use tracing_subscriber::fmt::{FmtContext, FormatFields};
46use tracing_subscriber::fmt::{FormattedFields, format::Writer};
47use tracing_subscriber::prelude::*;
48use tracing_subscriber::registry::LookupSpan;
49use tracing_subscriber::{filter::Directive, fmt};
50
51use crate::config::{
52 disable_ansi_logging, env_is_truthy, jsonl_logging_enabled, span_events_enabled,
53};
54use async_nats::{HeaderMap, HeaderValue};
55use axum::extract::FromRequestParts;
56use axum::http;
57use axum::http::Request;
58use axum::http::request::Parts;
59use serde_json::Value;
60use std::convert::Infallible;
61use std::time::Instant;
62use tower_http::trace::{DefaultMakeSpan, TraceLayer};
63use tracing::Id;
64use tracing::Span;
65use tracing::field::Field;
66use tracing::span;
67use tracing_subscriber::Layer;
68use tracing_subscriber::Registry;
69use tracing_subscriber::field::Visit;
70use tracing_subscriber::fmt::format::FmtSpan;
71use tracing_subscriber::layer::Context;
72use tracing_subscriber::layer::Filter;
73use tracing_subscriber::registry::SpanData;
74use uuid::Uuid;
75
76use opentelemetry::propagation::{Extractor, Injector, TextMapPropagator};
77use opentelemetry::trace::{Span as OtelSpan, TraceContextExt};
78use opentelemetry::{global, trace::Tracer};
79use opentelemetry_appender_tracing::layer::OpenTelemetryTracingBridge;
80use opentelemetry_otlp::WithExportConfig;
81
82use opentelemetry::trace::TracerProvider as _;
83use opentelemetry::{Key, KeyValue};
84use opentelemetry_sdk::Resource;
85use opentelemetry_sdk::logs::SdkLoggerProvider;
86use opentelemetry_sdk::trace::Sampler;
87use opentelemetry_sdk::trace::SdkTracerProvider;
88use tracing::error;
89use tracing_subscriber::layer::SubscriberExt;
90use std::time::Duration;
93use tracing::{info, instrument};
94use tracing_opentelemetry::OpenTelemetrySpanExt;
95use tracing_subscriber::util::SubscriberInitExt;
96
97use crate::config::environment_names::logging as env_logging;
98
99const DEFAULT_FILTER_LEVEL: &str = "info";
101
102const DEFAULT_OTLP_ENDPOINT: &str = "http://localhost:4317";
104
105const DEFAULT_OTLP_HTTP_ENDPOINT: &str = "http://localhost:4318";
107
108const DEFAULT_OTEL_SERVICE_NAME: &str = "dynamo";
110
111static INIT: Once = Once::new();
113
114#[derive(Serialize, Deserialize, Debug)]
115struct LoggingConfig {
116 log_level: String,
117 log_filters: HashMap<String, String>,
118}
119impl Default for LoggingConfig {
120 fn default() -> Self {
121 LoggingConfig {
122 log_level: DEFAULT_FILTER_LEVEL.to_string(),
123 log_filters: HashMap::from([
124 ("h2".to_string(), "error".to_string()),
125 ("tower".to_string(), "error".to_string()),
126 ("hyper_util".to_string(), "error".to_string()),
127 ("neli".to_string(), "error".to_string()),
128 ("async_nats".to_string(), "error".to_string()),
129 ("rustls".to_string(), "error".to_string()),
130 ("tokenizers".to_string(), "error".to_string()),
131 ("axum".to_string(), "error".to_string()),
132 ("tonic".to_string(), "error".to_string()),
133 ("hf_hub".to_string(), "error".to_string()),
134 ("opentelemetry".to_string(), "error".to_string()),
135 ("opentelemetry-otlp".to_string(), "error".to_string()),
136 ("opentelemetry_sdk".to_string(), "error".to_string()),
137 ]),
138 }
139 }
140}
141
142fn otlp_exporter_enabled() -> bool {
144 env_is_truthy(env_logging::otlp::OTEL_EXPORT_ENABLED)
145}
146
147fn get_service_name() -> String {
149 std::env::var(env_logging::otlp::OTEL_SERVICE_NAME)
150 .unwrap_or_else(|_| DEFAULT_OTEL_SERVICE_NAME.to_string())
151}
152
153#[derive(Clone, Copy, Debug, Eq, PartialEq)]
154enum OtlpProtocol {
155 Grpc,
156 HttpProtobuf,
157}
158
159impl OtlpProtocol {
160 fn as_str(self) -> &'static str {
161 match self {
162 Self::Grpc => "grpc",
163 Self::HttpProtobuf => "http/protobuf",
164 }
165 }
166}
167
168fn parse_otlp_protocol_for_env(value: Option<&str>, env_name: &str) -> OtlpProtocol {
169 match value.map(str::trim).filter(|value| !value.is_empty()) {
170 None => OtlpProtocol::Grpc,
171 Some(value) if value.eq_ignore_ascii_case("grpc") => OtlpProtocol::Grpc,
172 Some(value) if value.eq_ignore_ascii_case("http/protobuf") => OtlpProtocol::HttpProtobuf,
173 Some(value) => {
174 eprintln!(
175 "WARNING: unsupported {} '{}'; falling back to grpc",
176 env_name, value
177 );
178 OtlpProtocol::Grpc
179 }
180 }
181}
182
183fn parse_otlp_protocol(value: Option<&str>) -> OtlpProtocol {
184 parse_otlp_protocol_for_env(value, env_logging::otlp::OTEL_EXPORTER_OTLP_PROTOCOL)
185}
186
187fn otlp_protocol_from_env() -> OtlpProtocol {
188 parse_otlp_protocol(
189 std::env::var(env_logging::otlp::OTEL_EXPORTER_OTLP_PROTOCOL)
190 .ok()
191 .as_deref(),
192 )
193}
194
195fn resolve_signal_otlp_protocol(
196 generic_protocol: OtlpProtocol,
197 signal_protocol: Option<&str>,
198 signal_protocol_env: &str,
199) -> OtlpProtocol {
200 match signal_protocol
201 .map(str::trim)
202 .filter(|value| !value.is_empty())
203 {
204 Some(value) => parse_otlp_protocol_for_env(Some(value), signal_protocol_env),
205 None => generic_protocol,
206 }
207}
208
209fn append_otlp_http_path(endpoint: &str, path: &str) -> String {
210 let endpoint = endpoint.trim_end_matches('/');
211 format!("{endpoint}{path}")
212}
213
214fn resolve_otlp_endpoint(
215 protocol: OtlpProtocol,
216 signal_endpoint: Option<String>,
217 generic_endpoint: Option<String>,
218 http_path: &str,
219) -> String {
220 if let Some(endpoint) = signal_endpoint.filter(|value| !value.trim().is_empty()) {
221 return endpoint;
222 }
223
224 match protocol {
225 OtlpProtocol::Grpc => generic_endpoint
226 .filter(|value| !value.trim().is_empty())
227 .unwrap_or_else(|| DEFAULT_OTLP_ENDPOINT.to_string()),
228 OtlpProtocol::HttpProtobuf => append_otlp_http_path(
229 generic_endpoint
230 .filter(|value| !value.trim().is_empty())
231 .as_deref()
232 .unwrap_or(DEFAULT_OTLP_HTTP_ENDPOINT),
233 http_path,
234 ),
235 }
236}
237
238fn parse_trace_sample_ratio(value: Option<&str>) -> Option<f64> {
239 let raw = value?;
240 match raw.parse::<f64>() {
241 Ok(value) if value.is_finite() && (0.0..=1.0).contains(&value) => Some(value),
242 _ => {
243 eprintln!(
244 "WARNING: invalid OTEL_TRACES_SAMPLE_RATIO '{}'; expected a number between 0.0 and 1.0, keeping default sampler",
245 raw
246 );
247 None
248 }
249 }
250}
251
252fn trace_sample_ratio_from_env() -> Option<f64> {
253 parse_trace_sample_ratio(
254 std::env::var(env_logging::otlp::OTEL_TRACES_SAMPLE_RATIO)
255 .ok()
256 .as_deref(),
257 )
258}
259
260fn build_span_exporter(
261 protocol: OtlpProtocol,
262 endpoint: &str,
263) -> Result<opentelemetry_otlp::SpanExporter, opentelemetry_otlp::ExporterBuildError> {
264 match protocol {
265 OtlpProtocol::Grpc => opentelemetry_otlp::SpanExporter::builder()
266 .with_tonic()
267 .with_endpoint(endpoint)
268 .build(),
269 OtlpProtocol::HttpProtobuf => opentelemetry_otlp::SpanExporter::builder()
270 .with_http()
271 .with_endpoint(endpoint)
272 .build(),
273 }
274}
275
276fn build_log_exporter(
277 protocol: OtlpProtocol,
278 endpoint: &str,
279) -> Result<opentelemetry_otlp::LogExporter, opentelemetry_otlp::ExporterBuildError> {
280 match protocol {
281 OtlpProtocol::Grpc => opentelemetry_otlp::LogExporter::builder()
282 .with_tonic()
283 .with_endpoint(endpoint)
284 .build(),
285 OtlpProtocol::HttpProtobuf => opentelemetry_otlp::LogExporter::builder()
286 .with_http()
287 .with_endpoint(endpoint)
288 .build(),
289 }
290}
291
292fn span_events_for_logging() -> FmtSpan {
293 if span_events_enabled() {
294 FmtSpan::CLOSE
295 } else {
296 FmtSpan::NONE
297 }
298}
299
300fn log_otel_init_status(service_name: &str, endpoint_opt: Option<(OtlpProtocol, String)>) {
301 if let Some((protocol, endpoint)) = endpoint_opt {
302 tracing::info!(
303 endpoint = %endpoint,
304 protocol = %protocol.as_str(),
305 service = %service_name,
306 "OpenTelemetry OTLP export enabled (traces and logs)"
307 );
308 } else {
309 tracing::info!(
310 service = %service_name,
311 "OpenTelemetry OTLP export disabled, traces local only"
312 );
313 }
314}
315
316pub fn is_valid_trace_id(trace_id: &str) -> bool {
319 trace_id.len() == 32 && trace_id.chars().all(|c| c.is_ascii_hexdigit())
320}
321
322pub fn is_valid_span_id(span_id: &str) -> bool {
325 span_id.len() == 16 && span_id.chars().all(|c| c.is_ascii_hexdigit())
326}
327
328pub struct DistributedTraceIdLayer;
329
330#[derive(Debug, Clone, Serialize, Deserialize)]
331pub struct DistributedTraceContext {
332 pub trace_id: String,
333 pub span_id: String,
334 #[serde(
335 default = "default_trace_flags",
336 skip_serializing_if = "is_default_trace_flags"
337 )]
338 pub trace_flags: String,
339 #[serde(skip_serializing_if = "Option::is_none")]
340 pub parent_id: Option<String>,
341 #[serde(skip_serializing_if = "Option::is_none")]
342 pub tracestate: Option<String>,
343 #[serde(skip)]
344 start: Option<Instant>,
345 #[serde(skip)]
346 end: Option<Instant>,
347 #[serde(skip_serializing_if = "Option::is_none")]
348 pub x_request_id: Option<String>,
349 #[serde(skip_serializing_if = "Option::is_none")]
350 pub request_id: Option<String>,
351}
352
353#[derive(Debug, Clone)]
355struct PendingDistributedTraceContext {
356 trace_id: Option<String>,
357 span_id: Option<String>,
358 parent_id: Option<String>,
359 trace_flags: Option<String>,
360 tracestate: Option<String>,
361 x_request_id: Option<String>,
362 request_id: Option<String>,
363}
364
365macro_rules! emit_at_level {
367 ($level:expr, target: $target:expr, $($arg:tt)*) => {
368 match $level {
372 &tracing::Level::ERROR => tracing::event!(target: $target, tracing::Level::ERROR, $($arg)*),
373 &tracing::Level::WARN => tracing::event!(target: $target, tracing::Level::WARN, $($arg)*),
374 &tracing::Level::INFO => tracing::event!(target: $target, tracing::Level::INFO, $($arg)*),
375 &tracing::Level::DEBUG => tracing::event!(target: $target, tracing::Level::DEBUG, $($arg)*),
376 &tracing::Level::TRACE => tracing::event!(target: $target, tracing::Level::TRACE, $($arg)*),
377 }
378 };
379}
380
381impl DistributedTraceContext {
382 pub fn create_traceparent(&self) -> String {
384 format!(
385 "00-{}-{}-{}",
386 self.trace_id,
387 self.span_id,
388 normalize_trace_flags(&self.trace_flags)
389 )
390 }
391}
392
393fn default_trace_flags() -> String {
394 "01".to_string()
395}
396
397fn is_default_trace_flags(trace_flags: &str) -> bool {
398 trace_flags == "01"
399}
400
401fn is_valid_trace_flags(trace_flags: &str) -> bool {
402 trace_flags.len() == 2 && trace_flags.chars().all(|c| c.is_ascii_hexdigit())
403}
404
405fn is_valid_version(version: &str) -> bool {
409 version.len() == 2 && matches!(u8::from_str_radix(version, 16), Ok(v) if v != 0xff)
410}
411
412fn normalize_trace_flags(trace_flags: &str) -> String {
413 if is_valid_trace_flags(trace_flags) {
414 trace_flags.to_ascii_lowercase()
415 } else {
416 default_trace_flags()
417 }
418}
419
420fn current_otel_trace_flags() -> Option<String> {
421 let context = Span::current().context();
422 let span = context.span();
423 let span_context = span.span_context();
424 if !span_context.is_valid() {
425 return None;
426 }
427
428 Some(
429 if span_context.trace_flags().is_sampled() {
430 "01"
431 } else {
432 "00"
433 }
434 .to_string(),
435 )
436}
437
438pub fn parse_traceparent(traceparent: &str) -> (Option<String>, Option<String>, Option<String>) {
440 let pieces: Vec<_> = traceparent.split('-').collect();
441 if pieces.len() != 4 {
442 return (None, None, None);
443 }
444 let version = pieces[0];
445 let trace_id = pieces[1];
446 let parent_id = pieces[2];
447 let trace_flags = pieces[3];
448
449 if !is_valid_version(version)
450 || !is_valid_trace_id(trace_id)
451 || !is_valid_span_id(parent_id)
452 || !is_valid_trace_flags(trace_flags)
453 {
454 return (None, None, None);
455 }
456
457 (
458 Some(trace_id.to_string()),
459 Some(parent_id.to_string()),
460 Some(trace_flags.to_ascii_lowercase()),
461 )
462}
463
464#[derive(Debug, Clone, Default)]
465pub struct TraceParent {
466 pub trace_id: Option<String>,
467 pub parent_id: Option<String>,
468 pub trace_flags: Option<String>,
469 pub tracestate: Option<String>,
470 pub x_request_id: Option<String>,
471 pub request_id: Option<String>,
472}
473
474pub trait GenericHeaders {
475 fn get(&self, key: &str) -> Option<&str>;
476}
477
478impl GenericHeaders for async_nats::HeaderMap {
479 fn get(&self, key: &str) -> Option<&str> {
480 async_nats::HeaderMap::get(self, key).map(|value| value.as_str())
481 }
482}
483
484impl GenericHeaders for http::HeaderMap {
485 fn get(&self, key: &str) -> Option<&str> {
486 http::HeaderMap::get(self, key).and_then(|value| value.to_str().ok())
487 }
488}
489
490impl TraceParent {
491 pub fn from_headers<H: GenericHeaders>(headers: &H) -> TraceParent {
492 let mut trace_id = None;
493 let mut parent_id = None;
494 let mut trace_flags = None;
495 let mut tracestate = None;
496 let mut x_request_id = None;
497 let mut request_id = None;
498
499 if let Some(header_value) = headers.get("traceparent") {
500 (trace_id, parent_id, trace_flags) = parse_traceparent(header_value);
501 }
502
503 if let Some(header_value) = headers.get("x-request-id") {
504 x_request_id = Some(header_value.to_string());
505 }
506
507 if let Some(header_value) = headers.get("tracestate") {
508 tracestate = Some(header_value.to_string());
509 }
510
511 if let Some(header_value) = headers.get("request-id") {
513 request_id = Some(header_value.to_string());
514 } else if let Some(header_value) = headers.get("x-dynamo-request-id") {
515 request_id = Some(header_value.to_string());
516 }
517
518 let request_id = request_id.filter(|id| uuid::Uuid::parse_str(id).is_ok());
519 TraceParent {
520 trace_id,
521 parent_id,
522 trace_flags,
523 tracestate,
524 x_request_id,
525 request_id,
526 }
527 }
528}
529
530pub fn make_inference_request_span<B>(req: &Request<B>) -> Span {
536 let method = req.method();
537 let uri = req.uri();
538 let version = format!("{:?}", req.version());
539 let trace_parent = TraceParent::from_headers(req.headers());
540
541 let otel_context = extract_otel_context_from_http_headers(req.headers());
542
543 let request_id = trace_parent
547 .request_id
548 .unwrap_or_else(|| Uuid::new_v4().to_string());
549
550 let span = tracing::info_span!(
551 target: "request_span",
552 "http-request",
553 method = %method,
554 uri = %uri,
555 version = %version,
556 trace_id = trace_parent.trace_id,
557 parent_id = trace_parent.parent_id,
558 trace_flags = trace_parent.trace_flags,
559 x_request_id = trace_parent.x_request_id,
560 request_id = %request_id,
561 model = tracing::field::Empty,
562 input_tokens = tracing::field::Empty,
563 output_tokens = tracing::field::Empty,
564 image_count = tracing::field::Empty,
565 video_count = tracing::field::Empty,
566 audio_count = tracing::field::Empty,
567 ttft_ms = tracing::field::Empty,
568 avg_itl_ms = tracing::field::Empty,
569 prefill_worker_id = tracing::field::Empty,
570 decode_worker_id = tracing::field::Empty,
571 );
572
573 if let Some(context) = otel_context {
574 let _ = span.set_parent(context);
575 }
576
577 span
578}
579
580pub fn make_system_request_span<B>(req: &Request<B>) -> Span {
587 let method = req.method();
588 let uri = req.uri();
589 let version = format!("{:?}", req.version());
590 let trace_parent = TraceParent::from_headers(req.headers());
591 let otel_context = extract_otel_context_from_http_headers(req.headers());
592
593 let request_id = trace_parent
595 .request_id
596 .unwrap_or_else(|| Uuid::new_v4().to_string());
597
598 let span = tracing::debug_span!(
599 target: "system_span",
600 "http-request",
601 method = %method,
602 uri = %uri,
603 version = %version,
604 trace_id = trace_parent.trace_id,
605 parent_id = trace_parent.parent_id,
606 trace_flags = trace_parent.trace_flags,
607 x_request_id = trace_parent.x_request_id,
608 request_id = %request_id,
609 model = tracing::field::Empty,
610 input_tokens = tracing::field::Empty,
611 output_tokens = tracing::field::Empty,
612 image_count = tracing::field::Empty,
613 video_count = tracing::field::Empty,
614 audio_count = tracing::field::Empty,
615 ttft_ms = tracing::field::Empty,
616 avg_itl_ms = tracing::field::Empty,
617 prefill_worker_id = tracing::field::Empty,
618 decode_worker_id = tracing::field::Empty,
619 );
620
621 if let Some(context) = otel_context {
622 let _ = span.set_parent(context);
623 }
624
625 span
626}
627
628fn extract_otel_context_from_http_headers(
630 headers: &http::HeaderMap,
631) -> Option<opentelemetry::Context> {
632 let traceparent_value = headers.get("traceparent")?.to_str().ok()?;
633
634 struct HttpHeaderExtractor<'a>(&'a http::HeaderMap);
635
636 impl<'a> Extractor for HttpHeaderExtractor<'a> {
637 fn get(&self, key: &str) -> Option<&str> {
638 self.0.get(key).and_then(|v| v.to_str().ok())
639 }
640
641 fn keys(&self) -> Vec<&str> {
642 vec!["traceparent", "tracestate"]
643 .into_iter()
644 .filter(|&key| self.0.get(key).is_some())
645 .collect()
646 }
647 }
648
649 if traceparent_value.is_empty() {
651 return None;
652 }
653
654 let extractor = HttpHeaderExtractor(headers);
655 let otel_context = TRACE_PROPAGATOR.extract(&extractor);
656
657 if otel_context.span().span_context().is_valid() {
658 Some(otel_context)
659 } else {
660 None
661 }
662}
663
664pub fn make_handle_payload_span(
666 headers: &async_nats::HeaderMap,
667 component: &str,
668 endpoint: &str,
669 namespace: &str,
670 instance_id: u64,
671) -> Span {
672 let (otel_context, trace_id, parent_span_id) = extract_otel_context_from_nats_headers(headers);
673 let trace_parent = TraceParent::from_headers(headers);
674
675 if let (Some(trace_id), Some(parent_id)) = (trace_id.as_ref(), parent_span_id.as_ref()) {
676 let span = tracing::info_span!(
677 target: "request_span",
678 "handle_payload",
679 trace_id = trace_id.as_str(),
680 parent_id = parent_id.as_str(),
681 trace_flags = trace_parent.trace_flags,
682 x_request_id = trace_parent.x_request_id,
683 request_id = trace_parent.request_id,
684 tracestate = trace_parent.tracestate,
685 component = component,
686 endpoint = endpoint,
687 namespace = namespace,
688 instance_id = instance_id,
689 );
690
691 if let Some(context) = otel_context {
692 let _ = span.set_parent(context);
693 }
694 span
695 } else {
696 tracing::info_span!(
697 target: "request_span",
698 "handle_payload",
699 trace_flags = trace_parent.trace_flags,
700 x_request_id = trace_parent.x_request_id,
701 request_id = trace_parent.request_id,
702 tracestate = trace_parent.tracestate,
703 component = component,
704 endpoint = endpoint,
705 namespace = namespace,
706 instance_id = instance_id,
707 )
708 }
709}
710
711pub fn make_handle_payload_span_from_tcp_headers(
713 headers: &std::collections::HashMap<String, String>,
714 component: &str,
715 endpoint: &str,
716 namespace: &str,
717 instance_id: u64,
718) -> Span {
719 let (otel_context, trace_id, parent_span_id) = extract_otel_context_from_tcp_headers(headers);
720 let x_request_id = headers.get("x-request-id").cloned();
721 let request_id = headers
722 .get("request-id")
723 .or_else(|| headers.get("x-dynamo-request-id"))
724 .filter(|id| uuid::Uuid::parse_str(id).is_ok())
725 .cloned();
726 let tracestate = headers.get("tracestate").cloned();
727 let trace_flags = headers.get("traceparent").and_then(|value| {
728 let (_, _, flags) = parse_traceparent(value);
729 flags
730 });
731
732 if let (Some(trace_id), Some(parent_id)) = (trace_id.as_ref(), parent_span_id.as_ref()) {
733 let span = tracing::info_span!(
734 target: "request_span",
735 "handle_payload",
736 trace_id = trace_id.as_str(),
737 parent_id = parent_id.as_str(),
738 trace_flags = trace_flags,
739 x_request_id = x_request_id,
740 request_id = request_id,
741 tracestate = tracestate,
742 component = component,
743 endpoint = endpoint,
744 namespace = namespace,
745 instance_id = instance_id,
746 );
747
748 if let Some(context) = otel_context {
749 let _ = span.set_parent(context);
750 }
751 span
752 } else {
753 tracing::info_span!(
754 target: "request_span",
755 "handle_payload",
756 trace_flags = trace_flags,
757 x_request_id = x_request_id,
758 request_id = request_id,
759 tracestate = tracestate,
760 component = component,
761 endpoint = endpoint,
762 namespace = namespace,
763 instance_id = instance_id,
764 )
765 }
766}
767
768fn extract_otel_context_from_tcp_headers(
770 headers: &std::collections::HashMap<String, String>,
771) -> (
772 Option<opentelemetry::Context>,
773 Option<String>,
774 Option<String>,
775) {
776 let traceparent_value = match headers.get("traceparent") {
777 Some(value) => value.as_str(),
778 None => return (None, None, None),
779 };
780
781 let (trace_id, parent_span_id, _) = parse_traceparent(traceparent_value);
782
783 struct TcpHeaderExtractor<'a>(&'a std::collections::HashMap<String, String>);
784
785 impl<'a> Extractor for TcpHeaderExtractor<'a> {
786 fn get(&self, key: &str) -> Option<&str> {
787 self.0.get(key).map(|s| s.as_str())
788 }
789
790 fn keys(&self) -> Vec<&str> {
791 vec!["traceparent", "tracestate"]
792 .into_iter()
793 .filter(|&key| self.0.get(key).is_some())
794 .collect()
795 }
796 }
797
798 let extractor = TcpHeaderExtractor(headers);
799 let otel_context = TRACE_PROPAGATOR.extract(&extractor);
800
801 let context_with_trace = if otel_context.span().span_context().is_valid() {
802 Some(otel_context)
803 } else {
804 None
805 };
806
807 (context_with_trace, trace_id, parent_span_id)
808}
809
810pub fn extract_otel_context_from_nats_headers(
812 headers: &async_nats::HeaderMap,
813) -> (
814 Option<opentelemetry::Context>,
815 Option<String>,
816 Option<String>,
817) {
818 let traceparent_value = match headers.get("traceparent") {
819 Some(value) => value.as_str(),
820 None => return (None, None, None),
821 };
822
823 let (trace_id, parent_span_id, _) = parse_traceparent(traceparent_value);
824
825 struct NatsHeaderExtractor<'a>(&'a async_nats::HeaderMap);
826
827 impl<'a> Extractor for NatsHeaderExtractor<'a> {
828 fn get(&self, key: &str) -> Option<&str> {
829 self.0.get(key).map(|value| value.as_str())
830 }
831
832 fn keys(&self) -> Vec<&str> {
833 vec!["traceparent", "tracestate"]
834 .into_iter()
835 .filter(|&key| self.0.get(key).is_some())
836 .collect()
837 }
838 }
839
840 let extractor = NatsHeaderExtractor(headers);
841 let otel_context = TRACE_PROPAGATOR.extract(&extractor);
842
843 let context_with_trace = if otel_context.span().span_context().is_valid() {
844 Some(otel_context)
845 } else {
846 None
847 };
848
849 (context_with_trace, trace_id, parent_span_id)
850}
851
852pub fn inject_otel_context_into_nats_headers(
854 headers: &mut async_nats::HeaderMap,
855 context: Option<opentelemetry::Context>,
856) {
857 let otel_context = context.unwrap_or_else(|| Span::current().context());
858
859 struct NatsHeaderInjector<'a>(&'a mut async_nats::HeaderMap);
860
861 impl<'a> Injector for NatsHeaderInjector<'a> {
862 fn set(&mut self, key: &str, value: String) {
863 self.0.insert(key, value);
864 }
865 }
866
867 let mut injector = NatsHeaderInjector(headers);
868 TRACE_PROPAGATOR.inject_context(&otel_context, &mut injector);
869}
870
871pub fn inject_current_trace_into_nats_headers(headers: &mut async_nats::HeaderMap) {
873 inject_otel_context_into_nats_headers(headers, None);
874}
875
876pub fn inject_trace_headers_into_map(headers: &mut std::collections::HashMap<String, String>) {
878 if let Some(trace_context) = get_distributed_tracing_context() {
879 headers.insert(
881 "traceparent".to_string(),
882 trace_context.create_traceparent(),
883 );
884
885 if let Some(tracestate) = trace_context.tracestate {
887 headers.insert("tracestate".to_string(), tracestate);
888 }
889
890 if let Some(x_request_id) = trace_context.x_request_id {
892 headers.insert("x-request-id".to_string(), x_request_id);
893 }
894 if let Some(request_id) = trace_context.request_id {
895 headers.insert("request-id".to_string(), request_id);
896 }
897 }
898}
899
900pub fn otel_parent_context_from_distributed(
901 ctx: &DistributedTraceContext,
902) -> Option<opentelemetry::Context> {
903 let mut headers = async_nats::HeaderMap::new();
904 headers.insert("traceparent", ctx.create_traceparent());
905
906 if let Some(ref tracestate) = ctx.tracestate {
907 headers.insert("tracestate", tracestate.as_str());
908 }
909
910 let (otel_context, _trace_id, _parent_span_id) =
911 extract_otel_context_from_nats_headers(&headers);
912 otel_context
913}
914
915pub fn make_client_request_span(
917 operation: &str,
918 request_id: &str,
919 trace_context: Option<&DistributedTraceContext>,
920 instance_id: Option<&str>,
921) -> Span {
922 if let Some(ctx) = trace_context {
923 let otel_context = otel_parent_context_from_distributed(ctx);
924
925 let span = if let Some(inst_id) = instance_id {
926 tracing::info_span!(
927 "client_request",
928 operation = operation,
929 request_id = request_id,
930 instance_id = inst_id,
931 trace_id = ctx.trace_id.as_str(),
932 parent_id = ctx.span_id.as_str(),
933 trace_flags = ctx.trace_flags.as_str(),
934 x_request_id = ctx.x_request_id.as_deref(),
935 )
936 } else {
937 tracing::info_span!(
938 "client_request",
939 operation = operation,
940 request_id = request_id,
941 trace_id = ctx.trace_id.as_str(),
942 parent_id = ctx.span_id.as_str(),
943 trace_flags = ctx.trace_flags.as_str(),
944 x_request_id = ctx.x_request_id.as_deref(),
945 )
946 };
947
948 if let Some(context) = otel_context {
949 let _ = span.set_parent(context);
950 }
951
952 span
953 } else if let Some(inst_id) = instance_id {
954 tracing::info_span!(
955 "client_request",
956 operation = operation,
957 request_id = request_id,
958 instance_id = inst_id,
959 )
960 } else {
961 tracing::info_span!(
962 "client_request",
963 operation = operation,
964 request_id = request_id,
965 )
966 }
967}
968
969#[derive(Debug, Default)]
970pub struct FieldVisitor {
971 pub fields: HashMap<String, String>,
972}
973
974impl Visit for FieldVisitor {
975 fn record_str(&mut self, field: &Field, value: &str) {
976 self.fields
977 .insert(field.name().to_string(), value.to_string());
978 }
979
980 fn record_debug(&mut self, field: &Field, value: &dyn std::fmt::Debug) {
981 self.fields
982 .insert(field.name().to_string(), format!("{:?}", value).to_string());
983 }
984}
985
986impl<S> Layer<S> for DistributedTraceIdLayer
987where
988 S: Subscriber + for<'a> tracing_subscriber::registry::LookupSpan<'a>,
989{
990 fn on_close(&self, id: Id, ctx: Context<'_, S>) {
993 if let Some(span) = ctx.span(&id) {
994 let mut extensions = span.extensions_mut();
995 if let Some(distributed_tracing_context) =
996 extensions.get_mut::<DistributedTraceContext>()
997 {
998 distributed_tracing_context.end = Some(Instant::now());
999 }
1000 }
1001 }
1002
1003 fn on_new_span(&self, attrs: &span::Attributes<'_>, id: &Id, ctx: Context<'_, S>) {
1006 if let Some(span) = ctx.span(id) {
1007 let mut trace_id: Option<String> = None;
1008 let mut parent_id: Option<String> = None;
1009 let mut span_id: Option<String> = None;
1010 let mut trace_flags: Option<String> = None;
1011 let mut x_request_id: Option<String> = None;
1012 let mut request_id: Option<String> = None;
1013 let mut tracestate: Option<String> = None;
1014 let mut visitor = FieldVisitor::default();
1015 attrs.record(&mut visitor);
1016
1017 if let Some(trace_id_input) = visitor.fields.get("trace_id") {
1019 if !is_valid_trace_id(trace_id_input) {
1020 tracing::trace!("trace id '{trace_id_input}' is not valid! Ignoring.");
1021 } else {
1022 trace_id = Some(trace_id_input.to_string());
1023 }
1024 }
1025
1026 if let Some(span_id_input) = visitor.fields.get("span_id") {
1028 if !is_valid_span_id(span_id_input) {
1029 tracing::trace!("span id '{span_id_input}' is not valid! Ignoring.");
1030 } else {
1031 span_id = Some(span_id_input.to_string());
1032 }
1033 }
1034
1035 if let Some(parent_id_input) = visitor.fields.get("parent_id") {
1037 if !is_valid_span_id(parent_id_input) {
1038 tracing::trace!("parent id '{parent_id_input}' is not valid! Ignoring.");
1039 } else {
1040 parent_id = Some(parent_id_input.to_string());
1041 }
1042 }
1043
1044 if let Some(trace_flags_input) = visitor.fields.get("trace_flags") {
1045 if !is_valid_trace_flags(trace_flags_input) {
1046 tracing::trace!("trace flags '{trace_flags_input}' are not valid! Ignoring.");
1047 } else {
1048 trace_flags = Some(trace_flags_input.to_ascii_lowercase());
1049 }
1050 }
1051
1052 if let Some(tracestate_input) = visitor.fields.get("tracestate") {
1054 tracestate = Some(tracestate_input.to_string());
1055 }
1056
1057 if let Some(x_request_id_input) = visitor.fields.get("x_request_id") {
1059 x_request_id = Some(x_request_id_input.to_string());
1060 }
1061
1062 if let Some(request_id_input) = visitor.fields.get("request_id") {
1064 request_id = Some(request_id_input.to_string());
1065 } else if let Some(x_request_id_input) = visitor.fields.get("x_dynamo_request_id") {
1066 request_id = Some(x_request_id_input.to_string());
1067 }
1068
1069 if parent_id.is_none()
1071 && let Some(parent_span_id) = ctx.current_span().id()
1072 && let Some(parent_span) = ctx.span(parent_span_id)
1073 {
1074 let parent_ext = parent_span.extensions();
1075 if let Some(parent_tracing_context) = parent_ext.get::<DistributedTraceContext>() {
1076 trace_id = Some(parent_tracing_context.trace_id.clone());
1077 parent_id = Some(parent_tracing_context.span_id.clone());
1078 if trace_flags.is_none() {
1079 trace_flags = Some(parent_tracing_context.trace_flags.clone());
1080 }
1081 tracestate = parent_tracing_context.tracestate.clone();
1082 if x_request_id.is_none() {
1083 x_request_id = parent_tracing_context.x_request_id.clone();
1084 }
1085 if request_id.is_none() {
1086 request_id = parent_tracing_context.request_id.clone();
1087 }
1088 }
1089 }
1090
1091 if (parent_id.is_some() || span_id.is_some()) && trace_id.is_none() {
1093 tracing::error!("parent id or span id are set but trace id is not set!");
1094 parent_id = None;
1096 span_id = None;
1097 }
1098
1099 let mut extensions = span.extensions_mut();
1101 extensions.insert(PendingDistributedTraceContext {
1102 trace_id,
1103 span_id,
1104 parent_id,
1105 trace_flags,
1106 tracestate,
1107 x_request_id,
1108 request_id,
1109 });
1110 }
1111 }
1112
1113 fn on_enter(&self, id: &Id, ctx: Context<'_, S>) {
1116 if let Some(span) = ctx.span(id) {
1117 {
1119 let extensions = span.extensions();
1120 if extensions.get::<DistributedTraceContext>().is_some() {
1121 return;
1122 }
1123 }
1124
1125 let mut extensions = span.extensions_mut();
1127 let pending = match extensions.remove::<PendingDistributedTraceContext>() {
1128 Some(p) => p,
1129 None => {
1130 tracing::error!("PendingDistributedTraceContext not found in on_enter");
1132 return;
1133 }
1134 };
1135
1136 let mut trace_id = pending.trace_id;
1137 let mut span_id = pending.span_id;
1138 let parent_id = pending.parent_id;
1139 let mut trace_flags = pending.trace_flags;
1140 let tracestate = pending.tracestate;
1141 let x_request_id = pending.x_request_id;
1142 let request_id = pending.request_id;
1143
1144 drop(extensions);
1147
1148 if trace_id.is_none() || span_id.is_none() {
1149 let extensions = span.extensions();
1150 if let Some(otel_data) = extensions.get::<tracing_opentelemetry::OtelData>() {
1151 if trace_id.is_none()
1153 && let Some(otel_trace_id) = otel_data.trace_id()
1154 {
1155 let trace_id_str = format!("{}", otel_trace_id);
1156 if is_valid_trace_id(&trace_id_str) {
1157 trace_id = Some(trace_id_str);
1158 }
1159 }
1160
1161 if span_id.is_none()
1163 && let Some(otel_span_id) = otel_data.span_id()
1164 {
1165 let span_id_str = format!("{}", otel_span_id);
1166 if is_valid_span_id(&span_id_str) {
1167 span_id = Some(span_id_str);
1168 }
1169 }
1170 }
1171 }
1172
1173 if trace_flags.is_none() {
1174 trace_flags = current_otel_trace_flags();
1175 }
1176
1177 if trace_id.is_none() {
1179 panic!(
1180 "trace_id is not set in on_enter - OtelData may not be properly initialized"
1181 );
1182 }
1183
1184 if span_id.is_none() {
1185 panic!("span_id is not set in on_enter - OtelData may not be properly initialized");
1186 }
1187
1188 let span_level = span.metadata().level();
1189 let mut extensions = span.extensions_mut();
1190 extensions.insert(DistributedTraceContext {
1191 trace_id: trace_id.expect("Trace ID must be set"),
1192 span_id: span_id.expect("Span ID must be set"),
1193 trace_flags: trace_flags.unwrap_or_else(default_trace_flags),
1194 parent_id,
1195 tracestate,
1196 start: Some(Instant::now()),
1197 end: None,
1198 x_request_id,
1199 request_id,
1200 });
1201
1202 drop(extensions);
1203
1204 if span_events_enabled() {
1207 emit_at_level!(span_level, target: "span_event", message = "SPAN_FIRST_ENTRY");
1208 }
1209 }
1210 }
1211}
1212
1213pub fn get_distributed_tracing_context() -> Option<DistributedTraceContext> {
1216 Span::current()
1217 .with_subscriber(|(id, subscriber)| {
1218 subscriber
1219 .downcast_ref::<Registry>()
1220 .and_then(|registry| registry.span_data(id))
1221 .and_then(|span_data| {
1222 let extensions = span_data.extensions();
1223 extensions.get::<DistributedTraceContext>().cloned()
1224 })
1225 })
1226 .flatten()
1227 .map(|mut context| {
1228 if let Some(trace_flags) = current_otel_trace_flags() {
1232 context.trace_flags = trace_flags;
1233 }
1234 context
1235 })
1236}
1237
1238pub fn init() {
1240 INIT.call_once(|| {
1241 if let Err(e) = setup_logging() {
1242 eprintln!("Failed to initialize logging: {}", e);
1243 std::process::exit(1);
1244 }
1245 });
1246}
1247
1248#[cfg(feature = "tokio-console")]
1249fn setup_logging() -> Result<(), Box<dyn std::error::Error>> {
1250 let tokio_console_layer = console_subscriber::ConsoleLayer::builder()
1251 .with_default_env()
1252 .server_addr(([0, 0, 0, 0], console_subscriber::Server::DEFAULT_PORT))
1253 .spawn();
1254 let tokio_console_target = tracing_subscriber::filter::Targets::new()
1255 .with_default(LevelFilter::ERROR)
1256 .with_target("runtime", LevelFilter::TRACE)
1257 .with_target("tokio", LevelFilter::TRACE);
1258 let l = fmt::layer()
1259 .with_ansi(!disable_ansi_logging())
1260 .event_format(fmt::format().compact().with_timer(TimeFormatter::new()))
1261 .with_writer(std::io::stderr)
1262 .with_filter(filters(load_config()));
1263 tracing_subscriber::registry()
1264 .with(l)
1265 .with(tokio_console_layer.with_filter(tokio_console_target))
1266 .init();
1267 Ok(())
1268}
1269
1270#[cfg(not(feature = "tokio-console"))]
1271fn setup_logging() -> Result<(), Box<dyn std::error::Error>> {
1272 let fmt_filter_layer = filters(load_config());
1273 let trace_filter_layer = filters(load_config());
1274 let otel_filter_layer = filters(load_config());
1275 let otel_logs_filter_layer = filters(load_config());
1276 let jsonl_enabled = jsonl_logging_enabled();
1277 let otlp_enabled = otlp_exporter_enabled();
1278
1279 if jsonl_enabled || otlp_enabled {
1280 let service_name = get_service_name();
1281 let sample_ratio = trace_sample_ratio_from_env();
1282
1283 let (tracer_provider, logger_provider_opt, endpoint_opt) = if otlp_enabled {
1285 let protocol = otlp_protocol_from_env();
1287 let traces_protocol = resolve_signal_otlp_protocol(
1288 protocol,
1289 std::env::var(env_logging::otlp::OTEL_EXPORTER_OTLP_TRACES_PROTOCOL)
1290 .ok()
1291 .as_deref(),
1292 env_logging::otlp::OTEL_EXPORTER_OTLP_TRACES_PROTOCOL,
1293 );
1294 let logs_protocol = resolve_signal_otlp_protocol(
1295 protocol,
1296 std::env::var(env_logging::otlp::OTEL_EXPORTER_OTLP_LOGS_PROTOCOL)
1297 .ok()
1298 .as_deref(),
1299 env_logging::otlp::OTEL_EXPORTER_OTLP_LOGS_PROTOCOL,
1300 );
1301 let generic_endpoint =
1302 std::env::var(env_logging::otlp::OTEL_EXPORTER_OTLP_ENDPOINT).ok();
1303 let traces_endpoint_env =
1304 std::env::var(env_logging::otlp::OTEL_EXPORTER_OTLP_TRACES_ENDPOINT).ok();
1305 let logs_endpoint_env =
1306 std::env::var(env_logging::otlp::OTEL_EXPORTER_OTLP_LOGS_ENDPOINT).ok();
1307 let traces_endpoint = resolve_otlp_endpoint(
1308 traces_protocol,
1309 traces_endpoint_env,
1310 generic_endpoint.clone(),
1311 "/v1/traces",
1312 );
1313 let logs_endpoint = resolve_otlp_endpoint(
1314 logs_protocol,
1315 logs_endpoint_env,
1316 generic_endpoint,
1317 "/v1/logs",
1318 );
1319
1320 let resource = opentelemetry_sdk::Resource::builder_empty()
1321 .with_service_name(service_name.clone())
1322 .build();
1323
1324 let span_exporter = build_span_exporter(traces_protocol, &traces_endpoint)?;
1325
1326 let mut tracer_provider_builder =
1327 opentelemetry_sdk::trace::SdkTracerProvider::builder()
1328 .with_batch_exporter(span_exporter)
1329 .with_resource(resource.clone());
1330 if let Some(sample_ratio) = sample_ratio {
1331 tracer_provider_builder = tracer_provider_builder.with_sampler(
1332 Sampler::ParentBased(Box::new(Sampler::TraceIdRatioBased(sample_ratio))),
1333 );
1334 }
1335 let tracer_provider = tracer_provider_builder.build();
1336
1337 let log_exporter = build_log_exporter(logs_protocol, &logs_endpoint)?;
1338
1339 let logger_provider = SdkLoggerProvider::builder()
1340 .with_batch_exporter(log_exporter)
1341 .with_resource(resource)
1342 .build();
1343
1344 (
1345 tracer_provider,
1346 Some(logger_provider),
1347 Some((traces_protocol, traces_endpoint)),
1348 )
1349 } else {
1350 let mut provider_builder = opentelemetry_sdk::trace::SdkTracerProvider::builder()
1352 .with_resource(
1353 opentelemetry_sdk::Resource::builder_empty()
1354 .with_service_name(service_name.clone())
1355 .build(),
1356 );
1357 if let Some(sample_ratio) = sample_ratio {
1358 provider_builder = provider_builder.with_sampler(Sampler::ParentBased(Box::new(
1359 Sampler::TraceIdRatioBased(sample_ratio),
1360 )));
1361 }
1362 let provider = provider_builder.build();
1363
1364 (provider, None, None)
1365 };
1366
1367 opentelemetry::global::set_tracer_provider(tracer_provider.clone());
1374
1375 let tracer = tracer_provider.tracer(service_name.to_string());
1376 let otel_logs_layer = logger_provider_opt
1377 .as_ref()
1378 .map(|lp| OpenTelemetryTracingBridge::new(lp).with_filter(otel_logs_filter_layer));
1379
1380 macro_rules! init_otel_subscriber {
1381 ($fmt_layer:expr) => {
1382 tracing_subscriber::registry()
1383 .with(
1384 tracing_opentelemetry::layer()
1385 .with_tracer(tracer)
1386 .with_filter(otel_filter_layer),
1387 )
1388 .with(otel_logs_layer)
1389 .with(DistributedTraceIdLayer.with_filter(trace_filter_layer))
1390 .with($fmt_layer)
1391 .init();
1392 };
1393 }
1394
1395 if jsonl_enabled {
1396 let l = fmt::layer()
1397 .with_ansi(false)
1398 .with_span_events(span_events_for_logging())
1399 .event_format(CustomJsonFormatter::new())
1400 .with_writer(std::io::stderr)
1401 .with_filter(fmt_filter_layer);
1402 init_otel_subscriber!(l);
1403 } else {
1404 let l = fmt::layer()
1405 .with_ansi(!disable_ansi_logging())
1406 .event_format(fmt::format().compact().with_timer(TimeFormatter::new()))
1407 .with_writer(std::io::stderr)
1408 .with_filter(fmt_filter_layer);
1409 init_otel_subscriber!(l);
1410 }
1411
1412 log_otel_init_status(&service_name, endpoint_opt);
1413 } else {
1414 let l = fmt::layer()
1415 .with_ansi(!disable_ansi_logging())
1416 .event_format(fmt::format().compact().with_timer(TimeFormatter::new()))
1417 .with_writer(std::io::stderr)
1418 .with_filter(fmt_filter_layer);
1419
1420 tracing_subscriber::registry().with(l).init();
1421 }
1422
1423 Ok(())
1424}
1425
1426#[allow(clippy::large_enum_variant)] enum LoggingFilter {
1428 Targets(Targets),
1429 Env(EnvFilter),
1430}
1431
1432impl<S> Filter<S> for LoggingFilter {
1433 #[inline]
1434 fn enabled(&self, meta: &tracing::Metadata<'_>, cx: &Context<'_, S>) -> bool {
1435 match self {
1436 Self::Targets(filter) => <Targets as Filter<S>>::enabled(filter, meta, cx),
1437 Self::Env(filter) => <EnvFilter as Filter<S>>::enabled(filter, meta, cx),
1438 }
1439 }
1440
1441 #[inline]
1442 fn callsite_enabled(
1443 &self,
1444 meta: &'static tracing::Metadata<'static>,
1445 ) -> tracing::subscriber::Interest {
1446 match self {
1447 Self::Targets(filter) => <Targets as Filter<S>>::callsite_enabled(filter, meta),
1448 Self::Env(filter) => <EnvFilter as Filter<S>>::callsite_enabled(filter, meta),
1449 }
1450 }
1451
1452 #[inline]
1453 fn event_enabled(&self, event: &Event<'_>, cx: &Context<'_, S>) -> bool {
1454 match self {
1455 Self::Targets(filter) => <Targets as Filter<S>>::event_enabled(filter, event, cx),
1456 Self::Env(filter) => <EnvFilter as Filter<S>>::event_enabled(filter, event, cx),
1457 }
1458 }
1459
1460 #[inline]
1461 fn max_level_hint(&self) -> Option<LevelFilter> {
1462 match self {
1463 Self::Targets(filter) => <Targets as Filter<S>>::max_level_hint(filter),
1464 Self::Env(filter) => <EnvFilter as Filter<S>>::max_level_hint(filter),
1465 }
1466 }
1467
1468 #[inline]
1469 fn on_new_span(&self, attrs: &span::Attributes<'_>, id: &Id, cx: Context<'_, S>) {
1470 if let Self::Env(filter) = self {
1471 <EnvFilter as Filter<S>>::on_new_span(filter, attrs, id, cx);
1472 }
1473 }
1474
1475 #[inline]
1476 fn on_record(&self, id: &Id, values: &span::Record<'_>, cx: Context<'_, S>) {
1477 if let Self::Env(filter) = self {
1478 <EnvFilter as Filter<S>>::on_record(filter, id, values, cx);
1479 }
1480 }
1481
1482 #[inline]
1483 fn on_enter(&self, id: &Id, cx: Context<'_, S>) {
1484 if let Self::Env(filter) = self {
1485 <EnvFilter as Filter<S>>::on_enter(filter, id, cx);
1486 }
1487 }
1488
1489 #[inline]
1490 fn on_exit(&self, id: &Id, cx: Context<'_, S>) {
1491 if let Self::Env(filter) = self {
1492 <EnvFilter as Filter<S>>::on_exit(filter, id, cx);
1493 }
1494 }
1495
1496 #[inline]
1497 fn on_close(&self, id: Id, cx: Context<'_, S>) {
1498 if let Self::Env(filter) = self {
1499 <EnvFilter as Filter<S>>::on_close(filter, id, cx);
1500 }
1501 }
1502}
1503
1504#[derive(Debug)]
1505enum TargetsFilterFallback {
1506 Dynamic,
1507 Other,
1508}
1509
1510fn filters(config: LoggingConfig) -> LoggingFilter {
1511 let targets = match std::env::var(env_logging::DYN_LOG) {
1512 Ok(value) => targets_filter(&config, Some(&value)),
1513 Err(std::env::VarError::NotPresent) => targets_filter(&config, None),
1514 Err(std::env::VarError::NotUnicode(_)) => Err(TargetsFilterFallback::Other),
1515 };
1516
1517 match targets {
1518 Ok(filter) => LoggingFilter::Targets(filter),
1519 Err(TargetsFilterFallback::Dynamic) => {
1520 eprintln!(
1524 "\n!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!\n\
1525 !!! WARNING: DYNAMIC LOG FILTERS FORCE THE EnvFilter FALLBACK !!!\n\
1526 !!! This disables Dynamo's lock-free logging fast path and can severely\n\
1527 !!! degrade request performance, especially for streaming responses.\n\
1528 !!! Remove span/field selectors ([...]) from DYN_LOG or log_filters to\n\
1529 !!! re-enable the fast target/level filter.\n\
1530 !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!\n"
1531 );
1532 LoggingFilter::Env(env_filter(&config))
1533 }
1534 Err(TargetsFilterFallback::Other) => LoggingFilter::Env(env_filter(&config)),
1535 }
1536}
1537
1538fn targets_filter(
1543 config: &LoggingConfig,
1544 dyn_log: Option<&str>,
1545) -> Result<Targets, TargetsFilterFallback> {
1546 let dyn_directives = dyn_log
1550 .into_iter()
1551 .flat_map(|value| value.split(',').filter(|directive| !directive.is_empty()));
1552
1553 let mut directives = Vec::new();
1554 for directive in dyn_directives {
1555 targets_compatible(directive)?;
1558 directives.push(directive.to_string());
1559 }
1560
1561 if directives.is_empty() {
1565 directives.push(config.log_level.clone());
1566 }
1567
1568 for (module, level) in &config.log_filters {
1569 let directive = format!("{module}={level}");
1570 match targets_compatible(&directive) {
1571 Ok(()) => directives.push(directive),
1572 Err(fallback) => match directive.parse::<Directive>() {
1573 Ok(_) => return Err(fallback),
1576 Err(e) => {
1579 eprintln!("Failed parsing filter '{level}' for module '{module}': {e}");
1580 }
1581 },
1582 }
1583 }
1584
1585 if span_events_enabled() {
1586 directives.push("span_event=trace".to_string());
1587 }
1588
1589 directives.push("request_span=trace".to_string());
1590 directives
1591 .join(",")
1592 .parse::<Targets>()
1593 .map_err(|_| TargetsFilterFallback::Other)
1594}
1595
1596fn targets_compatible(directive: &str) -> Result<(), TargetsFilterFallback> {
1601 if directive.contains('[') {
1602 Err(TargetsFilterFallback::Dynamic)
1603 } else if directive.parse::<Targets>().is_ok() {
1604 Ok(())
1605 } else {
1606 Err(TargetsFilterFallback::Other)
1607 }
1608}
1609
1610fn env_filter(config: &LoggingConfig) -> EnvFilter {
1611 let mut filter_layer = EnvFilter::builder()
1612 .with_default_directive(config.log_level.parse().unwrap())
1613 .with_env_var(env_logging::DYN_LOG)
1614 .from_env_lossy();
1615
1616 for (module, level) in &config.log_filters {
1617 match format!("{module}={level}").parse::<Directive>() {
1618 Ok(d) => {
1619 filter_layer = filter_layer.add_directive(d);
1620 }
1621 Err(e) => {
1622 eprintln!("Failed parsing filter '{level}' for module '{module}': {e}");
1623 }
1624 }
1625 }
1626
1627 if span_events_enabled() {
1630 filter_layer = filter_layer.add_directive("span_event=trace".parse().unwrap());
1631 }
1632
1633 filter_layer = filter_layer.add_directive("request_span=trace".parse().unwrap());
1638
1639 filter_layer
1640}
1641
1642pub fn log_message(level: &str, message: &str, module: &str, file: &str, line: u32) {
1645 let level = match level {
1646 "debug" => log::Level::Debug,
1647 "info" => log::Level::Info,
1648 "warn" => log::Level::Warn,
1649 "error" => log::Level::Error,
1650 "warning" => log::Level::Warn,
1651 _ => log::Level::Info,
1652 };
1653 log::logger().log(
1654 &log::Record::builder()
1655 .args(format_args!("{}", message))
1656 .level(level)
1657 .target(module)
1658 .file(Some(file))
1659 .line(Some(line))
1660 .build(),
1661 );
1662}
1663
1664fn load_config() -> LoggingConfig {
1665 let config_path =
1666 std::env::var(env_logging::DYN_LOGGING_CONFIG_PATH).unwrap_or_else(|_| "".to_string());
1667 let figment = Figment::new()
1668 .merge(Serialized::defaults(LoggingConfig::default()))
1669 .merge(Toml::file("/opt/dynamo/etc/logging.toml"))
1670 .merge(Toml::file(config_path));
1671
1672 figment.extract().unwrap()
1673}
1674
1675#[derive(Serialize)]
1676struct JsonLog<'a> {
1677 time: String,
1678 level: String,
1679 #[serde(skip_serializing_if = "Option::is_none")]
1680 file: Option<&'a str>,
1681 #[serde(skip_serializing_if = "Option::is_none")]
1682 line: Option<u32>,
1683 target: String,
1684 message: serde_json::Value,
1685 #[serde(flatten)]
1686 fields: BTreeMap<String, serde_json::Value>,
1687}
1688
1689struct TimeFormatter {
1690 use_local_tz: bool,
1691}
1692
1693impl TimeFormatter {
1694 fn new() -> Self {
1695 Self {
1696 use_local_tz: crate::config::use_local_timezone(),
1697 }
1698 }
1699
1700 fn format_now(&self) -> String {
1701 if self.use_local_tz {
1702 chrono::Local::now()
1703 .format("%Y-%m-%dT%H:%M:%S%.6f%:z")
1704 .to_string()
1705 } else {
1706 chrono::Utc::now()
1707 .format("%Y-%m-%dT%H:%M:%S%.6fZ")
1708 .to_string()
1709 }
1710 }
1711}
1712
1713impl FormatTime for TimeFormatter {
1714 fn format_time(&self, w: &mut fmt::format::Writer<'_>) -> std::fmt::Result {
1715 write!(w, "{}", self.format_now())
1716 }
1717}
1718
1719struct CustomJsonFormatter {
1720 time_formatter: TimeFormatter,
1721}
1722
1723impl CustomJsonFormatter {
1724 fn new() -> Self {
1725 Self {
1726 time_formatter: TimeFormatter::new(),
1727 }
1728 }
1729}
1730
1731use once_cell::sync::Lazy;
1732use regex::Regex;
1733
1734static TRACE_PROPAGATOR: Lazy<opentelemetry_sdk::propagation::TraceContextPropagator> =
1736 Lazy::new(opentelemetry_sdk::propagation::TraceContextPropagator::new);
1737
1738fn parse_tracing_duration(s: &str) -> Option<u64> {
1739 static RE: Lazy<Regex> =
1740 Lazy::new(|| Regex::new(r#"^["']?\s*([0-9.]+)\s*(µs|us|ns|ms|s)\s*["']?$"#).unwrap());
1741 let captures = RE.captures(s)?;
1742 let value: f64 = captures[1].parse().ok()?;
1743 let unit = &captures[2];
1744 match unit {
1745 "ns" => Some((value / 1000.0) as u64),
1746 "µs" | "us" => Some(value as u64),
1747 "ms" => Some((value * 1000.0) as u64),
1748 "s" => Some((value * 1_000_000.0) as u64),
1749 _ => None,
1750 }
1751}
1752
1753impl<S, N> tracing_subscriber::fmt::FormatEvent<S, N> for CustomJsonFormatter
1754where
1755 S: Subscriber + for<'a> LookupSpan<'a>,
1756 N: for<'a> FormatFields<'a> + 'static,
1757{
1758 fn format_event(
1759 &self,
1760 ctx: &FmtContext<'_, S, N>,
1761 mut writer: Writer<'_>,
1762 event: &Event<'_>,
1763 ) -> std::fmt::Result {
1764 let mut visitor = JsonVisitor::default();
1765 let time = self.time_formatter.format_now();
1766 event.record(&mut visitor);
1767 let mut message = visitor
1768 .fields
1769 .remove("message")
1770 .unwrap_or(serde_json::Value::String("".to_string()));
1771
1772 let mut target_override: Option<String> = None;
1773
1774 let current_span = event
1775 .parent()
1776 .and_then(|id| ctx.span(id))
1777 .or_else(|| ctx.lookup_current());
1778 if let Some(span) = current_span {
1779 let ext = span.extensions();
1780 let data = ext.get::<FormattedFields<N>>().unwrap();
1781 let span_fields: Vec<(&str, &str)> = data
1782 .fields
1783 .split(' ')
1784 .filter_map(|entry| entry.split_once('='))
1785 .collect();
1786 for (name, value) in span_fields {
1787 visitor.fields.insert(
1788 name.to_string(),
1789 serde_json::Value::String(value.trim_matches('"').to_string()),
1790 );
1791 }
1792
1793 let busy_us = visitor
1794 .fields
1795 .remove("time.busy")
1796 .and_then(|v| parse_tracing_duration(&v.to_string()));
1797 let idle_us = visitor
1798 .fields
1799 .remove("time.idle")
1800 .and_then(|v| parse_tracing_duration(&v.to_string()));
1801
1802 if let (Some(busy_us), Some(idle_us)) = (busy_us, idle_us) {
1803 visitor.fields.insert(
1804 "time.busy_us".to_string(),
1805 serde_json::Value::Number(busy_us.into()),
1806 );
1807 visitor.fields.insert(
1808 "time.idle_us".to_string(),
1809 serde_json::Value::Number(idle_us.into()),
1810 );
1811 visitor.fields.insert(
1812 "time.duration_us".to_string(),
1813 serde_json::Value::Number((busy_us + idle_us).into()),
1814 );
1815 }
1816
1817 let is_span_created = message.as_str() == Some("SPAN_FIRST_ENTRY");
1818 let is_span_closed = message.as_str() == Some("close");
1819 if is_span_created || is_span_closed {
1820 target_override = Some(span.metadata().target().to_string());
1821 if is_span_closed {
1822 message = serde_json::Value::String("SPAN_CLOSED".to_string());
1823 }
1824 }
1825
1826 visitor.fields.insert(
1827 "span_name".to_string(),
1828 serde_json::Value::String(span.name().to_string()),
1829 );
1830
1831 if let Some(tracing_context) = ext.get::<DistributedTraceContext>() {
1832 visitor.fields.insert(
1833 "span_id".to_string(),
1834 serde_json::Value::String(tracing_context.span_id.clone()),
1835 );
1836 visitor.fields.insert(
1837 "trace_id".to_string(),
1838 serde_json::Value::String(tracing_context.trace_id.clone()),
1839 );
1840 if let Some(parent_id) = tracing_context.parent_id.clone() {
1841 visitor.fields.insert(
1842 "parent_id".to_string(),
1843 serde_json::Value::String(parent_id),
1844 );
1845 } else {
1846 visitor.fields.remove("parent_id");
1847 }
1848 if let Some(tracestate) = tracing_context.tracestate.clone() {
1849 visitor.fields.insert(
1850 "tracestate".to_string(),
1851 serde_json::Value::String(tracestate),
1852 );
1853 } else {
1854 visitor.fields.remove("tracestate");
1855 }
1856 if let Some(x_request_id) = tracing_context.x_request_id.clone() {
1857 visitor.fields.insert(
1858 "x_request_id".to_string(),
1859 serde_json::Value::String(x_request_id),
1860 );
1861 } else {
1862 visitor.fields.remove("x_request_id");
1863 }
1864
1865 if let Some(request_id) = tracing_context.request_id.clone() {
1866 visitor.fields.insert(
1867 "request_id".to_string(),
1868 serde_json::Value::String(request_id),
1869 );
1870 } else {
1871 visitor.fields.remove("request_id");
1872 }
1873 visitor.fields.remove("x_dynamo_request_id");
1875 } else {
1876 tracing::error!(
1877 "Distributed Trace Context not found, falling back to internal ids"
1878 );
1879 visitor.fields.insert(
1880 "span_id".to_string(),
1881 serde_json::Value::String(span.id().into_u64().to_string()),
1882 );
1883 if let Some(parent) = span.parent() {
1884 visitor.fields.insert(
1885 "parent_id".to_string(),
1886 serde_json::Value::String(parent.id().into_u64().to_string()),
1887 );
1888 }
1889 }
1890 } else {
1891 let reserved_fields = [
1892 "trace_id",
1893 "span_id",
1894 "parent_id",
1895 "span_name",
1896 "tracestate",
1897 ];
1898 for reserved_field in reserved_fields {
1899 visitor.fields.remove(reserved_field);
1900 }
1901 }
1902 let metadata = event.metadata();
1903 let log = JsonLog {
1904 level: metadata.level().to_string(),
1905 time,
1906 file: metadata.file(),
1907 line: metadata.line(),
1908 target: target_override.unwrap_or_else(|| metadata.target().to_string()),
1909 message,
1910 fields: visitor.fields,
1911 };
1912 let json = serde_json::to_string(&log).unwrap();
1913 writeln!(writer, "{json}")
1914 }
1915}
1916
1917#[derive(Default)]
1918struct JsonVisitor {
1919 fields: BTreeMap<String, serde_json::Value>,
1920}
1921
1922impl tracing::field::Visit for JsonVisitor {
1923 fn record_debug(&mut self, field: &tracing::field::Field, value: &dyn std::fmt::Debug) {
1924 self.fields.insert(
1925 field.name().to_string(),
1926 serde_json::Value::String(format!("{value:?}")),
1927 );
1928 }
1929
1930 fn record_str(&mut self, field: &tracing::field::Field, value: &str) {
1931 if field.name() != "message" {
1932 match serde_json::from_str::<Value>(value) {
1933 Ok(json_val) => self.fields.insert(field.name().to_string(), json_val),
1934 Err(_) => self.fields.insert(field.name().to_string(), value.into()),
1935 };
1936 } else {
1937 self.fields.insert(field.name().to_string(), value.into());
1938 }
1939 }
1940
1941 fn record_bool(&mut self, field: &tracing::field::Field, value: bool) {
1942 self.fields
1943 .insert(field.name().to_string(), serde_json::Value::Bool(value));
1944 }
1945
1946 fn record_i64(&mut self, field: &tracing::field::Field, value: i64) {
1947 self.fields.insert(
1948 field.name().to_string(),
1949 serde_json::Value::Number(value.into()),
1950 );
1951 }
1952
1953 fn record_u64(&mut self, field: &tracing::field::Field, value: u64) {
1954 self.fields.insert(
1955 field.name().to_string(),
1956 serde_json::Value::Number(value.into()),
1957 );
1958 }
1959
1960 fn record_f64(&mut self, field: &tracing::field::Field, value: f64) {
1961 use serde_json::value::Number;
1962 self.fields.insert(
1963 field.name().to_string(),
1964 serde_json::Value::Number(Number::from_f64(value).unwrap_or(0.into())),
1965 );
1966 }
1967}
1968
1969#[cfg(test)]
1970pub mod tests {
1971 use super::*;
1972 use anyhow::{Result, anyhow};
1973 use chrono::{DateTime, Utc};
1974 use jsonschema::{Draft, JSONSchema};
1975 use serde_json::Value;
1976 use std::fs::File;
1977 use std::io::{BufRead, BufReader};
1978 use stdio_override::*;
1979 use tempfile::NamedTempFile;
1980
1981 #[test]
1982 fn unset_dyn_log_uses_configured_default() {
1983 let filter = targets_filter(&LoggingConfig::default(), None)
1984 .expect("an unset DYN_LOG should use Targets");
1985
1986 assert!(filter.would_enable("request_span", &tracing::Level::TRACE));
1987 assert!(filter.would_enable("unlisted_target", &tracing::Level::INFO));
1988 assert!(!filter.would_enable("unlisted_target", &tracing::Level::DEBUG));
1989 assert!(!filter.would_enable("tower", &tracing::Level::WARN));
1990 }
1991
1992 #[test]
1993 fn empty_dyn_log_uses_configured_default() {
1994 for dyn_log in ["", ",,"] {
1995 let filter = targets_filter(&LoggingConfig::default(), Some(dyn_log))
1996 .expect("an empty DYN_LOG should use Targets");
1997
1998 assert!(filter.would_enable("request_span", &tracing::Level::TRACE));
1999 assert!(filter.would_enable("unlisted_target", &tracing::Level::INFO));
2000 assert!(!filter.would_enable("unlisted_target", &tracing::Level::DEBUG));
2001 assert!(!filter.would_enable("tower", &tracing::Level::WARN));
2002 }
2003 }
2004
2005 #[test]
2006 fn target_only_dyn_log_does_not_use_configured_default() {
2007 let filter = targets_filter(
2008 &LoggingConfig::default(),
2009 Some("dynamo_runtime::logging=debug"),
2010 )
2011 .expect("target/level directives should use Targets");
2012
2013 assert!(filter.would_enable("request_span", &tracing::Level::TRACE));
2014 assert!(!filter.would_enable("unlisted_target", &tracing::Level::INFO));
2015 assert!(!filter.would_enable("unlisted_target", &tracing::Level::DEBUG));
2016 assert!(filter.would_enable("dynamo_runtime::logging::child", &tracing::Level::DEBUG));
2017 assert!(!filter.would_enable("tower", &tracing::Level::WARN));
2018 }
2019
2020 #[test]
2021 fn trailing_empty_dyn_log_segments_do_not_enable_trace() {
2022 let filter = targets_filter(
2023 &LoggingConfig::default(),
2024 Some("dynamo_runtime::logging=debug,"),
2025 )
2026 .expect("target/level directives should use Targets");
2027
2028 assert!(filter.would_enable("dynamo_runtime::logging", &tracing::Level::DEBUG));
2029 assert!(!filter.would_enable("unlisted_target", &tracing::Level::TRACE));
2030 }
2031
2032 #[test]
2033 fn dynamic_span_directives_fall_back_to_env_filter() {
2034 assert!(matches!(
2035 targets_filter(
2036 &LoggingConfig::default(),
2037 Some("dynamo_runtime[request{model=foo}]=debug"),
2038 ),
2039 Err(TargetsFilterFallback::Dynamic)
2040 ));
2041 }
2042
2043 #[test]
2044 fn span_selector_dyn_log_falls_back_to_env_filter() {
2045 assert!(matches!(
2046 targets_filter(
2047 &LoggingConfig::default(),
2048 Some("dynamo_runtime[request]=debug"),
2049 ),
2050 Err(TargetsFilterFallback::Dynamic)
2051 ));
2052 }
2053
2054 #[test]
2055 fn field_presence_dyn_log_falls_back_to_env_filter() {
2056 assert!(matches!(
2057 targets_filter(
2058 &LoggingConfig::default(),
2059 Some("dynamo_runtime[{request_id}]=debug"),
2060 ),
2061 Err(TargetsFilterFallback::Dynamic)
2062 ));
2063 }
2064
2065 #[test]
2066 fn dynamic_log_filters_fall_back_to_env_filter() {
2067 let config = LoggingConfig {
2068 log_level: DEFAULT_FILTER_LEVEL.to_string(),
2069 log_filters: HashMap::from([(
2070 "dynamo_runtime[request{model=foo}]".to_string(),
2071 "debug".to_string(),
2072 )]),
2073 };
2074
2075 assert!(matches!(
2076 targets_filter(&config, None),
2077 Err(TargetsFilterFallback::Dynamic)
2078 ));
2079 }
2080
2081 #[test]
2082 fn span_selector_log_filters_fall_back_to_env_filter() {
2083 let config = LoggingConfig {
2084 log_level: DEFAULT_FILTER_LEVEL.to_string(),
2085 log_filters: HashMap::from([(
2086 "dynamo_runtime[request]".to_string(),
2087 "debug".to_string(),
2088 )]),
2089 };
2090
2091 assert!(matches!(
2092 targets_filter(&config, None),
2093 Err(TargetsFilterFallback::Dynamic)
2094 ));
2095 }
2096
2097 #[test]
2098 fn field_presence_log_filters_fall_back_to_env_filter() {
2099 let config = LoggingConfig {
2100 log_level: DEFAULT_FILTER_LEVEL.to_string(),
2101 log_filters: HashMap::from([(
2102 "dynamo_runtime[{request_id}]".to_string(),
2103 "debug".to_string(),
2104 )]),
2105 };
2106
2107 assert!(matches!(
2108 targets_filter(&config, None),
2109 Err(TargetsFilterFallback::Dynamic)
2110 ));
2111 }
2112
2113 #[test]
2114 fn otlp_protocol_defaults_to_grpc() {
2115 assert_eq!(parse_otlp_protocol(None), OtlpProtocol::Grpc);
2116 assert_eq!(parse_otlp_protocol(Some("")), OtlpProtocol::Grpc);
2117 assert_eq!(parse_otlp_protocol(Some("grpc")), OtlpProtocol::Grpc);
2118 assert_eq!(
2119 parse_otlp_protocol(Some("http/protobuf")),
2120 OtlpProtocol::HttpProtobuf
2121 );
2122 assert_eq!(
2123 parse_otlp_protocol(Some("HTTP/PROTOBUF")),
2124 OtlpProtocol::HttpProtobuf
2125 );
2126 assert_eq!(parse_otlp_protocol(Some("bad")), OtlpProtocol::Grpc);
2127 }
2128
2129 #[test]
2130 fn otlp_signal_protocol_overrides_generic_protocol() {
2131 let generic_protocol = OtlpProtocol::Grpc;
2132 assert_eq!(
2133 resolve_signal_otlp_protocol(
2134 generic_protocol,
2135 Some("http/protobuf"),
2136 env_logging::otlp::OTEL_EXPORTER_OTLP_TRACES_PROTOCOL,
2137 ),
2138 OtlpProtocol::HttpProtobuf
2139 );
2140 assert_eq!(
2141 resolve_signal_otlp_protocol(
2142 generic_protocol,
2143 Some(""),
2144 env_logging::otlp::OTEL_EXPORTER_OTLP_TRACES_PROTOCOL,
2145 ),
2146 OtlpProtocol::Grpc
2147 );
2148 assert_eq!(
2149 resolve_signal_otlp_protocol(
2150 generic_protocol,
2151 None,
2152 env_logging::otlp::OTEL_EXPORTER_OTLP_TRACES_PROTOCOL,
2153 ),
2154 OtlpProtocol::Grpc
2155 );
2156 }
2157
2158 #[test]
2159 fn otlp_http_endpoint_appends_signal_paths_from_generic_endpoint() {
2160 assert_eq!(
2161 resolve_otlp_endpoint(
2162 OtlpProtocol::HttpProtobuf,
2163 None,
2164 Some("https://llm-observe.weizhipin.com".to_string()),
2165 "/v1/traces",
2166 ),
2167 "https://llm-observe.weizhipin.com/v1/traces"
2168 );
2169 assert_eq!(
2170 resolve_otlp_endpoint(
2171 OtlpProtocol::HttpProtobuf,
2172 None,
2173 Some("https://llm-observe.weizhipin.com/".to_string()),
2174 "/v1/logs",
2175 ),
2176 "https://llm-observe.weizhipin.com/v1/logs"
2177 );
2178 assert_eq!(
2179 resolve_otlp_endpoint(
2180 OtlpProtocol::HttpProtobuf,
2181 None,
2182 Some("https://llm-observe.weizhipin.com/v1/traces".to_string()),
2183 "/v1/traces",
2184 ),
2185 "https://llm-observe.weizhipin.com/v1/traces/v1/traces"
2186 );
2187 }
2188
2189 #[test]
2190 fn otlp_signal_endpoint_is_used_verbatim() {
2191 assert_eq!(
2192 resolve_otlp_endpoint(
2193 OtlpProtocol::HttpProtobuf,
2194 Some("https://collector.example/custom/traces".to_string()),
2195 Some("https://collector.example".to_string()),
2196 "/v1/traces",
2197 ),
2198 "https://collector.example/custom/traces"
2199 );
2200 }
2201
2202 #[test]
2203 fn otlp_grpc_endpoint_keeps_generic_endpoint_verbatim() {
2204 assert_eq!(
2205 resolve_otlp_endpoint(
2206 OtlpProtocol::Grpc,
2207 None,
2208 Some("http://otel-collector:4317".to_string()),
2209 "/v1/traces",
2210 ),
2211 "http://otel-collector:4317"
2212 );
2213 }
2214
2215 #[test]
2216 fn trace_sample_ratio_is_optional_and_bounded() {
2217 assert_eq!(parse_trace_sample_ratio(None), None);
2218 assert_eq!(parse_trace_sample_ratio(Some("0")), Some(0.0));
2219 assert_eq!(parse_trace_sample_ratio(Some("0.01")), Some(0.01));
2220 assert_eq!(parse_trace_sample_ratio(Some("1")), Some(1.0));
2221 assert_eq!(parse_trace_sample_ratio(Some("-0.1")), None);
2222 assert_eq!(parse_trace_sample_ratio(Some("1.1")), None);
2223 assert_eq!(parse_trace_sample_ratio(Some("nan")), None);
2224 assert_eq!(parse_trace_sample_ratio(Some("bad")), None);
2225 }
2226
2227 static LOG_LINE_SCHEMA: &str = r#"
2228 {
2229 "$schema": "http://json-schema.org/draft-07/schema#",
2230 "title": "Runtime Log Line",
2231 "type": "object",
2232 "required": [
2233 "file",
2234 "level",
2235 "line",
2236 "message",
2237 "target",
2238 "time"
2239 ],
2240 "properties": {
2241 "file": { "type": "string" },
2242 "level": { "type": "string", "enum": ["ERROR", "WARN", "INFO", "DEBUG", "TRACE"] },
2243 "line": { "type": "integer" },
2244 "message": { "type": "string" },
2245 "target": { "type": "string" },
2246 "time": { "type": "string", "format": "date-time" },
2247 "span_id": { "type": "string", "pattern": "^[a-f0-9]{16}$" },
2248 "parent_id": { "type": "string", "pattern": "^[a-f0-9]{16}$" },
2249 "trace_id": { "type": "string", "pattern": "^[a-f0-9]{32}$" },
2250 "span_name": { "type": "string" },
2251 "time.busy_us": { "type": "integer" },
2252 "time.duration_us": { "type": "integer" },
2253 "time.idle_us": { "type": "integer" },
2254 "tracestate": { "type": "string" }
2255 },
2256 "additionalProperties": true
2257 }
2258 "#;
2259
2260 #[tracing::instrument(skip_all)]
2261 async fn parent() {
2262 tracing::trace!(message = "parent!");
2263 if let Some(my_ctx) = get_distributed_tracing_context() {
2264 tracing::info!(my_trace_id = my_ctx.trace_id);
2265 }
2266 child().await;
2267 }
2268
2269 #[tracing::instrument(skip_all)]
2270 async fn child() {
2271 tracing::trace!(message = "child");
2272 if let Some(my_ctx) = get_distributed_tracing_context() {
2273 tracing::info!(my_trace_id = my_ctx.trace_id);
2274 }
2275 grandchild().await;
2276 }
2277
2278 #[tracing::instrument(skip_all)]
2279 async fn grandchild() {
2280 tracing::trace!(message = "grandchild");
2281 if let Some(my_ctx) = get_distributed_tracing_context() {
2282 tracing::info!(my_trace_id = my_ctx.trace_id);
2283 }
2284 }
2285
2286 pub fn load_log(file_name: &str) -> Result<Vec<serde_json::Value>> {
2287 let schema_json: Value =
2288 serde_json::from_str(LOG_LINE_SCHEMA).expect("schema parse failure");
2289 let compiled_schema = JSONSchema::options()
2290 .with_draft(Draft::Draft7)
2291 .compile(&schema_json)
2292 .expect("Invalid schema");
2293
2294 let f = File::open(file_name)?;
2295 let reader = BufReader::new(f);
2296 let mut result = Vec::new();
2297
2298 for (line_num, line) in reader.lines().enumerate() {
2299 let line = line?;
2300 let val: Value = serde_json::from_str(&line)
2301 .map_err(|e| anyhow!("Line {}: invalid JSON: {}", line_num + 1, e))?;
2302
2303 if let Err(errors) = compiled_schema.validate(&val) {
2304 let errs = errors.map(|e| e.to_string()).collect::<Vec<_>>().join("; ");
2305 return Err(anyhow!(
2306 "Line {}: JSON Schema Validation errors: {}",
2307 line_num + 1,
2308 errs
2309 ));
2310 }
2311 println!("{}", val);
2312 result.push(val);
2313 }
2314 Ok(result)
2315 }
2316
2317 #[test]
2322 fn is_valid_version_accepts_00_to_fe_rejects_ff_and_malformed() {
2323 assert!(is_valid_version("00"));
2324 assert!(is_valid_version("01"));
2325 assert!(is_valid_version("fe")); assert!(!is_valid_version("ff")); assert!(!is_valid_version("FF")); assert!(!is_valid_version("zz")); assert!(!is_valid_version("0")); assert!(!is_valid_version("000")); assert!(!is_valid_version("")); }
2333
2334 #[test]
2335 fn is_valid_trace_id_requires_32_hex() {
2336 assert!(is_valid_trace_id(&"a".repeat(32)));
2337 assert!(is_valid_trace_id("0123456789abcdefABCDEF0123456789")); assert!(!is_valid_trace_id(&"1".repeat(31))); assert!(!is_valid_trace_id(&"1".repeat(33))); assert!(!is_valid_trace_id(&format!("{}g", "1".repeat(31)))); assert!(!is_valid_trace_id("")); }
2343
2344 #[test]
2345 fn is_valid_span_id_requires_16_hex() {
2346 assert!(is_valid_span_id(&"2".repeat(16)));
2347 assert!(!is_valid_span_id(&"2".repeat(15))); assert!(!is_valid_span_id(&"2".repeat(17))); assert!(!is_valid_span_id(&format!("{}g", "2".repeat(15)))); assert!(!is_valid_span_id("")); }
2352
2353 #[test]
2354 fn is_valid_trace_flags_requires_2_hex() {
2355 assert!(is_valid_trace_flags("00"));
2356 assert!(is_valid_trace_flags("ff")); assert!(is_valid_trace_flags("0A")); assert!(!is_valid_trace_flags("0")); assert!(!is_valid_trace_flags("000")); assert!(!is_valid_trace_flags("0x")); }
2362
2363 #[test]
2364 fn parse_traceparent_happy_path() {
2365 assert_eq!(
2367 parse_traceparent("00-11111111111111111111111111111111-2222222222222222-0A"),
2368 (
2369 Some("11111111111111111111111111111111".to_string()),
2370 Some("2222222222222222".to_string()),
2371 Some("0a".to_string()), )
2373 );
2374
2375 let (trace_id, _, trace_flags) =
2377 parse_traceparent("01-11111111111111111111111111111111-2222222222222222-01");
2378 assert_eq!(
2379 trace_id.as_deref(),
2380 Some("11111111111111111111111111111111")
2381 );
2382 assert_eq!(trace_flags.as_deref(), Some("01"));
2383 }
2384
2385 #[test]
2386 fn parse_traceparent_rejects_malformed() {
2387 assert_eq!(parse_traceparent("00-1111-2222"), (None, None, None)); assert_eq!(
2390 parse_traceparent("00-11111111111111111111111111111111-2222222222222222-00-extra"),
2391 (None, None, None)
2392 ); for tp in [
2397 "ff-11111111111111111111111111111111-2222222222222222-01", "00-bad-2222222222222222-01", "00-11111111111111111111111111111111-bad-01", "00-11111111111111111111111111111111-2222222222222222-0x", ] {
2402 assert_eq!(
2403 parse_traceparent(tp),
2404 (None, None, None),
2405 "should reject: {tp}"
2406 );
2407 }
2408 }
2409
2410 #[test]
2411 fn trace_parent_from_headers_preserves_unsampled_flag() {
2412 let mut headers = async_nats::HeaderMap::new();
2413 headers.insert(
2414 "traceparent",
2415 "00-11111111111111111111111111111111-2222222222222222-00",
2416 );
2417
2418 let trace_parent = TraceParent::from_headers(&headers);
2419
2420 assert_eq!(
2421 trace_parent.trace_id.as_deref(),
2422 Some("11111111111111111111111111111111")
2423 );
2424 assert_eq!(trace_parent.parent_id.as_deref(), Some("2222222222222222"));
2425 assert_eq!(trace_parent.trace_flags.as_deref(), Some("00"));
2426 }
2427
2428 #[test]
2429 fn distributed_context_creates_traceparent_with_stored_flags() {
2430 let context = DistributedTraceContext {
2431 trace_id: "11111111111111111111111111111111".to_string(),
2432 span_id: "2222222222222222".to_string(),
2433 trace_flags: "00".to_string(),
2434 parent_id: None,
2435 tracestate: None,
2436 start: None,
2437 end: None,
2438 x_request_id: None,
2439 request_id: None,
2440 };
2441
2442 assert_eq!(
2443 context.create_traceparent(),
2444 "00-11111111111111111111111111111111-2222222222222222-00"
2445 );
2446 }
2447
2448 #[test]
2449 fn inject_trace_headers_preserves_current_span_flags() {
2450 let _guard = tracing::subscriber::set_default(
2454 tracing_subscriber::registry().with(DistributedTraceIdLayer),
2455 );
2456 let span = tracing::info_span!(
2457 "root",
2458 trace_id = "11111111111111111111111111111111",
2459 span_id = "2222222222222222",
2460 trace_flags = "00"
2461 );
2462 let _enter = span.enter();
2463 let mut headers = std::collections::HashMap::new();
2464
2465 inject_trace_headers_into_map(&mut headers);
2466
2467 assert_eq!(
2468 headers.get("traceparent").map(String::as_str),
2469 Some("00-11111111111111111111111111111111-2222222222222222-00")
2470 );
2471 }
2472
2473 #[test]
2474 fn request_span_preserves_inbound_trace_flags() {
2475 let _guard = tracing::subscriber::set_default(
2479 tracing_subscriber::registry().with(DistributedTraceIdLayer),
2480 );
2481 let req = Request::builder()
2482 .header(
2483 "traceparent",
2484 "00-11111111111111111111111111111111-2222222222222222-00",
2485 )
2486 .body(())
2487 .unwrap();
2488 let trace_parent = TraceParent::from_headers(req.headers());
2489 let span = tracing::info_span!(
2490 "root",
2491 trace_id = trace_parent.trace_id,
2492 span_id = "3333333333333333",
2493 parent_id = trace_parent.parent_id,
2494 trace_flags = trace_parent.trace_flags
2495 );
2496 let _enter = span.enter();
2497 let mut headers = std::collections::HashMap::new();
2498
2499 inject_trace_headers_into_map(&mut headers);
2500
2501 assert_eq!(
2502 headers.get("traceparent").map(String::as_str),
2503 Some("00-11111111111111111111111111111111-3333333333333333-00")
2504 );
2505 }
2506
2507 #[test]
2508 fn root_context_uses_otel_unsampled_decision() {
2509 let provider = SdkTracerProvider::builder()
2510 .with_sampler(opentelemetry_sdk::trace::Sampler::AlwaysOff)
2511 .build();
2512 let tracer = provider.tracer("test");
2513 let _guard = tracing::subscriber::set_default(
2517 tracing_subscriber::registry()
2518 .with(tracing_opentelemetry::layer().with_tracer(tracer))
2519 .with(DistributedTraceIdLayer),
2520 );
2521 let span = tracing::info_span!("root");
2522 let _enter = span.enter();
2523 let mut headers = std::collections::HashMap::new();
2524
2525 inject_trace_headers_into_map(&mut headers);
2526
2527 assert!(headers["traceparent"].ends_with("-00"));
2528 assert_eq!(
2529 get_distributed_tracing_context()
2530 .as_ref()
2531 .map(|ctx| ctx.trace_flags.as_str()),
2532 Some("00")
2533 );
2534 }
2535
2536 #[test]
2537 fn root_context_uses_otel_sampled_decision() {
2538 let provider = SdkTracerProvider::builder()
2539 .with_sampler(opentelemetry_sdk::trace::Sampler::AlwaysOn)
2540 .build();
2541 let tracer = provider.tracer("test");
2542 let _guard = tracing::subscriber::set_default(
2546 tracing_subscriber::registry()
2547 .with(tracing_opentelemetry::layer().with_tracer(tracer))
2548 .with(DistributedTraceIdLayer),
2549 );
2550 let span = tracing::info_span!("root");
2551 let _enter = span.enter();
2552 let mut headers = std::collections::HashMap::new();
2553
2554 inject_trace_headers_into_map(&mut headers);
2555
2556 assert!(headers["traceparent"].ends_with("-01"));
2557 assert_eq!(
2558 get_distributed_tracing_context()
2559 .as_ref()
2560 .map(|ctx| ctx.trace_flags.as_str()),
2561 Some("01")
2562 );
2563 }
2564
2565 #[tokio::test]
2566 async fn test_json_log_capture() -> Result<()> {
2567 #[allow(clippy::redundant_closure_call)]
2568 let _ = temp_env::async_with_vars(
2569 [(env_logging::DYN_LOGGING_JSONL, Some("1"))],
2570 (async || {
2571 let tmp_file = NamedTempFile::new().unwrap();
2572 let file_name = tmp_file.path().to_str().unwrap();
2573 let guard = StderrOverride::from_file(file_name)?;
2574 init();
2575 parent().await;
2576 drop(guard);
2577
2578 let lines = load_log(file_name)?;
2579
2580 let Some(trace_id) = lines
2588 .iter()
2589 .find_map(|log_line| log_line.get("trace_id").and_then(|v| v.as_str()))
2590 .map(|s| s.to_string())
2591 else {
2592 return Ok(());
2594 };
2595
2596 assert_ne!(
2598 trace_id, "00000000000000000000000000000000",
2599 "trace_id should not be a zero/invalid ID"
2600 );
2601 assert!(
2602 !trace_id.chars().all(|c| c == '0'),
2603 "trace_id should not be all zeros"
2604 );
2605
2606 for log_line in &lines {
2608 if let Some(line_trace_id) = log_line.get("trace_id") {
2609 assert_eq!(
2610 line_trace_id.as_str().unwrap(),
2611 &trace_id,
2612 "All logs should have the same trace_id"
2613 );
2614 }
2615 }
2616
2617 for log_line in &lines {
2619 if let Some(my_trace_id) = log_line.get("my_trace_id") {
2620 assert_eq!(
2621 my_trace_id,
2622 &serde_json::Value::String(trace_id.clone()),
2623 "my_trace_id should match the trace_id from distributed tracing context"
2624 );
2625 }
2626 }
2627
2628 let mut span_ids_seen: std::collections::HashSet<String> = std::collections::HashSet::new();
2630 let mut span_timestamps: std::collections::HashMap<String, DateTime<Utc>> = std::collections::HashMap::new();
2631
2632 for log_line in &lines {
2633 if let Some(span_id) = log_line.get("span_id") {
2634 let span_id_str = span_id.as_str().unwrap();
2635 assert!(
2636 is_valid_span_id(span_id_str),
2637 "Invalid span_id format: {}",
2638 span_id_str
2639 );
2640 span_ids_seen.insert(span_id_str.to_string());
2641 }
2642
2643 if let Some(time_str) = log_line.get("time").and_then(|v| v.as_str()) {
2645 let timestamp = DateTime::parse_from_rfc3339(time_str)
2646 .expect("All timestamps should be valid RFC3339 format")
2647 .with_timezone(&Utc);
2648
2649 if let Some(span_name) = log_line.get("span_name").and_then(|v| v.as_str()) {
2651 span_timestamps.insert(span_name.to_string(), timestamp);
2652 }
2653 }
2654 }
2655
2656 let parent_span_id = lines
2659 .iter()
2660 .find(|log_line| {
2661 log_line.get("span_name")
2662 .and_then(|v| v.as_str()) == Some("parent")
2663 })
2664 .and_then(|log_line| {
2665 log_line.get("span_id")
2666 .and_then(|v| v.as_str())
2667 .map(|s| s.to_string())
2668 })
2669 .expect("Should find parent span with span_id");
2670
2671 let child_span_id = lines
2672 .iter()
2673 .find(|log_line| {
2674 log_line.get("span_name")
2675 .and_then(|v| v.as_str()) == Some("child")
2676 })
2677 .and_then(|log_line| {
2678 log_line.get("span_id")
2679 .and_then(|v| v.as_str())
2680 .map(|s| s.to_string())
2681 })
2682 .expect("Should find child span with span_id");
2683
2684 let grandchild_span_id = lines
2685 .iter()
2686 .find(|log_line| {
2687 log_line.get("span_name")
2688 .and_then(|v| v.as_str()) == Some("grandchild")
2689 })
2690 .and_then(|log_line| {
2691 log_line.get("span_id")
2692 .and_then(|v| v.as_str())
2693 .map(|s| s.to_string())
2694 })
2695 .expect("Should find grandchild span with span_id");
2696
2697 assert_ne!(parent_span_id, child_span_id, "Parent and child should have different span IDs");
2699 assert_ne!(child_span_id, grandchild_span_id, "Child and grandchild should have different span IDs");
2700 assert_ne!(parent_span_id, grandchild_span_id, "Parent and grandchild should have different span IDs");
2701
2702 for log_line in &lines {
2704 if let Some(span_name) = log_line.get("span_name")
2705 && let Some(span_name_str) = span_name.as_str()
2706 && span_name_str == "parent"
2707 {
2708 assert!(
2709 log_line.get("parent_id").is_none(),
2710 "Parent span should not have a parent_id"
2711 );
2712 }
2713 }
2714
2715 for log_line in &lines {
2717 if let Some(span_name) = log_line.get("span_name")
2718 && let Some(span_name_str) = span_name.as_str()
2719 && span_name_str == "child"
2720 {
2721 let parent_id = log_line.get("parent_id")
2722 .and_then(|v| v.as_str())
2723 .expect("Child span should have a parent_id");
2724 assert_eq!(
2725 parent_id,
2726 parent_span_id,
2727 "Child's parent_id should match parent's span_id"
2728 );
2729 }
2730 }
2731
2732 for log_line in &lines {
2734 if let Some(span_name) = log_line.get("span_name")
2735 && let Some(span_name_str) = span_name.as_str()
2736 && span_name_str == "grandchild"
2737 {
2738 let parent_id = log_line.get("parent_id")
2739 .and_then(|v| v.as_str())
2740 .expect("Grandchild span should have a parent_id");
2741 assert_eq!(
2742 parent_id,
2743 child_span_id,
2744 "Grandchild's parent_id should match child's span_id"
2745 );
2746 }
2747 }
2748
2749 let parent_time = span_timestamps.get("parent")
2751 .expect("Should have timestamp for parent span");
2752 let child_time = span_timestamps.get("child")
2753 .expect("Should have timestamp for child span");
2754 let grandchild_time = span_timestamps.get("grandchild")
2755 .expect("Should have timestamp for grandchild span");
2756
2757 assert!(
2759 parent_time <= child_time,
2760 "Parent span should log before or at same time as child span (parent: {}, child: {})",
2761 parent_time,
2762 child_time
2763 );
2764 assert!(
2765 child_time <= grandchild_time,
2766 "Child span should log before or at same time as grandchild span (child: {}, grandchild: {})",
2767 child_time,
2768 grandchild_time
2769 );
2770
2771 Ok::<(), anyhow::Error>(())
2772 })(),
2773 )
2774 .await;
2775 Ok(())
2776 }
2777
2778 #[test]
2779 fn test_otlp_export_works_without_json_logging() {
2780 use std::process::Command;
2781
2782 let output = Command::new("cargo")
2783 .args([
2784 "test",
2785 "-p",
2786 "dynamo-runtime",
2787 "logging::tests::test_otlp_export_without_json_logging_subprocess",
2788 "--",
2789 "--exact",
2790 "--nocapture",
2791 ])
2792 .env("OTEL_EXPORT_ENABLED", "1")
2793 .env_remove("DYN_LOGGING_JSONL")
2794 .output()
2795 .expect("Failed to execute subprocess test");
2796
2797 let stderr = String::from_utf8_lossy(&output.stderr);
2798 if !output.status.success() {
2799 eprintln!(
2800 "=== STDOUT ===\n{}",
2801 String::from_utf8_lossy(&output.stdout)
2802 );
2803 eprintln!("=== STDERR ===\n{}", stderr);
2804 }
2805
2806 assert!(
2807 output.status.success(),
2808 "Subprocess test failed with exit code: {:?}",
2809 output.status.code()
2810 );
2811 assert!(
2812 !stderr.contains("has no effect without DYN_LOGGING_JSONL"),
2813 "OTLP export should not depend on JSONL logging: {stderr}"
2814 );
2815 assert!(
2816 stderr.contains("OpenTelemetry OTLP export enabled"),
2817 "OTLP export should initialize with readable logging: {stderr}"
2818 );
2819 }
2820
2821 #[tokio::test]
2822 async fn test_otlp_export_without_json_logging_subprocess() {
2823 if std::env::var("OTEL_EXPORT_ENABLED").is_err() {
2824 return;
2825 }
2826
2827 init();
2828 tracing::info!("readable log with OTLP export");
2829 }
2830
2831 #[tracing::instrument(level = "debug", skip_all)]
2833 async fn debug_level_span() {
2834 tracing::debug!("inside debug span");
2835 }
2836
2837 #[tracing::instrument(level = "info", skip_all)]
2838 async fn info_level_span() {
2839 tracing::info!("inside info span");
2840 }
2841
2842 #[tracing::instrument(level = "warn", skip_all)]
2843 async fn warn_level_span() {
2844 tracing::warn!("inside warn span");
2845 }
2846
2847 #[tracing::instrument(level = "info", target = "other_module", skip_all)]
2850 async fn other_target_info_span() {
2851 tracing::info!(target: "other_module", "inside other target span");
2852 }
2853
2854 #[test]
2865 fn test_span_events() {
2866 use std::process::Command;
2867
2868 let output = Command::new("cargo")
2870 .args([
2871 "test",
2872 "-p",
2873 "dynamo-runtime",
2874 "test_span_events_subprocess",
2875 "--",
2876 "--exact",
2877 "--nocapture",
2878 ])
2879 .env("DYN_LOGGING_JSONL", "1")
2880 .env("DYN_LOGGING_SPAN_EVENTS", "1")
2881 .env("DYN_LOG", "warn,dynamo_runtime::logging::tests=debug")
2882 .output()
2883 .expect("Failed to execute subprocess test");
2884
2885 if !output.status.success() {
2887 eprintln!(
2888 "=== STDOUT ===\n{}",
2889 String::from_utf8_lossy(&output.stdout)
2890 );
2891 eprintln!(
2892 "=== STDERR ===\n{}",
2893 String::from_utf8_lossy(&output.stderr)
2894 );
2895 }
2896
2897 assert!(
2898 output.status.success(),
2899 "Subprocess test failed with exit code: {:?}",
2900 output.status.code()
2901 );
2902 }
2903
2904 #[tokio::test]
2907 async fn test_span_events_subprocess() -> Result<()> {
2908 if std::env::var("DYN_LOGGING_SPAN_EVENTS").is_err() {
2910 return Ok(());
2911 }
2912
2913 let tmp_file = NamedTempFile::new().unwrap();
2914 let file_name = tmp_file.path().to_str().unwrap();
2915 let guard = StderrOverride::from_file(file_name)?;
2916 init();
2917
2918 parent().await;
2920
2921 debug_level_span().await;
2923 info_level_span().await;
2924 warn_level_span().await;
2925
2926 other_target_info_span().await;
2928
2929 drop(guard);
2930
2931 let lines = load_log(file_name)?;
2932
2933 let has_span_event = |msg: &str, span_name: &str| {
2935 lines.iter().any(|log| {
2936 log.get("message").and_then(|v| v.as_str()) == Some(msg)
2937 && log.get("span_name").and_then(|v| v.as_str()) == Some(span_name)
2938 })
2939 };
2940
2941 let get_span_events = |msg: &str| -> Vec<&serde_json::Value> {
2943 lines
2944 .iter()
2945 .filter(|log| log.get("message").and_then(|v| v.as_str()) == Some(msg))
2946 .collect()
2947 };
2948
2949 let span_created_events = get_span_events("SPAN_FIRST_ENTRY");
2951 for event in &span_created_events {
2952 assert!(
2954 event.get("span_name").is_some(),
2955 "SPAN_FIRST_ENTRY must have span_name"
2956 );
2957 let trace_id = event
2959 .get("trace_id")
2960 .and_then(|v| v.as_str())
2961 .expect("SPAN_FIRST_ENTRY must have trace_id");
2962 assert!(
2963 trace_id.len() == 32 && trace_id.chars().all(|c| c.is_ascii_hexdigit()),
2964 "SPAN_FIRST_ENTRY must have valid trace_id format"
2965 );
2966 let span_id = event
2968 .get("span_id")
2969 .and_then(|v| v.as_str())
2970 .expect("SPAN_FIRST_ENTRY must have span_id");
2971 assert!(
2972 is_valid_span_id(span_id),
2973 "SPAN_FIRST_ENTRY must have valid span_id"
2974 );
2975 }
2976
2977 let span_closed_events = get_span_events("SPAN_CLOSED");
2979 for event in &span_closed_events {
2980 assert!(
2981 event.get("span_name").is_some(),
2982 "SPAN_CLOSED must have span_name"
2983 );
2984 assert!(
2985 event.get("time.busy_us").is_some()
2986 || event.get("time.idle_us").is_some()
2987 || event.get("time.duration_us").is_some(),
2988 "SPAN_CLOSED must have timing information"
2989 );
2990 let trace_id = event
2992 .get("trace_id")
2993 .and_then(|v| v.as_str())
2994 .expect("SPAN_CLOSED must have trace_id");
2995 assert!(
2996 trace_id.len() == 32 && trace_id.chars().all(|c| c.is_ascii_hexdigit()),
2997 "SPAN_CLOSED must have valid trace_id format"
2998 );
2999 }
3000
3001 assert!(
3005 has_span_event("SPAN_FIRST_ENTRY", "debug_level_span"),
3006 "DEBUG span from allowed target MUST pass (target=debug filter)"
3007 );
3008 assert!(
3009 has_span_event("SPAN_FIRST_ENTRY", "info_level_span"),
3010 "INFO span from allowed target MUST pass (target=debug filter)"
3011 );
3012 assert!(
3013 has_span_event("SPAN_FIRST_ENTRY", "warn_level_span"),
3014 "WARN span from allowed target MUST pass (target=debug filter)"
3015 );
3016
3017 assert!(
3019 has_span_event("SPAN_FIRST_ENTRY", "parent"),
3020 "parent span (INFO) from allowed target MUST pass"
3021 );
3022 assert!(
3023 has_span_event("SPAN_FIRST_ENTRY", "child"),
3024 "child span (INFO) from allowed target MUST pass"
3025 );
3026 assert!(
3027 has_span_event("SPAN_FIRST_ENTRY", "grandchild"),
3028 "grandchild span (INFO) from allowed target MUST pass"
3029 );
3030
3031 assert!(
3034 !has_span_event("SPAN_FIRST_ENTRY", "other_target_info_span"),
3035 "INFO span from non-allowed target (other_module) MUST be filtered out"
3036 );
3037
3038 for event in &span_created_events {
3040 let target = event.get("target").and_then(|v| v.as_str()).unwrap_or("");
3041 let level = event.get("level").and_then(|v| v.as_str()).unwrap_or("");
3042
3043 if level == "DEBUG" || level == "INFO" {
3045 assert!(
3046 target.contains("dynamo_runtime::logging::tests"),
3047 "DEBUG/INFO span must be from allowed target, got target={target}"
3048 );
3049 }
3050 }
3051
3052 Ok(())
3053 }
3054}