contextvm-sdk 0.1.0

Rust SDK for the ContextVM protocol — MCP over Nostr
Documentation
//! Internal tracing subscriber setup for ContextVM transports.

use std::fmt;
use std::fs::{File, OpenOptions};
use std::io::{self, Write};
use std::path::{Path, PathBuf};
use std::sync::{Mutex, OnceLock};

use tracing::Event;
use tracing_subscriber::fmt::format::Writer;
use tracing_subscriber::fmt::writer::MakeWriter;
use tracing_subscriber::fmt::{FmtContext, FormatEvent, FormatFields};
use tracing_subscriber::layer::{Layer, SubscriberExt};
use tracing_subscriber::registry::LookupSpan;
use tracing_subscriber::{EnvFilter, Registry};

use crate::core::error::{Error, Result};

static TRACING_SETUP_LOCK: OnceLock<Mutex<()>> = OnceLock::new();
static TRACING_INITIALIZED: OnceLock<()> = OnceLock::new();
static LOG_DESTINATION: OnceLock<Mutex<LogDestination>> = OnceLock::new();

fn tracing_setup_lock() -> &'static Mutex<()> {
    TRACING_SETUP_LOCK.get_or_init(|| Mutex::new(()))
}

fn log_destination() -> &'static Mutex<LogDestination> {
    LOG_DESTINATION.get_or_init(|| Mutex::new(LogDestination::default()))
}

pub(crate) fn init_tracer(log_file_path: Option<&str>) -> Result<()> {
    let _guard = tracing_setup_lock()
        .lock()
        .map_err(|_| Error::Other("failed to acquire tracing setup lock".to_string()))?;

    configure_file_output(log_file_path)?;

    if TRACING_INITIALIZED.get().is_some() {
        return Ok(());
    }

    let subscriber = Registry::default().with(
        tracing_subscriber::fmt::layer()
            .with_ansi(false)
            .with_writer(ContextVmMakeWriter)
            .event_format(ContextVmEventFormatter)
            .with_filter(build_env_filter()),
    );

    match tracing::subscriber::set_global_default(subscriber) {
        Ok(()) => {
            let _ = TRACING_INITIALIZED.set(());
            Ok(())
        }
        Err(error) => {
            let text = error.to_string();
            if text.contains("global default trace dispatcher has already been set") {
                let _ = TRACING_INITIALIZED.set(());
                Ok(())
            } else {
                Err(Error::Other(format!(
                    "failed to initialize tracing subscriber: {text}"
                )))
            }
        }
    }
}

fn configure_file_output(log_file_path: Option<&str>) -> Result<()> {
    let Some(path) = normalize_log_file_path(log_file_path) else {
        return Ok(());
    };

    ensure_parent_exists(&path)?;

    let file = OpenOptions::new()
        .create(true)
        .append(true)
        .open(&path)
        .map_err(|error| {
            Error::Other(format!(
                "failed to open log file {}: {error}",
                path.display()
            ))
        })?;

    let mut destination = log_destination()
        .lock()
        .map_err(|_| Error::Other("failed to acquire log destination lock".to_string()))?;
    destination.file = Some(file);

    Ok(())
}

fn normalize_log_file_path(log_file_path: Option<&str>) -> Option<PathBuf> {
    let trimmed = log_file_path?.trim();
    if trimmed.is_empty() {
        None
    } else {
        Some(PathBuf::from(trimmed))
    }
}

fn ensure_parent_exists(path: &Path) -> Result<()> {
    if let Some(parent) = path.parent() {
        if !parent.as_os_str().is_empty() {
            std::fs::create_dir_all(parent).map_err(|error| {
                Error::Other(format!(
                    "failed to create log directory {}: {error}",
                    parent.display()
                ))
            })?;
        }
    }

    Ok(())
}

fn build_env_filter() -> EnvFilter {
    EnvFilter::try_from_default_env()
        .unwrap_or_else(|_| EnvFilter::new("contextvm_sdk=info,rmcp=warn"))
}

#[derive(Default)]
struct LogDestination {
    file: Option<File>,
}

#[derive(Clone, Copy)]
struct ContextVmMakeWriter;

impl<'a> MakeWriter<'a> for ContextVmMakeWriter {
    type Writer = ContextVmWriter;

    fn make_writer(&'a self) -> Self::Writer {
        ContextVmWriter {
            stdout: io::stdout(),
        }
    }
}

struct ContextVmWriter {
    stdout: io::Stdout,
}

impl Write for ContextVmWriter {
    fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
        self.stdout.write_all(buf)?;

        if let Ok(mut destination) = log_destination().lock() {
            if let Some(file) = destination.file.as_mut() {
                let _ = file.write_all(buf);
            }
        }

        Ok(buf.len())
    }

    fn flush(&mut self) -> io::Result<()> {
        self.stdout.flush()?;

        if let Ok(mut destination) = log_destination().lock() {
            if let Some(file) = destination.file.as_mut() {
                let _ = file.flush();
            }
        }

        Ok(())
    }
}

#[derive(Default)]
struct MessageVisitor {
    message: Option<String>,
    extra_fields: Vec<(String, String)>,
}

impl MessageVisitor {
    fn record_field(&mut self, name: &str, value: String) {
        if name == "message" {
            self.message = Some(value);
        } else {
            self.extra_fields.push((name.to_string(), value));
        }
    }
}

impl tracing::field::Visit for MessageVisitor {
    fn record_i64(&mut self, field: &tracing::field::Field, value: i64) {
        self.record_field(field.name(), value.to_string());
    }

    fn record_u64(&mut self, field: &tracing::field::Field, value: u64) {
        self.record_field(field.name(), value.to_string());
    }

    fn record_bool(&mut self, field: &tracing::field::Field, value: bool) {
        self.record_field(field.name(), value.to_string());
    }

    fn record_str(&mut self, field: &tracing::field::Field, value: &str) {
        self.record_field(field.name(), value.to_string());
    }

    fn record_error(
        &mut self,
        field: &tracing::field::Field,
        value: &(dyn std::error::Error + 'static),
    ) {
        self.record_field(field.name(), value.to_string());
    }

    fn record_debug(&mut self, field: &tracing::field::Field, value: &dyn fmt::Debug) {
        self.record_field(field.name(), format!("{value:?}"));
    }
}

struct ContextVmEventFormatter;

impl<S, N> FormatEvent<S, N> for ContextVmEventFormatter
where
    S: tracing::Subscriber + for<'span> LookupSpan<'span>,
    N: for<'writer> FormatFields<'writer> + 'static,
{
    fn format_event(
        &self,
        _ctx: &FmtContext<'_, S, N>,
        mut writer: Writer<'_>,
        event: &Event<'_>,
    ) -> fmt::Result {
        let mut visitor = MessageVisitor::default();
        event.record(&mut visitor);

        let metadata = event.metadata();
        let timestamp = unix_timestamp();
        let level = metadata.level().to_string().to_lowercase();
        let message = visitor.message.unwrap_or_default();

        write!(
            writer,
            "{timestamp}:{level}::{}:{message}",
            metadata.target()
        )?;

        for (key, value) in visitor.extra_fields {
            write!(writer, " {key}={value}")?;
        }

        writeln!(writer)
    }
}

fn unix_timestamp() -> String {
    use std::time::{SystemTime, UNIX_EPOCH};

    let now = SystemTime::now();
    let duration = now.duration_since(UNIX_EPOCH).unwrap_or_default();
    format!("{}.{:03}", duration.as_secs(), duration.subsec_millis())
}