Skip to main content

dynamo_runtime/
logging.rs

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