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