Skip to main content

dynamo_runtime/
logging.rs

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