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::fmt::time::FormatTime;
41use tracing_subscriber::fmt::time::LocalTime;
42use tracing_subscriber::fmt::time::SystemTime;
43use tracing_subscriber::fmt::time::UtcTime;
44use tracing_subscriber::fmt::{FmtContext, FormatFields};
45use tracing_subscriber::fmt::{FormattedFields, format::Writer};
46use tracing_subscriber::prelude::*;
47use tracing_subscriber::registry::LookupSpan;
48use tracing_subscriber::{filter::Directive, fmt};
49
50use crate::config::{
51 disable_ansi_logging, env_is_truthy, jsonl_logging_enabled, span_events_enabled,
52};
53use async_nats::{HeaderMap, HeaderValue};
54use axum::extract::FromRequestParts;
55use axum::http;
56use axum::http::Request;
57use axum::http::request::Parts;
58use serde_json::Value;
59use std::convert::Infallible;
60use std::time::Instant;
61use tower_http::trace::{DefaultMakeSpan, TraceLayer};
62use tracing::Id;
63use tracing::Span;
64use tracing::field::Field;
65use tracing::span;
66use tracing_subscriber::Layer;
67use tracing_subscriber::Registry;
68use tracing_subscriber::field::Visit;
69use tracing_subscriber::fmt::format::FmtSpan;
70use tracing_subscriber::layer::Context;
71use tracing_subscriber::registry::SpanData;
72use uuid::Uuid;
73
74use opentelemetry::propagation::{Extractor, Injector, TextMapPropagator};
75use opentelemetry::trace::{Span as OtelSpan, TraceContextExt};
76use opentelemetry::{global, trace::Tracer};
77use opentelemetry_appender_tracing::layer::OpenTelemetryTracingBridge;
78use opentelemetry_otlp::WithExportConfig;
79
80use opentelemetry::trace::TracerProvider as _;
81use opentelemetry::{Key, KeyValue};
82use opentelemetry_sdk::Resource;
83use opentelemetry_sdk::logs::SdkLoggerProvider;
84use opentelemetry_sdk::trace::Sampler;
85use opentelemetry_sdk::trace::SdkTracerProvider;
86use tracing::error;
87use tracing_subscriber::layer::SubscriberExt;
88use std::time::Duration;
91use tracing::{info, instrument};
92use tracing_opentelemetry::OpenTelemetrySpanExt;
93use tracing_subscriber::util::SubscriberInitExt;
94
95use crate::config::environment_names::logging as env_logging;
96
97const DEFAULT_FILTER_LEVEL: &str = "info";
99
100const DEFAULT_OTLP_ENDPOINT: &str = "http://localhost:4317";
102
103const DEFAULT_OTLP_HTTP_ENDPOINT: &str = "http://localhost:4318";
105
106const DEFAULT_OTEL_SERVICE_NAME: &str = "dynamo";
108
109static INIT: Once = Once::new();
111
112#[derive(Serialize, Deserialize, Debug)]
113struct LoggingConfig {
114 log_level: String,
115 log_filters: HashMap<String, String>,
116}
117impl Default for LoggingConfig {
118 fn default() -> Self {
119 LoggingConfig {
120 log_level: DEFAULT_FILTER_LEVEL.to_string(),
121 log_filters: HashMap::from([
122 ("h2".to_string(), "error".to_string()),
123 ("tower".to_string(), "error".to_string()),
124 ("hyper_util".to_string(), "error".to_string()),
125 ("neli".to_string(), "error".to_string()),
126 ("async_nats".to_string(), "error".to_string()),
127 ("rustls".to_string(), "error".to_string()),
128 ("tokenizers".to_string(), "error".to_string()),
129 ("axum".to_string(), "error".to_string()),
130 ("tonic".to_string(), "error".to_string()),
131 ("hf_hub".to_string(), "error".to_string()),
132 ("opentelemetry".to_string(), "error".to_string()),
133 ("opentelemetry-otlp".to_string(), "error".to_string()),
134 ("opentelemetry_sdk".to_string(), "error".to_string()),
135 ]),
136 }
137 }
138}
139
140fn otlp_exporter_enabled() -> bool {
142 env_is_truthy(env_logging::otlp::OTEL_EXPORT_ENABLED)
143}
144
145fn get_service_name() -> String {
147 std::env::var(env_logging::otlp::OTEL_SERVICE_NAME)
148 .unwrap_or_else(|_| DEFAULT_OTEL_SERVICE_NAME.to_string())
149}
150
151#[derive(Clone, Copy, Debug, Eq, PartialEq)]
152enum OtlpProtocol {
153 Grpc,
154 HttpProtobuf,
155}
156
157impl OtlpProtocol {
158 fn as_str(self) -> &'static str {
159 match self {
160 Self::Grpc => "grpc",
161 Self::HttpProtobuf => "http/protobuf",
162 }
163 }
164}
165
166fn parse_otlp_protocol_for_env(value: Option<&str>, env_name: &str) -> OtlpProtocol {
167 match value.map(str::trim).filter(|value| !value.is_empty()) {
168 None => OtlpProtocol::Grpc,
169 Some(value) if value.eq_ignore_ascii_case("grpc") => OtlpProtocol::Grpc,
170 Some(value) if value.eq_ignore_ascii_case("http/protobuf") => OtlpProtocol::HttpProtobuf,
171 Some(value) => {
172 eprintln!(
173 "WARNING: unsupported {} '{}'; falling back to grpc",
174 env_name, value
175 );
176 OtlpProtocol::Grpc
177 }
178 }
179}
180
181fn parse_otlp_protocol(value: Option<&str>) -> OtlpProtocol {
182 parse_otlp_protocol_for_env(value, env_logging::otlp::OTEL_EXPORTER_OTLP_PROTOCOL)
183}
184
185fn otlp_protocol_from_env() -> OtlpProtocol {
186 parse_otlp_protocol(
187 std::env::var(env_logging::otlp::OTEL_EXPORTER_OTLP_PROTOCOL)
188 .ok()
189 .as_deref(),
190 )
191}
192
193fn resolve_signal_otlp_protocol(
194 generic_protocol: OtlpProtocol,
195 signal_protocol: Option<&str>,
196 signal_protocol_env: &str,
197) -> OtlpProtocol {
198 match signal_protocol
199 .map(str::trim)
200 .filter(|value| !value.is_empty())
201 {
202 Some(value) => parse_otlp_protocol_for_env(Some(value), signal_protocol_env),
203 None => generic_protocol,
204 }
205}
206
207fn append_otlp_http_path(endpoint: &str, path: &str) -> String {
208 let endpoint = endpoint.trim_end_matches('/');
209 format!("{endpoint}{path}")
210}
211
212fn resolve_otlp_endpoint(
213 protocol: OtlpProtocol,
214 signal_endpoint: Option<String>,
215 generic_endpoint: Option<String>,
216 http_path: &str,
217) -> String {
218 if let Some(endpoint) = signal_endpoint.filter(|value| !value.trim().is_empty()) {
219 return endpoint;
220 }
221
222 match protocol {
223 OtlpProtocol::Grpc => generic_endpoint
224 .filter(|value| !value.trim().is_empty())
225 .unwrap_or_else(|| DEFAULT_OTLP_ENDPOINT.to_string()),
226 OtlpProtocol::HttpProtobuf => append_otlp_http_path(
227 generic_endpoint
228 .filter(|value| !value.trim().is_empty())
229 .as_deref()
230 .unwrap_or(DEFAULT_OTLP_HTTP_ENDPOINT),
231 http_path,
232 ),
233 }
234}
235
236fn parse_trace_sample_ratio(value: Option<&str>) -> Option<f64> {
237 let raw = value?;
238 match raw.parse::<f64>() {
239 Ok(value) if value.is_finite() && (0.0..=1.0).contains(&value) => Some(value),
240 _ => {
241 eprintln!(
242 "WARNING: invalid OTEL_TRACES_SAMPLE_RATIO '{}'; expected a number between 0.0 and 1.0, keeping default sampler",
243 raw
244 );
245 None
246 }
247 }
248}
249
250fn trace_sample_ratio_from_env() -> Option<f64> {
251 parse_trace_sample_ratio(
252 std::env::var(env_logging::otlp::OTEL_TRACES_SAMPLE_RATIO)
253 .ok()
254 .as_deref(),
255 )
256}
257
258fn build_span_exporter(
259 protocol: OtlpProtocol,
260 endpoint: &str,
261) -> Result<opentelemetry_otlp::SpanExporter, opentelemetry_otlp::ExporterBuildError> {
262 match protocol {
263 OtlpProtocol::Grpc => opentelemetry_otlp::SpanExporter::builder()
264 .with_tonic()
265 .with_endpoint(endpoint)
266 .build(),
267 OtlpProtocol::HttpProtobuf => opentelemetry_otlp::SpanExporter::builder()
268 .with_http()
269 .with_endpoint(endpoint)
270 .build(),
271 }
272}
273
274fn build_log_exporter(
275 protocol: OtlpProtocol,
276 endpoint: &str,
277) -> Result<opentelemetry_otlp::LogExporter, opentelemetry_otlp::ExporterBuildError> {
278 match protocol {
279 OtlpProtocol::Grpc => opentelemetry_otlp::LogExporter::builder()
280 .with_tonic()
281 .with_endpoint(endpoint)
282 .build(),
283 OtlpProtocol::HttpProtobuf => opentelemetry_otlp::LogExporter::builder()
284 .with_http()
285 .with_endpoint(endpoint)
286 .build(),
287 }
288}
289
290pub fn is_valid_trace_id(trace_id: &str) -> bool {
293 trace_id.len() == 32 && trace_id.chars().all(|c| c.is_ascii_hexdigit())
294}
295
296pub fn is_valid_span_id(span_id: &str) -> bool {
299 span_id.len() == 16 && span_id.chars().all(|c| c.is_ascii_hexdigit())
300}
301
302pub struct DistributedTraceIdLayer;
303
304#[derive(Debug, Clone, Serialize, Deserialize)]
305pub struct DistributedTraceContext {
306 pub trace_id: String,
307 pub span_id: String,
308 #[serde(
309 default = "default_trace_flags",
310 skip_serializing_if = "is_default_trace_flags"
311 )]
312 pub trace_flags: String,
313 #[serde(skip_serializing_if = "Option::is_none")]
314 pub parent_id: Option<String>,
315 #[serde(skip_serializing_if = "Option::is_none")]
316 pub tracestate: Option<String>,
317 #[serde(skip)]
318 start: Option<Instant>,
319 #[serde(skip)]
320 end: Option<Instant>,
321 #[serde(skip_serializing_if = "Option::is_none")]
322 pub x_request_id: Option<String>,
323 #[serde(skip_serializing_if = "Option::is_none")]
324 pub request_id: Option<String>,
325}
326
327#[derive(Debug, Clone)]
329struct PendingDistributedTraceContext {
330 trace_id: Option<String>,
331 span_id: Option<String>,
332 parent_id: Option<String>,
333 trace_flags: Option<String>,
334 tracestate: Option<String>,
335 x_request_id: Option<String>,
336 request_id: Option<String>,
337}
338
339macro_rules! emit_at_level {
341 ($level:expr, target: $target:expr, $($arg:tt)*) => {
342 match $level {
346 &tracing::Level::ERROR => tracing::event!(target: $target, tracing::Level::ERROR, $($arg)*),
347 &tracing::Level::WARN => tracing::event!(target: $target, tracing::Level::WARN, $($arg)*),
348 &tracing::Level::INFO => tracing::event!(target: $target, tracing::Level::INFO, $($arg)*),
349 &tracing::Level::DEBUG => tracing::event!(target: $target, tracing::Level::DEBUG, $($arg)*),
350 &tracing::Level::TRACE => tracing::event!(target: $target, tracing::Level::TRACE, $($arg)*),
351 }
352 };
353}
354
355impl DistributedTraceContext {
356 pub fn create_traceparent(&self) -> String {
358 format!(
359 "00-{}-{}-{}",
360 self.trace_id,
361 self.span_id,
362 normalize_trace_flags(&self.trace_flags)
363 )
364 }
365}
366
367fn default_trace_flags() -> String {
368 "01".to_string()
369}
370
371fn is_default_trace_flags(trace_flags: &str) -> bool {
372 trace_flags == "01"
373}
374
375fn is_valid_trace_flags(trace_flags: &str) -> bool {
376 trace_flags.len() == 2 && trace_flags.chars().all(|c| c.is_ascii_hexdigit())
377}
378
379fn is_valid_version(version: &str) -> bool {
383 version.len() == 2 && matches!(u8::from_str_radix(version, 16), Ok(v) if v != 0xff)
384}
385
386fn normalize_trace_flags(trace_flags: &str) -> String {
387 if is_valid_trace_flags(trace_flags) {
388 trace_flags.to_ascii_lowercase()
389 } else {
390 default_trace_flags()
391 }
392}
393
394fn current_otel_trace_flags() -> Option<String> {
395 let context = Span::current().context();
396 let span = context.span();
397 let span_context = span.span_context();
398 if !span_context.is_valid() {
399 return None;
400 }
401
402 Some(
403 if span_context.trace_flags().is_sampled() {
404 "01"
405 } else {
406 "00"
407 }
408 .to_string(),
409 )
410}
411
412pub fn parse_traceparent(traceparent: &str) -> (Option<String>, Option<String>, Option<String>) {
414 let pieces: Vec<_> = traceparent.split('-').collect();
415 if pieces.len() != 4 {
416 return (None, None, None);
417 }
418 let version = pieces[0];
419 let trace_id = pieces[1];
420 let parent_id = pieces[2];
421 let trace_flags = pieces[3];
422
423 if !is_valid_version(version)
424 || !is_valid_trace_id(trace_id)
425 || !is_valid_span_id(parent_id)
426 || !is_valid_trace_flags(trace_flags)
427 {
428 return (None, None, None);
429 }
430
431 (
432 Some(trace_id.to_string()),
433 Some(parent_id.to_string()),
434 Some(trace_flags.to_ascii_lowercase()),
435 )
436}
437
438#[derive(Debug, Clone, Default)]
439pub struct TraceParent {
440 pub trace_id: Option<String>,
441 pub parent_id: Option<String>,
442 pub trace_flags: Option<String>,
443 pub tracestate: Option<String>,
444 pub x_request_id: Option<String>,
445 pub request_id: Option<String>,
446}
447
448pub trait GenericHeaders {
449 fn get(&self, key: &str) -> Option<&str>;
450}
451
452impl GenericHeaders for async_nats::HeaderMap {
453 fn get(&self, key: &str) -> Option<&str> {
454 async_nats::HeaderMap::get(self, key).map(|value| value.as_str())
455 }
456}
457
458impl GenericHeaders for http::HeaderMap {
459 fn get(&self, key: &str) -> Option<&str> {
460 http::HeaderMap::get(self, key).and_then(|value| value.to_str().ok())
461 }
462}
463
464impl TraceParent {
465 pub fn from_headers<H: GenericHeaders>(headers: &H) -> TraceParent {
466 let mut trace_id = None;
467 let mut parent_id = None;
468 let mut trace_flags = None;
469 let mut tracestate = None;
470 let mut x_request_id = None;
471 let mut request_id = None;
472
473 if let Some(header_value) = headers.get("traceparent") {
474 (trace_id, parent_id, trace_flags) = parse_traceparent(header_value);
475 }
476
477 if let Some(header_value) = headers.get("x-request-id") {
478 x_request_id = Some(header_value.to_string());
479 }
480
481 if let Some(header_value) = headers.get("tracestate") {
482 tracestate = Some(header_value.to_string());
483 }
484
485 if let Some(header_value) = headers.get("request-id") {
487 request_id = Some(header_value.to_string());
488 } else if let Some(header_value) = headers.get("x-dynamo-request-id") {
489 request_id = Some(header_value.to_string());
490 }
491
492 let request_id = request_id.filter(|id| uuid::Uuid::parse_str(id).is_ok());
493 TraceParent {
494 trace_id,
495 parent_id,
496 trace_flags,
497 tracestate,
498 x_request_id,
499 request_id,
500 }
501 }
502}
503
504pub fn make_inference_request_span<B>(req: &Request<B>) -> Span {
510 let method = req.method();
511 let uri = req.uri();
512 let version = format!("{:?}", req.version());
513 let trace_parent = TraceParent::from_headers(req.headers());
514
515 let otel_context = extract_otel_context_from_http_headers(req.headers());
516
517 let request_id = trace_parent
521 .request_id
522 .unwrap_or_else(|| Uuid::new_v4().to_string());
523
524 let span = tracing::info_span!(
525 target: "request_span",
526 "http-request",
527 method = %method,
528 uri = %uri,
529 version = %version,
530 trace_id = trace_parent.trace_id,
531 parent_id = trace_parent.parent_id,
532 trace_flags = trace_parent.trace_flags,
533 x_request_id = trace_parent.x_request_id,
534 request_id = %request_id,
535 model = tracing::field::Empty,
536 input_tokens = tracing::field::Empty,
537 output_tokens = tracing::field::Empty,
538 ttft_ms = tracing::field::Empty,
539 avg_itl_ms = tracing::field::Empty,
540 prefill_worker_id = tracing::field::Empty,
541 decode_worker_id = tracing::field::Empty,
542 );
543
544 if let Some(context) = otel_context {
545 let _ = span.set_parent(context);
546 }
547
548 span
549}
550
551pub fn make_system_request_span<B>(req: &Request<B>) -> Span {
558 let method = req.method();
559 let uri = req.uri();
560 let version = format!("{:?}", req.version());
561 let trace_parent = TraceParent::from_headers(req.headers());
562 let otel_context = extract_otel_context_from_http_headers(req.headers());
563
564 let request_id = trace_parent
566 .request_id
567 .unwrap_or_else(|| Uuid::new_v4().to_string());
568
569 let span = tracing::debug_span!(
570 target: "system_span",
571 "http-request",
572 method = %method,
573 uri = %uri,
574 version = %version,
575 trace_id = trace_parent.trace_id,
576 parent_id = trace_parent.parent_id,
577 trace_flags = trace_parent.trace_flags,
578 x_request_id = trace_parent.x_request_id,
579 request_id = %request_id,
580 model = tracing::field::Empty,
581 input_tokens = tracing::field::Empty,
582 output_tokens = tracing::field::Empty,
583 ttft_ms = tracing::field::Empty,
584 avg_itl_ms = tracing::field::Empty,
585 prefill_worker_id = tracing::field::Empty,
586 decode_worker_id = tracing::field::Empty,
587 );
588
589 if let Some(context) = otel_context {
590 let _ = span.set_parent(context);
591 }
592
593 span
594}
595
596fn extract_otel_context_from_http_headers(
598 headers: &http::HeaderMap,
599) -> Option<opentelemetry::Context> {
600 let traceparent_value = headers.get("traceparent")?.to_str().ok()?;
601
602 struct HttpHeaderExtractor<'a>(&'a http::HeaderMap);
603
604 impl<'a> Extractor for HttpHeaderExtractor<'a> {
605 fn get(&self, key: &str) -> Option<&str> {
606 self.0.get(key).and_then(|v| v.to_str().ok())
607 }
608
609 fn keys(&self) -> Vec<&str> {
610 vec!["traceparent", "tracestate"]
611 .into_iter()
612 .filter(|&key| self.0.get(key).is_some())
613 .collect()
614 }
615 }
616
617 if traceparent_value.is_empty() {
619 return None;
620 }
621
622 let extractor = HttpHeaderExtractor(headers);
623 let otel_context = TRACE_PROPAGATOR.extract(&extractor);
624
625 if otel_context.span().span_context().is_valid() {
626 Some(otel_context)
627 } else {
628 None
629 }
630}
631
632pub fn make_handle_payload_span(
634 headers: &async_nats::HeaderMap,
635 component: &str,
636 endpoint: &str,
637 namespace: &str,
638 instance_id: u64,
639) -> Span {
640 let (otel_context, trace_id, parent_span_id) = extract_otel_context_from_nats_headers(headers);
641 let trace_parent = TraceParent::from_headers(headers);
642
643 if let (Some(trace_id), Some(parent_id)) = (trace_id.as_ref(), parent_span_id.as_ref()) {
644 let span = tracing::info_span!(
645 target: "request_span",
646 "handle_payload",
647 trace_id = trace_id.as_str(),
648 parent_id = parent_id.as_str(),
649 trace_flags = trace_parent.trace_flags,
650 x_request_id = trace_parent.x_request_id,
651 request_id = trace_parent.request_id,
652 tracestate = trace_parent.tracestate,
653 component = component,
654 endpoint = endpoint,
655 namespace = namespace,
656 instance_id = instance_id,
657 );
658
659 if let Some(context) = otel_context {
660 let _ = span.set_parent(context);
661 }
662 span
663 } else {
664 tracing::info_span!(
665 target: "request_span",
666 "handle_payload",
667 trace_flags = trace_parent.trace_flags,
668 x_request_id = trace_parent.x_request_id,
669 request_id = trace_parent.request_id,
670 tracestate = trace_parent.tracestate,
671 component = component,
672 endpoint = endpoint,
673 namespace = namespace,
674 instance_id = instance_id,
675 )
676 }
677}
678
679pub fn make_handle_payload_span_from_tcp_headers(
681 headers: &std::collections::HashMap<String, String>,
682 component: &str,
683 endpoint: &str,
684 namespace: &str,
685 instance_id: u64,
686) -> Span {
687 let (otel_context, trace_id, parent_span_id) = extract_otel_context_from_tcp_headers(headers);
688 let x_request_id = headers.get("x-request-id").cloned();
689 let request_id = headers
690 .get("request-id")
691 .or_else(|| headers.get("x-dynamo-request-id"))
692 .filter(|id| uuid::Uuid::parse_str(id).is_ok())
693 .cloned();
694 let tracestate = headers.get("tracestate").cloned();
695 let trace_flags = headers.get("traceparent").and_then(|value| {
696 let (_, _, flags) = parse_traceparent(value);
697 flags
698 });
699
700 if let (Some(trace_id), Some(parent_id)) = (trace_id.as_ref(), parent_span_id.as_ref()) {
701 let span = tracing::info_span!(
702 target: "request_span",
703 "handle_payload",
704 trace_id = trace_id.as_str(),
705 parent_id = parent_id.as_str(),
706 trace_flags = trace_flags,
707 x_request_id = x_request_id,
708 request_id = request_id,
709 tracestate = tracestate,
710 component = component,
711 endpoint = endpoint,
712 namespace = namespace,
713 instance_id = instance_id,
714 );
715
716 if let Some(context) = otel_context {
717 let _ = span.set_parent(context);
718 }
719 span
720 } else {
721 tracing::info_span!(
722 target: "request_span",
723 "handle_payload",
724 trace_flags = trace_flags,
725 x_request_id = x_request_id,
726 request_id = request_id,
727 tracestate = tracestate,
728 component = component,
729 endpoint = endpoint,
730 namespace = namespace,
731 instance_id = instance_id,
732 )
733 }
734}
735
736fn extract_otel_context_from_tcp_headers(
738 headers: &std::collections::HashMap<String, String>,
739) -> (
740 Option<opentelemetry::Context>,
741 Option<String>,
742 Option<String>,
743) {
744 let traceparent_value = match headers.get("traceparent") {
745 Some(value) => value.as_str(),
746 None => return (None, None, None),
747 };
748
749 let (trace_id, parent_span_id, _) = parse_traceparent(traceparent_value);
750
751 struct TcpHeaderExtractor<'a>(&'a std::collections::HashMap<String, String>);
752
753 impl<'a> Extractor for TcpHeaderExtractor<'a> {
754 fn get(&self, key: &str) -> Option<&str> {
755 self.0.get(key).map(|s| s.as_str())
756 }
757
758 fn keys(&self) -> Vec<&str> {
759 vec!["traceparent", "tracestate"]
760 .into_iter()
761 .filter(|&key| self.0.get(key).is_some())
762 .collect()
763 }
764 }
765
766 let extractor = TcpHeaderExtractor(headers);
767 let otel_context = TRACE_PROPAGATOR.extract(&extractor);
768
769 let context_with_trace = if otel_context.span().span_context().is_valid() {
770 Some(otel_context)
771 } else {
772 None
773 };
774
775 (context_with_trace, trace_id, parent_span_id)
776}
777
778pub fn extract_otel_context_from_nats_headers(
780 headers: &async_nats::HeaderMap,
781) -> (
782 Option<opentelemetry::Context>,
783 Option<String>,
784 Option<String>,
785) {
786 let traceparent_value = match headers.get("traceparent") {
787 Some(value) => value.as_str(),
788 None => return (None, None, None),
789 };
790
791 let (trace_id, parent_span_id, _) = parse_traceparent(traceparent_value);
792
793 struct NatsHeaderExtractor<'a>(&'a async_nats::HeaderMap);
794
795 impl<'a> Extractor for NatsHeaderExtractor<'a> {
796 fn get(&self, key: &str) -> Option<&str> {
797 self.0.get(key).map(|value| value.as_str())
798 }
799
800 fn keys(&self) -> Vec<&str> {
801 vec!["traceparent", "tracestate"]
802 .into_iter()
803 .filter(|&key| self.0.get(key).is_some())
804 .collect()
805 }
806 }
807
808 let extractor = NatsHeaderExtractor(headers);
809 let otel_context = TRACE_PROPAGATOR.extract(&extractor);
810
811 let context_with_trace = if otel_context.span().span_context().is_valid() {
812 Some(otel_context)
813 } else {
814 None
815 };
816
817 (context_with_trace, trace_id, parent_span_id)
818}
819
820pub fn inject_otel_context_into_nats_headers(
822 headers: &mut async_nats::HeaderMap,
823 context: Option<opentelemetry::Context>,
824) {
825 let otel_context = context.unwrap_or_else(|| Span::current().context());
826
827 struct NatsHeaderInjector<'a>(&'a mut async_nats::HeaderMap);
828
829 impl<'a> Injector for NatsHeaderInjector<'a> {
830 fn set(&mut self, key: &str, value: String) {
831 self.0.insert(key, value);
832 }
833 }
834
835 let mut injector = NatsHeaderInjector(headers);
836 TRACE_PROPAGATOR.inject_context(&otel_context, &mut injector);
837}
838
839pub fn inject_current_trace_into_nats_headers(headers: &mut async_nats::HeaderMap) {
841 inject_otel_context_into_nats_headers(headers, None);
842}
843
844pub fn inject_trace_headers_into_map(headers: &mut std::collections::HashMap<String, String>) {
846 if let Some(trace_context) = get_distributed_tracing_context() {
847 headers.insert(
849 "traceparent".to_string(),
850 trace_context.create_traceparent(),
851 );
852
853 if let Some(tracestate) = trace_context.tracestate {
855 headers.insert("tracestate".to_string(), tracestate);
856 }
857
858 if let Some(x_request_id) = trace_context.x_request_id {
860 headers.insert("x-request-id".to_string(), x_request_id);
861 }
862 if let Some(request_id) = trace_context.request_id {
863 headers.insert("request-id".to_string(), request_id);
864 }
865 }
866}
867
868pub fn make_client_request_span(
870 operation: &str,
871 request_id: &str,
872 trace_context: Option<&DistributedTraceContext>,
873 instance_id: Option<&str>,
874) -> Span {
875 if let Some(ctx) = trace_context {
876 let mut headers = async_nats::HeaderMap::new();
877 headers.insert("traceparent", ctx.create_traceparent());
878
879 if let Some(ref tracestate) = ctx.tracestate {
880 headers.insert("tracestate", tracestate.as_str());
881 }
882
883 let (otel_context, _extracted_trace_id, _extracted_parent_span_id) =
884 extract_otel_context_from_nats_headers(&headers);
885
886 let span = if let Some(inst_id) = instance_id {
887 tracing::info_span!(
888 "client_request",
889 operation = operation,
890 request_id = request_id,
891 instance_id = inst_id,
892 trace_id = ctx.trace_id.as_str(),
893 parent_id = ctx.span_id.as_str(),
894 trace_flags = ctx.trace_flags.as_str(),
895 x_request_id = ctx.x_request_id.as_deref(),
896 )
897 } else {
898 tracing::info_span!(
899 "client_request",
900 operation = operation,
901 request_id = request_id,
902 trace_id = ctx.trace_id.as_str(),
903 parent_id = ctx.span_id.as_str(),
904 trace_flags = ctx.trace_flags.as_str(),
905 x_request_id = ctx.x_request_id.as_deref(),
906 )
907 };
908
909 if let Some(context) = otel_context {
910 let _ = span.set_parent(context);
911 }
912
913 span
914 } else if let Some(inst_id) = instance_id {
915 tracing::info_span!(
916 "client_request",
917 operation = operation,
918 request_id = request_id,
919 instance_id = inst_id,
920 )
921 } else {
922 tracing::info_span!(
923 "client_request",
924 operation = operation,
925 request_id = request_id,
926 )
927 }
928}
929
930#[derive(Debug, Default)]
931pub struct FieldVisitor {
932 pub fields: HashMap<String, String>,
933}
934
935impl Visit for FieldVisitor {
936 fn record_str(&mut self, field: &Field, value: &str) {
937 self.fields
938 .insert(field.name().to_string(), value.to_string());
939 }
940
941 fn record_debug(&mut self, field: &Field, value: &dyn std::fmt::Debug) {
942 self.fields
943 .insert(field.name().to_string(), format!("{:?}", value).to_string());
944 }
945}
946
947impl<S> Layer<S> for DistributedTraceIdLayer
948where
949 S: Subscriber + for<'a> tracing_subscriber::registry::LookupSpan<'a>,
950{
951 fn on_close(&self, id: Id, ctx: Context<'_, S>) {
954 if let Some(span) = ctx.span(&id) {
955 let mut extensions = span.extensions_mut();
956 if let Some(distributed_tracing_context) =
957 extensions.get_mut::<DistributedTraceContext>()
958 {
959 distributed_tracing_context.end = Some(Instant::now());
960 }
961 }
962 }
963
964 fn on_new_span(&self, attrs: &span::Attributes<'_>, id: &Id, ctx: Context<'_, S>) {
967 if let Some(span) = ctx.span(id) {
968 let mut trace_id: Option<String> = None;
969 let mut parent_id: Option<String> = None;
970 let mut span_id: Option<String> = None;
971 let mut trace_flags: Option<String> = None;
972 let mut x_request_id: Option<String> = None;
973 let mut request_id: Option<String> = None;
974 let mut tracestate: Option<String> = None;
975 let mut visitor = FieldVisitor::default();
976 attrs.record(&mut visitor);
977
978 if let Some(trace_id_input) = visitor.fields.get("trace_id") {
980 if !is_valid_trace_id(trace_id_input) {
981 tracing::trace!("trace id '{trace_id_input}' is not valid! Ignoring.");
982 } else {
983 trace_id = Some(trace_id_input.to_string());
984 }
985 }
986
987 if let Some(span_id_input) = visitor.fields.get("span_id") {
989 if !is_valid_span_id(span_id_input) {
990 tracing::trace!("span id '{span_id_input}' is not valid! Ignoring.");
991 } else {
992 span_id = Some(span_id_input.to_string());
993 }
994 }
995
996 if let Some(parent_id_input) = visitor.fields.get("parent_id") {
998 if !is_valid_span_id(parent_id_input) {
999 tracing::trace!("parent id '{parent_id_input}' is not valid! Ignoring.");
1000 } else {
1001 parent_id = Some(parent_id_input.to_string());
1002 }
1003 }
1004
1005 if let Some(trace_flags_input) = visitor.fields.get("trace_flags") {
1006 if !is_valid_trace_flags(trace_flags_input) {
1007 tracing::trace!("trace flags '{trace_flags_input}' are not valid! Ignoring.");
1008 } else {
1009 trace_flags = Some(trace_flags_input.to_ascii_lowercase());
1010 }
1011 }
1012
1013 if let Some(tracestate_input) = visitor.fields.get("tracestate") {
1015 tracestate = Some(tracestate_input.to_string());
1016 }
1017
1018 if let Some(x_request_id_input) = visitor.fields.get("x_request_id") {
1020 x_request_id = Some(x_request_id_input.to_string());
1021 }
1022
1023 if let Some(request_id_input) = visitor.fields.get("request_id") {
1025 request_id = Some(request_id_input.to_string());
1026 } else if let Some(x_request_id_input) = visitor.fields.get("x_dynamo_request_id") {
1027 request_id = Some(x_request_id_input.to_string());
1028 }
1029
1030 if parent_id.is_none()
1032 && let Some(parent_span_id) = ctx.current_span().id()
1033 && let Some(parent_span) = ctx.span(parent_span_id)
1034 {
1035 let parent_ext = parent_span.extensions();
1036 if let Some(parent_tracing_context) = parent_ext.get::<DistributedTraceContext>() {
1037 trace_id = Some(parent_tracing_context.trace_id.clone());
1038 parent_id = Some(parent_tracing_context.span_id.clone());
1039 if trace_flags.is_none() {
1040 trace_flags = Some(parent_tracing_context.trace_flags.clone());
1041 }
1042 tracestate = parent_tracing_context.tracestate.clone();
1043 if x_request_id.is_none() {
1044 x_request_id = parent_tracing_context.x_request_id.clone();
1045 }
1046 if request_id.is_none() {
1047 request_id = parent_tracing_context.request_id.clone();
1048 }
1049 }
1050 }
1051
1052 if (parent_id.is_some() || span_id.is_some()) && trace_id.is_none() {
1054 tracing::error!("parent id or span id are set but trace id is not set!");
1055 parent_id = None;
1057 span_id = None;
1058 }
1059
1060 let mut extensions = span.extensions_mut();
1062 extensions.insert(PendingDistributedTraceContext {
1063 trace_id,
1064 span_id,
1065 parent_id,
1066 trace_flags,
1067 tracestate,
1068 x_request_id,
1069 request_id,
1070 });
1071 }
1072 }
1073
1074 fn on_enter(&self, id: &Id, ctx: Context<'_, S>) {
1077 if let Some(span) = ctx.span(id) {
1078 {
1080 let extensions = span.extensions();
1081 if extensions.get::<DistributedTraceContext>().is_some() {
1082 return;
1083 }
1084 }
1085
1086 let mut extensions = span.extensions_mut();
1088 let pending = match extensions.remove::<PendingDistributedTraceContext>() {
1089 Some(p) => p,
1090 None => {
1091 tracing::error!("PendingDistributedTraceContext not found in on_enter");
1093 return;
1094 }
1095 };
1096
1097 let mut trace_id = pending.trace_id;
1098 let mut span_id = pending.span_id;
1099 let parent_id = pending.parent_id;
1100 let mut trace_flags = pending.trace_flags;
1101 let tracestate = pending.tracestate;
1102 let x_request_id = pending.x_request_id;
1103 let request_id = pending.request_id;
1104
1105 drop(extensions);
1108
1109 if trace_id.is_none() || span_id.is_none() {
1110 let extensions = span.extensions();
1111 if let Some(otel_data) = extensions.get::<tracing_opentelemetry::OtelData>() {
1112 if trace_id.is_none()
1114 && let Some(otel_trace_id) = otel_data.trace_id()
1115 {
1116 let trace_id_str = format!("{}", otel_trace_id);
1117 if is_valid_trace_id(&trace_id_str) {
1118 trace_id = Some(trace_id_str);
1119 }
1120 }
1121
1122 if span_id.is_none()
1124 && let Some(otel_span_id) = otel_data.span_id()
1125 {
1126 let span_id_str = format!("{}", otel_span_id);
1127 if is_valid_span_id(&span_id_str) {
1128 span_id = Some(span_id_str);
1129 }
1130 }
1131 }
1132 }
1133
1134 if trace_flags.is_none() {
1135 trace_flags = current_otel_trace_flags();
1136 }
1137
1138 if trace_id.is_none() {
1140 panic!(
1141 "trace_id is not set in on_enter - OtelData may not be properly initialized"
1142 );
1143 }
1144
1145 if span_id.is_none() {
1146 panic!("span_id is not set in on_enter - OtelData may not be properly initialized");
1147 }
1148
1149 let span_level = span.metadata().level();
1150 let mut extensions = span.extensions_mut();
1151 extensions.insert(DistributedTraceContext {
1152 trace_id: trace_id.expect("Trace ID must be set"),
1153 span_id: span_id.expect("Span ID must be set"),
1154 trace_flags: trace_flags.unwrap_or_else(default_trace_flags),
1155 parent_id,
1156 tracestate,
1157 start: Some(Instant::now()),
1158 end: None,
1159 x_request_id,
1160 request_id,
1161 });
1162
1163 drop(extensions);
1164
1165 if span_events_enabled() {
1168 emit_at_level!(span_level, target: "span_event", message = "SPAN_FIRST_ENTRY");
1169 }
1170 }
1171 }
1172}
1173
1174pub fn get_distributed_tracing_context() -> Option<DistributedTraceContext> {
1177 Span::current()
1178 .with_subscriber(|(id, subscriber)| {
1179 subscriber
1180 .downcast_ref::<Registry>()
1181 .and_then(|registry| registry.span_data(id))
1182 .and_then(|span_data| {
1183 let extensions = span_data.extensions();
1184 extensions.get::<DistributedTraceContext>().cloned()
1185 })
1186 })
1187 .flatten()
1188 .map(|mut context| {
1189 if let Some(trace_flags) = current_otel_trace_flags() {
1193 context.trace_flags = trace_flags;
1194 }
1195 context
1196 })
1197}
1198
1199pub fn init() {
1201 INIT.call_once(|| {
1202 if let Err(e) = setup_logging() {
1203 eprintln!("Failed to initialize logging: {}", e);
1204 std::process::exit(1);
1205 }
1206 });
1207}
1208
1209#[cfg(feature = "tokio-console")]
1210fn setup_logging() -> Result<(), Box<dyn std::error::Error>> {
1211 let tokio_console_layer = console_subscriber::ConsoleLayer::builder()
1212 .with_default_env()
1213 .server_addr(([0, 0, 0, 0], console_subscriber::Server::DEFAULT_PORT))
1214 .spawn();
1215 let tokio_console_target = tracing_subscriber::filter::Targets::new()
1216 .with_default(LevelFilter::ERROR)
1217 .with_target("runtime", LevelFilter::TRACE)
1218 .with_target("tokio", LevelFilter::TRACE);
1219 let l = fmt::layer()
1220 .with_ansi(!disable_ansi_logging())
1221 .event_format(fmt::format().compact().with_timer(TimeFormatter::new()))
1222 .with_writer(std::io::stderr)
1223 .with_filter(filters(load_config()));
1224 tracing_subscriber::registry()
1225 .with(l)
1226 .with(tokio_console_layer.with_filter(tokio_console_target))
1227 .init();
1228 Ok(())
1229}
1230
1231#[cfg(not(feature = "tokio-console"))]
1232fn setup_logging() -> Result<(), Box<dyn std::error::Error>> {
1233 let fmt_filter_layer = filters(load_config());
1234 let trace_filter_layer = filters(load_config());
1235 let otel_filter_layer = filters(load_config());
1236 let otel_logs_filter_layer = filters(load_config());
1237
1238 if jsonl_logging_enabled() {
1239 let span_events = if span_events_enabled() {
1240 FmtSpan::CLOSE
1241 } else {
1242 FmtSpan::NONE
1243 };
1244 let l = fmt::layer()
1245 .with_ansi(false)
1246 .with_span_events(span_events)
1247 .event_format(CustomJsonFormatter::new())
1248 .with_writer(std::io::stderr)
1249 .with_filter(fmt_filter_layer);
1250
1251 let service_name = get_service_name();
1253 let sample_ratio = trace_sample_ratio_from_env();
1254
1255 let (tracer_provider, logger_provider_opt, endpoint_opt) = if otlp_exporter_enabled() {
1257 let protocol = otlp_protocol_from_env();
1259 let traces_protocol = resolve_signal_otlp_protocol(
1260 protocol,
1261 std::env::var(env_logging::otlp::OTEL_EXPORTER_OTLP_TRACES_PROTOCOL)
1262 .ok()
1263 .as_deref(),
1264 env_logging::otlp::OTEL_EXPORTER_OTLP_TRACES_PROTOCOL,
1265 );
1266 let logs_protocol = resolve_signal_otlp_protocol(
1267 protocol,
1268 std::env::var(env_logging::otlp::OTEL_EXPORTER_OTLP_LOGS_PROTOCOL)
1269 .ok()
1270 .as_deref(),
1271 env_logging::otlp::OTEL_EXPORTER_OTLP_LOGS_PROTOCOL,
1272 );
1273 let generic_endpoint =
1274 std::env::var(env_logging::otlp::OTEL_EXPORTER_OTLP_ENDPOINT).ok();
1275 let traces_endpoint_env =
1276 std::env::var(env_logging::otlp::OTEL_EXPORTER_OTLP_TRACES_ENDPOINT).ok();
1277 let logs_endpoint_env =
1278 std::env::var(env_logging::otlp::OTEL_EXPORTER_OTLP_LOGS_ENDPOINT).ok();
1279 let traces_endpoint = resolve_otlp_endpoint(
1280 traces_protocol,
1281 traces_endpoint_env,
1282 generic_endpoint.clone(),
1283 "/v1/traces",
1284 );
1285 let logs_endpoint = resolve_otlp_endpoint(
1286 logs_protocol,
1287 logs_endpoint_env,
1288 generic_endpoint,
1289 "/v1/logs",
1290 );
1291
1292 let resource = opentelemetry_sdk::Resource::builder_empty()
1293 .with_service_name(service_name.clone())
1294 .build();
1295
1296 let span_exporter = build_span_exporter(traces_protocol, &traces_endpoint)?;
1297
1298 let mut tracer_provider_builder =
1299 opentelemetry_sdk::trace::SdkTracerProvider::builder()
1300 .with_batch_exporter(span_exporter)
1301 .with_resource(resource.clone());
1302 if let Some(sample_ratio) = sample_ratio {
1303 tracer_provider_builder = tracer_provider_builder.with_sampler(
1304 Sampler::ParentBased(Box::new(Sampler::TraceIdRatioBased(sample_ratio))),
1305 );
1306 }
1307 let tracer_provider = tracer_provider_builder.build();
1308
1309 let log_exporter = build_log_exporter(logs_protocol, &logs_endpoint)?;
1310
1311 let logger_provider = SdkLoggerProvider::builder()
1312 .with_batch_exporter(log_exporter)
1313 .with_resource(resource)
1314 .build();
1315
1316 (
1317 tracer_provider,
1318 Some(logger_provider),
1319 Some((traces_protocol, traces_endpoint)),
1320 )
1321 } else {
1322 let mut provider_builder = opentelemetry_sdk::trace::SdkTracerProvider::builder()
1324 .with_resource(
1325 opentelemetry_sdk::Resource::builder_empty()
1326 .with_service_name(service_name.clone())
1327 .build(),
1328 );
1329 if let Some(sample_ratio) = sample_ratio {
1330 provider_builder = provider_builder.with_sampler(Sampler::ParentBased(Box::new(
1331 Sampler::TraceIdRatioBased(sample_ratio),
1332 )));
1333 }
1334 let provider = provider_builder.build();
1335
1336 (provider, None, None)
1337 };
1338
1339 opentelemetry::global::set_tracer_provider(tracer_provider.clone());
1346
1347 let tracer = tracer_provider.tracer(service_name.clone());
1349
1350 let otel_logs_layer = logger_provider_opt
1352 .as_ref()
1353 .map(|lp| OpenTelemetryTracingBridge::new(lp).with_filter(otel_logs_filter_layer));
1354
1355 tracing_subscriber::registry()
1356 .with(
1357 tracing_opentelemetry::layer()
1358 .with_tracer(tracer)
1359 .with_filter(otel_filter_layer),
1360 )
1361 .with(otel_logs_layer)
1362 .with(DistributedTraceIdLayer.with_filter(trace_filter_layer))
1363 .with(l)
1364 .init();
1365
1366 if let Some((protocol, endpoint)) = endpoint_opt {
1368 tracing::info!(
1369 endpoint = %endpoint,
1370 protocol = %protocol.as_str(),
1371 service = %service_name,
1372 "OpenTelemetry OTLP export enabled (traces and logs)"
1373 );
1374 } else {
1375 tracing::info!(
1376 service = %service_name,
1377 "OpenTelemetry OTLP export disabled, traces local only"
1378 );
1379 }
1380 } else {
1381 if otlp_exporter_enabled() {
1385 eprintln!(
1386 "WARNING: OTEL_EXPORT_ENABLED=1 has no effect without DYN_LOGGING_JSONL=1. \
1387 OTel layers and OTLP exporter are not installed. Set DYN_LOGGING_JSONL=1 \
1388 to enable trace/log export."
1389 );
1390 }
1391 let l = fmt::layer()
1392 .with_ansi(!disable_ansi_logging())
1393 .event_format(fmt::format().compact().with_timer(TimeFormatter::new()))
1394 .with_writer(std::io::stderr)
1395 .with_filter(fmt_filter_layer);
1396
1397 tracing_subscriber::registry().with(l).init();
1398 }
1399
1400 Ok(())
1401}
1402
1403fn filters(config: LoggingConfig) -> EnvFilter {
1404 let mut filter_layer = EnvFilter::builder()
1405 .with_default_directive(config.log_level.parse().unwrap())
1406 .with_env_var(env_logging::DYN_LOG)
1407 .from_env_lossy();
1408
1409 for (module, level) in config.log_filters {
1410 match format!("{module}={level}").parse::<Directive>() {
1411 Ok(d) => {
1412 filter_layer = filter_layer.add_directive(d);
1413 }
1414 Err(e) => {
1415 eprintln!("Failed parsing filter '{level}' for module '{module}': {e}");
1416 }
1417 }
1418 }
1419
1420 if span_events_enabled() {
1423 filter_layer = filter_layer.add_directive("span_event=trace".parse().unwrap());
1424 }
1425
1426 filter_layer = filter_layer.add_directive("request_span=trace".parse().unwrap());
1431
1432 filter_layer
1433}
1434
1435pub fn log_message(level: &str, message: &str, module: &str, file: &str, line: u32) {
1438 let level = match level {
1439 "debug" => log::Level::Debug,
1440 "info" => log::Level::Info,
1441 "warn" => log::Level::Warn,
1442 "error" => log::Level::Error,
1443 "warning" => log::Level::Warn,
1444 _ => log::Level::Info,
1445 };
1446 log::logger().log(
1447 &log::Record::builder()
1448 .args(format_args!("{}", message))
1449 .level(level)
1450 .target(module)
1451 .file(Some(file))
1452 .line(Some(line))
1453 .build(),
1454 );
1455}
1456
1457fn load_config() -> LoggingConfig {
1458 let config_path =
1459 std::env::var(env_logging::DYN_LOGGING_CONFIG_PATH).unwrap_or_else(|_| "".to_string());
1460 let figment = Figment::new()
1461 .merge(Serialized::defaults(LoggingConfig::default()))
1462 .merge(Toml::file("/opt/dynamo/etc/logging.toml"))
1463 .merge(Toml::file(config_path));
1464
1465 figment.extract().unwrap()
1466}
1467
1468#[derive(Serialize)]
1469struct JsonLog<'a> {
1470 time: String,
1471 level: String,
1472 #[serde(skip_serializing_if = "Option::is_none")]
1473 file: Option<&'a str>,
1474 #[serde(skip_serializing_if = "Option::is_none")]
1475 line: Option<u32>,
1476 target: String,
1477 message: serde_json::Value,
1478 #[serde(flatten)]
1479 fields: BTreeMap<String, serde_json::Value>,
1480}
1481
1482struct TimeFormatter {
1483 use_local_tz: bool,
1484}
1485
1486impl TimeFormatter {
1487 fn new() -> Self {
1488 Self {
1489 use_local_tz: crate::config::use_local_timezone(),
1490 }
1491 }
1492
1493 fn format_now(&self) -> String {
1494 if self.use_local_tz {
1495 chrono::Local::now()
1496 .format("%Y-%m-%dT%H:%M:%S%.6f%:z")
1497 .to_string()
1498 } else {
1499 chrono::Utc::now()
1500 .format("%Y-%m-%dT%H:%M:%S%.6fZ")
1501 .to_string()
1502 }
1503 }
1504}
1505
1506impl FormatTime for TimeFormatter {
1507 fn format_time(&self, w: &mut fmt::format::Writer<'_>) -> std::fmt::Result {
1508 write!(w, "{}", self.format_now())
1509 }
1510}
1511
1512struct CustomJsonFormatter {
1513 time_formatter: TimeFormatter,
1514}
1515
1516impl CustomJsonFormatter {
1517 fn new() -> Self {
1518 Self {
1519 time_formatter: TimeFormatter::new(),
1520 }
1521 }
1522}
1523
1524use once_cell::sync::Lazy;
1525use regex::Regex;
1526
1527static TRACE_PROPAGATOR: Lazy<opentelemetry_sdk::propagation::TraceContextPropagator> =
1529 Lazy::new(opentelemetry_sdk::propagation::TraceContextPropagator::new);
1530
1531fn parse_tracing_duration(s: &str) -> Option<u64> {
1532 static RE: Lazy<Regex> =
1533 Lazy::new(|| Regex::new(r#"^["']?\s*([0-9.]+)\s*(µs|us|ns|ms|s)\s*["']?$"#).unwrap());
1534 let captures = RE.captures(s)?;
1535 let value: f64 = captures[1].parse().ok()?;
1536 let unit = &captures[2];
1537 match unit {
1538 "ns" => Some((value / 1000.0) as u64),
1539 "µs" | "us" => Some(value as u64),
1540 "ms" => Some((value * 1000.0) as u64),
1541 "s" => Some((value * 1_000_000.0) as u64),
1542 _ => None,
1543 }
1544}
1545
1546impl<S, N> tracing_subscriber::fmt::FormatEvent<S, N> for CustomJsonFormatter
1547where
1548 S: Subscriber + for<'a> LookupSpan<'a>,
1549 N: for<'a> FormatFields<'a> + 'static,
1550{
1551 fn format_event(
1552 &self,
1553 ctx: &FmtContext<'_, S, N>,
1554 mut writer: Writer<'_>,
1555 event: &Event<'_>,
1556 ) -> std::fmt::Result {
1557 let mut visitor = JsonVisitor::default();
1558 let time = self.time_formatter.format_now();
1559 event.record(&mut visitor);
1560 let mut message = visitor
1561 .fields
1562 .remove("message")
1563 .unwrap_or(serde_json::Value::String("".to_string()));
1564
1565 let mut target_override: Option<String> = None;
1566
1567 let current_span = event
1568 .parent()
1569 .and_then(|id| ctx.span(id))
1570 .or_else(|| ctx.lookup_current());
1571 if let Some(span) = current_span {
1572 let ext = span.extensions();
1573 let data = ext.get::<FormattedFields<N>>().unwrap();
1574 let span_fields: Vec<(&str, &str)> = data
1575 .fields
1576 .split(' ')
1577 .filter_map(|entry| entry.split_once('='))
1578 .collect();
1579 for (name, value) in span_fields {
1580 visitor.fields.insert(
1581 name.to_string(),
1582 serde_json::Value::String(value.trim_matches('"').to_string()),
1583 );
1584 }
1585
1586 let busy_us = visitor
1587 .fields
1588 .remove("time.busy")
1589 .and_then(|v| parse_tracing_duration(&v.to_string()));
1590 let idle_us = visitor
1591 .fields
1592 .remove("time.idle")
1593 .and_then(|v| parse_tracing_duration(&v.to_string()));
1594
1595 if let (Some(busy_us), Some(idle_us)) = (busy_us, idle_us) {
1596 visitor.fields.insert(
1597 "time.busy_us".to_string(),
1598 serde_json::Value::Number(busy_us.into()),
1599 );
1600 visitor.fields.insert(
1601 "time.idle_us".to_string(),
1602 serde_json::Value::Number(idle_us.into()),
1603 );
1604 visitor.fields.insert(
1605 "time.duration_us".to_string(),
1606 serde_json::Value::Number((busy_us + idle_us).into()),
1607 );
1608 }
1609
1610 let is_span_created = message.as_str() == Some("SPAN_FIRST_ENTRY");
1611 let is_span_closed = message.as_str() == Some("close");
1612 if is_span_created || is_span_closed {
1613 target_override = Some(span.metadata().target().to_string());
1614 if is_span_closed {
1615 message = serde_json::Value::String("SPAN_CLOSED".to_string());
1616 }
1617 }
1618
1619 visitor.fields.insert(
1620 "span_name".to_string(),
1621 serde_json::Value::String(span.name().to_string()),
1622 );
1623
1624 if let Some(tracing_context) = ext.get::<DistributedTraceContext>() {
1625 visitor.fields.insert(
1626 "span_id".to_string(),
1627 serde_json::Value::String(tracing_context.span_id.clone()),
1628 );
1629 visitor.fields.insert(
1630 "trace_id".to_string(),
1631 serde_json::Value::String(tracing_context.trace_id.clone()),
1632 );
1633 if let Some(parent_id) = tracing_context.parent_id.clone() {
1634 visitor.fields.insert(
1635 "parent_id".to_string(),
1636 serde_json::Value::String(parent_id),
1637 );
1638 } else {
1639 visitor.fields.remove("parent_id");
1640 }
1641 if let Some(tracestate) = tracing_context.tracestate.clone() {
1642 visitor.fields.insert(
1643 "tracestate".to_string(),
1644 serde_json::Value::String(tracestate),
1645 );
1646 } else {
1647 visitor.fields.remove("tracestate");
1648 }
1649 if let Some(x_request_id) = tracing_context.x_request_id.clone() {
1650 visitor.fields.insert(
1651 "x_request_id".to_string(),
1652 serde_json::Value::String(x_request_id),
1653 );
1654 } else {
1655 visitor.fields.remove("x_request_id");
1656 }
1657
1658 if let Some(request_id) = tracing_context.request_id.clone() {
1659 visitor.fields.insert(
1660 "request_id".to_string(),
1661 serde_json::Value::String(request_id),
1662 );
1663 } else {
1664 visitor.fields.remove("request_id");
1665 }
1666 visitor.fields.remove("x_dynamo_request_id");
1668 } else {
1669 tracing::error!(
1670 "Distributed Trace Context not found, falling back to internal ids"
1671 );
1672 visitor.fields.insert(
1673 "span_id".to_string(),
1674 serde_json::Value::String(span.id().into_u64().to_string()),
1675 );
1676 if let Some(parent) = span.parent() {
1677 visitor.fields.insert(
1678 "parent_id".to_string(),
1679 serde_json::Value::String(parent.id().into_u64().to_string()),
1680 );
1681 }
1682 }
1683 } else {
1684 let reserved_fields = [
1685 "trace_id",
1686 "span_id",
1687 "parent_id",
1688 "span_name",
1689 "tracestate",
1690 ];
1691 for reserved_field in reserved_fields {
1692 visitor.fields.remove(reserved_field);
1693 }
1694 }
1695 let metadata = event.metadata();
1696 let log = JsonLog {
1697 level: metadata.level().to_string(),
1698 time,
1699 file: metadata.file(),
1700 line: metadata.line(),
1701 target: target_override.unwrap_or_else(|| metadata.target().to_string()),
1702 message,
1703 fields: visitor.fields,
1704 };
1705 let json = serde_json::to_string(&log).unwrap();
1706 writeln!(writer, "{json}")
1707 }
1708}
1709
1710#[derive(Default)]
1711struct JsonVisitor {
1712 fields: BTreeMap<String, serde_json::Value>,
1713}
1714
1715impl tracing::field::Visit for JsonVisitor {
1716 fn record_debug(&mut self, field: &tracing::field::Field, value: &dyn std::fmt::Debug) {
1717 self.fields.insert(
1718 field.name().to_string(),
1719 serde_json::Value::String(format!("{value:?}")),
1720 );
1721 }
1722
1723 fn record_str(&mut self, field: &tracing::field::Field, value: &str) {
1724 if field.name() != "message" {
1725 match serde_json::from_str::<Value>(value) {
1726 Ok(json_val) => self.fields.insert(field.name().to_string(), json_val),
1727 Err(_) => self.fields.insert(field.name().to_string(), value.into()),
1728 };
1729 } else {
1730 self.fields.insert(field.name().to_string(), value.into());
1731 }
1732 }
1733
1734 fn record_bool(&mut self, field: &tracing::field::Field, value: bool) {
1735 self.fields
1736 .insert(field.name().to_string(), serde_json::Value::Bool(value));
1737 }
1738
1739 fn record_i64(&mut self, field: &tracing::field::Field, value: i64) {
1740 self.fields.insert(
1741 field.name().to_string(),
1742 serde_json::Value::Number(value.into()),
1743 );
1744 }
1745
1746 fn record_u64(&mut self, field: &tracing::field::Field, value: u64) {
1747 self.fields.insert(
1748 field.name().to_string(),
1749 serde_json::Value::Number(value.into()),
1750 );
1751 }
1752
1753 fn record_f64(&mut self, field: &tracing::field::Field, value: f64) {
1754 use serde_json::value::Number;
1755 self.fields.insert(
1756 field.name().to_string(),
1757 serde_json::Value::Number(Number::from_f64(value).unwrap_or(0.into())),
1758 );
1759 }
1760}
1761
1762#[cfg(test)]
1763pub mod tests {
1764 use super::*;
1765 use anyhow::{Result, anyhow};
1766 use chrono::{DateTime, Utc};
1767 use jsonschema::{Draft, JSONSchema};
1768 use serde_json::Value;
1769 use std::fs::File;
1770 use std::io::{BufRead, BufReader};
1771 use stdio_override::*;
1772 use tempfile::NamedTempFile;
1773
1774 #[test]
1775 fn otlp_protocol_defaults_to_grpc() {
1776 assert_eq!(parse_otlp_protocol(None), OtlpProtocol::Grpc);
1777 assert_eq!(parse_otlp_protocol(Some("")), OtlpProtocol::Grpc);
1778 assert_eq!(parse_otlp_protocol(Some("grpc")), OtlpProtocol::Grpc);
1779 assert_eq!(
1780 parse_otlp_protocol(Some("http/protobuf")),
1781 OtlpProtocol::HttpProtobuf
1782 );
1783 assert_eq!(
1784 parse_otlp_protocol(Some("HTTP/PROTOBUF")),
1785 OtlpProtocol::HttpProtobuf
1786 );
1787 assert_eq!(parse_otlp_protocol(Some("bad")), OtlpProtocol::Grpc);
1788 }
1789
1790 #[test]
1791 fn otlp_signal_protocol_overrides_generic_protocol() {
1792 let generic_protocol = OtlpProtocol::Grpc;
1793 assert_eq!(
1794 resolve_signal_otlp_protocol(
1795 generic_protocol,
1796 Some("http/protobuf"),
1797 env_logging::otlp::OTEL_EXPORTER_OTLP_TRACES_PROTOCOL,
1798 ),
1799 OtlpProtocol::HttpProtobuf
1800 );
1801 assert_eq!(
1802 resolve_signal_otlp_protocol(
1803 generic_protocol,
1804 Some(""),
1805 env_logging::otlp::OTEL_EXPORTER_OTLP_TRACES_PROTOCOL,
1806 ),
1807 OtlpProtocol::Grpc
1808 );
1809 assert_eq!(
1810 resolve_signal_otlp_protocol(
1811 generic_protocol,
1812 None,
1813 env_logging::otlp::OTEL_EXPORTER_OTLP_TRACES_PROTOCOL,
1814 ),
1815 OtlpProtocol::Grpc
1816 );
1817 }
1818
1819 #[test]
1820 fn otlp_http_endpoint_appends_signal_paths_from_generic_endpoint() {
1821 assert_eq!(
1822 resolve_otlp_endpoint(
1823 OtlpProtocol::HttpProtobuf,
1824 None,
1825 Some("https://llm-observe.weizhipin.com".to_string()),
1826 "/v1/traces",
1827 ),
1828 "https://llm-observe.weizhipin.com/v1/traces"
1829 );
1830 assert_eq!(
1831 resolve_otlp_endpoint(
1832 OtlpProtocol::HttpProtobuf,
1833 None,
1834 Some("https://llm-observe.weizhipin.com/".to_string()),
1835 "/v1/logs",
1836 ),
1837 "https://llm-observe.weizhipin.com/v1/logs"
1838 );
1839 assert_eq!(
1840 resolve_otlp_endpoint(
1841 OtlpProtocol::HttpProtobuf,
1842 None,
1843 Some("https://llm-observe.weizhipin.com/v1/traces".to_string()),
1844 "/v1/traces",
1845 ),
1846 "https://llm-observe.weizhipin.com/v1/traces/v1/traces"
1847 );
1848 }
1849
1850 #[test]
1851 fn otlp_signal_endpoint_is_used_verbatim() {
1852 assert_eq!(
1853 resolve_otlp_endpoint(
1854 OtlpProtocol::HttpProtobuf,
1855 Some("https://collector.example/custom/traces".to_string()),
1856 Some("https://collector.example".to_string()),
1857 "/v1/traces",
1858 ),
1859 "https://collector.example/custom/traces"
1860 );
1861 }
1862
1863 #[test]
1864 fn otlp_grpc_endpoint_keeps_generic_endpoint_verbatim() {
1865 assert_eq!(
1866 resolve_otlp_endpoint(
1867 OtlpProtocol::Grpc,
1868 None,
1869 Some("http://otel-collector:4317".to_string()),
1870 "/v1/traces",
1871 ),
1872 "http://otel-collector:4317"
1873 );
1874 }
1875
1876 #[test]
1877 fn trace_sample_ratio_is_optional_and_bounded() {
1878 assert_eq!(parse_trace_sample_ratio(None), None);
1879 assert_eq!(parse_trace_sample_ratio(Some("0")), Some(0.0));
1880 assert_eq!(parse_trace_sample_ratio(Some("0.01")), Some(0.01));
1881 assert_eq!(parse_trace_sample_ratio(Some("1")), Some(1.0));
1882 assert_eq!(parse_trace_sample_ratio(Some("-0.1")), None);
1883 assert_eq!(parse_trace_sample_ratio(Some("1.1")), None);
1884 assert_eq!(parse_trace_sample_ratio(Some("nan")), None);
1885 assert_eq!(parse_trace_sample_ratio(Some("bad")), None);
1886 }
1887
1888 static LOG_LINE_SCHEMA: &str = r#"
1889 {
1890 "$schema": "http://json-schema.org/draft-07/schema#",
1891 "title": "Runtime Log Line",
1892 "type": "object",
1893 "required": [
1894 "file",
1895 "level",
1896 "line",
1897 "message",
1898 "target",
1899 "time"
1900 ],
1901 "properties": {
1902 "file": { "type": "string" },
1903 "level": { "type": "string", "enum": ["ERROR", "WARN", "INFO", "DEBUG", "TRACE"] },
1904 "line": { "type": "integer" },
1905 "message": { "type": "string" },
1906 "target": { "type": "string" },
1907 "time": { "type": "string", "format": "date-time" },
1908 "span_id": { "type": "string", "pattern": "^[a-f0-9]{16}$" },
1909 "parent_id": { "type": "string", "pattern": "^[a-f0-9]{16}$" },
1910 "trace_id": { "type": "string", "pattern": "^[a-f0-9]{32}$" },
1911 "span_name": { "type": "string" },
1912 "time.busy_us": { "type": "integer" },
1913 "time.duration_us": { "type": "integer" },
1914 "time.idle_us": { "type": "integer" },
1915 "tracestate": { "type": "string" }
1916 },
1917 "additionalProperties": true
1918 }
1919 "#;
1920
1921 #[tracing::instrument(skip_all)]
1922 async fn parent() {
1923 tracing::trace!(message = "parent!");
1924 if let Some(my_ctx) = get_distributed_tracing_context() {
1925 tracing::info!(my_trace_id = my_ctx.trace_id);
1926 }
1927 child().await;
1928 }
1929
1930 #[tracing::instrument(skip_all)]
1931 async fn child() {
1932 tracing::trace!(message = "child");
1933 if let Some(my_ctx) = get_distributed_tracing_context() {
1934 tracing::info!(my_trace_id = my_ctx.trace_id);
1935 }
1936 grandchild().await;
1937 }
1938
1939 #[tracing::instrument(skip_all)]
1940 async fn grandchild() {
1941 tracing::trace!(message = "grandchild");
1942 if let Some(my_ctx) = get_distributed_tracing_context() {
1943 tracing::info!(my_trace_id = my_ctx.trace_id);
1944 }
1945 }
1946
1947 pub fn load_log(file_name: &str) -> Result<Vec<serde_json::Value>> {
1948 let schema_json: Value =
1949 serde_json::from_str(LOG_LINE_SCHEMA).expect("schema parse failure");
1950 let compiled_schema = JSONSchema::options()
1951 .with_draft(Draft::Draft7)
1952 .compile(&schema_json)
1953 .expect("Invalid schema");
1954
1955 let f = File::open(file_name)?;
1956 let reader = BufReader::new(f);
1957 let mut result = Vec::new();
1958
1959 for (line_num, line) in reader.lines().enumerate() {
1960 let line = line?;
1961 let val: Value = serde_json::from_str(&line)
1962 .map_err(|e| anyhow!("Line {}: invalid JSON: {}", line_num + 1, e))?;
1963
1964 if let Err(errors) = compiled_schema.validate(&val) {
1965 let errs = errors.map(|e| e.to_string()).collect::<Vec<_>>().join("; ");
1966 return Err(anyhow!(
1967 "Line {}: JSON Schema Validation errors: {}",
1968 line_num + 1,
1969 errs
1970 ));
1971 }
1972 println!("{}", val);
1973 result.push(val);
1974 }
1975 Ok(result)
1976 }
1977
1978 #[test]
1983 fn is_valid_version_accepts_00_to_fe_rejects_ff_and_malformed() {
1984 assert!(is_valid_version("00"));
1985 assert!(is_valid_version("01"));
1986 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("")); }
1994
1995 #[test]
1996 fn is_valid_trace_id_requires_32_hex() {
1997 assert!(is_valid_trace_id(&"a".repeat(32)));
1998 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("")); }
2004
2005 #[test]
2006 fn is_valid_span_id_requires_16_hex() {
2007 assert!(is_valid_span_id(&"2".repeat(16)));
2008 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("")); }
2013
2014 #[test]
2015 fn is_valid_trace_flags_requires_2_hex() {
2016 assert!(is_valid_trace_flags("00"));
2017 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")); }
2023
2024 #[test]
2025 fn parse_traceparent_happy_path() {
2026 assert_eq!(
2028 parse_traceparent("00-11111111111111111111111111111111-2222222222222222-0A"),
2029 (
2030 Some("11111111111111111111111111111111".to_string()),
2031 Some("2222222222222222".to_string()),
2032 Some("0a".to_string()), )
2034 );
2035
2036 let (trace_id, _, trace_flags) =
2038 parse_traceparent("01-11111111111111111111111111111111-2222222222222222-01");
2039 assert_eq!(
2040 trace_id.as_deref(),
2041 Some("11111111111111111111111111111111")
2042 );
2043 assert_eq!(trace_flags.as_deref(), Some("01"));
2044 }
2045
2046 #[test]
2047 fn parse_traceparent_rejects_malformed() {
2048 assert_eq!(parse_traceparent("00-1111-2222"), (None, None, None)); assert_eq!(
2051 parse_traceparent("00-11111111111111111111111111111111-2222222222222222-00-extra"),
2052 (None, None, None)
2053 ); for tp in [
2058 "ff-11111111111111111111111111111111-2222222222222222-01", "00-bad-2222222222222222-01", "00-11111111111111111111111111111111-bad-01", "00-11111111111111111111111111111111-2222222222222222-0x", ] {
2063 assert_eq!(
2064 parse_traceparent(tp),
2065 (None, None, None),
2066 "should reject: {tp}"
2067 );
2068 }
2069 }
2070
2071 #[test]
2072 fn trace_parent_from_headers_preserves_unsampled_flag() {
2073 let mut headers = async_nats::HeaderMap::new();
2074 headers.insert(
2075 "traceparent",
2076 "00-11111111111111111111111111111111-2222222222222222-00",
2077 );
2078
2079 let trace_parent = TraceParent::from_headers(&headers);
2080
2081 assert_eq!(
2082 trace_parent.trace_id.as_deref(),
2083 Some("11111111111111111111111111111111")
2084 );
2085 assert_eq!(trace_parent.parent_id.as_deref(), Some("2222222222222222"));
2086 assert_eq!(trace_parent.trace_flags.as_deref(), Some("00"));
2087 }
2088
2089 #[test]
2090 fn distributed_context_creates_traceparent_with_stored_flags() {
2091 let context = DistributedTraceContext {
2092 trace_id: "11111111111111111111111111111111".to_string(),
2093 span_id: "2222222222222222".to_string(),
2094 trace_flags: "00".to_string(),
2095 parent_id: None,
2096 tracestate: None,
2097 start: None,
2098 end: None,
2099 x_request_id: None,
2100 request_id: None,
2101 };
2102
2103 assert_eq!(
2104 context.create_traceparent(),
2105 "00-11111111111111111111111111111111-2222222222222222-00"
2106 );
2107 }
2108
2109 #[test]
2110 fn inject_trace_headers_preserves_current_span_flags() {
2111 let _guard = tracing::subscriber::set_default(
2115 tracing_subscriber::registry().with(DistributedTraceIdLayer),
2116 );
2117 let span = tracing::info_span!(
2118 "root",
2119 trace_id = "11111111111111111111111111111111",
2120 span_id = "2222222222222222",
2121 trace_flags = "00"
2122 );
2123 let _enter = span.enter();
2124 let mut headers = std::collections::HashMap::new();
2125
2126 inject_trace_headers_into_map(&mut headers);
2127
2128 assert_eq!(
2129 headers.get("traceparent").map(String::as_str),
2130 Some("00-11111111111111111111111111111111-2222222222222222-00")
2131 );
2132 }
2133
2134 #[test]
2135 fn request_span_preserves_inbound_trace_flags() {
2136 let _guard = tracing::subscriber::set_default(
2140 tracing_subscriber::registry().with(DistributedTraceIdLayer),
2141 );
2142 let req = Request::builder()
2143 .header(
2144 "traceparent",
2145 "00-11111111111111111111111111111111-2222222222222222-00",
2146 )
2147 .body(())
2148 .unwrap();
2149 let trace_parent = TraceParent::from_headers(req.headers());
2150 let span = tracing::info_span!(
2151 "root",
2152 trace_id = trace_parent.trace_id,
2153 span_id = "3333333333333333",
2154 parent_id = trace_parent.parent_id,
2155 trace_flags = trace_parent.trace_flags
2156 );
2157 let _enter = span.enter();
2158 let mut headers = std::collections::HashMap::new();
2159
2160 inject_trace_headers_into_map(&mut headers);
2161
2162 assert_eq!(
2163 headers.get("traceparent").map(String::as_str),
2164 Some("00-11111111111111111111111111111111-3333333333333333-00")
2165 );
2166 }
2167
2168 #[test]
2169 fn root_context_uses_otel_unsampled_decision() {
2170 let provider = SdkTracerProvider::builder()
2171 .with_sampler(opentelemetry_sdk::trace::Sampler::AlwaysOff)
2172 .build();
2173 let tracer = provider.tracer("test");
2174 let _guard = tracing::subscriber::set_default(
2178 tracing_subscriber::registry()
2179 .with(tracing_opentelemetry::layer().with_tracer(tracer))
2180 .with(DistributedTraceIdLayer),
2181 );
2182 let span = tracing::info_span!("root");
2183 let _enter = span.enter();
2184 let mut headers = std::collections::HashMap::new();
2185
2186 inject_trace_headers_into_map(&mut headers);
2187
2188 assert!(headers["traceparent"].ends_with("-00"));
2189 assert_eq!(
2190 get_distributed_tracing_context()
2191 .as_ref()
2192 .map(|ctx| ctx.trace_flags.as_str()),
2193 Some("00")
2194 );
2195 }
2196
2197 #[test]
2198 fn root_context_uses_otel_sampled_decision() {
2199 let provider = SdkTracerProvider::builder()
2200 .with_sampler(opentelemetry_sdk::trace::Sampler::AlwaysOn)
2201 .build();
2202 let tracer = provider.tracer("test");
2203 let _guard = tracing::subscriber::set_default(
2207 tracing_subscriber::registry()
2208 .with(tracing_opentelemetry::layer().with_tracer(tracer))
2209 .with(DistributedTraceIdLayer),
2210 );
2211 let span = tracing::info_span!("root");
2212 let _enter = span.enter();
2213 let mut headers = std::collections::HashMap::new();
2214
2215 inject_trace_headers_into_map(&mut headers);
2216
2217 assert!(headers["traceparent"].ends_with("-01"));
2218 assert_eq!(
2219 get_distributed_tracing_context()
2220 .as_ref()
2221 .map(|ctx| ctx.trace_flags.as_str()),
2222 Some("01")
2223 );
2224 }
2225
2226 #[tokio::test]
2227 async fn test_json_log_capture() -> Result<()> {
2228 #[allow(clippy::redundant_closure_call)]
2229 let _ = temp_env::async_with_vars(
2230 [(env_logging::DYN_LOGGING_JSONL, Some("1"))],
2231 (async || {
2232 let tmp_file = NamedTempFile::new().unwrap();
2233 let file_name = tmp_file.path().to_str().unwrap();
2234 let guard = StderrOverride::from_file(file_name)?;
2235 init();
2236 parent().await;
2237 drop(guard);
2238
2239 let lines = load_log(file_name)?;
2240
2241 let Some(trace_id) = lines
2249 .iter()
2250 .find_map(|log_line| log_line.get("trace_id").and_then(|v| v.as_str()))
2251 .map(|s| s.to_string())
2252 else {
2253 return Ok(());
2255 };
2256
2257 assert_ne!(
2259 trace_id, "00000000000000000000000000000000",
2260 "trace_id should not be a zero/invalid ID"
2261 );
2262 assert!(
2263 !trace_id.chars().all(|c| c == '0'),
2264 "trace_id should not be all zeros"
2265 );
2266
2267 for log_line in &lines {
2269 if let Some(line_trace_id) = log_line.get("trace_id") {
2270 assert_eq!(
2271 line_trace_id.as_str().unwrap(),
2272 &trace_id,
2273 "All logs should have the same trace_id"
2274 );
2275 }
2276 }
2277
2278 for log_line in &lines {
2280 if let Some(my_trace_id) = log_line.get("my_trace_id") {
2281 assert_eq!(
2282 my_trace_id,
2283 &serde_json::Value::String(trace_id.clone()),
2284 "my_trace_id should match the trace_id from distributed tracing context"
2285 );
2286 }
2287 }
2288
2289 let mut span_ids_seen: std::collections::HashSet<String> = std::collections::HashSet::new();
2291 let mut span_timestamps: std::collections::HashMap<String, DateTime<Utc>> = std::collections::HashMap::new();
2292
2293 for log_line in &lines {
2294 if let Some(span_id) = log_line.get("span_id") {
2295 let span_id_str = span_id.as_str().unwrap();
2296 assert!(
2297 is_valid_span_id(span_id_str),
2298 "Invalid span_id format: {}",
2299 span_id_str
2300 );
2301 span_ids_seen.insert(span_id_str.to_string());
2302 }
2303
2304 if let Some(time_str) = log_line.get("time").and_then(|v| v.as_str()) {
2306 let timestamp = DateTime::parse_from_rfc3339(time_str)
2307 .expect("All timestamps should be valid RFC3339 format")
2308 .with_timezone(&Utc);
2309
2310 if let Some(span_name) = log_line.get("span_name").and_then(|v| v.as_str()) {
2312 span_timestamps.insert(span_name.to_string(), timestamp);
2313 }
2314 }
2315 }
2316
2317 let parent_span_id = lines
2320 .iter()
2321 .find(|log_line| {
2322 log_line.get("span_name")
2323 .and_then(|v| v.as_str()) == Some("parent")
2324 })
2325 .and_then(|log_line| {
2326 log_line.get("span_id")
2327 .and_then(|v| v.as_str())
2328 .map(|s| s.to_string())
2329 })
2330 .expect("Should find parent span with span_id");
2331
2332 let child_span_id = lines
2333 .iter()
2334 .find(|log_line| {
2335 log_line.get("span_name")
2336 .and_then(|v| v.as_str()) == Some("child")
2337 })
2338 .and_then(|log_line| {
2339 log_line.get("span_id")
2340 .and_then(|v| v.as_str())
2341 .map(|s| s.to_string())
2342 })
2343 .expect("Should find child span with span_id");
2344
2345 let grandchild_span_id = lines
2346 .iter()
2347 .find(|log_line| {
2348 log_line.get("span_name")
2349 .and_then(|v| v.as_str()) == Some("grandchild")
2350 })
2351 .and_then(|log_line| {
2352 log_line.get("span_id")
2353 .and_then(|v| v.as_str())
2354 .map(|s| s.to_string())
2355 })
2356 .expect("Should find grandchild span with span_id");
2357
2358 assert_ne!(parent_span_id, child_span_id, "Parent and child should have different span IDs");
2360 assert_ne!(child_span_id, grandchild_span_id, "Child and grandchild should have different span IDs");
2361 assert_ne!(parent_span_id, grandchild_span_id, "Parent and grandchild should have different span IDs");
2362
2363 for log_line in &lines {
2365 if let Some(span_name) = log_line.get("span_name")
2366 && let Some(span_name_str) = span_name.as_str()
2367 && span_name_str == "parent"
2368 {
2369 assert!(
2370 log_line.get("parent_id").is_none(),
2371 "Parent span should not have a parent_id"
2372 );
2373 }
2374 }
2375
2376 for log_line in &lines {
2378 if let Some(span_name) = log_line.get("span_name")
2379 && let Some(span_name_str) = span_name.as_str()
2380 && span_name_str == "child"
2381 {
2382 let parent_id = log_line.get("parent_id")
2383 .and_then(|v| v.as_str())
2384 .expect("Child span should have a parent_id");
2385 assert_eq!(
2386 parent_id,
2387 parent_span_id,
2388 "Child's parent_id should match parent's span_id"
2389 );
2390 }
2391 }
2392
2393 for log_line in &lines {
2395 if let Some(span_name) = log_line.get("span_name")
2396 && let Some(span_name_str) = span_name.as_str()
2397 && span_name_str == "grandchild"
2398 {
2399 let parent_id = log_line.get("parent_id")
2400 .and_then(|v| v.as_str())
2401 .expect("Grandchild span should have a parent_id");
2402 assert_eq!(
2403 parent_id,
2404 child_span_id,
2405 "Grandchild's parent_id should match child's span_id"
2406 );
2407 }
2408 }
2409
2410 let parent_time = span_timestamps.get("parent")
2412 .expect("Should have timestamp for parent span");
2413 let child_time = span_timestamps.get("child")
2414 .expect("Should have timestamp for child span");
2415 let grandchild_time = span_timestamps.get("grandchild")
2416 .expect("Should have timestamp for grandchild span");
2417
2418 assert!(
2420 parent_time <= child_time,
2421 "Parent span should log before or at same time as child span (parent: {}, child: {})",
2422 parent_time,
2423 child_time
2424 );
2425 assert!(
2426 child_time <= grandchild_time,
2427 "Child span should log before or at same time as grandchild span (child: {}, grandchild: {})",
2428 child_time,
2429 grandchild_time
2430 );
2431
2432 Ok::<(), anyhow::Error>(())
2433 })(),
2434 )
2435 .await;
2436 Ok(())
2437 }
2438
2439 #[tracing::instrument(level = "debug", skip_all)]
2441 async fn debug_level_span() {
2442 tracing::debug!("inside debug span");
2443 }
2444
2445 #[tracing::instrument(level = "info", skip_all)]
2446 async fn info_level_span() {
2447 tracing::info!("inside info span");
2448 }
2449
2450 #[tracing::instrument(level = "warn", skip_all)]
2451 async fn warn_level_span() {
2452 tracing::warn!("inside warn span");
2453 }
2454
2455 #[tracing::instrument(level = "info", target = "other_module", skip_all)]
2458 async fn other_target_info_span() {
2459 tracing::info!(target: "other_module", "inside other target span");
2460 }
2461
2462 #[test]
2473 fn test_span_events() {
2474 use std::process::Command;
2475
2476 let output = Command::new("cargo")
2478 .args([
2479 "test",
2480 "-p",
2481 "dynamo-runtime",
2482 "test_span_events_subprocess",
2483 "--",
2484 "--exact",
2485 "--nocapture",
2486 ])
2487 .env("DYN_LOGGING_JSONL", "1")
2488 .env("DYN_LOGGING_SPAN_EVENTS", "1")
2489 .env("DYN_LOG", "warn,dynamo_runtime::logging::tests=debug")
2490 .output()
2491 .expect("Failed to execute subprocess test");
2492
2493 if !output.status.success() {
2495 eprintln!(
2496 "=== STDOUT ===\n{}",
2497 String::from_utf8_lossy(&output.stdout)
2498 );
2499 eprintln!(
2500 "=== STDERR ===\n{}",
2501 String::from_utf8_lossy(&output.stderr)
2502 );
2503 }
2504
2505 assert!(
2506 output.status.success(),
2507 "Subprocess test failed with exit code: {:?}",
2508 output.status.code()
2509 );
2510 }
2511
2512 #[tokio::test]
2515 async fn test_span_events_subprocess() -> Result<()> {
2516 if std::env::var("DYN_LOGGING_SPAN_EVENTS").is_err() {
2518 return Ok(());
2519 }
2520
2521 let tmp_file = NamedTempFile::new().unwrap();
2522 let file_name = tmp_file.path().to_str().unwrap();
2523 let guard = StderrOverride::from_file(file_name)?;
2524 init();
2525
2526 parent().await;
2528
2529 debug_level_span().await;
2531 info_level_span().await;
2532 warn_level_span().await;
2533
2534 other_target_info_span().await;
2536
2537 drop(guard);
2538
2539 let lines = load_log(file_name)?;
2540
2541 let has_span_event = |msg: &str, span_name: &str| {
2543 lines.iter().any(|log| {
2544 log.get("message").and_then(|v| v.as_str()) == Some(msg)
2545 && log.get("span_name").and_then(|v| v.as_str()) == Some(span_name)
2546 })
2547 };
2548
2549 let get_span_events = |msg: &str| -> Vec<&serde_json::Value> {
2551 lines
2552 .iter()
2553 .filter(|log| log.get("message").and_then(|v| v.as_str()) == Some(msg))
2554 .collect()
2555 };
2556
2557 let span_created_events = get_span_events("SPAN_FIRST_ENTRY");
2559 for event in &span_created_events {
2560 assert!(
2562 event.get("span_name").is_some(),
2563 "SPAN_FIRST_ENTRY must have span_name"
2564 );
2565 let trace_id = event
2567 .get("trace_id")
2568 .and_then(|v| v.as_str())
2569 .expect("SPAN_FIRST_ENTRY must have trace_id");
2570 assert!(
2571 trace_id.len() == 32 && trace_id.chars().all(|c| c.is_ascii_hexdigit()),
2572 "SPAN_FIRST_ENTRY must have valid trace_id format"
2573 );
2574 let span_id = event
2576 .get("span_id")
2577 .and_then(|v| v.as_str())
2578 .expect("SPAN_FIRST_ENTRY must have span_id");
2579 assert!(
2580 is_valid_span_id(span_id),
2581 "SPAN_FIRST_ENTRY must have valid span_id"
2582 );
2583 }
2584
2585 let span_closed_events = get_span_events("SPAN_CLOSED");
2587 for event in &span_closed_events {
2588 assert!(
2589 event.get("span_name").is_some(),
2590 "SPAN_CLOSED must have span_name"
2591 );
2592 assert!(
2593 event.get("time.busy_us").is_some()
2594 || event.get("time.idle_us").is_some()
2595 || event.get("time.duration_us").is_some(),
2596 "SPAN_CLOSED must have timing information"
2597 );
2598 let trace_id = event
2600 .get("trace_id")
2601 .and_then(|v| v.as_str())
2602 .expect("SPAN_CLOSED must have trace_id");
2603 assert!(
2604 trace_id.len() == 32 && trace_id.chars().all(|c| c.is_ascii_hexdigit()),
2605 "SPAN_CLOSED must have valid trace_id format"
2606 );
2607 }
2608
2609 assert!(
2613 has_span_event("SPAN_FIRST_ENTRY", "debug_level_span"),
2614 "DEBUG span from allowed target MUST pass (target=debug filter)"
2615 );
2616 assert!(
2617 has_span_event("SPAN_FIRST_ENTRY", "info_level_span"),
2618 "INFO span from allowed target MUST pass (target=debug filter)"
2619 );
2620 assert!(
2621 has_span_event("SPAN_FIRST_ENTRY", "warn_level_span"),
2622 "WARN span from allowed target MUST pass (target=debug filter)"
2623 );
2624
2625 assert!(
2627 has_span_event("SPAN_FIRST_ENTRY", "parent"),
2628 "parent span (INFO) from allowed target MUST pass"
2629 );
2630 assert!(
2631 has_span_event("SPAN_FIRST_ENTRY", "child"),
2632 "child span (INFO) from allowed target MUST pass"
2633 );
2634 assert!(
2635 has_span_event("SPAN_FIRST_ENTRY", "grandchild"),
2636 "grandchild span (INFO) from allowed target MUST pass"
2637 );
2638
2639 assert!(
2642 !has_span_event("SPAN_FIRST_ENTRY", "other_target_info_span"),
2643 "INFO span from non-allowed target (other_module) MUST be filtered out"
2644 );
2645
2646 for event in &span_created_events {
2648 let target = event.get("target").and_then(|v| v.as_str()).unwrap_or("");
2649 let level = event.get("level").and_then(|v| v.as_str()).unwrap_or("");
2650
2651 if level == "DEBUG" || level == "INFO" {
2653 assert!(
2654 target.contains("dynamo_runtime::logging::tests"),
2655 "DEBUG/INFO span must be from allowed target, got target={target}"
2656 );
2657 }
2658 }
2659
2660 Ok(())
2661 }
2662}