Skip to content

Commit 352c50d

Browse files
Add unified file and LSP logging using tracing to server (#178)
1 parent fd0fc0a commit 352c50d

File tree

4 files changed

+149
-3
lines changed

4 files changed

+149
-3
lines changed

crates/djls-server/Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ serde_json = { workspace = true }
2121
tokio = { workspace = true }
2222
tower-lsp-server = { workspace = true }
2323
tracing = { workspace = true }
24+
tracing-appender = { workspace = true }
25+
tracing-subscriber = { workspace = true }
2426

2527
[build-dependencies]
2628
djls-dev = { workspace = true }

crates/djls-server/src/lib.rs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,12 @@ pub fn run() -> Result<()> {
4848

4949
let (service, socket) = LspService::build(|client| {
5050
client::init_client(client);
51-
DjangoLanguageServer::new()
51+
52+
let log_guard = logging::init_tracing(|message_type, message| {
53+
client::log_message(message_type, message);
54+
});
55+
56+
DjangoLanguageServer::new(log_guard)
5257
})
5358
.finish();
5459

crates/djls-server/src/logging.rs

Lines changed: 137 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,16 @@
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
214
//!
315
//! These macros bridge the gap during our migration from `client::log_message`
416
//! to the tracing infrastructure. They ensure messages are sent to both systems
@@ -27,6 +39,130 @@
2739
//! - For format strings, we format once for the client but pass the original
2840
//! format string and args to tracing to preserve structured data
2941
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+
30166
#[macro_export]
31167
macro_rules! log_info {
32168
($msg:literal) => {

crates/djls-server/src/server.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ use tower_lsp_server::lsp_types::TextDocumentSyncOptions;
2323
use tower_lsp_server::lsp_types::WorkspaceFoldersServerCapabilities;
2424
use tower_lsp_server::lsp_types::WorkspaceServerCapabilities;
2525
use tower_lsp_server::LanguageServer;
26+
use tracing_appender::non_blocking::WorkerGuard;
2627

2728
use crate::log_error;
2829
use crate::log_info;
@@ -35,14 +36,16 @@ const SERVER_VERSION: &str = "0.1.0";
3536
pub struct DjangoLanguageServer {
3637
session: Arc<RwLock<Option<Session>>>,
3738
queue: Queue,
39+
_log_guard: WorkerGuard,
3840
}
3941

4042
impl DjangoLanguageServer {
4143
#[must_use]
42-
pub fn new() -> Self {
44+
pub fn new(log_guard: WorkerGuard) -> Self {
4345
Self {
4446
session: Arc::new(RwLock::new(None)),
4547
queue: Queue::new(),
48+
_log_guard: log_guard,
4649
}
4750
}
4851

0 commit comments

Comments
 (0)