zlog: Init (#27273)

Scaffolding for a revised way of logging in Zed. Very WIP, but the idea
is to allow maintainers to tell users to paste
```json
{
    "log": {
        "project.format": "trace"
    }
}
```
into their settings so that even trace logs are emitted for the log
statements emitted from a logger under the `project.format` scope.

The plan is to eventually implement the `Log` trait from the `log` crate
instead of just wrapping the `log` crate, which will simplify the
implementation greatly, and remove our need for both the `env_logger`
and `simplelog` crates.
Additionally, work will be done to transition to using the scoped
logging APIs throughout the app, focusing on bug hotspots to start
(currently, scoped logging is only used in the format codepath).

Release Notes:

- N/A
This commit is contained in:
Ben Kunkle 2025-03-21 15:08:03 -05:00 committed by GitHub
parent b32c792b68
commit 16ad7424d6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 656 additions and 29 deletions

21
Cargo.lock generated
View File

@ -10604,6 +10604,7 @@ dependencies = [
"util",
"which 6.0.3",
"worktree",
"zlog",
]
[[package]]
@ -17465,6 +17466,7 @@ dependencies = [
"workspace",
"zed_actions",
"zeta",
"zlog_settings",
]
[[package]]
@ -17772,6 +17774,25 @@ dependencies = [
"zstd",
]
[[package]]
name = "zlog"
version = "0.1.0"
dependencies = [
"log",
]
[[package]]
name = "zlog_settings"
version = "0.1.0"
dependencies = [
"anyhow",
"gpui",
"schemars",
"serde",
"settings",
"zlog",
]
[[package]]
name = "zstd"
version = "0.11.2+zstd.1.5.2"

View File

@ -171,6 +171,8 @@ members = [
"crates/zed",
"crates/zed_actions",
"crates/zeta",
"crates/zlog",
"crates/zlog_settings",
#
# Extensions
@ -374,6 +376,8 @@ worktree = { path = "crates/worktree" }
zed = { path = "crates/zed" }
zed_actions = { path = "crates/zed_actions" }
zeta = { path = "crates/zeta" }
zlog = { path = "crates/zlog" }
zlog_settings = { path = "crates/zlog_settings" }
#
# External crates

View File

@ -83,6 +83,7 @@ url.workspace = true
util.workspace = true
which.workspace = true
worktree.workspace = true
zlog.workspace = true
[dev-dependencies]
client = { workspace = true, features = ["test-support"] }

View File

@ -1142,6 +1142,7 @@ impl LocalLspStore {
mut buffers: Vec<FormattableBuffer>,
push_to_history: bool,
trigger: FormatTrigger,
logger: zlog::Logger,
cx: &mut AsyncApp,
) -> anyhow::Result<ProjectTransaction> {
// Do not allow multiple concurrent formatting requests for the
@ -1201,6 +1202,7 @@ impl LocalLspStore {
// handle whitespace formatting
{
if settings.remove_trailing_whitespace_on_save {
zlog::trace!(logger => "removing trailing whitespace");
let diff = buffer
.handle
.read_with(cx, |buffer, cx| buffer.remove_trailing_whitespace(cx))?
@ -1217,6 +1219,7 @@ impl LocalLspStore {
}
if settings.ensure_final_newline_on_save {
zlog::trace!(logger => "ensuring final newline");
buffer.handle.update(cx, |buffer, cx| {
buffer.start_transaction();
buffer.ensure_final_newline(cx);
@ -1243,6 +1246,7 @@ impl LocalLspStore {
if should_run_code_actions_on_format {
if have_code_actions_to_run_on_format {
zlog::trace!(logger => "going to run code actions on format");
break 'ca_formatter Some(Formatter::CodeActions(
settings.code_actions_on_format.clone(),
));
@ -1258,8 +1262,10 @@ impl LocalLspStore {
match &settings.formatter {
SelectedFormatter::Auto => {
if settings.prettier.allowed {
zlog::trace!(logger => "Formatter set to auto: defaulting to prettier");
std::slice::from_ref(&Formatter::Prettier)
} else {
zlog::trace!(logger => "Formatter set to auto: defaulting to primary language server");
std::slice::from_ref(&Formatter::LanguageServer { name: None })
}
}
@ -1307,6 +1313,10 @@ impl LocalLspStore {
'formatters: for formatter in formatters {
match formatter {
Formatter::Prettier => {
let logger = zlog::scoped!(logger => "prettier");
zlog::trace!(logger => "formatting");
let _timer = zlog::time!(logger => "Formatting buffer via prettier");
let prettier = lsp_store.read_with(cx, |lsp_store, _cx| {
lsp_store.prettier_store().unwrap().downgrade()
})?;
@ -1316,17 +1326,21 @@ impl LocalLspStore {
.transpose();
let Ok(diff) = diff_result else {
result = Err(diff_result.unwrap_err());
zlog::error!(logger => "failed, reason: {:?}", result.as_ref());
break 'formatters;
};
let Some(diff) = diff else {
zlog::trace!(logger => "No changes");
continue 'formatters;
};
if let Some(err) =
err_if_buffer_edited_since_start(buffer, transaction_id_format, &cx)
{
zlog::warn!(logger => "Buffer edited while formatting. Aborting");
result = Err(err);
break 'formatters;
}
zlog::trace!(logger => "Applying changes");
buffer.handle.update(cx, |buffer, cx| {
buffer.start_transaction();
buffer.apply_diff(diff, cx);
@ -1338,6 +1352,11 @@ impl LocalLspStore {
})?;
}
Formatter::External { command, arguments } => {
let logger = zlog::scoped!(logger => "command");
zlog::trace!(logger => "formatting");
let _timer =
zlog::time!(logger => "Formatting buffer via external command");
let diff_result = Self::format_via_external_command(
buffer,
command.as_ref(),
@ -1350,17 +1369,21 @@ impl LocalLspStore {
});
let Ok(diff) = diff_result else {
result = Err(diff_result.unwrap_err());
zlog::error!(logger => "failed, reason: {:?}", result.as_ref());
break 'formatters;
};
let Some(diff) = diff else {
zlog::trace!(logger => "No changes");
continue 'formatters;
};
if let Some(err) =
err_if_buffer_edited_since_start(buffer, transaction_id_format, &cx)
{
zlog::warn!(logger => "Buffer edited while formatting. Aborting");
result = Err(err);
break 'formatters;
}
zlog::trace!(logger => "Applying changes");
buffer.handle.update(cx, |buffer, cx| {
buffer.start_transaction();
buffer.apply_diff(diff, cx);
@ -1372,8 +1395,13 @@ impl LocalLspStore {
})?;
}
Formatter::LanguageServer { name } => {
let logger = zlog::scoped!(logger => "language-server");
zlog::trace!(logger => "formatting");
let _timer =
zlog::time!(logger => "Formatting buffer using language server");
let Some(buffer_path_abs) = buffer.abs_path.as_ref() else {
log::warn!("Cannot format buffer that is not backed by a file on disk using language servers. Skipping");
zlog::warn!(logger => "Cannot format buffer that is not backed by a file on disk using language servers. Skipping");
continue 'formatters;
};
@ -1409,7 +1437,15 @@ impl LocalLspStore {
}
};
zlog::trace!(
logger =>
"Formatting buffer '{:?}' using language server '{:?}'",
buffer_path_abs.as_path().to_string_lossy(),
language_server.name()
);
let edits_result = if let Some(ranges) = buffer.ranges.as_ref() {
zlog::trace!(logger => "formatting ranges");
Self::format_ranges_via_lsp(
&lsp_store,
&buffer.handle,
@ -1422,6 +1458,7 @@ impl LocalLspStore {
.await
.context("Failed to format ranges via language server")
} else {
zlog::trace!(logger => "formatting full");
Self::format_via_lsp(
&lsp_store,
&buffer.handle,
@ -1436,19 +1473,22 @@ impl LocalLspStore {
let Ok(edits) = edits_result else {
result = Err(edits_result.unwrap_err());
zlog::error!(logger => "Failed, reason: {:?}", result.as_ref());
break 'formatters;
};
if edits.is_empty() {
zlog::trace!(logger => "No changes");
continue 'formatters;
}
if let Some(err) =
err_if_buffer_edited_since_start(buffer, transaction_id_format, &cx)
{
zlog::warn!(logger => "Buffer edited while formatting. Aborting");
result = Err(err);
break 'formatters;
}
zlog::trace!(logger => "Applying changes");
buffer.handle.update(cx, |buffer, cx| {
buffer.start_transaction();
buffer.edit(edits, None, cx);
@ -1460,8 +1500,12 @@ impl LocalLspStore {
})?;
}
Formatter::CodeActions(code_actions) => {
let logger = zlog::scoped!(logger => "code-actions");
zlog::trace!(logger => "formatting");
let _timer = zlog::time!(logger => "Formatting buffer using code actions");
let Some(buffer_path_abs) = buffer.abs_path.as_ref() else {
log::warn!("Cannot format buffer that is not backed by a file on disk using code actions. Skipping");
zlog::warn!(logger => "Cannot format buffer that is not backed by a file on disk using code actions. Skipping");
continue 'formatters;
};
let code_action_kinds = code_actions
@ -1471,6 +1515,7 @@ impl LocalLspStore {
})
.collect::<Vec<_>>();
if code_action_kinds.is_empty() {
zlog::trace!(logger => "No code action kinds enabled, skipping");
continue 'formatters;
}
@ -1494,7 +1539,8 @@ impl LocalLspStore {
let Ok(actions) = actions_result else {
// note: it may be better to set result to the error and break formatters here
// but for now we try to execute the actions that we can resolve and skip the rest
log::error!(
zlog::error!(
logger =>
"Failed to resolve code actions with kinds {:?} with language server {}",
code_action_kinds.iter().map(|kind| kind.as_str()).join(", "),
language_server.name()
@ -1507,6 +1553,7 @@ impl LocalLspStore {
}
if actions_and_servers.is_empty() {
zlog::trace!(logger => "No code actions were resolved, continuing");
continue 'formatters;
}
@ -1525,6 +1572,8 @@ impl LocalLspStore {
)
};
zlog::trace!(logger => "Executing {}", describe_code_action(&action));
// NOTE: code below duplicated from `Self::deserialize_workspace_edit`
// but filters out and logs warnings for code actions that cause unreasonably
// difficult handling on our part, such as:
@ -1542,7 +1591,8 @@ impl LocalLspStore {
if let Err(err) =
Self::try_resolve_code_action(server, &mut action).await
{
log::error!(
zlog::error!(
logger =>
"Failed to resolve {}. Error: {}",
describe_code_action(&action),
err
@ -1550,14 +1600,16 @@ impl LocalLspStore {
continue 'actions;
}
if let Some(_) = action.lsp_action.command() {
log::warn!(
zlog::warn!(
logger =>
"Code actions with commands are not supported while formatting. Skipping {}",
describe_code_action(&action),
);
continue 'actions;
}
let Some(edit) = action.lsp_action.edit().cloned() else {
log::warn!(
zlog::warn!(
logger =>
"No edit found for while formatting. Skipping {}",
describe_code_action(&action),
);
@ -1565,6 +1617,11 @@ impl LocalLspStore {
};
if edit.changes.is_none() && edit.document_changes.is_none() {
zlog::trace!(
logger =>
"No changes for code action. Skipping {}",
describe_code_action(&action),
);
continue 'actions;
}
@ -1591,11 +1648,20 @@ impl LocalLspStore {
let mut edits = Vec::with_capacity(operations.len());
if operations.is_empty() {
zlog::trace!(
logger =>
"No changes for code action. Skipping {}",
describe_code_action(&action),
);
continue 'actions;
}
for operation in operations {
let op = match operation {
lsp::DocumentChangeOperation::Edit(op) => op,
lsp::DocumentChangeOperation::Op(_) => {
log::warn!(
zlog::warn!(
logger =>
"Code actions which create, delete, or rename files are not supported on format. Skipping {}",
describe_code_action(&action),
);
@ -1603,7 +1669,8 @@ impl LocalLspStore {
}
};
let Ok(file_path) = op.text_document.uri.to_file_path() else {
log::warn!(
zlog::warn!(
logger =>
"Failed to convert URI '{:?}' to file path. Skipping {}",
&op.text_document.uri,
describe_code_action(&action),
@ -1611,7 +1678,8 @@ impl LocalLspStore {
continue 'actions;
};
if &file_path != buffer_path_abs {
log::warn!(
zlog::warn!(
logger =>
"File path '{:?}' does not match buffer path '{:?}'. Skipping {}",
file_path,
buffer_path_abs,
@ -1634,7 +1702,8 @@ impl LocalLspStore {
}
}
Edit::Snippet(_) => {
log::warn!(
zlog::warn!(
logger =>
"Code actions which produce snippet edits are not supported during formatting. Skipping {}",
describe_code_action(&action),
);
@ -1643,35 +1712,41 @@ impl LocalLspStore {
}
}
let edits_result = lsp_store
.update(cx, |lsp_store, cx| {
lsp_store.as_local_mut().unwrap().edits_from_lsp(
&buffer.handle,
lsp_edits,
server.server_id(),
op.text_document.version,
cx,
)
})?
.await
.with_context(
|| format!(
"Failed to resolve edits from LSP for buffer {:?} while handling {}",
buffer_path_abs.as_path(),
describe_code_action(&action),
)
).log_err();
let Some(resolved_edits) = edits_result else {
.update(cx, |lsp_store, cx| {
lsp_store.as_local_mut().unwrap().edits_from_lsp(
&buffer.handle,
lsp_edits,
server.server_id(),
op.text_document.version,
cx,
)
})?
.await;
let Ok(resolved_edits) = edits_result else {
zlog::warn!(
logger =>
"Failed to resolve edits from LSP for buffer {:?} while handling {}",
buffer_path_abs.as_path(),
describe_code_action(&action),
);
continue 'actions;
};
edits.extend(resolved_edits);
}
if edits.is_empty() {
zlog::warn!(logger => "No edits resolved from LSP");
continue 'actions;
}
if let Some(err) =
err_if_buffer_edited_since_start(buffer, transaction_id_format, &cx)
{
zlog::warn!(logger => "Buffer edited while formatting. Aborting");
result = Err(err);
break 'formatters;
}
zlog::info!(logger => "Applying changes");
buffer.handle.update(cx, |buffer, cx| {
buffer.start_transaction();
buffer.edit(edits, None, cx);
@ -1689,6 +1764,7 @@ impl LocalLspStore {
let buffer_handle = buffer.handle.clone();
buffer.handle.update(cx, |buffer, _| {
let Some(transaction_id) = transaction_id_format else {
zlog::trace!(logger => "No formatting transaction id");
return result;
};
let Some(transaction_id_last) =
@ -1697,9 +1773,11 @@ impl LocalLspStore {
// unwrapping should work here, how would we get a transaction id
// with no transaction on the undo stack?
// *but* it occasionally panics. Avoiding panics for now...
zlog::warn!(logger => "No transaction present on undo stack, despite having a formatting transaction id?");
return result;
};
if transaction_id_last != transaction_id {
zlog::trace!(logger => "Last transaction on undo stack is not the formatting transaction, skipping finalization & update of project transaction");
return result;
}
let transaction = buffer
@ -1708,6 +1786,7 @@ impl LocalLspStore {
.expect("There is a transaction on the undo stack if we were able to peek it");
// debug_assert_eq!(transaction.id, transaction_id);
if !push_to_history {
zlog::trace!(logger => "forgetting format transaction");
buffer.forget_transaction(transaction.id);
}
project_transaction
@ -1799,6 +1878,9 @@ impl LocalLspStore {
settings: &LanguageSettings,
cx: &mut AsyncApp,
) -> Result<Vec<(Range<Anchor>, Arc<str>)>> {
let logger = zlog::scoped!("lsp_format");
zlog::info!(logger => "Formatting via LSP");
let uri = lsp::Url::from_file_path(abs_path)
.map_err(|_| anyhow!("failed to convert abs path to uri"))?;
let text_document = lsp::TextDocumentIdentifier::new(uri);
@ -1808,6 +1890,8 @@ impl LocalLspStore {
let range_formatting_provider = capabilities.document_range_formatting_provider.as_ref();
let lsp_edits = if matches!(formatting_provider, Some(p) if *p != OneOf::Left(false)) {
let _timer = zlog::time!(logger => "format-full")
.warn_if_gt(std::time::Duration::from_millis(0));
language_server
.request::<lsp::request::Formatting>(lsp::DocumentFormattingParams {
text_document,
@ -1816,6 +1900,8 @@ impl LocalLspStore {
})
.await?
} else if matches!(range_formatting_provider, Some(p) if *p != OneOf::Left(false)) {
let _timer = zlog::time!(logger => "format-range")
.warn_if_gt(std::time::Duration::from_millis(0));
let buffer_start = lsp::Position::new(0, 0);
let buffer_end = buffer.update(cx, |b, _| point_to_lsp(b.max_point_utf16()))?;
language_server
@ -7840,7 +7926,10 @@ impl LspStore {
trigger: FormatTrigger,
cx: &mut Context<Self>,
) -> Task<anyhow::Result<ProjectTransaction>> {
let logger = zlog::scoped!("format");
if let Some(_) = self.as_local() {
zlog::trace!(logger => "Formatting locally");
let logger = zlog::scoped!(logger => "local");
let buffers = buffers
.into_iter()
.map(|buffer_handle| {
@ -7879,15 +7968,22 @@ impl LspStore {
ranges,
});
}
zlog::trace!(logger => "Formatting {:?} buffers", formattable_buffers.len());
let format_timer = zlog::time!(logger => "Formatting buffers");
let result = LocalLspStore::format_locally(
lsp_store.clone(),
formattable_buffers,
push_to_history,
trigger,
logger,
cx,
)
.await;
format_timer.end();
zlog::trace!(logger => "Formatting completed with result {:?}", result.as_ref().map(|_| "<project-transaction>"));
lsp_store.update(cx, |lsp_store, _| {
lsp_store.update_last_formatting_failure(&result);
})?;
@ -7895,16 +7991,21 @@ impl LspStore {
result
})
} else if let Some((client, project_id)) = self.upstream_client() {
zlog::trace!(logger => "Formatting remotely");
let logger = zlog::scoped!(logger => "remote");
// Don't support formatting ranges via remote
match target {
LspFormatTarget::Buffers => {}
LspFormatTarget::Ranges(_) => {
zlog::trace!(logger => "Ignoring unsupported remote range formatting request");
return Task::ready(Ok(ProjectTransaction::default()));
}
}
let buffer_store = self.buffer_store();
cx.spawn(async move |lsp_store, cx| {
zlog::trace!(logger => "Sending remote format request");
let request_timer = zlog::time!(logger => "remote format request");
let result = client
.request(proto::FormatBuffers {
project_id,
@ -7916,12 +8017,16 @@ impl LspStore {
})
.await
.and_then(|result| result.transaction.context("missing transaction"));
request_timer.end();
zlog::trace!(logger => "Remote format request resolved to {:?}", result.as_ref().map(|_| "<project_transaction>"));
lsp_store.update(cx, |lsp_store, _| {
lsp_store.update_last_formatting_failure(&result);
})?;
let transaction_response = result?;
let _timer = zlog::time!(logger => "deserializing project transaction");
buffer_store
.update(cx, |buffer_store, cx| {
buffer_store.deserialize_project_transaction(
@ -7933,6 +8038,7 @@ impl LspStore {
.await
})
} else {
zlog::trace!(logger => "Not formatting");
Task::ready(Ok(ProjectTransaction::default()))
}
}

View File

@ -135,6 +135,7 @@ welcome.workspace = true
workspace.workspace = true
zed_actions.workspace = true
zeta.workspace = true
zlog_settings.workspace = true
[target.'cfg(target_os = "windows")'.dependencies]
windows.workspace = true

View File

@ -302,6 +302,7 @@ fn main() {
AppCommitSha::set_global(app_commit_sha, cx);
}
settings::init(cx);
zlog_settings::init(cx);
handle_settings_file_changes(user_settings_file_rx, cx, handle_settings_changed);
handle_keymap_file_changes(user_keymap_file_rx, cx);
client::init_settings(cx);

18
crates/zlog/Cargo.toml Normal file
View File

@ -0,0 +1,18 @@
[package]
name = "zlog"
version = "0.1.0"
edition.workspace = true
publish.workspace = true
license = "GPL-3.0-or-later"
[lints]
workspace = true
[lib]
path = "src/zlog.rs"
[features]
default = []
[dependencies]
log.workspace = true

1
crates/zlog/LICENSE-GPL Symbolic link
View File

@ -0,0 +1 @@
../../LICENSE-GPL

415
crates/zlog/src/zlog.rs Normal file
View File

@ -0,0 +1,415 @@
//! # logger
pub use log as log_impl;
pub const SCOPE_DEPTH_MAX: usize = 4;
/// because we are currently just wrapping the `log` crate in `zlog`,
/// we need to work around the fact that the `log` crate only provides a
/// single global level filter. In order to have more precise control until
/// we no longer wrap `log`, we bump up the priority of log level so that it
/// will be logged, even if the actual level is lower
/// This is fine for now, as we use a `info` level filter by default in releases,
/// which hopefully won't result in confusion like `warn` or `error` levels might.
pub fn min_printed_log_level(level: log_impl::Level) -> log_impl::Level {
// this logic is defined based on the logic used in the `log` crate,
// which checks that a logs level is <= both of these values,
// so we take the minimum of the two values to ensure that check passes
let level_min_static = log_impl::STATIC_MAX_LEVEL;
let level_min_dynamic = log_impl::max_level();
if level <= level_min_static && level <= level_min_dynamic {
return level;
}
return log_impl::LevelFilter::min(level_min_static, level_min_dynamic)
.to_level()
.unwrap_or(level);
}
#[macro_export]
macro_rules! log {
($logger:expr, $level:expr, $($arg:tt)+) => {
let level = $level;
let logger = $logger;
let (enabled, level) = $crate::scope_map::is_scope_enabled(&logger.scope, level);
if enabled {
$crate::log_impl::log!(level, "[{}]: {}", &logger.fmt_scope(), format!($($arg)+));
}
}
}
#[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 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) => {{
let parent = $parent;
let name = $name;
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 {}", name);
}
#[cfg(not(debug_assertions))]
{
$crate::warn!(
parent =>
"Scope overflow trying to add scope {}... ignoring scope",
name
);
}
}
scope[index] = name;
$crate::Logger { scope }
}};
($name:expr) => {
$crate::scoped!($crate::default_logger!() => $name)
};
}
#[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 fn extract_crate_name_from_module_path(module_path: &'static str) -> &'static str {
return module_path
.split_once("::")
.map(|(crate_name, _)| crate_name)
.unwrap_or(module_path);
}
pub fn scope_new(scopes: &[&'static str]) -> Scope {
assert!(scopes.len() <= SCOPE_DEPTH_MAX);
let mut scope = [""; SCOPE_DEPTH_MAX];
scope[0..scopes.len()].copy_from_slice(scopes);
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 {
return 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: &'static str = ".";
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Logger {
pub scope: Scope,
}
impl Logger {
pub fn fmt_scope(&self) -> String {
let mut last = 0;
for s in self.scope {
if s.is_empty() {
break;
}
last += 1;
}
return self.scope[0..last].join(SCOPE_STRING_SEP);
}
}
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 {
return 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);
return 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 {
if 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;
}
}
pub mod scope_map {
use std::{
collections::HashMap,
hash::{DefaultHasher, Hasher},
sync::{
atomic::{AtomicU64, Ordering},
RwLock,
},
};
use super::*;
type ScopeMap = HashMap<ScopeAlloc, log_impl::Level>;
static SCOPE_MAP: RwLock<Option<ScopeMap>> = RwLock::new(None);
static SCOPE_MAP_HASH: AtomicU64 = AtomicU64::new(0);
pub fn is_scope_enabled(scope: &Scope, level: log_impl::Level) -> (bool, log_impl::Level) {
let level_min = min_printed_log_level(level);
if level <= level_min {
// [FAST PATH]
// if the message is at or below the minimum printed log level
// (where error < warn < info etc) then always enable
return (true, level);
}
let Ok(map) = SCOPE_MAP.read() else {
// on failure, default to enabled detection done by `log` crate
return (true, level);
};
let Some(map) = map.as_ref() else {
// on failure, default to enabled detection done by `log` crate
return (true, level);
};
if map.is_empty() {
// if no scopes are enabled, default to enabled detection done by `log` crate
return (true, level);
}
let mut scope_alloc = private::scope_to_alloc(scope);
let mut level_enabled = map.get(&scope_alloc);
if level_enabled.is_none() {
for i in (0..SCOPE_DEPTH_MAX).rev() {
if scope_alloc[i] == "" {
continue;
}
scope_alloc[i].clear();
if let Some(level) = map.get(&scope_alloc) {
level_enabled = Some(level);
break;
}
}
}
let Some(level_enabled) = level_enabled else {
// if this scope isn't configured, default to enabled detection done by `log` crate
return (true, level);
};
if level_enabled < &level {
// if the configured level is lower than the requested level, disable logging
// note: err = 0, warn = 1, etc.
return (false, level);
}
// note: bumping level to min level that will be printed
// to work around log crate limitations
return (true, level_min);
}
fn hash_scope_map_settings(map: &HashMap<String, String>) -> u64 {
let mut hasher = DefaultHasher::new();
let mut items = map.iter().collect::<Vec<_>>();
items.sort();
for (key, value) in items {
Hasher::write(&mut hasher, key.as_bytes());
Hasher::write(&mut hasher, value.as_bytes());
}
return hasher.finish();
}
pub fn refresh(settings: &HashMap<String, String>) {
let hash_old = SCOPE_MAP_HASH.load(Ordering::Acquire);
let hash_new = hash_scope_map_settings(settings);
if hash_old == hash_new && hash_old != 0 {
return;
}
// compute new scope map then atomically swap it, instead of
// updating in place to reduce contention
let mut map_new = ScopeMap::with_capacity(settings.len());
'settings: for (key, value) in settings {
let level = match value.to_ascii_lowercase().as_str() {
"" => log_impl::Level::Trace,
"trace" => log_impl::Level::Trace,
"debug" => log_impl::Level::Debug,
"info" => log_impl::Level::Info,
"warn" => log_impl::Level::Warn,
"error" => log_impl::Level::Error,
"off" | "disable" | "no" | "none" | "disabled" => {
crate::warn!("Invalid log level \"{value}\", set to error to disable non-error logging. Defaulting to error");
log_impl::Level::Error
}
_ => {
crate::warn!("Invalid log level \"{value}\", ignoring");
continue 'settings;
}
};
let mut scope_buf = [""; SCOPE_DEPTH_MAX];
for (index, scope) in key.split(SCOPE_STRING_SEP).enumerate() {
let Some(scope_ptr) = scope_buf.get_mut(index) else {
crate::warn!("Invalid scope key, too many nested scopes: '{key}'");
continue 'settings;
};
*scope_ptr = scope;
}
let scope = scope_buf.map(|s| s.to_string());
map_new.insert(scope, level);
}
if let Ok(_) = SCOPE_MAP_HASH.compare_exchange(
hash_old,
hash_new,
Ordering::Release,
Ordering::Relaxed,
) {
let mut map = SCOPE_MAP.write().unwrap_or_else(|err| {
SCOPE_MAP.clear_poison();
err.into_inner()
});
*map = Some(map_new.clone());
// note: hash update done here to ensure consistency with scope map
}
eprintln!("Updated log scope settings :: map = {:?}", map_new);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_crate_name() {
assert_eq!(crate_name!(), "zlog");
}
}

View File

@ -0,0 +1,23 @@
[package]
name = "zlog_settings"
version = "0.1.0"
edition.workspace = true
publish.workspace = true
license = "GPL-3.0-or-later"
[lints]
workspace = true
[lib]
path = "src/zlog_settings.rs"
[features]
default = []
[dependencies]
anyhow.workspace = true
gpui.workspace = true
schemars.workspace = true
serde.workspace = true
settings.workspace = true
zlog.workspace = true

View File

@ -0,0 +1 @@
../../LICENSE-GPL

View File

@ -0,0 +1,35 @@
//! # zlog_settings
use anyhow::Result;
use gpui::App;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use settings::{Settings, SettingsStore};
pub fn init(cx: &mut App) {
ZlogSettings::register(cx);
cx.observe_global::<SettingsStore>(|cx| {
let zlog_settings = ZlogSettings::get_global(cx);
zlog::scope_map::refresh(&zlog_settings.scopes);
})
.detach();
}
#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
pub struct ZlogSettings {
#[serde(default, flatten)]
pub scopes: std::collections::HashMap<String, String>,
}
impl Settings for ZlogSettings {
const KEY: Option<&'static str> = Some("log");
type FileContent = Self;
fn load(sources: settings::SettingsSources<Self::FileContent>, _: &mut App) -> Result<Self>
where
Self: Sized,
{
sources.json_merge()
}
}