Add unified file and LSP logging using tracing to server (#178)

This commit is contained in:
Josh Thomas 2025-08-17 18:28:50 -05:00 committed by GitHub
parent fd0fc0a8d2
commit 352c50d1b4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 149 additions and 3 deletions

View file

@ -21,6 +21,8 @@ serde_json = { workspace = true }
tokio = { workspace = true }
tower-lsp-server = { workspace = true }
tracing = { workspace = true }
tracing-appender = { workspace = true }
tracing-subscriber = { workspace = true }
[build-dependencies]
djls-dev = { workspace = true }

View file

@ -48,7 +48,12 @@ pub fn run() -> Result<()> {
let (service, socket) = LspService::build(|client| {
client::init_client(client);
DjangoLanguageServer::new()
let log_guard = logging::init_tracing(|message_type, message| {
client::log_message(message_type, message);
});
DjangoLanguageServer::new(log_guard)
})
.finish();

View file

@ -1,4 +1,16 @@
//! Temporary logging macros for dual-dispatch to both LSP client and tracing.
//! Logging infrastructure bridging tracing events to LSP client messages.
//!
//! This module provides both temporary dual-dispatch macros and the permanent
//! `LspLayer` implementation for forwarding tracing events to the LSP client.
//!
//! ## `LspLayer`
//!
//! The `LspLayer` is a tracing `Layer` that intercepts tracing events and
//! forwards appropriate ones to the LSP client. It filters events by level:
//! - ERROR, WARN, INFO, DEBUG → forwarded to LSP client
//! - TRACE → kept server-side only (for performance)
//!
//! ## Temporary Macros
//!
//! These macros bridge the gap during our migration from `client::log_message`
//! to the tracing infrastructure. They ensure messages are sent to both systems
@ -27,6 +39,130 @@
//! - For format strings, we format once for the client but pass the original
//! format string and args to tracing to preserve structured data
use std::sync::Arc;
use tower_lsp_server::lsp_types::MessageType;
use tracing::field::Visit;
use tracing::Level;
use tracing_appender::non_blocking::WorkerGuard;
use tracing_subscriber::fmt;
use tracing_subscriber::layer::SubscriberExt;
use tracing_subscriber::util::SubscriberInitExt;
use tracing_subscriber::EnvFilter;
use tracing_subscriber::Layer;
use tracing_subscriber::Registry;
/// A tracing Layer that forwards events to the LSP client.
///
/// This layer intercepts tracing events and converts them to LSP log messages
/// that are sent to the client. It filters events by level to avoid overwhelming
/// the client with verbose trace logs.
pub struct LspLayer {
send_message: Arc<dyn Fn(MessageType, String) + Send + Sync>,
}
impl LspLayer {
pub fn new<F>(send_message: F) -> Self
where
F: Fn(MessageType, String) + Send + Sync + 'static,
{
Self {
send_message: Arc::new(send_message),
}
}
}
/// Visitor that extracts the message field from tracing events.
struct MessageVisitor {
message: Option<String>,
}
impl MessageVisitor {
fn new() -> Self {
Self { message: None }
}
}
impl Visit for MessageVisitor {
fn record_debug(&mut self, field: &tracing::field::Field, value: &dyn std::fmt::Debug) {
if field.name() == "message" {
self.message = Some(format!("{value:?}"));
}
}
fn record_str(&mut self, field: &tracing::field::Field, value: &str) {
if field.name() == "message" {
self.message = Some(value.to_string());
}
}
}
impl<S> Layer<S> for LspLayer
where
S: tracing::Subscriber,
{
fn on_event(
&self,
event: &tracing::Event<'_>,
_ctx: tracing_subscriber::layer::Context<'_, S>,
) {
let metadata = event.metadata();
let message_type = match *metadata.level() {
Level::ERROR => MessageType::ERROR,
Level::WARN => MessageType::WARNING,
Level::INFO => MessageType::INFO,
Level::DEBUG => MessageType::LOG,
Level::TRACE => {
// Skip TRACE level - too verbose for LSP client
// TODO: Add MessageType::Debug in LSP 3.18.0
return;
}
};
let mut visitor = MessageVisitor::new();
event.record(&mut visitor);
if let Some(message) = visitor.message {
(self.send_message)(message_type, message);
}
}
}
/// Initialize the dual-layer tracing subscriber.
///
/// Sets up:
/// - File layer: writes to /tmp/djls.log with daily rotation
/// - LSP layer: forwards INFO+ messages to the client
/// - `EnvFilter`: respects `RUST_LOG` env var, defaults to "info"
///
/// Returns a `WorkerGuard` that must be kept alive for the file logging to work.
pub fn init_tracing<F>(send_message: F) -> WorkerGuard
where
F: Fn(MessageType, String) + Send + Sync + 'static,
{
let file_appender = tracing_appender::rolling::daily("/tmp", "djls.log");
let (non_blocking, guard) = tracing_appender::non_blocking(file_appender);
let env_filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info"));
let file_layer = fmt::layer()
.with_writer(non_blocking)
.with_ansi(false)
.with_thread_ids(true)
.with_thread_names(true)
.with_target(true)
.with_file(true)
.with_line_number(true)
.with_filter(env_filter);
let lsp_layer =
LspLayer::new(send_message).with_filter(tracing_subscriber::filter::LevelFilter::INFO);
Registry::default().with(file_layer).with(lsp_layer).init();
guard
}
#[macro_export]
macro_rules! log_info {
($msg:literal) => {

View file

@ -23,6 +23,7 @@ use tower_lsp_server::lsp_types::TextDocumentSyncOptions;
use tower_lsp_server::lsp_types::WorkspaceFoldersServerCapabilities;
use tower_lsp_server::lsp_types::WorkspaceServerCapabilities;
use tower_lsp_server::LanguageServer;
use tracing_appender::non_blocking::WorkerGuard;
use crate::log_error;
use crate::log_info;
@ -35,14 +36,16 @@ const SERVER_VERSION: &str = "0.1.0";
pub struct DjangoLanguageServer {
session: Arc<RwLock<Option<Session>>>,
queue: Queue,
_log_guard: WorkerGuard,
}
impl DjangoLanguageServer {
#[must_use]
pub fn new() -> Self {
pub fn new(log_guard: WorkerGuard) -> Self {
Self {
session: Arc::new(RwLock::new(None)),
queue: Queue::new(),
_log_guard: log_guard,
}
}