ZIm/crates/zlog/src/zlog.rs
Ben Kunkle 3824751e61
Add meta description tag to docs pages (#35112)
Closes #ISSUE

Adds basic frontmatter support to `.md` files in docs. The only
supported keys currently are `description` which becomes a `<meta
name="description" contents="...">` tag, and `title` which becomes a
normal `title` tag, with the title contents prefixed with the subject of
the file.

An example of the syntax can be found in `git.md`, as well as below

```md
---
title: Some more detailed title for this page
description: A page-specific description
---

# Editor
```

The above will be transformed into (with non-relevant tags removed)

```html
<head>
    <title>Editor | Some more detailed title for this page</title>
    <meta name="description" contents="A page-specific description">
</head>
<body>
<h1>Editor</h1>
</body>
```

If no front-matter is provided, or If one or both keys aren't provided,
the title and description will be set based on the `default-title` and
`default-description` keys in `book.toml` respectively.

## Implementation details

Unfortunately, `mdbook` does not support post-processing like it does
pre-processing, and only supports defining one description to put in the
meta tag per book rather than per file. So in order to apply
post-processing (necessary to modify the html head tags) the global book
description is set to a marker value `#description#` and the html
renderer is replaced with a sub-command of `docs_preprocessor` that
wraps the builtin `html` renderer and applies post-processing to the
`html` files, replacing the marker value and the `<title>(.*)</title>`
with the contents of the front-matter if there is one.

## Known limitations

The front-matter parsing is extremely simple, which avoids needing to
take on an additional dependency, or implement full yaml parsing.

* Double quotes and multi-line values are not supported, i.e. Keys and
values must be entirely on the same line, with no double quotes around
the value.

The following will not work:

```md
---
title: Some
 Multi-line
 Title
---
```

* The front-matter must be at the top of the file, with only white-space
preceding it

* The contents of the title and description will not be html-escaped.
They should be simple ascii text with no unicode or emoji characters

Release Notes:

- N/A *or* Added/Fixed/Improved ...

---------

Co-authored-by: Katie Greer <katie@zed.dev>
2025-07-29 23:01:03 +00:00

394 lines
11 KiB
Rust
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//! # 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() {
match try_init() {
Err(err) => {
log::error!("{err}");
eprintln!("{err}");
}
Ok(()) => {}
}
}
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() {
if 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) => {{
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)]
{
unreachable!("Scope overflow trying to add scope... ignoring scope");
}
}
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 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;
};
return 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 {
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_STR: &'static 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 {
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;
}
}
#[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_"
);
}
}