|
1 | | -//! Temporary logging macros for dual-dispatch to both LSP client and tracing. |
| 1 | +//! Logging infrastructure bridging tracing events to LSP client messages. |
| 2 | +//! |
| 3 | +//! This module provides both temporary dual-dispatch macros and the permanent |
| 4 | +//! `LspLayer` implementation for forwarding tracing events to the LSP client. |
| 5 | +//! |
| 6 | +//! ## `LspLayer` |
| 7 | +//! |
| 8 | +//! The `LspLayer` is a tracing `Layer` that intercepts tracing events and |
| 9 | +//! forwards appropriate ones to the LSP client. It filters events by level: |
| 10 | +//! - ERROR, WARN, INFO, DEBUG → forwarded to LSP client |
| 11 | +//! - TRACE → kept server-side only (for performance) |
| 12 | +//! |
| 13 | +//! ## Temporary Macros |
2 | 14 | //! |
3 | 15 | //! These macros bridge the gap during our migration from `client::log_message` |
4 | 16 | //! to the tracing infrastructure. They ensure messages are sent to both systems |
|
27 | 39 | //! - For format strings, we format once for the client but pass the original |
28 | 40 | //! format string and args to tracing to preserve structured data |
29 | 41 |
|
| 42 | +use std::sync::Arc; |
| 43 | + |
| 44 | +use tower_lsp_server::lsp_types::MessageType; |
| 45 | +use tracing::field::Visit; |
| 46 | +use tracing::Level; |
| 47 | +use tracing_appender::non_blocking::WorkerGuard; |
| 48 | +use tracing_subscriber::fmt; |
| 49 | +use tracing_subscriber::layer::SubscriberExt; |
| 50 | +use tracing_subscriber::util::SubscriberInitExt; |
| 51 | +use tracing_subscriber::EnvFilter; |
| 52 | +use tracing_subscriber::Layer; |
| 53 | +use tracing_subscriber::Registry; |
| 54 | + |
| 55 | +/// A tracing Layer that forwards events to the LSP client. |
| 56 | +/// |
| 57 | +/// This layer intercepts tracing events and converts them to LSP log messages |
| 58 | +/// that are sent to the client. It filters events by level to avoid overwhelming |
| 59 | +/// the client with verbose trace logs. |
| 60 | +pub struct LspLayer { |
| 61 | + send_message: Arc<dyn Fn(MessageType, String) + Send + Sync>, |
| 62 | +} |
| 63 | + |
| 64 | +impl LspLayer { |
| 65 | + pub fn new<F>(send_message: F) -> Self |
| 66 | + where |
| 67 | + F: Fn(MessageType, String) + Send + Sync + 'static, |
| 68 | + { |
| 69 | + Self { |
| 70 | + send_message: Arc::new(send_message), |
| 71 | + } |
| 72 | + } |
| 73 | +} |
| 74 | + |
| 75 | +/// Visitor that extracts the message field from tracing events. |
| 76 | +struct MessageVisitor { |
| 77 | + message: Option<String>, |
| 78 | +} |
| 79 | + |
| 80 | +impl MessageVisitor { |
| 81 | + fn new() -> Self { |
| 82 | + Self { message: None } |
| 83 | + } |
| 84 | +} |
| 85 | + |
| 86 | +impl Visit for MessageVisitor { |
| 87 | + fn record_debug(&mut self, field: &tracing::field::Field, value: &dyn std::fmt::Debug) { |
| 88 | + if field.name() == "message" { |
| 89 | + self.message = Some(format!("{value:?}")); |
| 90 | + } |
| 91 | + } |
| 92 | + |
| 93 | + fn record_str(&mut self, field: &tracing::field::Field, value: &str) { |
| 94 | + if field.name() == "message" { |
| 95 | + self.message = Some(value.to_string()); |
| 96 | + } |
| 97 | + } |
| 98 | +} |
| 99 | + |
| 100 | +impl<S> Layer<S> for LspLayer |
| 101 | +where |
| 102 | + S: tracing::Subscriber, |
| 103 | +{ |
| 104 | + fn on_event( |
| 105 | + &self, |
| 106 | + event: &tracing::Event<'_>, |
| 107 | + _ctx: tracing_subscriber::layer::Context<'_, S>, |
| 108 | + ) { |
| 109 | + let metadata = event.metadata(); |
| 110 | + |
| 111 | + let message_type = match *metadata.level() { |
| 112 | + Level::ERROR => MessageType::ERROR, |
| 113 | + Level::WARN => MessageType::WARNING, |
| 114 | + Level::INFO => MessageType::INFO, |
| 115 | + Level::DEBUG => MessageType::LOG, |
| 116 | + Level::TRACE => { |
| 117 | + // Skip TRACE level - too verbose for LSP client |
| 118 | + // TODO: Add MessageType::Debug in LSP 3.18.0 |
| 119 | + return; |
| 120 | + } |
| 121 | + }; |
| 122 | + |
| 123 | + let mut visitor = MessageVisitor::new(); |
| 124 | + event.record(&mut visitor); |
| 125 | + |
| 126 | + if let Some(message) = visitor.message { |
| 127 | + (self.send_message)(message_type, message); |
| 128 | + } |
| 129 | + } |
| 130 | +} |
| 131 | + |
| 132 | +/// Initialize the dual-layer tracing subscriber. |
| 133 | +/// |
| 134 | +/// Sets up: |
| 135 | +/// - File layer: writes to /tmp/djls.log with daily rotation |
| 136 | +/// - LSP layer: forwards INFO+ messages to the client |
| 137 | +/// - `EnvFilter`: respects `RUST_LOG` env var, defaults to "info" |
| 138 | +/// |
| 139 | +/// Returns a `WorkerGuard` that must be kept alive for the file logging to work. |
| 140 | +pub fn init_tracing<F>(send_message: F) -> WorkerGuard |
| 141 | +where |
| 142 | + F: Fn(MessageType, String) + Send + Sync + 'static, |
| 143 | +{ |
| 144 | + let file_appender = tracing_appender::rolling::daily("/tmp", "djls.log"); |
| 145 | + let (non_blocking, guard) = tracing_appender::non_blocking(file_appender); |
| 146 | + |
| 147 | + let env_filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info")); |
| 148 | + let file_layer = fmt::layer() |
| 149 | + .with_writer(non_blocking) |
| 150 | + .with_ansi(false) |
| 151 | + .with_thread_ids(true) |
| 152 | + .with_thread_names(true) |
| 153 | + .with_target(true) |
| 154 | + .with_file(true) |
| 155 | + .with_line_number(true) |
| 156 | + .with_filter(env_filter); |
| 157 | + |
| 158 | + let lsp_layer = |
| 159 | + LspLayer::new(send_message).with_filter(tracing_subscriber::filter::LevelFilter::INFO); |
| 160 | + |
| 161 | + Registry::default().with(file_layer).with(lsp_layer).init(); |
| 162 | + |
| 163 | + guard |
| 164 | +} |
| 165 | + |
30 | 166 | #[macro_export] |
31 | 167 | macro_rules! log_info { |
32 | 168 | ($msg:literal) => { |
|
0 commit comments