Closes #ISSUE Improves the efficiency of our interactions with the Zed language server. Previously, on startup and after every workspace configuration changed notification, we would send >1MB of JSON Schemas to the JSON LSP. The only reason this had to happen was due to the case where an extension was installed that would result in a change to the JSON schema for settings (i.e. added language, theme, etc). This PR changes the behavior to use the URI LSP extensions of `vscode-json-language-server` in order to send the server URI's that it can then use to fetch the schemas as needed (i.e. the settings schema is only generated and sent when `settings.json` is opened. This brings the JSON we send to on startup and after every workspace configuration changed notification down to a couple of KB. Additionally, using another LSP extension request we can notify the server when a schema has changed using the URI as a key, so we no longer have to send a workspace configuration changed notification, and the schema contents will only be re-requested and regenerated if the schema is in use. Release Notes: - Improved the efficiency of communication with the builtin JSON LSP. JSON Schemas are no longer sent to the JSON language server in their full form. If you wish to view a builtin JSON schema in the language server info tab of the language server logs (`dev: open language server logs`), you must now use the `editor: open url` action with your cursor over the URL that is sent to the server. - Made it so that Zed urls (`zed://...`) are resolved locally when opened within the editor instead of being resolved through the OS. Users who could not previously open `zed://*` URLs in the editor can now do so by pasting the link into a buffer and using the `editor: open url` action (please open an issue if this is the case for you!). --------- Co-authored-by: Michael <michael@zed.dev>
392 lines
11 KiB
Rust
392 lines
11 KiB
Rust
//! # logger
|
|
pub use log as log_impl;
|
|
|
|
mod env_config;
|
|
pub mod filter;
|
|
pub mod sink;
|
|
|
|
pub use sink::{flush, init_output_file, init_output_stderr, init_output_stdout};
|
|
|
|
pub const SCOPE_DEPTH_MAX: usize = 4;
|
|
|
|
pub fn init() {
|
|
if let Err(err) = try_init() {
|
|
log::error!("{err}");
|
|
eprintln!("{err}");
|
|
}
|
|
}
|
|
|
|
pub fn try_init() -> anyhow::Result<()> {
|
|
log::set_logger(&ZLOG)?;
|
|
log::set_max_level(log::LevelFilter::max());
|
|
process_env();
|
|
filter::refresh_from_settings(&std::collections::HashMap::default());
|
|
Ok(())
|
|
}
|
|
|
|
pub fn init_test() {
|
|
if get_env_config().is_some() && try_init().is_ok() {
|
|
init_output_stdout();
|
|
}
|
|
}
|
|
|
|
fn get_env_config() -> Option<String> {
|
|
std::env::var("ZED_LOG")
|
|
.or_else(|_| std::env::var("RUST_LOG"))
|
|
.ok()
|
|
}
|
|
|
|
pub fn process_env() {
|
|
let Some(env_config) = get_env_config() else {
|
|
return;
|
|
};
|
|
match env_config::parse(&env_config) {
|
|
Ok(filter) => {
|
|
filter::init_env_filter(filter);
|
|
}
|
|
Err(err) => {
|
|
eprintln!("Failed to parse log filter: {}", err);
|
|
}
|
|
}
|
|
}
|
|
|
|
static ZLOG: Zlog = Zlog {};
|
|
|
|
pub struct Zlog {}
|
|
|
|
impl log::Log for Zlog {
|
|
fn enabled(&self, metadata: &log::Metadata) -> bool {
|
|
filter::is_possibly_enabled_level(metadata.level())
|
|
}
|
|
|
|
fn log(&self, record: &log::Record) {
|
|
if !self.enabled(record.metadata()) {
|
|
return;
|
|
}
|
|
let (crate_name_scope, module_scope) = match record.module_path_static() {
|
|
Some(module_path) => {
|
|
let crate_name = private::extract_crate_name_from_module_path(module_path);
|
|
let crate_name_scope = private::scope_new(&[crate_name]);
|
|
let module_scope = private::scope_new(&[module_path]);
|
|
(crate_name_scope, module_scope)
|
|
}
|
|
// TODO: when do we hit this
|
|
None => (private::scope_new(&[]), private::scope_new(&["*unknown*"])),
|
|
};
|
|
let level = record.metadata().level();
|
|
if !filter::is_scope_enabled(&crate_name_scope, record.module_path(), level) {
|
|
return;
|
|
}
|
|
sink::submit(sink::Record {
|
|
scope: module_scope,
|
|
level,
|
|
message: record.args(),
|
|
// PERF(batching): store non-static paths in a cache + leak them and pass static str here
|
|
module_path: record.module_path().or(record.file()),
|
|
});
|
|
}
|
|
|
|
fn flush(&self) {
|
|
sink::flush();
|
|
}
|
|
}
|
|
|
|
#[macro_export]
|
|
macro_rules! log {
|
|
($logger:expr, $level:expr, $($arg:tt)+) => {
|
|
let level = $level;
|
|
let logger = $logger;
|
|
let enabled = $crate::filter::is_scope_enabled(&logger.scope, Some(module_path!()), level);
|
|
if enabled {
|
|
$crate::sink::submit($crate::sink::Record {
|
|
scope: logger.scope,
|
|
level,
|
|
message: &format_args!($($arg)+),
|
|
module_path: Some(module_path!()),
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
#[macro_export]
|
|
macro_rules! trace {
|
|
($logger:expr => $($arg:tt)+) => {
|
|
$crate::log!($logger, $crate::log_impl::Level::Trace, $($arg)+);
|
|
};
|
|
($($arg:tt)+) => {
|
|
$crate::log!($crate::default_logger!(), $crate::log_impl::Level::Trace, $($arg)+);
|
|
};
|
|
}
|
|
|
|
#[macro_export]
|
|
macro_rules! debug {
|
|
($logger:expr => $($arg:tt)+) => {
|
|
$crate::log!($logger, $crate::log_impl::Level::Debug, $($arg)+);
|
|
};
|
|
($($arg:tt)+) => {
|
|
$crate::log!($crate::default_logger!(), $crate::log_impl::Level::Debug, $($arg)+);
|
|
};
|
|
}
|
|
|
|
#[macro_export]
|
|
macro_rules! info {
|
|
($logger:expr => $($arg:tt)+) => {
|
|
$crate::log!($logger, $crate::log_impl::Level::Info, $($arg)+);
|
|
};
|
|
($($arg:tt)+) => {
|
|
$crate::log!($crate::default_logger!(), $crate::log_impl::Level::Info, $($arg)+);
|
|
};
|
|
}
|
|
|
|
#[macro_export]
|
|
macro_rules! warn {
|
|
($logger:expr => $($arg:tt)+) => {
|
|
$crate::log!($logger, $crate::log_impl::Level::Warn, $($arg)+);
|
|
};
|
|
($($arg:tt)+) => {
|
|
$crate::log!($crate::default_logger!(), $crate::log_impl::Level::Warn, $($arg)+);
|
|
};
|
|
}
|
|
|
|
#[macro_export]
|
|
macro_rules! error {
|
|
($logger:expr => $($arg:tt)+) => {
|
|
$crate::log!($logger, $crate::log_impl::Level::Error, $($arg)+);
|
|
};
|
|
($($arg:tt)+) => {
|
|
$crate::log!($crate::default_logger!(), $crate::log_impl::Level::Error, $($arg)+);
|
|
};
|
|
}
|
|
|
|
/// Creates a timer that logs the duration it was active for either when
|
|
/// it is dropped, or when explicitly stopped using the `end` method.
|
|
/// Logs at the `trace` level.
|
|
/// Note that it will include time spent across await points
|
|
/// (i.e. should not be used to measure the performance of async code)
|
|
/// However, this is a feature not a bug, as it allows for a more accurate
|
|
/// understanding of how long the action actually took to complete, including
|
|
/// interruptions, which can help explain why something may have timed out,
|
|
/// why it took longer to complete than it would have had the await points resolved
|
|
/// immediately, etc.
|
|
#[macro_export]
|
|
macro_rules! time {
|
|
($logger:expr => $name:expr) => {
|
|
$crate::Timer::new($logger, $name)
|
|
};
|
|
($name:expr) => {
|
|
time!($crate::default_logger!() => $name)
|
|
};
|
|
}
|
|
|
|
#[macro_export]
|
|
macro_rules! scoped {
|
|
($parent:expr => $name:expr) => {{
|
|
$crate::scoped_logger($parent, $name)
|
|
}};
|
|
($name:expr) => {
|
|
$crate::scoped!($crate::default_logger!() => $name)
|
|
};
|
|
}
|
|
|
|
pub const fn scoped_logger(parent: Logger, name: &'static str) -> Logger {
|
|
let mut scope = parent.scope;
|
|
let mut index = 1; // always have crate/module name
|
|
while index < scope.len() && !scope[index].is_empty() {
|
|
index += 1;
|
|
}
|
|
if index >= scope.len() {
|
|
#[cfg(debug_assertions)]
|
|
{
|
|
panic!("Scope overflow trying to add scope... ignoring scope");
|
|
}
|
|
}
|
|
scope[index] = name;
|
|
Logger { scope }
|
|
}
|
|
|
|
#[macro_export]
|
|
macro_rules! default_logger {
|
|
() => {
|
|
$crate::Logger {
|
|
scope: $crate::private::scope_new(&[$crate::crate_name!()]),
|
|
}
|
|
};
|
|
}
|
|
|
|
#[macro_export]
|
|
macro_rules! crate_name {
|
|
() => {
|
|
$crate::private::extract_crate_name_from_module_path(module_path!())
|
|
};
|
|
}
|
|
|
|
/// functions that are used in macros, and therefore must be public,
|
|
/// but should not be used directly
|
|
pub mod private {
|
|
use super::*;
|
|
|
|
pub const fn extract_crate_name_from_module_path(module_path: &str) -> &str {
|
|
let mut i = 0;
|
|
let mod_path_bytes = module_path.as_bytes();
|
|
let mut index = mod_path_bytes.len();
|
|
while i + 1 < mod_path_bytes.len() {
|
|
if mod_path_bytes[i] == b':' && mod_path_bytes[i + 1] == b':' {
|
|
index = i;
|
|
break;
|
|
}
|
|
i += 1;
|
|
}
|
|
let Some((crate_name, _)) = module_path.split_at_checked(index) else {
|
|
return module_path;
|
|
};
|
|
crate_name
|
|
}
|
|
|
|
pub const fn scope_new(scopes: &[&'static str]) -> Scope {
|
|
assert!(scopes.len() <= SCOPE_DEPTH_MAX);
|
|
let mut scope = [""; SCOPE_DEPTH_MAX];
|
|
let mut i = 0;
|
|
while i < scopes.len() {
|
|
scope[i] = scopes[i];
|
|
i += 1;
|
|
}
|
|
scope
|
|
}
|
|
|
|
pub fn scope_alloc_new(scopes: &[&str]) -> ScopeAlloc {
|
|
assert!(scopes.len() <= SCOPE_DEPTH_MAX);
|
|
let mut scope = [""; SCOPE_DEPTH_MAX];
|
|
scope[0..scopes.len()].copy_from_slice(scopes);
|
|
scope.map(|s| s.to_string())
|
|
}
|
|
|
|
pub fn scope_to_alloc(scope: &Scope) -> ScopeAlloc {
|
|
scope.map(|s| s.to_string())
|
|
}
|
|
}
|
|
|
|
pub type Scope = [&'static str; SCOPE_DEPTH_MAX];
|
|
pub type ScopeAlloc = [String; SCOPE_DEPTH_MAX];
|
|
const SCOPE_STRING_SEP_STR: &str = ".";
|
|
const SCOPE_STRING_SEP_CHAR: char = '.';
|
|
|
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
|
pub struct Logger {
|
|
pub scope: Scope,
|
|
}
|
|
|
|
impl log::Log for Logger {
|
|
fn enabled(&self, metadata: &log::Metadata) -> bool {
|
|
filter::is_possibly_enabled_level(metadata.level())
|
|
}
|
|
|
|
fn log(&self, record: &log::Record) {
|
|
if !self.enabled(record.metadata()) {
|
|
return;
|
|
}
|
|
let level = record.metadata().level();
|
|
if !filter::is_scope_enabled(&self.scope, record.module_path(), level) {
|
|
return;
|
|
}
|
|
sink::submit(sink::Record {
|
|
scope: self.scope,
|
|
level,
|
|
message: record.args(),
|
|
module_path: record.module_path(),
|
|
});
|
|
}
|
|
|
|
fn flush(&self) {
|
|
sink::flush();
|
|
}
|
|
}
|
|
|
|
pub struct Timer {
|
|
pub logger: Logger,
|
|
pub start_time: std::time::Instant,
|
|
pub name: &'static str,
|
|
pub warn_if_longer_than: Option<std::time::Duration>,
|
|
pub done: bool,
|
|
}
|
|
|
|
impl Drop for Timer {
|
|
fn drop(&mut self) {
|
|
self.finish();
|
|
}
|
|
}
|
|
|
|
impl Timer {
|
|
#[must_use = "Timer will stop when dropped, the result of this function should be saved in a variable prefixed with `_` if it should stop when dropped"]
|
|
pub fn new(logger: Logger, name: &'static str) -> Self {
|
|
Self {
|
|
logger,
|
|
name,
|
|
start_time: std::time::Instant::now(),
|
|
warn_if_longer_than: None,
|
|
done: false,
|
|
}
|
|
}
|
|
|
|
pub fn warn_if_gt(mut self, warn_limit: std::time::Duration) -> Self {
|
|
self.warn_if_longer_than = Some(warn_limit);
|
|
self
|
|
}
|
|
|
|
pub fn end(mut self) {
|
|
self.finish();
|
|
}
|
|
|
|
fn finish(&mut self) {
|
|
if self.done {
|
|
return;
|
|
}
|
|
let elapsed = self.start_time.elapsed();
|
|
if let Some(warn_limit) = self.warn_if_longer_than
|
|
&& elapsed > warn_limit
|
|
{
|
|
crate::warn!(
|
|
self.logger =>
|
|
"Timer '{}' took {:?}. Which was longer than the expected limit of {:?}",
|
|
self.name,
|
|
elapsed,
|
|
warn_limit
|
|
);
|
|
self.done = true;
|
|
return;
|
|
}
|
|
crate::trace!(
|
|
self.logger =>
|
|
"Timer '{}' finished in {:?}",
|
|
self.name,
|
|
elapsed
|
|
);
|
|
self.done = true;
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn test_crate_name() {
|
|
assert_eq!(crate_name!(), "zlog");
|
|
assert_eq!(
|
|
private::extract_crate_name_from_module_path("my_speedy_⚡️_crate::some_module"),
|
|
"my_speedy_⚡️_crate"
|
|
);
|
|
assert_eq!(
|
|
private::extract_crate_name_from_module_path("my_speedy_crate_⚡️::some_module"),
|
|
"my_speedy_crate_⚡️"
|
|
);
|
|
assert_eq!(
|
|
private::extract_crate_name_from_module_path("my_speedy_crate_:⚡️:some_module"),
|
|
"my_speedy_crate_:⚡️:some_module"
|
|
);
|
|
assert_eq!(
|
|
private::extract_crate_name_from_module_path("my_speedy_crate_::⚡️some_module"),
|
|
"my_speedy_crate_"
|
|
);
|
|
}
|
|
}
|