Skip to main content

dynamo_runtime/
logging.rs

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