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