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