1use std::collections::{HashMap, HashSet, VecDeque};
47use std::path::Path;
48use std::sync::Arc;
49use std::time::SystemTime;
50
51use std::sync::Mutex;
52
53use crate::events::LspLogPushed;
54
55#[derive(Debug, Clone, PartialEq, Eq, Hash)]
61pub struct InstanceKey {
62 pub server_id: Arc<str>,
63 pub workspace: Arc<Path>,
64}
65
66impl InstanceKey {
67 pub fn new(server_id: impl Into<Arc<str>>, workspace: impl Into<Arc<Path>>) -> Self {
71 Self {
72 server_id: server_id.into(),
73 workspace: workspace.into(),
74 }
75 }
76}
77
78pub fn format_log_event_line(
85 server_id: Option<&str>,
86 level: &str,
87 source: &str,
88 message: &str,
89) -> String {
90 let elapsed = SystemTime::now().duration_since(std::time::UNIX_EPOCH).ok();
91 let secs = elapsed.map(|d| d.as_secs()).unwrap_or(0);
92 let ms = elapsed.map(|d| d.subsec_millis()).unwrap_or(0);
93 let hh = (secs / 3600) % 24;
94 let mm = (secs / 60) % 60;
95 let ss = secs % 60;
96 let prefix = server_id.map(|id| format!("[{id}] ")).unwrap_or_default();
97 let msg = one_line(message);
98 format!("{hh:02}:{mm:02}:{ss:02}.{ms:03} {prefix}{level} {source:>6}: {msg}")
99}
100
101fn one_line(s: &str) -> String {
104 s.replace(['\n', '\r', '\t'], " ")
105}
106
107pub type LogEventPublisher = Arc<dyn Fn(LspLogPushed) + Send + Sync>;
114
115pub fn level_tag(l: LogLevel) -> &'static str {
118 match l {
119 LogLevel::Trace => "trace",
120 LogLevel::Debug => "debug",
121 LogLevel::Info => "info",
122 LogLevel::Warn => "warn",
123 LogLevel::Error => "error",
124 }
125}
126
127fn lock<'a, T>(m: &'a Mutex<T>) -> std::sync::MutexGuard<'a, T> {
132 m.lock().expect("LspLogger mutex poisoned")
133}
134
135#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
140pub enum LogLevel {
141 Trace,
145 Debug,
148 Info,
150 Warn,
152 Error,
154}
155
156impl LogLevel {
157 pub fn parse(s: &str) -> Option<Self> {
160 match s.trim().to_ascii_lowercase().as_str() {
161 "error" => Some(LogLevel::Error),
162 "warn" | "warning" => Some(LogLevel::Warn),
163 "info" => Some(LogLevel::Info),
164 "debug" => Some(LogLevel::Debug),
165 "trace" => Some(LogLevel::Trace),
166 _ => None,
167 }
168 }
169
170 pub fn all() -> &'static [LogLevel] {
172 &[
173 LogLevel::Trace,
174 LogLevel::Debug,
175 LogLevel::Info,
176 LogLevel::Warn,
177 LogLevel::Error,
178 ]
179 }
180
181 pub fn short(self) -> char {
183 match self {
184 LogLevel::Error => 'E',
185 LogLevel::Warn => 'W',
186 LogLevel::Info => 'I',
187 LogLevel::Debug => 'D',
188 LogLevel::Trace => 'T',
189 }
190 }
191}
192
193#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
195pub enum LogSource {
196 Telemetry,
203 Client,
206 Stderr,
208 LspMessage,
210 LspShowMessage,
213 Trace,
215}
216
217impl LogSource {
218 pub fn tag(self) -> &'static str {
220 match self {
221 LogSource::Client => "client",
222 LogSource::Stderr => "stderr",
223 LogSource::LspMessage => "log",
224 LogSource::LspShowMessage => "show",
225 LogSource::Trace => "trace",
226 LogSource::Telemetry => "telemetry",
227 }
228 }
229}
230
231#[derive(Debug, Clone)]
234pub struct LogRecord {
235 pub timestamp: SystemTime,
239 pub server_id: Option<Arc<str>>,
244 pub workspace: Option<Arc<Path>>,
249 pub level: LogLevel,
250 pub source: LogSource,
251 pub message: String,
252}
253
254#[derive(Debug)]
257pub struct LogRing {
258 buf: VecDeque<LogRecord>,
259 capacity: usize,
260}
261
262impl LogRing {
263 pub fn new(capacity: usize) -> Self {
266 Self {
267 buf: VecDeque::with_capacity(capacity.min(1024)),
268 capacity,
269 }
270 }
271
272 pub fn push(&mut self, record: LogRecord) {
274 if self.capacity == 0 {
275 return;
276 }
277 while self.buf.len() >= self.capacity {
278 self.buf.pop_front();
279 }
280 self.buf.push_back(record);
281 }
282
283 pub fn len(&self) -> usize {
285 self.buf.len()
286 }
287
288 pub fn is_empty(&self) -> bool {
289 self.buf.is_empty()
290 }
291
292 pub fn snapshot(&self) -> Vec<LogRecord> {
296 self.buf.iter().cloned().collect()
297 }
298
299 pub fn clear(&mut self) {
301 self.buf.clear();
302 }
303
304 pub fn set_capacity(&mut self, capacity: usize) {
307 self.capacity = capacity;
308 while self.buf.len() > self.capacity {
309 self.buf.pop_front();
310 }
311 }
312}
313
314#[derive(Clone)]
322pub struct LspLogger {
323 state: Arc<LoggerState>,
324}
325
326struct LoggerState {
327 global: Mutex<LogRing>,
330 per_instance: Mutex<HashMap<InstanceKey, LogRing>>,
334 event_publisher: Mutex<Option<LogEventPublisher>>,
338 default_capacity: Mutex<usize>,
340 default_min_level: Mutex<LogLevel>,
342 instance_levels: Mutex<HashMap<InstanceKey, LogLevel>>,
345 instance_trace: Mutex<HashSet<InstanceKey>>,
349}
350
351impl LspLogger {
352 pub fn new(default_min_level: LogLevel, default_capacity: usize) -> Self {
357 Self {
358 state: Arc::new(LoggerState {
359 global: Mutex::new(LogRing::new(default_capacity)),
360 per_instance: Mutex::new(HashMap::new()),
361 default_capacity: Mutex::new(default_capacity),
362 default_min_level: Mutex::new(default_min_level),
363 instance_levels: Mutex::new(HashMap::new()),
364 instance_trace: Mutex::new(HashSet::new()),
365 event_publisher: Mutex::new(None),
366 }),
367 }
368 }
369
370 pub fn set_event_publisher(&self, publisher: LogEventPublisher) {
376 *lock(&self.state.event_publisher) = Some(publisher);
377 }
378
379 pub fn with_defaults() -> Self {
381 Self::new(LogLevel::Info, 10_000)
382 }
383
384 pub fn log(
390 &self,
391 instance: Option<&InstanceKey>,
392 level: LogLevel,
393 source: LogSource,
394 message: impl Into<String>,
395 ) {
396 let trace_bypass = if level == LogLevel::Trace {
404 match instance {
405 Some(key) if self.is_tracing(key) => true,
406 Some(_) => return, None => false, }
409 } else {
410 false
411 };
412
413 if !trace_bypass {
414 let min = self.effective_min_level(instance);
415 if level < min {
416 return;
417 }
418 }
419
420 let message = message.into();
421
422 let id_disp = instance.map(|key| key.server_id.to_string());
425 let ws_disp = instance.map(|key| key.workspace.display().to_string());
426 match level {
427 LogLevel::Error => tracing::error!(
428 server_id = id_disp.as_deref(),
429 workspace = ws_disp.as_deref(),
430 source = source.tag(),
431 "{}",
432 message
433 ),
434 LogLevel::Warn => tracing::warn!(
435 server_id = id_disp.as_deref(),
436 workspace = ws_disp.as_deref(),
437 source = source.tag(),
438 "{}",
439 message
440 ),
441 LogLevel::Info => tracing::info!(
442 server_id = id_disp.as_deref(),
443 workspace = ws_disp.as_deref(),
444 source = source.tag(),
445 "{}",
446 message
447 ),
448 LogLevel::Debug => tracing::debug!(
449 server_id = id_disp.as_deref(),
450 workspace = ws_disp.as_deref(),
451 source = source.tag(),
452 "{}",
453 message
454 ),
455 LogLevel::Trace => tracing::trace!(
456 server_id = id_disp.as_deref(),
457 workspace = ws_disp.as_deref(),
458 source = source.tag(),
459 "{}",
460 message
461 ),
462 }
463
464 let record = LogRecord {
465 timestamp: SystemTime::now(),
466 server_id: instance.map(|k| Arc::clone(&k.server_id)),
467 workspace: instance.map(|k| Arc::clone(&k.workspace)),
468 level,
469 source,
470 message,
471 };
472
473 let publish_payload = LspLogPushed {
479 server_id: record.server_id.clone(),
480 workspace: record.workspace.clone(),
481 level: level_tag(record.level).to_string(),
482 source: record.source.tag().to_string(),
483 message: record.message.clone(),
484 };
485
486 match instance {
487 None => {
488 lock(&self.state.global).push(record);
489 }
490 Some(key) => {
491 let cap = *lock(&self.state.default_capacity);
492 let mut per = lock(&self.state.per_instance);
493 per.entry(key.clone())
494 .or_insert_with(|| LogRing::new(cap))
495 .push(record);
496 }
497 }
498
499 let publisher = lock(&self.state.event_publisher).clone();
503 if let Some(p) = publisher {
504 p(publish_payload);
505 }
506 }
507
508 fn effective_min_level(&self, instance: Option<&InstanceKey>) -> LogLevel {
510 if let Some(key) = instance
511 && let Some(level) = lock(&self.state.instance_levels).get(key).copied()
512 {
513 return level;
514 }
515 *lock(&self.state.default_min_level)
516 }
517
518 pub fn is_tracing(&self, instance: &InstanceKey) -> bool {
522 lock(&self.state.instance_trace).contains(instance)
523 }
524
525 pub fn enable_trace(&self, instance: InstanceKey) {
528 lock(&self.state.instance_trace).insert(instance);
529 }
530
531 pub fn disable_trace(&self, instance: &InstanceKey) {
533 lock(&self.state.instance_trace).remove(instance);
534 }
535
536 pub fn toggle_trace(&self, instance: InstanceKey) -> bool {
539 let mut guard = lock(&self.state.instance_trace);
540 if guard.contains(&instance) {
541 guard.remove(&instance);
542 false
543 } else {
544 guard.insert(instance);
545 true
546 }
547 }
548
549 pub fn set_instance_level(&self, instance: InstanceKey, level: Option<LogLevel>) {
552 let mut guard = lock(&self.state.instance_levels);
553 match level {
554 Some(l) => {
555 guard.insert(instance, l);
556 }
557 None => {
558 guard.remove(&instance);
559 }
560 }
561 }
562
563 pub fn set_default_level(&self, level: LogLevel) {
566 *lock(&self.state.default_min_level) = level;
567 }
568
569 pub fn set_default_capacity(&self, capacity: usize) {
572 *lock(&self.state.default_capacity) = capacity;
573 lock(&self.state.global).set_capacity(capacity);
574 for (_, ring) in lock(&self.state.per_instance).iter_mut() {
575 ring.set_capacity(capacity);
576 }
577 }
578
579 pub fn snapshot_global(&self) -> Vec<LogRecord> {
582 lock(&self.state.global).snapshot()
583 }
584
585 pub fn snapshot_instance(&self, instance: &InstanceKey) -> Vec<LogRecord> {
588 lock(&self.state.per_instance)
589 .get(instance)
590 .map(LogRing::snapshot)
591 .unwrap_or_default()
592 }
593
594 pub fn known_instances(&self) -> Vec<InstanceKey> {
597 lock(&self.state.per_instance).keys().cloned().collect()
598 }
599
600 pub fn clear_global(&self) {
602 lock(&self.state.global).clear();
603 }
604
605 pub fn clear_instance(&self, instance: &InstanceKey) {
607 if let Some(ring) = lock(&self.state.per_instance).get_mut(instance) {
608 ring.clear();
609 }
610 }
611}
612
613impl Default for LspLogger {
614 fn default() -> Self {
615 Self::with_defaults()
616 }
617}
618
619impl std::fmt::Debug for LspLogger {
620 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
621 let global_len = lock(&self.state.global).len();
622 let n_instances = lock(&self.state.per_instance).len();
623 f.debug_struct("LspLogger")
624 .field("global_records", &global_len)
625 .field("instance_count", &n_instances)
626 .finish_non_exhaustive()
627 }
628}
629
630#[cfg(test)]
631mod tests {
632 use super::*;
633 use std::path::PathBuf;
634
635 fn key(server: &str, workspace: &str) -> InstanceKey {
636 InstanceKey::new(
637 Arc::<str>::from(server),
638 Arc::<Path>::from(PathBuf::from(workspace).as_path()),
639 )
640 }
641
642 #[test]
643 fn log_level_parse_round_trips() {
644 assert_eq!(LogLevel::parse("error"), Some(LogLevel::Error));
645 assert_eq!(LogLevel::parse("WARN"), Some(LogLevel::Warn));
646 assert_eq!(LogLevel::parse("warning"), Some(LogLevel::Warn));
647 assert_eq!(LogLevel::parse("Info"), Some(LogLevel::Info));
648 assert_eq!(LogLevel::parse("debug"), Some(LogLevel::Debug));
649 assert_eq!(LogLevel::parse("trace"), Some(LogLevel::Trace));
650 assert_eq!(LogLevel::parse("nope"), None);
651 }
652
653 #[test]
654 fn log_level_ordering_matches_severity() {
655 assert!(LogLevel::Error > LogLevel::Warn);
658 assert!(LogLevel::Warn > LogLevel::Info);
659 assert!(LogLevel::Info > LogLevel::Debug);
660 assert!(LogLevel::Debug > LogLevel::Trace);
661 }
662
663 #[test]
664 fn ring_evicts_oldest_at_capacity() {
665 let mut ring = LogRing::new(3);
666 for i in 0..5 {
667 ring.push(LogRecord {
668 timestamp: SystemTime::now(),
669 server_id: None,
670 workspace: None,
671 level: LogLevel::Info,
672 source: LogSource::Client,
673 message: format!("msg {i}"),
674 });
675 }
676 assert_eq!(ring.len(), 3);
677 let snap = ring.snapshot();
678 assert_eq!(snap[0].message, "msg 2");
679 assert_eq!(snap[1].message, "msg 3");
680 assert_eq!(snap[2].message, "msg 4");
681 }
682
683 #[test]
684 fn ring_zero_capacity_drops_everything() {
685 let mut ring = LogRing::new(0);
686 ring.push(LogRecord {
687 timestamp: SystemTime::now(),
688 server_id: None,
689 workspace: None,
690 level: LogLevel::Info,
691 source: LogSource::Client,
692 message: "lost".into(),
693 });
694 assert_eq!(ring.len(), 0);
695 }
696
697 #[test]
698 fn log_routes_to_correct_ring() {
699 let logger = LspLogger::with_defaults();
700 let rust = key("rust", "/work/A");
701 let py = key("python", "/work/A");
702
703 logger.log(None, LogLevel::Info, LogSource::Client, "subsys event");
704 logger.log(
705 Some(&rust),
706 LogLevel::Info,
707 LogSource::LspMessage,
708 "rust evt",
709 );
710 logger.log(Some(&py), LogLevel::Warn, LogSource::Stderr, "python evt");
711
712 let g = logger.snapshot_global();
713 assert_eq!(g.len(), 1);
714 assert_eq!(g[0].message, "subsys event");
715
716 let r = logger.snapshot_instance(&rust);
717 assert_eq!(r.len(), 1);
718 assert_eq!(r[0].message, "rust evt");
719
720 let p = logger.snapshot_instance(&py);
721 assert_eq!(p.len(), 1);
722 assert_eq!(p[0].message, "python evt");
723
724 let unknown = key("zzz", "/work/A");
727 assert!(logger.snapshot_instance(&unknown).is_empty());
728 }
729
730 #[test]
731 fn same_server_id_different_workspaces_stay_distinct() {
732 let logger = LspLogger::with_defaults();
738 let rust_a = key("rust", "/work/A");
739 let rust_b = key("rust", "/work/B");
740 logger.log(
741 Some(&rust_a),
742 LogLevel::Info,
743 LogSource::Client,
744 "msg from A",
745 );
746 logger.log(
747 Some(&rust_b),
748 LogLevel::Info,
749 LogSource::Client,
750 "msg from B",
751 );
752 let a = logger.snapshot_instance(&rust_a);
753 let b = logger.snapshot_instance(&rust_b);
754 assert_eq!(a.len(), 1, "instance A has its own record");
755 assert_eq!(a[0].message, "msg from A");
756 assert_eq!(b.len(), 1, "instance B has its own record");
757 assert_eq!(b[0].message, "msg from B");
758 assert_eq!(a[0].workspace.as_deref(), Some(Path::new("/work/A")));
761 assert_eq!(b[0].workspace.as_deref(), Some(Path::new("/work/B")));
762 }
763
764 #[test]
765 fn log_below_min_level_is_dropped() {
766 let logger = LspLogger::new(LogLevel::Warn, 100);
767 let id = key("rust", "/work/A");
768 logger.log(Some(&id), LogLevel::Info, LogSource::Client, "below");
769 logger.log(Some(&id), LogLevel::Warn, LogSource::Client, "at");
770 logger.log(Some(&id), LogLevel::Error, LogSource::Client, "above");
771 let snap = logger.snapshot_instance(&id);
772 assert_eq!(snap.len(), 2);
773 assert_eq!(snap[0].message, "at");
774 assert_eq!(snap[1].message, "above");
775 }
776
777 #[test]
778 fn per_instance_level_overrides_default() {
779 let logger = LspLogger::new(LogLevel::Info, 100);
780 let rust = key("rust", "/work/A");
781 let py = key("python", "/work/A");
782 logger.set_instance_level(rust.clone(), Some(LogLevel::Debug));
783
784 logger.log(Some(&rust), LogLevel::Debug, LogSource::Client, "rust dbg");
785 logger.log(Some(&py), LogLevel::Debug, LogSource::Client, "py dbg");
786
787 assert_eq!(logger.snapshot_instance(&rust).len(), 1);
789 assert_eq!(logger.snapshot_instance(&py).len(), 0);
791 }
792
793 #[test]
794 fn trace_records_gated_by_per_instance_toggle() {
795 let logger = LspLogger::new(LogLevel::Trace, 100); let id = key("rust", "/work/A");
797 logger.log(Some(&id), LogLevel::Trace, LogSource::Trace, "t1");
799 assert_eq!(logger.snapshot_instance(&id).len(), 0);
800 logger.enable_trace(id.clone());
802 assert!(logger.is_tracing(&id));
803 logger.log(Some(&id), LogLevel::Trace, LogSource::Trace, "t2");
804 let snap = logger.snapshot_instance(&id);
805 assert_eq!(snap.len(), 1);
806 assert_eq!(snap[0].message, "t2");
807 logger.disable_trace(&id);
809 assert!(!logger.is_tracing(&id));
810 logger.log(Some(&id), LogLevel::Trace, LogSource::Trace, "t3");
811 assert_eq!(logger.snapshot_instance(&id).len(), 1, "no new records");
812 }
813
814 #[test]
815 fn toggle_trace_returns_new_state() {
816 let logger = LspLogger::with_defaults();
817 let id = key("rust", "/work/A");
818 assert!(logger.toggle_trace(id.clone()));
819 assert!(!logger.toggle_trace(id.clone()));
820 }
821
822 #[test]
823 fn known_instances_lists_only_seen_instances() {
824 let logger = LspLogger::with_defaults();
825 let rust = key("rust", "/work/A");
826 let py = key("python", "/work/A");
827 logger.log(Some(&rust), LogLevel::Info, LogSource::Client, "x");
828 logger.log(Some(&py), LogLevel::Info, LogSource::Client, "y");
829 let mut known: Vec<String> = logger
830 .known_instances()
831 .into_iter()
832 .map(|k| k.server_id.to_string())
833 .collect();
834 known.sort();
835 assert_eq!(known, vec!["python".to_string(), "rust".to_string()]);
836 }
837
838 #[test]
839 fn clearing_drops_records() {
840 let logger = LspLogger::with_defaults();
841 let id = key("rust", "/work/A");
842 logger.log(None, LogLevel::Info, LogSource::Client, "a");
843 logger.log(Some(&id), LogLevel::Info, LogSource::Client, "b");
844 logger.clear_global();
845 assert!(logger.snapshot_global().is_empty());
846 assert_eq!(logger.snapshot_instance(&id).len(), 1);
848 logger.clear_instance(&id);
849 assert!(logger.snapshot_instance(&id).is_empty());
850 }
851
852 #[test]
853 fn set_default_capacity_resizes_existing_rings() {
854 let logger = LspLogger::new(LogLevel::Info, 100);
855 let id = key("rust", "/work/A");
856 for i in 0..50 {
857 logger.log(
858 Some(&id),
859 LogLevel::Info,
860 LogSource::Client,
861 format!("r{i}"),
862 );
863 }
864 assert_eq!(logger.snapshot_instance(&id).len(), 50);
865 logger.set_default_capacity(10);
867 let snap = logger.snapshot_instance(&id);
868 assert_eq!(snap.len(), 10);
869 assert_eq!(snap[0].message, "r40");
870 assert_eq!(snap[9].message, "r49");
871 }
872
873 #[test]
874 fn cheap_clone_shares_state() {
875 let a = LspLogger::with_defaults();
876 let b = a.clone();
877 a.log(None, LogLevel::Info, LogSource::Client, "from a");
878 assert_eq!(b.snapshot_global().len(), 1);
880 }
881}