feat: Rebrand Polkadot/Substrate references to PezkuwiChain

This commit systematically rebrands various references from Parity Technologies'
Polkadot/Substrate ecosystem to PezkuwiChain within the kurdistan-sdk.

Key changes include:
- Updated external repository URLs (zombienet-sdk, parity-db, parity-scale-codec, wasm-instrument) to point to pezkuwichain forks.
- Modified internal documentation and code comments to reflect PezkuwiChain naming and structure.
- Replaced direct references to  with  or specific paths within the  for XCM, Pezkuwi, and other modules.
- Cleaned up deprecated  issue and PR references in various  and  files, particularly in  and  modules.
- Adjusted image and logo URLs in documentation to point to PezkuwiChain assets.
- Removed or rephrased comments related to external Polkadot/Substrate PRs and issues.

This is a significant step towards fully customizing the SDK for the PezkuwiChain ecosystem.
This commit is contained in:
2025-12-14 00:04:10 +03:00
parent 286de54384
commit 1c0e57d984
9084 changed files with 997839 additions and 997557 deletions
+71
View File
@@ -0,0 +1,71 @@
[package]
name = "pezsc-tracing"
version = "28.0.0"
license = "GPL-3.0-or-later WITH Classpath-exception-2.0"
authors.workspace = true
edition.workspace = true
homepage.workspace = true
repository.workspace = true
description = "Instrumentation implementation for bizinikiwi."
readme = "README.md"
[lints]
workspace = true
[package.metadata.docs.rs]
targets = ["x86_64-unknown-linux-gnu"]
[[bench]]
name = "bench"
harness = false
[[bench]]
name = "interest_cache_realistic"
harness = false
[[bench]]
name = "interest_cache_diverse"
harness = false
[[bench]]
name = "interest_cache_multithreaded"
harness = false
[dependencies]
chrono = { workspace = true }
codec = { workspace = true, default-features = true }
console = { workspace = true }
is-terminal = { workspace = true }
libc = { workspace = true }
log = { workspace = true, default-features = true }
parking_lot = { workspace = true, default-features = true }
rustc-hash = { workspace = true }
pezsc-client-api = { workspace = true, default-features = true }
pezsc-tracing-proc-macro = { workspace = true, default-features = true }
serde = { workspace = true, default-features = true }
pezsp-api = { workspace = true, default-features = true }
pezsp-blockchain = { workspace = true, default-features = true }
pezsp-core = { workspace = true, default-features = true }
pezsp-rpc = { workspace = true, default-features = true }
pezsp-runtime = { workspace = true, default-features = true }
pezsp-tracing = { workspace = true, default-features = true }
thiserror = { workspace = true }
tracing = { workspace = true, default-features = true }
tracing-log = { workspace = true, features = ["interest-cache"] }
tracing-subscriber = { workspace = true, features = [
"env-filter",
"parking_lot",
] }
[dev-dependencies]
criterion = { workspace = true, default-features = true }
regex = { workspace = true }
tracing-subscriber = { workspace = true, features = ["chrono", "parking_lot"] }
[features]
runtime-benchmarks = [
"pezsc-client-api/runtime-benchmarks",
"pezsp-api/runtime-benchmarks",
"pezsp-blockchain/runtime-benchmarks",
"pezsp-runtime/runtime-benchmarks",
]
+11
View File
@@ -0,0 +1,11 @@
Instrumentation implementation for Bizinikiwi.
This crate is unstable and the API and usage may change.
# Usage
See `pezsp-tracing` for examples on how to use tracing.
Currently we provide `Log` (default), `Telemetry` variants for `Receiver`
License: GPL-3.0-or-later WITH Classpath-exception-2.0
@@ -0,0 +1,55 @@
// This file is part of Bizinikiwi.
// Copyright (C) Parity Technologies (UK) Ltd.
// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
use criterion::{criterion_group, criterion_main, Criterion};
use tracing_subscriber::fmt::{
format,
time::{ChronoLocal, FormatTime},
};
fn bench_fast_local_time(c: &mut Criterion) {
c.bench_function("fast_local_time", |b| {
let mut buffer = String::new();
let t = pezsc_tracing::logging::FastLocalTime { with_fractional: true };
b.iter(|| {
buffer.clear();
let mut writer = format::Writer::new(&mut buffer);
t.format_time(&mut writer).unwrap();
})
});
}
// This is here just as a point of comparison.
fn bench_chrono_local(c: &mut Criterion) {
c.bench_function("chrono_local", |b| {
let mut buffer = String::new();
let t = ChronoLocal::new("%Y-%m-%d %H:%M:%S%.3f".to_string());
b.iter(|| {
buffer.clear();
let mut writer: format::Writer<'_> = format::Writer::new(&mut buffer);
t.format_time(&mut writer).unwrap();
})
});
}
criterion_group! {
name = benches;
config = Criterion::default();
targets = bench_fast_local_time, bench_chrono_local
}
criterion_main!(benches);
@@ -0,0 +1,105 @@
// This file is part of Bizinikiwi.
//
// Copyright (C) Parity Technologies (UK) Ltd.
// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
//! Common utilities for interest cache benchmarks.
use log::{Level, LevelFilter};
use tracing_subscriber::{EnvFilter, FmtSubscriber};
/// Initialize logger with interest cache configuration from INTEREST_CACHE environment variable.
pub fn init_logger() {
let mut log_tracer = tracing_log::LogTracer::builder().with_max_level(LevelFilter::Trace);
if let Some(config) = parse_interest_cache_config() {
eprintln!("Interest cache config: {config:?}");
log_tracer = log_tracer.with_interest_cache(config);
}
log_tracer.init().expect("Failed to init LogTracer");
let env_filter = EnvFilter::default()
.add_directive("info".parse().unwrap())
.add_directive("dummy=trace".parse().unwrap());
let subscriber = FmtSubscriber::builder()
.with_env_filter(env_filter)
.with_writer(std::io::sink)
.with_filter_reloading()
.finish();
tracing::subscriber::set_global_default(subscriber).expect("Failed to set subscriber");
}
/// Parse interest cache configuration from INTEREST_CACHE environment variable.
///
/// Returns `None` if cache should be disabled, `Some(config)` otherwise.
///
/// Supported formats:
/// - "disabled", "none", "no", "false" - cache disabled
/// - "default" - cache enabled with default config
/// - "key=value,key=value" - cache enabled with custom config
/// - lru_cache_size=<number> - set LRU cache size
/// - min_verbosity=<level> - set minimum verbosity level (error, warn, info, debug, trace)
fn parse_interest_cache_config() -> Option<tracing_log::InterestCacheConfig> {
let config_str = std::env::var("INTEREST_CACHE").ok()?;
let config_lower = config_str.to_lowercase();
let mut config = tracing_log::InterestCacheConfig::default();
// Check if disabled
if matches!(config_lower.as_str(), "none" | "disabled" | "no" | "false") {
eprintln!("Interest cache: disabled");
return Some(config.with_lru_cache_size(0));
}
// If "default", use default config
if config_lower == "default" {
eprintln!("Interest cache: default config");
return Some(config);
}
// Parse key=value pairs
for pair in config_str.split(',') {
let parts: Vec<&str> = pair.trim().split('=').collect();
if parts.len() != 2 {
eprintln!("Invalid config pair '{}', expected key=value", pair);
continue;
}
let key = parts[0].trim();
let value = parts[1].trim();
match key {
"lru_cache_size" =>
if let Ok(size) = value.parse::<usize>() {
config = config.with_lru_cache_size(size);
eprintln!("Interest cache: lru_cache_size = {}", size);
} else {
eprintln!("Invalid lru_cache_size value '{}'", value);
},
"min_verbosity" => {
let level = match value.to_lowercase().as_str() {
"error" => Some(Level::Error),
"warn" => Some(Level::Warn),
"info" => Some(Level::Info),
"debug" => Some(Level::Debug),
"trace" => Some(Level::Trace),
_ => {
eprintln!("Invalid min_verbosity '{}', using default", value);
None
},
};
if let Some(level) = level {
config = config.with_min_verbosity(level);
eprintln!("Interest cache: min_verbosity = {:?}", level);
}
},
_ => {
eprintln!("Unknown config key '{}'", key);
},
}
}
Some(config)
}
@@ -0,0 +1,33 @@
// This file is part of Bizinikiwi.
//
// Copyright (C) Parity Technologies (UK) Ltd.
// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
//! Benchmark diverse targets with interest cache configuration from INTEREST_CACHE env var.
//!
//! Usage:
//! ```
//! INTEREST_CACHE=lru_cache_size=1024,min_verbosity=debug cargo bench --bench interest_cache_diverse
//! INTEREST_CACHE=default cargo bench --bench interest_cache_diverse
//! INTEREST_CACHE=disabled cargo bench --bench interest_cache_diverse
//! ```
use criterion::{criterion_group, criterion_main, Criterion};
mod common;
fn bench_diverse_targets(c: &mut Criterion) {
common::init_logger();
c.bench_function("diverse_targets", |b| {
b.iter(|| {
for i in 0..1000 {
let target = format!("module_{}", i % 50);
log::debug!(target: &target, "message {}", i);
log::trace!(target: &target, "trace {}", i);
}
})
});
}
criterion_group!(benches, bench_diverse_targets);
criterion_main!(benches);
@@ -0,0 +1,44 @@
// This file is part of Bizinikiwi.
//
// Copyright (C) Parity Technologies (UK) Ltd.
// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
//! Benchmark multithreaded logging with interest cache configuration from INTEREST_CACHE env var.
//!
//! Usage:
//! ```
//! INTEREST_CACHE=lru_cache_size=1024,min_verbosity=debug cargo bench --bench interest_cache_multithreaded
//! INTEREST_CACHE=default cargo bench --bench interest_cache_multithreaded
//! INTEREST_CACHE=disabled cargo bench --bench interest_cache_multithreaded
//! ```
use criterion::{criterion_group, criterion_main, Criterion};
mod common;
fn bench_multithreaded(c: &mut Criterion) {
common::init_logger();
let mut group = c.benchmark_group("multithreaded");
group.bench_function("8_threads", |b| {
b.iter(|| {
let handles: Vec<_> = (0..8)
.map(|thread_id| {
std::thread::spawn(move || {
for i in 0..1000 {
log::debug!(target: "bizinikiwi", "thread {} msg {}", thread_id, i);
log::trace!(target: "runtime", "thread {} trace {}", thread_id, i);
}
})
})
.collect();
for handle in handles {
handle.join().unwrap();
}
})
});
group.finish();
}
criterion_group!(benches, bench_multithreaded);
criterion_main!(benches);
@@ -0,0 +1,38 @@
// This file is part of Bizinikiwi.
//
// Copyright (C) Parity Technologies (UK) Ltd.
// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
//! Benchmark realistic logging with interest cache configuration from INTEREST_CACHE env var.
//!
//! Usage:
//! ```
//! INTEREST_CACHE=lru_cache_size=1024,min_verbosity=debug cargo bench --bench interest_cache_realistic
//! INTEREST_CACHE=default cargo bench --bench interest_cache_realistic
//! INTEREST_CACHE=disabled cargo bench --bench interest_cache_realistic
//! ```
use criterion::{criterion_group, criterion_main, Criterion};
mod common;
fn bench_realistic_logging(c: &mut Criterion) {
common::init_logger();
c.bench_function("realistic_logging", |b| {
b.iter(|| {
for i in 0..1000 {
log::trace!(target: "bizinikiwi", "trace message {}", i);
log::debug!(target: "runtime", "debug message {}", i);
log::debug!(target: "sync", "sync debug {}", i);
log::trace!(target: "consensus", "consensus trace {}", i);
log::debug!(target: "network", "network debug {}", i);
if i % 10 == 0 {
log::info!(target: "bizinikiwi", "info message {}", i);
}
}
})
});
}
criterion_group!(benches, bench_realistic_logging);
criterion_main!(benches);
@@ -0,0 +1,29 @@
[package]
name = "pezsc-tracing-proc-macro"
version = "11.0.0"
authors.workspace = true
edition.workspace = true
license = "Apache-2.0"
homepage.workspace = true
repository.workspace = true
description = "Helper macros for Bizinikiwi's client CLI"
[lints]
workspace = true
[package.metadata.docs.rs]
targets = ["x86_64-unknown-linux-gnu"]
[lib]
proc-macro = true
[dependencies]
proc-macro-crate = { workspace = true }
proc-macro2 = { workspace = true }
quote = { features = ["proc-macro"], workspace = true }
syn = { features = [
"extra-traits",
"full",
"parsing",
"proc-macro",
], workspace = true }
@@ -0,0 +1,154 @@
// This file is part of Bizinikiwi.
// Copyright (C) Parity Technologies (UK) Ltd.
// SPDX-License-Identifier: Apache-2.0
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use proc_macro::TokenStream;
use proc_macro2::Span;
use proc_macro_crate::{crate_name, FoundCrate};
use quote::quote;
use syn::{Error, Expr, ItemFn, Path, Result};
/// This prefixes all the log lines with `[<name>]` (after the timestamp). It works by making a
/// tracing's span that is propagated to all the child calls and child tasks (futures) if they are
/// spawned properly with the `SpawnHandle` (see `TaskManager` in sc-cli) or if the futures use
/// `.in_current_span()` (see tracing-futures).
///
/// See Tokio's [tracing documentation](https://docs.rs/tracing-core/) and
/// [tracing-futures documentation](https://docs.rs/tracing-futures/) for more details.
///
/// # Implementation notes
///
/// If there are multiple spans with a log prefix, only the latest will be shown.
///
/// # Example with a literal
///
/// ```ignore
/// Builds a new service for a light client.
/// #[pezsc_cli::prefix_logs_with("light")]
/// pub fn new_light(config: Configuration) -> Result<TaskManager, ServiceError> {
/// let (client, backend, keystore, mut task_manager, on_demand) =
/// pezsc_service::new_light_parts::<Block, RuntimeApi, Executor>(&config)?;
///
/// ...
/// }
/// ```
///
/// Will produce logs that look like this:
///
/// ```text
/// 2020-10-16 08:03:14 Bizinikiwi Node
/// 2020-10-16 08:03:14 ✌️ version 2.0.0-47f7d3f2e-x86_64-linux-gnu
/// 2020-10-16 08:03:14 ❤️ by Anonymous, 2017-2020
/// 2020-10-16 08:03:14 📋 Chain specification: Local Testnet
/// 2020-10-16 08:03:14 🏷 Node name: nice-glove-1401
/// 2020-10-16 08:03:14 👤 Role: LIGHT
/// 2020-10-16 08:03:14 💾 Database: RocksDb at /tmp/bizinikiwi95w2Dk/chains/local_testnet/db
/// 2020-10-16 08:03:14 ⛓ Native runtime: node-template-1 (node-template-1.tx1.au1)
/// 2020-10-16 08:03:14 [light] 🔨 Initializing Genesis block/state (state: 0x121d…8e36, header-hash: 0x24ef…8ff6)
/// 2020-10-16 08:03:14 [light] Loading GRANDPA authorities from genesis on what appears to be first startup.
/// 2020-10-16 08:03:15 [light] ⏱ Loaded block-time = 6000 milliseconds from genesis on first-launch
/// 2020-10-16 08:03:15 [light] Using default protocol ID "sup" because none is configured in the chain specs
/// 2020-10-16 08:03:15 [light] 🏷 Local node identity is: 12D3KooWHX4rkWT6a6N55Km7ZnvenGdShSKPkzJ3yj9DU5nqDtWR
/// 2020-10-16 08:03:15 [light] 📦 Highest known block at #0
/// 2020-10-16 08:03:15 [light] 〽️ Prometheus server started at 127.0.0.1:9615
/// 2020-10-16 08:03:15 [light] Listening for new connections on 127.0.0.1:9944.
/// ```
///
/// # Example using the actual node name
///
/// ```ignore
/// Builds a new service for a light client.
/// #[pezsc_cli::prefix_logs_with(config.network.node_name.as_str())]
/// pub fn new_light(config: Configuration) -> Result<TaskManager, ServiceError> {
/// let (client, backend, keystore, mut task_manager, on_demand) =
/// pezsc_service::new_light_parts::<Block, RuntimeApi, Executor>(&config)?;
///
/// ...
/// }
/// ```
///
/// Will produce logs that look like this:
///
/// ```text
/// 2020-10-16 08:12:57 Bizinikiwi Node
/// 2020-10-16 08:12:57 ✌️ version 2.0.0-efb9b822a-x86_64-linux-gnu
/// 2020-10-16 08:12:57 ❤️ by Anonymous, 2017-2020
/// 2020-10-16 08:12:57 📋 Chain specification: Local Testnet
/// 2020-10-16 08:12:57 🏷 Node name: open-harbor-1619
/// 2020-10-16 08:12:57 👤 Role: LIGHT
/// 2020-10-16 08:12:57 💾 Database: RocksDb at /tmp/bizinikiwi9T9Mtb/chains/local_testnet/db
/// 2020-10-16 08:12:57 ⛓ Native runtime: node-template-1 (node-template-1.tx1.au1)
/// 2020-10-16 08:12:58 [open-harbor-1619] 🔨 Initializing Genesis block/state (state: 0x121d…8e36, header-hash: 0x24ef…8ff6)
/// 2020-10-16 08:12:58 [open-harbor-1619] Loading GRANDPA authorities from genesis on what appears to be first startup.
/// 2020-10-16 08:12:58 [open-harbor-1619] ⏱ Loaded block-time = 6000 milliseconds from genesis on first-launch
/// 2020-10-16 08:12:58 [open-harbor-1619] Using default protocol ID "sup" because none is configured in the chain specs
/// 2020-10-16 08:12:58 [open-harbor-1619] 🏷 Local node identity is: 12D3KooWRzmYC8QTK1Pm8Cfvid3skTS4Hn54jc4AUtje8Rqbfgtp
/// 2020-10-16 08:12:58 [open-harbor-1619] 📦 Highest known block at #0
/// 2020-10-16 08:12:58 [open-harbor-1619] 〽️ Prometheus server started at 127.0.0.1:9615
/// 2020-10-16 08:12:58 [open-harbor-1619] Listening for new connections on 127.0.0.1:9944.
/// ```
#[proc_macro_attribute]
pub fn prefix_logs_with(arg: TokenStream, item: TokenStream) -> TokenStream {
// Ensure an argument was provided.
if arg.is_empty() {
return Error::new(
proc_macro2::Span::call_site(),
"missing argument: prefix. Example: prefix_logs_with(\"Relaychain\")",
)
.to_compile_error()
.into();
}
let prefix_expr = syn::parse_macro_input!(arg as Expr);
let item_fn = syn::parse_macro_input!(item as ItemFn);
// Resolve the proper pezsc_tracing path.
let crate_name = match resolve_sc_tracing() {
Ok(path) => path,
Err(err) => return err.to_compile_error().into(),
};
let syn::ItemFn { attrs, vis, sig, block } = item_fn;
(quote! {
#(#attrs)*
#vis #sig {
let span = #crate_name::tracing::info_span!(
#crate_name::logging::PREFIX_LOG_SPAN,
name = #prefix_expr,
);
let _enter = span.enter();
#block
}
})
.into()
}
/// Resolve the correct path for pezsc_tracing:
/// - If `pezkuwi-sdk` is in scope, returns a Path corresponding to `pezkuwi_sdk::pezsc_tracing`
/// - Otherwise, falls back to `pezsc_tracing`
fn resolve_sc_tracing() -> Result<Path> {
match crate_name("pezkuwi-sdk") {
Ok(FoundCrate::Itself) => syn::parse_str("pezkuwi_sdk::pezsc_tracing"),
Ok(FoundCrate::Name(sdk_name)) => syn::parse_str(&format!("{}::pezsc_tracing", sdk_name)),
Err(_) => match crate_name("sc-tracing") {
Ok(FoundCrate::Itself) => syn::parse_str("pezsc_tracing"),
Ok(FoundCrate::Name(name)) => syn::parse_str(&name),
Err(e) => Err(syn::Error::new(Span::call_site(), e)),
},
}
}
+387
View File
@@ -0,0 +1,387 @@
// Copyright (C) Parity Technologies (UK) Ltd.
// This file is part of Bizinikiwi.
// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
// Bizinikiwi is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Bizinikiwi is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Bizinikiwi. If not, see <https://www.gnu.org/licenses/>.
//! Utilities for tracing block execution
use std::{
collections::HashMap,
sync::{
atomic::{AtomicU64, Ordering},
Arc,
},
time::Instant,
};
use codec::Encode;
use parking_lot::Mutex;
use tracing::{
dispatcher,
span::{Attributes, Id, Record},
Dispatch, Level, Subscriber,
};
use crate::{SpanDatum, TraceEvent, Values};
use pezsc_client_api::BlockBackend;
use pezsp_api::{Core, ProvideRuntimeApi};
use pezsp_blockchain::HeaderBackend;
use pezsp_core::hexdisplay::HexDisplay;
use pezsp_rpc::tracing::{BlockTrace, Span, TraceBlockResponse};
use pezsp_runtime::{
generic::BlockId,
traits::{Block as BlockT, Header},
};
use pezsp_tracing::{WASM_NAME_KEY, WASM_TARGET_KEY, WASM_TRACE_IDENTIFIER};
// Default to only pallet, frame support and state related traces
const DEFAULT_TARGETS: &str = "pallet,frame,state";
const TRACE_TARGET: &str = "block_trace";
// The name of a field required for all events.
const REQUIRED_EVENT_FIELD: &str = "method";
/// Something that can execute a block in a tracing context.
pub trait TracingExecuteBlock<Block: BlockT>: Send + Sync {
/// Execute the given `block`.
///
/// The `block` is prepared to be executed right away, this means that any `Seal` was already
/// removed from the header. As this changes the `hash` of the block, `orig_hash` is passed
/// alongside to the callee.
///
/// The execution should be done sync on the same thread, because the caller will register
/// special tracing collectors.
fn execute_block(&self, orig_hash: Block::Hash, block: Block) -> pezsp_blockchain::Result<()>;
}
/// Default implementation of [`ExecuteBlock`].
///
/// Uses [`Core::execute_block`] to directly execute a block.
struct DefaultExecuteBlock<Client> {
client: Arc<Client>,
}
impl<Client> DefaultExecuteBlock<Client> {
/// Creates a new instance.
pub fn new(client: Arc<Client>) -> Self {
Self { client }
}
}
impl<Client, Block> TracingExecuteBlock<Block> for DefaultExecuteBlock<Client>
where
Client: ProvideRuntimeApi<Block> + Send + Sync + 'static,
Client::Api: Core<Block>,
Block: BlockT,
{
fn execute_block(&self, _: Block::Hash, block: Block) -> pezsp_blockchain::Result<()> {
self.client
.runtime_api()
.execute_block(*block.header().parent_hash(), block.into())
.map_err(Into::into)
}
}
/// Tracing Block Result type alias
pub type TraceBlockResult<T> = Result<T, Error>;
/// Tracing Block error
#[derive(Debug, thiserror::Error)]
#[allow(missing_docs)]
#[non_exhaustive]
pub enum Error {
#[error("Invalid block Id: {0}")]
InvalidBlockId(#[from] pezsp_blockchain::Error),
#[error("Missing block component: {0}")]
MissingBlockComponent(String),
#[error("Dispatch error: {0}")]
Dispatch(String),
}
struct BlockSubscriber {
targets: Vec<(String, Level)>,
next_id: AtomicU64,
spans: Mutex<HashMap<Id, SpanDatum>>,
events: Mutex<Vec<TraceEvent>>,
}
impl BlockSubscriber {
fn new(targets: &str) -> Self {
let next_id = AtomicU64::new(1);
let mut targets: Vec<_> = targets.split(',').map(crate::parse_target).collect();
// Ensure that WASM traces are always enabled
// Filtering happens when decoding the actual target / level
targets.push((WASM_TRACE_IDENTIFIER.to_owned(), Level::TRACE));
BlockSubscriber {
targets,
next_id,
spans: Mutex::new(HashMap::new()),
events: Mutex::new(Vec::new()),
}
}
}
impl Subscriber for BlockSubscriber {
fn enabled(&self, metadata: &tracing::Metadata<'_>) -> bool {
if !metadata.is_span() && metadata.fields().field(REQUIRED_EVENT_FIELD).is_none() {
return false;
}
for (target, level) in &self.targets {
if metadata.level() <= level && metadata.target().starts_with(target) {
return true;
}
}
false
}
fn new_span(&self, attrs: &Attributes<'_>) -> Id {
let id = Id::from_u64(self.next_id.fetch_add(1, Ordering::Relaxed));
let mut values = Values::default();
attrs.record(&mut values);
let parent_id = attrs.parent().cloned();
let span = SpanDatum {
id: id.clone(),
parent_id,
name: attrs.metadata().name().to_owned(),
target: attrs.metadata().target().to_owned(),
level: *attrs.metadata().level(),
line: attrs.metadata().line().unwrap_or(0),
start_time: Instant::now(),
values,
overall_time: Default::default(),
};
self.spans.lock().insert(id.clone(), span);
id
}
fn record(&self, span: &Id, values: &Record<'_>) {
let mut span_data = self.spans.lock();
if let Some(s) = span_data.get_mut(span) {
values.record(&mut s.values);
}
}
fn record_follows_from(&self, _span: &Id, _follows: &Id) {
// Not currently used
unimplemented!("record_follows_from is not implemented");
}
fn event(&self, event: &tracing::Event<'_>) {
let mut values = crate::Values::default();
event.record(&mut values);
let parent_id = event.parent().cloned();
let trace_event = TraceEvent {
name: event.metadata().name().to_owned(),
target: event.metadata().target().to_owned(),
level: *event.metadata().level(),
values,
parent_id,
};
self.events.lock().push(trace_event);
}
fn enter(&self, _id: &Id) {}
fn exit(&self, _span: &Id) {}
}
/// Holds a reference to the client in order to execute the given block.
/// Records spans & events for the supplied targets (eg. "pallet,frame,state") and
/// only records events with the specified hex encoded storage key prefixes.
/// Note: if `targets` or `storage_keys` is an empty string then nothing is
/// filtered out.
pub struct BlockExecutor<Block: BlockT, Client> {
client: Arc<Client>,
block: Block::Hash,
targets: Option<String>,
storage_keys: Option<String>,
methods: Option<String>,
execute_block: Arc<dyn TracingExecuteBlock<Block>>,
}
impl<Block, Client> BlockExecutor<Block, Client>
where
Block: BlockT + 'static,
Client: HeaderBackend<Block>
+ BlockBackend<Block>
+ ProvideRuntimeApi<Block>
+ Send
+ Sync
+ 'static,
Client::Api: Core<Block>,
{
/// Create a new `BlockExecutor`
pub fn new(
client: Arc<Client>,
block: Block::Hash,
targets: Option<String>,
storage_keys: Option<String>,
methods: Option<String>,
execute_block: Option<Arc<dyn TracingExecuteBlock<Block>>>,
) -> Self {
Self {
client: client.clone(),
block,
targets,
storage_keys,
methods,
execute_block: execute_block
.unwrap_or_else(|| Arc::new(DefaultExecuteBlock::new(client))),
}
}
/// Execute block, record all spans and events belonging to `Self::targets`
/// and filter out events which do not have keys starting with one of the
/// prefixes in `Self::storage_keys`.
pub fn trace_block(&self) -> TraceBlockResult<TraceBlockResponse> {
tracing::debug!(target: "state_tracing", "Tracing block: {}", self.block);
// Prepare the block
let mut header = self
.client
.header(self.block)
.map_err(Error::InvalidBlockId)?
.ok_or_else(|| Error::MissingBlockComponent("Header not found".to_string()))?;
let extrinsics = self
.client
.block_body(self.block)
.map_err(Error::InvalidBlockId)?
.ok_or_else(|| Error::MissingBlockComponent("Extrinsics not found".to_string()))?;
tracing::debug!(target: "state_tracing", "Found {} extrinsics", extrinsics.len());
let parent_hash = *header.parent_hash();
// Remove all `Seal`s as they are added by the consensus engines after building the block.
// On import they are normally removed by the consensus engine.
header.digest_mut().logs.retain(|d| d.as_seal().is_none());
let block = Block::new(header, extrinsics);
let targets = if let Some(t) = &self.targets { t } else { DEFAULT_TARGETS };
let block_subscriber = BlockSubscriber::new(targets);
let dispatch = Dispatch::new(block_subscriber);
{
let dispatcher_span = tracing::debug_span!(
target: "state_tracing",
"execute_block",
extrinsics_len = block.extrinsics().len(),
);
let _guard = dispatcher_span.enter();
if let Err(e) = dispatcher::with_default(&dispatch, || {
let span = tracing::info_span!(target: TRACE_TARGET, "trace_block");
let _enter = span.enter();
self.execute_block.execute_block(self.block, block)
}) {
return Err(Error::Dispatch(format!(
"Failed to collect traces and execute block: {e:?}"
)));
}
}
let block_subscriber = dispatch.downcast_ref::<BlockSubscriber>().ok_or_else(|| {
Error::Dispatch(
"Cannot downcast Dispatch to BlockSubscriber after tracing block".to_string(),
)
})?;
let spans: Vec<_> = block_subscriber
.spans
.lock()
.drain()
// Patch wasm identifiers
.filter_map(|(_, s)| patch_and_filter(s, targets))
.collect();
let events: Vec<_> = block_subscriber
.events
.lock()
.drain(..)
.filter(|e| {
self.storage_keys
.as_ref()
.map(|keys| event_values_filter(e, "key", keys))
.unwrap_or(false)
})
.filter(|e| {
self.methods
.as_ref()
.map(|methods| event_values_filter(e, "method", methods))
.unwrap_or(false)
})
.map(|s| s.into())
.collect();
tracing::debug!(target: "state_tracing", "Captured {} spans and {} events", spans.len(), events.len());
Ok(TraceBlockResponse::BlockTrace(BlockTrace {
block_hash: block_id_as_string(BlockId::<Block>::Hash(self.block)),
parent_hash: block_id_as_string(BlockId::<Block>::Hash(parent_hash)),
tracing_targets: targets.to_string(),
storage_keys: self.storage_keys.clone().unwrap_or_default(),
methods: self.methods.clone().unwrap_or_default(),
spans,
events,
}))
}
}
fn event_values_filter(event: &TraceEvent, filter_kind: &str, values: &str) -> bool {
event
.values
.string_values
.get(filter_kind)
.map(|value| check_target(values, value, &event.level))
.unwrap_or(false)
}
/// Filter out spans that do not match our targets and if the span is from WASM update its `name`
/// and `target` fields to the WASM values for those fields.
// The `tracing` crate requires trace metadata to be static. This does not work for wasm code in
// bizinikiwi, as it is regularly updated with new code from on-chain events. The workaround for this
// is for bizinikiwi's WASM tracing wrappers to put the `name` and `target` data in the `values` map
// (normally they would be in the static metadata assembled at compile time). Here, if a special
// WASM `name` or `target` key is found in the `values` we remove it and put the key value pair in
// the span's metadata, making it consistent with spans that come from native code.
fn patch_and_filter(mut span: SpanDatum, targets: &str) -> Option<Span> {
if span.name == WASM_TRACE_IDENTIFIER {
span.values.bool_values.insert("wasm".to_owned(), true);
if let Some(n) = span.values.string_values.remove(WASM_NAME_KEY) {
span.name = n;
}
if let Some(t) = span.values.string_values.remove(WASM_TARGET_KEY) {
span.target = t;
}
if !check_target(targets, &span.target, &span.level) {
return None;
}
}
Some(span.into())
}
/// Check if a `target` matches any `targets` by prefix
fn check_target(targets: &str, target: &str, level: &Level) -> bool {
for (t, l) in targets.split(',').map(crate::parse_target) {
if target.starts_with(t.as_str()) && level <= &l {
return true;
}
}
false
}
fn block_id_as_string<T: BlockT>(block_id: BlockId<T>) -> String {
match block_id {
BlockId::Hash(h) => HexDisplay::from(&h.encode()).to_string(),
BlockId::Number(n) => HexDisplay::from(&n.encode()).to_string(),
}
}
+675
View File
@@ -0,0 +1,675 @@
// This file is part of Bizinikiwi.
// Copyright (C) Parity Technologies (UK) Ltd.
// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
//! Instrumentation implementation for bizinikiwi.
//!
//! This crate is unstable and the API and usage may change.
//!
//! # Usage
//!
//! See `sp-tracing` for examples on how to use tracing.
//!
//! Currently we only provide `Log` (default).
#![warn(missing_docs)]
pub mod block;
pub mod logging;
use rustc_hash::FxHashMap;
use serde::ser::{Serialize, SerializeMap, Serializer};
use pezsp_tracing::{WASM_NAME_KEY, WASM_TARGET_KEY, WASM_TRACE_IDENTIFIER};
use std::{
fmt,
time::{Duration, Instant},
};
use tracing::{
event::Event,
field::{Field, Visit},
span::{Attributes, Id, Record},
subscriber::Subscriber,
Level,
};
use tracing_subscriber::{
layer::{Context, Layer},
registry::LookupSpan,
};
#[doc(hidden)]
pub use tracing;
const ZERO_DURATION: Duration = Duration::from_nanos(0);
/// Responsible for assigning ids to new spans, which are not re-used.
pub struct ProfilingLayer {
targets: Vec<(String, Level)>,
trace_handlers: Vec<Box<dyn TraceHandler>>,
}
/// Used to configure how to receive the metrics
#[derive(Debug, Clone)]
pub enum TracingReceiver {
/// Output to logger
Log,
}
impl Default for TracingReceiver {
fn default() -> Self {
Self::Log
}
}
/// A handler for tracing `SpanDatum`
pub trait TraceHandler: Send + Sync {
/// Process a `SpanDatum`.
fn handle_span(&self, span: &SpanDatum);
/// Process a `TraceEvent`.
fn handle_event(&self, event: &TraceEvent);
}
/// Represents a tracing event, complete with values
#[derive(Debug, Clone)]
pub struct TraceEvent {
/// Name of the event.
pub name: String,
/// Target of the event.
pub target: String,
/// Level of the event.
pub level: Level,
/// Values for this event.
pub values: Values,
/// Id of the parent tracing event, if any.
pub parent_id: Option<Id>,
}
/// Represents a single instance of a tracing span
#[derive(Debug, Clone)]
pub struct SpanDatum {
/// id for this span
pub id: Id,
/// id of the parent span, if any
pub parent_id: Option<Id>,
/// Name of this span
pub name: String,
/// Target, typically module
pub target: String,
/// Tracing Level - ERROR, WARN, INFO, DEBUG or TRACE
pub level: Level,
/// Line number in source
pub line: u32,
/// Time that the span was last entered
pub start_time: Instant,
/// Total duration of span while entered
pub overall_time: Duration,
/// Values recorded to this span
pub values: Values,
}
/// Holds associated values for a tracing span
#[derive(Default, Clone, Debug)]
pub struct Values {
/// FxHashMap of `bool` values
pub bool_values: FxHashMap<String, bool>,
/// FxHashMap of `i64` values
pub i64_values: FxHashMap<String, i64>,
/// FxHashMap of `u64` values
pub u64_values: FxHashMap<String, u64>,
/// FxHashMap of `String` values
pub string_values: FxHashMap<String, String>,
}
impl Values {
/// Returns a new instance of Values
pub fn new() -> Self {
Default::default()
}
/// Checks if all individual collections are empty
pub fn is_empty(&self) -> bool {
self.bool_values.is_empty() &&
self.i64_values.is_empty() &&
self.u64_values.is_empty() &&
self.string_values.is_empty()
}
}
impl Visit for Values {
fn record_i64(&mut self, field: &Field, value: i64) {
self.i64_values.insert(field.name().to_string(), value);
}
fn record_u64(&mut self, field: &Field, value: u64) {
self.u64_values.insert(field.name().to_string(), value);
}
fn record_bool(&mut self, field: &Field, value: bool) {
self.bool_values.insert(field.name().to_string(), value);
}
fn record_str(&mut self, field: &Field, value: &str) {
self.string_values.insert(field.name().to_string(), value.to_owned());
}
fn record_debug(&mut self, field: &Field, value: &dyn std::fmt::Debug) {
self.string_values.insert(field.name().to_string(), format!("{:?}", value));
}
}
impl Serialize for Values {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let len = self.bool_values.len() +
self.i64_values.len() +
self.u64_values.len() +
self.string_values.len();
let mut map = serializer.serialize_map(Some(len))?;
for (k, v) in &self.bool_values {
map.serialize_entry(k, v)?;
}
for (k, v) in &self.i64_values {
map.serialize_entry(k, v)?;
}
for (k, v) in &self.u64_values {
map.serialize_entry(k, v)?;
}
for (k, v) in &self.string_values {
map.serialize_entry(k, v)?;
}
map.end()
}
}
impl fmt::Display for Values {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let bool_iter = self.bool_values.iter().map(|(k, v)| format!("{}={}", k, v));
let i64_iter = self.i64_values.iter().map(|(k, v)| format!("{}={}", k, v));
let u64_iter = self.u64_values.iter().map(|(k, v)| format!("{}={}", k, v));
let string_iter = self.string_values.iter().map(|(k, v)| format!("{}=\"{}\"", k, v));
let values = bool_iter
.chain(i64_iter)
.chain(u64_iter)
.chain(string_iter)
.collect::<Vec<String>>()
.join(", ");
write!(f, "{}", values)
}
}
/// Trace handler event types.
#[derive(Debug)]
pub enum TraceHandlerEvents {
/// An event.
Event(TraceEvent),
/// A span.
Span(SpanDatum),
}
impl ProfilingLayer {
/// Takes a `TracingReceiver` and a comma separated list of targets,
/// either with a level: "pallet=trace,frame=debug"
/// or without: "pallet,frame" in which case the level defaults to `trace`.
/// wasm_tracing indicates whether to enable wasm traces
pub fn new(receiver: TracingReceiver, targets: &str) -> Self {
match receiver {
TracingReceiver::Log => Self::new_with_handler(Box::new(LogTraceHandler), targets),
}
}
/// Allows use of a custom TraceHandler to create a new instance of ProfilingSubscriber.
/// Takes a comma separated list of targets,
/// either with a level, eg: "pallet=trace"
/// or without: "pallet" in which case the level defaults to `trace`.
/// wasm_tracing indicates whether to enable wasm traces
pub fn new_with_handler(trace_handler: Box<dyn TraceHandler>, targets: &str) -> Self {
let targets: Vec<_> = targets.split(',').map(parse_target).collect();
Self { targets, trace_handlers: vec![trace_handler] }
}
/// Attach additional handlers to allow handling of custom events/spans.
pub fn add_handler(&mut self, trace_handler: Box<dyn TraceHandler>) {
self.trace_handlers.push(trace_handler);
}
fn check_target(&self, target: &str, level: &Level) -> bool {
for t in &self.targets {
if target.starts_with(t.0.as_str()) && level <= &t.1 {
return true;
}
}
false
}
/// Sequentially dispatch a trace event to all handlers.
fn dispatch_event(&self, event: TraceHandlerEvents) {
match &event {
TraceHandlerEvents::Span(span_datum) => {
self.trace_handlers.iter().for_each(|handler| handler.handle_span(span_datum));
},
TraceHandlerEvents::Event(event) => {
self.trace_handlers.iter().for_each(|handler| handler.handle_event(event));
},
}
}
}
// Default to TRACE if no level given or unable to parse Level
// We do not support a global `Level` currently
fn parse_target(s: &str) -> (String, Level) {
match s.find('=') {
Some(i) => {
let target = s[0..i].to_string();
if s.len() > i {
let level = s[i + 1..].parse::<Level>().unwrap_or(Level::TRACE);
(target, level)
} else {
(target, Level::TRACE)
}
},
None => (s.to_string(), Level::TRACE),
}
}
impl<S> Layer<S> for ProfilingLayer
where
S: Subscriber + for<'span> LookupSpan<'span>,
{
fn on_new_span(&self, attrs: &Attributes<'_>, id: &Id, ctx: Context<S>) {
if let Some(span) = ctx.span(id) {
let mut extension = span.extensions_mut();
let parent_id = attrs.parent().cloned().or_else(|| {
if attrs.is_contextual() {
ctx.lookup_current().map(|span| span.id())
} else {
None
}
});
let mut values = Values::default();
attrs.record(&mut values);
let span_datum = SpanDatum {
id: id.clone(),
parent_id,
name: attrs.metadata().name().to_owned(),
target: attrs.metadata().target().to_owned(),
level: *attrs.metadata().level(),
line: attrs.metadata().line().unwrap_or(0),
start_time: Instant::now(),
overall_time: ZERO_DURATION,
values,
};
extension.insert(span_datum);
}
}
fn on_record(&self, id: &Id, values: &Record<'_>, ctx: Context<S>) {
if let Some(span) = ctx.span(id) {
let mut extensions = span.extensions_mut();
if let Some(s) = extensions.get_mut::<SpanDatum>() {
values.record(&mut s.values);
}
}
}
fn on_event(&self, event: &Event<'_>, ctx: Context<S>) {
if !self.check_target(event.metadata().target(), &event.metadata().level()) {
return;
}
let parent_id = event.parent().cloned().or_else(|| {
if event.is_contextual() {
ctx.lookup_current().map(|span| span.id())
} else {
None
}
});
let mut values = Values::default();
event.record(&mut values);
let trace_event = TraceEvent {
name: event.metadata().name().to_owned(),
target: event.metadata().target().to_owned(),
level: *event.metadata().level(),
values,
parent_id,
};
self.dispatch_event(TraceHandlerEvents::Event(trace_event));
}
fn on_enter(&self, span: &Id, ctx: Context<S>) {
if let Some(span) = ctx.span(span) {
let mut extensions = span.extensions_mut();
if let Some(s) = extensions.get_mut::<SpanDatum>() {
let start_time = Instant::now();
s.start_time = start_time;
}
}
}
fn on_exit(&self, span: &Id, ctx: Context<S>) {
if let Some(span) = ctx.span(span) {
let end_time = Instant::now();
let mut extensions = span.extensions_mut();
if let Some(mut span_datum) = extensions.remove::<SpanDatum>() {
span_datum.overall_time += end_time - span_datum.start_time;
if span_datum.name == WASM_TRACE_IDENTIFIER {
span_datum.values.bool_values.insert("wasm".to_owned(), true);
if let Some(n) = span_datum.values.string_values.remove(WASM_NAME_KEY) {
span_datum.name = n;
}
if let Some(t) = span_datum.values.string_values.remove(WASM_TARGET_KEY) {
span_datum.target = t;
}
if self.check_target(&span_datum.target, &span_datum.level) {
self.dispatch_event(TraceHandlerEvents::Span(span_datum));
}
} else {
self.dispatch_event(TraceHandlerEvents::Span(span_datum));
}
}
}
}
fn on_close(&self, _span: Id, _ctx: Context<S>) {}
}
/// TraceHandler for sending span data to the logger
pub struct LogTraceHandler;
fn log_level(level: Level) -> log::Level {
match level {
Level::TRACE => log::Level::Trace,
Level::DEBUG => log::Level::Debug,
Level::INFO => log::Level::Info,
Level::WARN => log::Level::Warn,
Level::ERROR => log::Level::Error,
}
}
impl TraceHandler for LogTraceHandler {
fn handle_span(&self, span_datum: &SpanDatum) {
if span_datum.values.is_empty() {
log::log!(
log_level(span_datum.level),
"{}: {}, time: {}, id: {}, parent_id: {:?}",
span_datum.target,
span_datum.name,
span_datum.overall_time.as_nanos(),
span_datum.id.into_u64(),
span_datum.parent_id.as_ref().map(|s| s.into_u64()),
);
} else {
log::log!(
log_level(span_datum.level),
"{}: {}, time: {}, id: {}, parent_id: {:?}, values: {}",
span_datum.target,
span_datum.name,
span_datum.overall_time.as_nanos(),
span_datum.id.into_u64(),
span_datum.parent_id.as_ref().map(|s| s.into_u64()),
span_datum.values,
);
}
}
fn handle_event(&self, event: &TraceEvent) {
log::log!(
log_level(event.level),
"{}, parent_id: {:?}, {}",
event.target,
event.parent_id.as_ref().map(|s| s.into_u64()),
event.values,
);
}
}
impl From<TraceEvent> for pezsp_rpc::tracing::Event {
fn from(trace_event: TraceEvent) -> Self {
let data = pezsp_rpc::tracing::Data { string_values: trace_event.values.string_values };
pezsp_rpc::tracing::Event {
target: trace_event.target,
data,
parent_id: trace_event.parent_id.map(|id| id.into_u64()),
}
}
}
impl From<SpanDatum> for pezsp_rpc::tracing::Span {
fn from(span_datum: SpanDatum) -> Self {
let wasm = span_datum.values.bool_values.get("wasm").is_some();
pezsp_rpc::tracing::Span {
id: span_datum.id.into_u64(),
parent_id: span_datum.parent_id.map(|id| id.into_u64()),
name: span_datum.name,
target: span_datum.target,
wasm,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use parking_lot::Mutex;
use std::sync::{
mpsc::{Receiver, Sender},
Arc,
};
use tracing_subscriber::layer::SubscriberExt;
struct TestTraceHandler {
spans: Arc<Mutex<Vec<SpanDatum>>>,
events: Arc<Mutex<Vec<TraceEvent>>>,
}
impl TraceHandler for TestTraceHandler {
fn handle_span(&self, sd: &SpanDatum) {
self.spans.lock().push(sd.clone());
}
fn handle_event(&self, event: &TraceEvent) {
self.events.lock().push(event.clone());
}
}
fn setup_subscriber() -> (
impl tracing::Subscriber + Send + Sync,
Arc<Mutex<Vec<SpanDatum>>>,
Arc<Mutex<Vec<TraceEvent>>>,
) {
let spans = Arc::new(Mutex::new(Vec::new()));
let events = Arc::new(Mutex::new(Vec::new()));
let handler = TestTraceHandler { spans: spans.clone(), events: events.clone() };
let layer = ProfilingLayer::new_with_handler(Box::new(handler), "test_target");
let subscriber = tracing_subscriber::fmt().with_writer(std::io::sink).finish().with(layer);
(subscriber, spans, events)
}
#[test]
fn test_span() {
let (sub, spans, events) = setup_subscriber();
let _sub_guard = tracing::subscriber::set_default(sub);
let span = tracing::info_span!(target: "test_target", "test_span1");
assert_eq!(spans.lock().len(), 0);
assert_eq!(events.lock().len(), 0);
let _guard = span.enter();
assert_eq!(spans.lock().len(), 0);
assert_eq!(events.lock().len(), 0);
drop(_guard);
drop(span);
assert_eq!(spans.lock().len(), 1);
assert_eq!(events.lock().len(), 0);
let sd = spans.lock().remove(0);
assert_eq!(sd.name, "test_span1");
assert_eq!(sd.target, "test_target");
let time: u128 = sd.overall_time.as_nanos();
assert!(time > 0);
}
#[test]
fn test_span_parent_id() {
let (sub, spans, _events) = setup_subscriber();
let _sub_guard = tracing::subscriber::set_default(sub);
let span1 = tracing::info_span!(target: "test_target", "test_span1");
let _guard1 = span1.enter();
let span2 = tracing::info_span!(target: "test_target", "test_span2");
let _guard2 = span2.enter();
drop(_guard2);
drop(span2);
let sd2 = spans.lock().remove(0);
drop(_guard1);
drop(span1);
let sd1 = spans.lock().remove(0);
assert_eq!(sd1.id, sd2.parent_id.unwrap())
}
#[test]
fn test_span_values() {
let (sub, spans, _events) = setup_subscriber();
let _sub_guard = tracing::subscriber::set_default(sub);
let test_bool = true;
let test_u64 = 1u64;
let test_i64 = 2i64;
let test_str = "test_str";
let span = tracing::info_span!(
target: "test_target",
"test_span1",
test_bool,
test_u64,
test_i64,
test_str
);
let _guard = span.enter();
drop(_guard);
drop(span);
let sd = spans.lock().remove(0);
assert_eq!(sd.name, "test_span1");
assert_eq!(sd.target, "test_target");
let values = sd.values;
assert_eq!(values.bool_values.get("test_bool").unwrap(), &test_bool);
assert_eq!(values.u64_values.get("test_u64").unwrap(), &test_u64);
assert_eq!(values.i64_values.get("test_i64").unwrap(), &test_i64);
assert_eq!(values.string_values.get("test_str").unwrap(), &test_str.to_owned());
}
#[test]
fn test_event() {
let (sub, _spans, events) = setup_subscriber();
let _sub_guard = tracing::subscriber::set_default(sub);
tracing::event!(target: "test_target", tracing::Level::INFO, "test_event");
let mut te1 = events.lock().remove(0);
assert_eq!(
te1.values.string_values.remove(&"message".to_owned()).unwrap(),
"test_event".to_owned()
);
}
#[test]
fn test_event_parent_id() {
let (sub, spans, events) = setup_subscriber();
let _sub_guard = tracing::subscriber::set_default(sub);
// enter span
let span1 = tracing::info_span!(target: "test_target", "test_span1");
let _guard1 = span1.enter();
// emit event
tracing::event!(target: "test_target", tracing::Level::INFO, "test_event");
// exit span
drop(_guard1);
drop(span1);
let sd1 = spans.lock().remove(0);
let te1 = events.lock().remove(0);
assert_eq!(sd1.id, te1.parent_id.unwrap());
}
#[test]
fn test_parent_id_with_threads() {
use std::{sync::mpsc, thread};
if std::env::var("RUN_TEST_PARENT_ID_WITH_THREADS").is_err() {
let executable = std::env::current_exe().unwrap();
let mut command = std::process::Command::new(executable);
let res = command
.env("RUN_TEST_PARENT_ID_WITH_THREADS", "1")
.args(&["--nocapture", "test_parent_id_with_threads"])
.output()
.unwrap()
.status;
assert!(res.success());
} else {
let (sub, spans, events) = setup_subscriber();
let _sub_guard = tracing::subscriber::set_global_default(sub);
let span1 = tracing::info_span!(target: "test_target", "test_span1");
let _guard1 = span1.enter();
let (tx, rx): (Sender<bool>, Receiver<bool>) = mpsc::channel();
let handle = thread::spawn(move || {
let span2 = tracing::info_span!(target: "test_target", "test_span2");
let _guard2 = span2.enter();
// emit event
tracing::event!(target: "test_target", tracing::Level::INFO, "test_event1");
let _ = rx.recv();
// guard2 and span2 dropped / exited
});
// wait for Event to be dispatched and stored
while events.lock().is_empty() {
thread::sleep(Duration::from_millis(1));
}
// emit new event (will be second item in Vec) while span2 still active in other thread
tracing::event!(target: "test_target", tracing::Level::INFO, "test_event2");
// stop thread and drop span
let _ = tx.send(false);
let _ = handle.join();
// wait for Span to be dispatched and stored
while spans.lock().is_empty() {
thread::sleep(Duration::from_millis(1));
}
let span2 = spans.lock().remove(0);
let event1 = events.lock().remove(0);
drop(_guard1);
drop(span1);
// emit event with no parent
tracing::event!(target: "test_target", tracing::Level::INFO, "test_event3");
let span1 = spans.lock().remove(0);
let event2 = events.lock().remove(0);
assert_eq!(event1.values.string_values.get("message").unwrap(), "test_event1");
assert_eq!(event2.values.string_values.get("message").unwrap(), "test_event2");
assert!(span1.parent_id.is_none());
assert!(span2.parent_id.is_none());
assert_eq!(span2.id, event1.parent_id.unwrap());
assert_eq!(span1.id, event2.parent_id.unwrap());
assert_ne!(span2.id, span1.id);
let event3 = events.lock().remove(0);
assert!(event3.parent_id.is_none());
}
}
}
@@ -0,0 +1,119 @@
// Copyright (C) Parity Technologies (UK) Ltd.
// This file is part of Bizinikiwi.
// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
// Bizinikiwi is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Bizinikiwi is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Bizinikiwi. If not, see <https://www.gnu.org/licenses/>.
use parking_lot::Mutex;
use std::sync::OnceLock;
use tracing_subscriber::{
filter::Directive, fmt as tracing_fmt, layer, reload::Handle, EnvFilter, Registry,
};
// Handle to reload the tracing log filter
static FILTER_RELOAD_HANDLE: OnceLock<Handle<EnvFilter, SCSubscriber>> = OnceLock::new();
// Directives that are defaulted to when resetting the log filter
static DEFAULT_DIRECTIVES: OnceLock<Mutex<Vec<String>>> = OnceLock::new();
// Current state of log filter
static CURRENT_DIRECTIVES: OnceLock<Mutex<Vec<String>>> = OnceLock::new();
/// Add log filter directive(s) to the defaults
///
/// The syntax is identical to the CLI `<target>=<level>`:
///
/// `sync=debug,state=trace`
pub(crate) fn add_default_directives(directives: &str) {
DEFAULT_DIRECTIVES
.get_or_init(|| Mutex::new(Vec::new()))
.lock()
.push(directives.to_owned());
add_directives(directives);
}
/// Add directives to current directives.
pub fn add_directives(directives: &str) {
CURRENT_DIRECTIVES
.get_or_init(|| Mutex::new(Vec::new()))
.lock()
.push(directives.to_owned());
}
/// Returns the current directives.
pub fn get_directives() -> Vec<String> {
CURRENT_DIRECTIVES.get_or_init(|| Mutex::new(Vec::new())).lock().clone()
}
/// Parse `Directive` and add to default directives if successful.
///
/// Ensures the supplied directive will be restored when resetting the log filter.
pub(crate) fn parse_default_directive(directive: &str) -> super::Result<Directive> {
let dir = directive.parse()?;
add_default_directives(directive);
Ok(dir)
}
/// Reload the logging filter with the supplied directives added to the existing directives
pub fn reload_filter() -> Result<(), String> {
let mut env_filter = EnvFilter::default();
if let Some(current_directives) = CURRENT_DIRECTIVES.get() {
// Use join and then split in case any directives added together
for directive in current_directives.lock().join(",").split(',').map(|d| d.parse()) {
match directive {
Ok(dir) => env_filter = env_filter.add_directive(dir),
Err(invalid_directive) => {
log::warn!(
target: "tracing",
"Unable to parse directive while setting log filter: {:?}",
invalid_directive,
);
},
}
}
}
// Set the max logging level for the `log` macros.
let max_level_hint =
tracing_subscriber::Layer::<tracing_subscriber::FmtSubscriber>::max_level_hint(&env_filter);
log::set_max_level(super::to_log_level_filter(max_level_hint));
log::debug!(target: "tracing", "Reloading log filter with: {}", env_filter);
FILTER_RELOAD_HANDLE
.get()
.ok_or("No reload handle present")?
.reload(env_filter)
.map_err(|e| format!("{}", e))
}
/// Resets the log filter back to the original state when the node was started.
///
/// Includes bizinikiwi defaults and CLI supplied directives.
pub fn reset_log_filter() -> Result<(), String> {
let directive = DEFAULT_DIRECTIVES.get_or_init(|| Mutex::new(Vec::new())).lock().clone();
*CURRENT_DIRECTIVES.get_or_init(|| Mutex::new(Vec::new())).lock() = directive;
reload_filter()
}
/// Initialize FILTER_RELOAD_HANDLE, only possible once
pub(crate) fn set_reload_handle(handle: Handle<EnvFilter, SCSubscriber>) {
let _ = FILTER_RELOAD_HANDLE.set(handle);
}
// The layered Subscriber as built up in `LoggerBuilder::init()`.
// Used in the reload `Handle`.
type SCSubscriber<
N = tracing_fmt::format::DefaultFields,
E = crate::logging::EventFormat,
W = crate::logging::DefaultLogger,
> = layer::Layered<tracing_fmt::Layer<Registry, N, E, W>, Registry>;
@@ -0,0 +1,232 @@
// This file is part of Bizinikiwi.
// Copyright (C) Parity Technologies (UK) Ltd.
// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
use crate::logging::fast_local_time::FastLocalTime;
use console::style;
use std::fmt;
use tracing::{Event, Level, Subscriber};
use tracing_log::NormalizeEvent;
use tracing_subscriber::{
fmt::{format, time::FormatTime, FmtContext, FormatEvent, FormatFields},
registry::LookupSpan,
};
/// A pre-configured event formatter.
pub struct EventFormat<T = FastLocalTime> {
/// Use the given timer for log message timestamps.
pub timer: T,
/// Sets whether or not an event's target is displayed.
pub display_target: bool,
/// Sets whether or not an event's level is displayed.
pub display_level: bool,
/// Sets whether or not the name of the current thread is displayed when formatting events.
pub display_thread_name: bool,
/// Duplicate INFO, WARN and ERROR messages to stdout.
pub dup_to_stdout: bool,
}
impl<T> EventFormat<T>
where
T: FormatTime,
{
// NOTE: the following code took inspiration from tracing-subscriber
//
// https://github.com/tokio-rs/tracing/blob/2f59b32/tracing-subscriber/src/fmt/format/mod.rs#L449
pub(crate) fn format_event_custom<'b, 'w, S, N>(
&self,
ctx: &FmtContext<'b, S, N>,
mut writer: format::Writer<'w>,
event: &Event,
) -> fmt::Result
where
S: Subscriber + for<'a> LookupSpan<'a>,
N: for<'a> FormatFields<'a> + 'static,
{
let normalized_meta = event.normalized_metadata();
let meta = normalized_meta.as_ref().unwrap_or_else(|| event.metadata());
time::write(&self.timer, &mut format::Writer::new(&mut writer))?;
if self.display_level {
let fmt_level = FmtLevel::new(meta.level());
write!(writer, "{} ", fmt_level)?;
}
if self.display_thread_name {
let current_thread = std::thread::current();
match current_thread.name() {
Some(name) => {
write!(&mut writer, "{} ", FmtThreadName::new(name))?;
},
// fall-back to thread id when name is absent and ids are not enabled
None => {
write!(&mut writer, "{:0>2?} ", current_thread.id())?;
},
}
}
if self.display_target {
write!(&mut writer, "{}: ", meta.target())?;
}
// Custom code to display node name
if let Some(span) = ctx.lookup_current() {
for span in span.scope() {
let exts = span.extensions();
if let Some(prefix) = exts.get::<super::layers::Prefix>() {
write!(&mut writer, "{}", prefix.as_str())?;
break;
}
}
}
ctx.format_fields(format::Writer::new(&mut writer), event)?;
writeln!(&mut writer)?;
Ok(())
}
}
// NOTE: the following code took inspiration from tracing-subscriber
//
// https://github.com/tokio-rs/tracing/blob/2f59b32/tracing-subscriber/src/fmt/format/mod.rs#L449
impl<S, N, T> FormatEvent<S, N> for EventFormat<T>
where
S: Subscriber + for<'a> LookupSpan<'a>,
N: for<'a> FormatFields<'a> + 'static,
T: FormatTime,
{
fn format_event(
&self,
ctx: &FmtContext<S, N>,
mut writer: format::Writer<'_>,
event: &Event,
) -> fmt::Result {
if self.dup_to_stdout &&
(event.metadata().level() == &Level::INFO ||
event.metadata().level() == &Level::WARN ||
event.metadata().level() == &Level::ERROR)
{
let mut out = String::new();
let buf_writer = format::Writer::new(&mut out);
self.format_event_custom(ctx, buf_writer, event)?;
writer.write_str(&out)?;
print!("{}", out);
Ok(())
} else {
self.format_event_custom(ctx, writer, event)
}
}
}
struct FmtLevel<'a> {
level: &'a Level,
}
impl<'a> FmtLevel<'a> {
pub(crate) fn new(level: &'a Level) -> Self {
Self { level }
}
}
const TRACE_STR: &str = "TRACE";
const DEBUG_STR: &str = "DEBUG";
const INFO_STR: &str = " INFO";
const WARN_STR: &str = " WARN";
const ERROR_STR: &str = "ERROR";
impl<'a> fmt::Display for FmtLevel<'a> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match *self.level {
Level::TRACE => write!(f, "{}", style(TRACE_STR).magenta()),
Level::DEBUG => write!(f, "{}", style(DEBUG_STR).blue()),
Level::INFO => write!(f, "{}", style(INFO_STR).green()),
Level::WARN => write!(f, "{}", style(WARN_STR).yellow()),
Level::ERROR => write!(f, "{}", style(ERROR_STR).red()),
}
}
}
struct FmtThreadName<'a> {
name: &'a str,
}
impl<'a> FmtThreadName<'a> {
pub(crate) fn new(name: &'a str) -> Self {
Self { name }
}
}
// NOTE: the following code has been duplicated from tracing-subscriber
//
// https://github.com/tokio-rs/tracing/blob/2f59b32/tracing-subscriber/src/fmt/format/mod.rs#L845
impl<'a> fmt::Display for FmtThreadName<'a> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
use std::sync::atomic::{
AtomicUsize,
Ordering::{AcqRel, Acquire, Relaxed},
};
// Track the longest thread name length we've seen so far in an atomic,
// so that it can be updated by any thread.
static MAX_LEN: AtomicUsize = AtomicUsize::new(0);
let len = self.name.len();
// Snapshot the current max thread name length.
let mut max_len = MAX_LEN.load(Relaxed);
while len > max_len {
// Try to set a new max length, if it is still the value we took a
// snapshot of.
match MAX_LEN.compare_exchange(max_len, len, AcqRel, Acquire) {
// We successfully set the new max value
Ok(_) => break,
// Another thread set a new max value since we last observed
// it! It's possible that the new length is actually longer than
// ours, so we'll loop again and check whether our length is
// still the longest. If not, we'll just use the newer value.
Err(actual) => max_len = actual,
}
}
// pad thread name using `max_len`
write!(f, "{:>width$}", self.name, width = max_len)
}
}
// NOTE: the following code has been duplicated from tracing-subscriber
//
// https://github.com/tokio-rs/tracing/blob/2f59b32/tracing-subscriber/src/fmt/time/mod.rs#L252
mod time {
use std::fmt;
use tracing_subscriber::fmt::{format, time::FormatTime};
pub(crate) fn write<T>(timer: T, writer: &mut format::Writer<'_>) -> fmt::Result
where
T: FormatTime,
{
if console::colors_enabled() {
write!(writer, "\x1B[2m")?;
timer.format_time(writer)?;
write!(writer, "\x1B[0m")?;
} else {
timer.format_time(writer)?;
}
writer.write_char(' ')?;
Ok(())
}
}
@@ -0,0 +1,160 @@
// This file is part of Bizinikiwi.
// Copyright (C) Parity Technologies (UK) Ltd.
// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
use chrono::{Datelike, Timelike};
use std::{cell::RefCell, fmt::Write, time::SystemTime};
use tracing_subscriber::fmt::{format, time::FormatTime};
/// A structure which, when `Display`d, will print out the current local time.
#[derive(Debug, Clone, Copy, Eq, PartialEq, Default)]
pub struct FastLocalTime {
/// Decides whenever the fractional timestamp with be included in the output.
///
/// If `false` the output will match the following `chrono` format string:
/// `%Y-%m-%d %H:%M:%S`
///
/// If `true` the output will match the following `chrono` format string:
/// `%Y-%m-%d %H:%M:%S%.3f`
pub with_fractional: bool,
}
// This is deliberately slightly larger than we actually need, just in case.
const TIMESTAMP_MAXIMUM_LENGTH: usize = 32;
#[derive(Default)]
struct InlineString {
buffer: [u8; TIMESTAMP_MAXIMUM_LENGTH],
length: usize,
}
impl Write for InlineString {
fn write_str(&mut self, s: &str) -> std::fmt::Result {
let new_length = self.length + s.len();
assert!(
new_length <= TIMESTAMP_MAXIMUM_LENGTH,
"buffer overflow when formatting the current timestamp"
);
self.buffer[self.length..new_length].copy_from_slice(s.as_bytes());
self.length = new_length;
Ok(())
}
}
impl InlineString {
fn as_str(&self) -> &str {
// SAFETY: this is safe since the only place we append to the buffer
// is in `write_str` from an `&str`
unsafe { std::str::from_utf8_unchecked(&self.buffer[..self.length]) }
}
}
#[derive(Default)]
struct CachedTimestamp {
buffer: InlineString,
last_regenerated_at: u64,
last_fractional: u32,
}
thread_local! {
static TIMESTAMP: RefCell<CachedTimestamp> = Default::default();
}
impl FormatTime for FastLocalTime {
fn format_time(&self, w: &mut format::Writer<'_>) -> std::fmt::Result {
const TIMESTAMP_PARTIAL_LENGTH: usize = "0000-00-00 00:00:00".len();
let elapsed = SystemTime::now()
.duration_since(SystemTime::UNIX_EPOCH)
.expect("system time is never before UNIX epoch; qed");
let unix_time = elapsed.as_secs();
TIMESTAMP.with(|cache| {
let mut cache = cache.borrow_mut();
// Regenerate the timestamp only at most once each second.
if cache.last_regenerated_at != unix_time {
let ts = chrono::Local::now();
let fractional = (ts.nanosecond() % 1_000_000_000) / 1_000_000;
cache.last_regenerated_at = unix_time;
cache.last_fractional = fractional;
cache.buffer.length = 0;
write!(
&mut cache.buffer,
"{:04}-{:02}-{:02} {:02}:{:02}:{:02}.{:03}",
ts.year(),
ts.month(),
ts.day(),
ts.hour(),
ts.minute(),
ts.second(),
fractional
)?;
} else if self.with_fractional {
let fractional = elapsed.subsec_millis();
// Regenerate the fractional part at most once each millisecond.
if cache.last_fractional != fractional {
cache.last_fractional = fractional;
cache.buffer.length = TIMESTAMP_PARTIAL_LENGTH + 1;
write!(&mut cache.buffer, "{:03}", fractional)?;
}
}
let mut slice = cache.buffer.as_str();
if !self.with_fractional {
slice = &slice[..TIMESTAMP_PARTIAL_LENGTH];
}
w.write_str(slice)
})
}
}
impl std::fmt::Display for FastLocalTime {
fn fmt(&self, mut w: &mut std::fmt::Formatter) -> std::fmt::Result {
self.format_time(&mut format::Writer::new(&mut w))
}
}
#[test]
fn test_format_fast_local_time() {
assert_eq!(
chrono::Local::now().format("%Y-%m-%d %H:%M:%S").to_string().len(),
FastLocalTime { with_fractional: false }.to_string().len()
);
assert_eq!(
chrono::Local::now().format("%Y-%m-%d %H:%M:%S%.3f").to_string().len(),
FastLocalTime { with_fractional: true }.to_string().len()
);
// A simple trick to make sure this test won't randomly fail if we so happen
// to land on the exact moment when we tick over to the next second.
let now_1 = FastLocalTime { with_fractional: false }.to_string();
let expected = chrono::Local::now().format("%Y-%m-%d %H:%M:%S").to_string();
let now_2 = FastLocalTime { with_fractional: false }.to_string();
assert!(
now_1 == expected || now_2 == expected,
"'{}' or '{}' should have been equal to '{}'",
now_1,
now_2,
expected
);
}
@@ -0,0 +1,21 @@
// This file is part of Bizinikiwi.
// Copyright (C) Parity Technologies (UK) Ltd.
// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
mod prefix_layer;
pub use prefix_layer::*;
@@ -0,0 +1,95 @@
// This file is part of Bizinikiwi.
// Copyright (C) Parity Technologies (UK) Ltd.
// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
use tracing::{span::Attributes, Id, Subscriber};
use tracing_subscriber::{layer::Context, registry::LookupSpan, Layer};
/// Span name used for the logging prefix. See macro `pezsc_tracing::logging::prefix_logs_with!`
pub const PREFIX_LOG_SPAN: &str = "bizinikiwi-log-prefix";
/// A `Layer` that captures the prefix span ([`PREFIX_LOG_SPAN`]) which is then used by
/// [`crate::logging::EventFormat`] to prefix the log lines by customizable string.
///
/// See the macro `pezsc_cli::prefix_logs_with!` for more details.
pub struct PrefixLayer;
impl<S> Layer<S> for PrefixLayer
where
S: Subscriber + for<'a> LookupSpan<'a>,
{
fn on_new_span(&self, attrs: &Attributes<'_>, id: &Id, ctx: Context<'_, S>) {
let span = match ctx.span(id) {
Some(span) => span,
None => {
// this shouldn't happen!
debug_assert!(
false,
"newly created span with ID {:?} did not exist in the registry; this is a bug!",
id
);
return;
},
};
if span.name() != PREFIX_LOG_SPAN {
return;
}
let mut extensions = span.extensions_mut();
if extensions.get_mut::<Prefix>().is_none() {
let mut s = String::new();
let mut v = PrefixVisitor(&mut s);
attrs.record(&mut v);
if !s.is_empty() {
let fmt_fields = Prefix(s);
extensions.insert(fmt_fields);
}
}
}
}
struct PrefixVisitor<'a, W: std::fmt::Write>(&'a mut W);
macro_rules! write_node_name {
($method:ident, $type:ty, $format:expr) => {
fn $method(&mut self, field: &tracing::field::Field, value: $type) {
if field.name() == "name" {
let _ = write!(self.0, $format, value);
}
}
};
}
impl<'a, W: std::fmt::Write> tracing::field::Visit for PrefixVisitor<'a, W> {
write_node_name!(record_debug, &dyn std::fmt::Debug, "[{:?}] ");
write_node_name!(record_str, &str, "[{}] ");
write_node_name!(record_i64, i64, "[{}] ");
write_node_name!(record_u64, u64, "[{}] ");
write_node_name!(record_bool, bool, "[{}] ");
}
#[derive(Debug)]
pub(crate) struct Prefix(String);
impl Prefix {
pub(crate) fn as_str(&self) -> &str {
self.0.as_str()
}
}
@@ -0,0 +1,664 @@
// This file is part of Bizinikiwi.
// Copyright (C) Parity Technologies (UK) Ltd.
// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
//! Bizinikiwi logging library.
//!
//! This crate uses tokio's [tracing](https://github.com/tokio-rs/tracing/) library for logging.
#![warn(missing_docs)]
mod directives;
mod event_format;
mod fast_local_time;
mod layers;
mod stderr_writer;
pub(crate) type DefaultLogger = stderr_writer::MakeStderrWriter;
pub use directives::*;
pub use pezsc_tracing_proc_macro::*;
use is_terminal::IsTerminal;
use std::io;
use tracing::Subscriber;
use tracing_subscriber::{
filter::LevelFilter,
fmt::{
format, FormatEvent, FormatFields, Formatter, Layer as FmtLayer, MakeWriter,
SubscriberBuilder,
},
layer::{self, SubscriberExt},
registry::LookupSpan,
EnvFilter, FmtSubscriber, Layer, Registry,
};
pub use event_format::*;
pub use fast_local_time::FastLocalTime;
pub use layers::*;
use stderr_writer::MakeStderrWriter;
/// Logging Result typedef.
pub type Result<T> = std::result::Result<T, Error>;
/// Logging errors.
#[derive(Debug, thiserror::Error)]
#[allow(missing_docs)]
#[non_exhaustive]
#[error(transparent)]
pub enum Error {
IoError(#[from] io::Error),
SetGlobalDefaultError(#[from] tracing::subscriber::SetGlobalDefaultError),
DirectiveParseError(#[from] tracing_subscriber::filter::ParseError),
SetLoggerError(#[from] tracing_log::log_tracer::SetLoggerError),
}
macro_rules! enable_log_reloading {
($builder:expr) => {{
let builder = $builder.with_filter_reloading();
let handle = builder.reload_handle();
set_reload_handle(handle);
builder
}};
}
/// Convert a `Option<LevelFilter>` to a [`log::LevelFilter`].
///
/// `None` is interpreted as `Info`.
fn to_log_level_filter(level_filter: Option<LevelFilter>) -> log::LevelFilter {
match level_filter {
Some(LevelFilter::INFO) | None => log::LevelFilter::Info,
Some(LevelFilter::TRACE) => log::LevelFilter::Trace,
Some(LevelFilter::WARN) => log::LevelFilter::Warn,
Some(LevelFilter::ERROR) => log::LevelFilter::Error,
Some(LevelFilter::DEBUG) => log::LevelFilter::Debug,
Some(LevelFilter::OFF) => log::LevelFilter::Off,
}
}
/// Common implementation to get the subscriber.
fn prepare_subscriber<N, E, F, W>(
directives: &str,
profiling_targets: Option<&str>,
force_colors: Option<bool>,
detailed_output: bool,
builder_hook: impl Fn(
SubscriberBuilder<format::DefaultFields, EventFormat, EnvFilter, DefaultLogger>,
) -> SubscriberBuilder<N, E, F, W>,
) -> Result<impl Subscriber + for<'a> LookupSpan<'a>>
where
N: for<'writer> FormatFields<'writer> + 'static,
E: FormatEvent<Registry, N> + 'static,
W: for<'writer> MakeWriter<'writer> + 'static,
F: layer::Layer<Formatter<N, E, W>> + Send + Sync + 'static,
FmtLayer<Registry, N, E, W>: layer::Layer<Registry> + Send + Sync + 'static,
{
// Accept all valid directives and print invalid ones
fn parse_user_directives(mut env_filter: EnvFilter, dirs: &str) -> Result<EnvFilter> {
for dir in dirs.split(',') {
env_filter = env_filter.add_directive(parse_default_directive(dir)?);
}
Ok(env_filter)
}
// Initialize filter - ensure to use `parse_default_directive` for any defaults to persist
// after log filter reloading by RPC
let mut env_filter = EnvFilter::default()
// Enable info
.add_directive(parse_default_directive("info").expect("provided directive is valid"))
// Disable info logging by default for some modules.
.add_directive(parse_default_directive("ws=off").expect("provided directive is valid"))
.add_directive(parse_default_directive("yamux=off").expect("provided directive is valid"))
.add_directive(
parse_default_directive("regalloc=off").expect("provided directive is valid"),
)
.add_directive(
parse_default_directive("cranelift_codegen=off").expect("provided directive is valid"),
)
// Set warn logging by default for some modules.
.add_directive(
parse_default_directive("cranelift_wasm=warn").expect("provided directive is valid"),
)
.add_directive(parse_default_directive("hyper=warn").expect("provided directive is valid"))
.add_directive(
parse_default_directive("trust_dns_proto=off").expect("provided directive is valid"),
)
.add_directive(
parse_default_directive("hickory_proto=off").expect("provided directive is valid"),
)
.add_directive(
parse_default_directive("libp2p_mdns::behaviour::iface=off")
.expect("provided directive is valid"),
)
// Disable annoying log messages from rustls
.add_directive(
parse_default_directive("rustls::common_state=off")
.expect("provided directive is valid"),
)
.add_directive(
parse_default_directive("rustls::conn=off").expect("provided directive is valid"),
);
if let Ok(lvl) = std::env::var("RUST_LOG") {
if lvl != "" {
env_filter = parse_user_directives(env_filter, &lvl)?;
}
}
if directives != "" {
env_filter = parse_user_directives(env_filter, directives)?;
}
if let Some(profiling_targets) = profiling_targets {
env_filter = parse_user_directives(env_filter, profiling_targets)?;
env_filter = env_filter.add_directive(
parse_default_directive("pezsc_tracing=trace").expect("provided directive is valid"),
);
}
let max_level_hint = Layer::<FmtSubscriber>::max_level_hint(&env_filter);
let max_level = to_log_level_filter(max_level_hint);
tracing_log::LogTracer::builder()
.with_interest_cache(tracing_log::InterestCacheConfig::default())
.with_max_level(max_level)
.init()?;
// If we're only logging `INFO` entries then we'll use a simplified logging format.
let detailed_output = match max_level_hint {
Some(level) if level <= tracing_subscriber::filter::LevelFilter::INFO => false,
_ => true,
} || detailed_output;
let enable_color = force_colors.unwrap_or_else(|| io::stderr().is_terminal());
let timer = fast_local_time::FastLocalTime { with_fractional: detailed_output };
// We need to set both together, because we are may printing to `stdout` and `stderr`.
console::set_colors_enabled(enable_color);
console::set_colors_enabled_stderr(enable_color);
let event_format = EventFormat {
timer,
display_target: detailed_output,
display_level: detailed_output,
display_thread_name: detailed_output,
dup_to_stdout: !io::stderr().is_terminal() && io::stdout().is_terminal(),
};
let builder = FmtSubscriber::builder().with_env_filter(env_filter);
let builder = builder.with_span_events(format::FmtSpan::NONE);
let builder = builder.with_writer(MakeStderrWriter::default());
let builder = builder.event_format(event_format);
let builder = builder_hook(builder);
let subscriber = builder.finish().with(PrefixLayer);
Ok(subscriber)
}
/// A builder that is used to initialize the global logger.
pub struct LoggerBuilder {
directives: String,
profiling: Option<(crate::TracingReceiver, String)>,
custom_profiler: Option<Box<dyn crate::TraceHandler>>,
log_reloading: bool,
force_colors: Option<bool>,
detailed_output: bool,
}
impl LoggerBuilder {
/// Create a new [`LoggerBuilder`] which can be used to initialize the global logger.
pub fn new<S: Into<String>>(directives: S) -> Self {
Self {
directives: directives.into(),
profiling: None,
custom_profiler: None,
log_reloading: false,
force_colors: None,
detailed_output: false,
}
}
/// Set up the profiling.
pub fn with_profiling<S: Into<String>>(
&mut self,
tracing_receiver: crate::TracingReceiver,
profiling_targets: S,
) -> &mut Self {
self.profiling = Some((tracing_receiver, profiling_targets.into()));
self
}
/// Add a custom profiler.
pub fn with_custom_profiling(
&mut self,
custom_profiler: Box<dyn crate::TraceHandler>,
) -> &mut Self {
self.custom_profiler = Some(custom_profiler);
self
}
/// Wether or not to disable log reloading.
pub fn with_log_reloading(&mut self, enabled: bool) -> &mut Self {
self.log_reloading = enabled;
self
}
/// Whether detailed log output should be enabled.
///
/// This includes showing the log target, log level and thread name.
///
/// This will be automatically enabled when there is a log level enabled that is higher than
/// `info`.
pub fn with_detailed_output(&mut self, detailed: bool) -> &mut Self {
self.detailed_output = detailed;
self
}
/// Force enable/disable colors.
pub fn with_colors(&mut self, enable: bool) -> &mut Self {
self.force_colors = Some(enable);
self
}
/// Initialize the global logger
///
/// This sets various global logging and tracing instances and thus may only be called once.
pub fn init(self) -> Result<()> {
if let Some((tracing_receiver, profiling_targets)) = self.profiling {
if self.log_reloading {
let subscriber = prepare_subscriber(
&self.directives,
Some(&profiling_targets),
self.force_colors,
self.detailed_output,
|builder| enable_log_reloading!(builder),
)?;
let mut profiling =
crate::ProfilingLayer::new(tracing_receiver, &profiling_targets);
self.custom_profiler
.into_iter()
.for_each(|profiler| profiling.add_handler(profiler));
tracing::subscriber::set_global_default(subscriber.with(profiling))?;
Ok(())
} else {
let subscriber = prepare_subscriber(
&self.directives,
Some(&profiling_targets),
self.force_colors,
self.detailed_output,
|builder| builder,
)?;
let mut profiling =
crate::ProfilingLayer::new(tracing_receiver, &profiling_targets);
self.custom_profiler
.into_iter()
.for_each(|profiler| profiling.add_handler(profiler));
tracing::subscriber::set_global_default(subscriber.with(profiling))?;
Ok(())
}
} else if self.log_reloading {
let subscriber = prepare_subscriber(
&self.directives,
None,
self.force_colors,
self.detailed_output,
|builder| enable_log_reloading!(builder),
)?;
tracing::subscriber::set_global_default(subscriber)?;
Ok(())
} else {
let subscriber = prepare_subscriber(
&self.directives,
None,
self.force_colors,
self.detailed_output,
|builder| builder,
)?;
tracing::subscriber::set_global_default(subscriber)?;
Ok(())
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate as pezsc_tracing;
use log::info;
use std::{
collections::BTreeMap,
env,
process::Command,
sync::{
atomic::{AtomicBool, AtomicUsize, Ordering},
Arc,
},
};
use tracing::{metadata::Kind, subscriber::Interest, Callsite, Level, Metadata};
const EXPECTED_LOG_MESSAGE: &'static str = "yeah logging works as expected";
const EXPECTED_NODE_NAME: &'static str = "THE_NODE";
fn init_logger(directives: &str) {
let _ = LoggerBuilder::new(directives).init().unwrap();
}
fn run_test_in_another_process(
test_name: &str,
test_body: impl FnOnce(),
) -> Option<std::process::Output> {
if env::var("RUN_FORKED_TEST").is_ok() {
test_body();
None
} else {
let output = Command::new(env::current_exe().unwrap())
.arg(test_name)
.env("RUN_FORKED_TEST", "1")
.output()
.unwrap();
assert!(output.status.success());
Some(output)
}
}
#[test]
fn test_logger_filters() {
run_test_in_another_process("test_logger_filters", || {
let test_directives =
"grandpa=debug,sync=trace,client=warn,telemetry,something-with-dash=error";
init_logger(&test_directives);
tracing::dispatcher::get_default(|dispatcher| {
let test_filter = |target, level| {
struct DummyCallSite;
impl Callsite for DummyCallSite {
fn set_interest(&self, _: Interest) {}
fn metadata(&self) -> &Metadata<'_> {
unreachable!();
}
}
let metadata = tracing::metadata!(
name: "",
target: target,
level: level,
fields: &[],
callsite: &DummyCallSite,
kind: Kind::SPAN,
);
dispatcher.enabled(&metadata)
};
assert!(test_filter("grandpa", Level::INFO));
assert!(test_filter("grandpa", Level::DEBUG));
assert!(!test_filter("grandpa", Level::TRACE));
assert!(test_filter("sync", Level::TRACE));
assert!(test_filter("client", Level::WARN));
assert!(test_filter("telemetry", Level::TRACE));
assert!(test_filter("something-with-dash", Level::ERROR));
});
});
}
/// This test ensures that using dash (`-`) in the target name in logs and directives actually
/// work.
#[test]
fn dash_in_target_name_works() {
let executable = env::current_exe().unwrap();
let output = Command::new(executable)
.env("ENABLE_LOGGING", "1")
.args(&["--nocapture", "log_something_with_dash_target_name"])
.output()
.unwrap();
let output = String::from_utf8(output.stderr).unwrap();
assert!(output.contains(EXPECTED_LOG_MESSAGE));
}
/// This is not an actual test, it is used by the `dash_in_target_name_works` test.
/// The given test will call the test executable and only execute this one test that
/// only prints `EXPECTED_LOG_MESSAGE` through logging while using a target
/// name that contains a dash. This ensures that target names with dashes work.
#[test]
fn log_something_with_dash_target_name() {
if env::var("ENABLE_LOGGING").is_ok() {
let test_directives = "test-target=info";
let _guard = init_logger(&test_directives);
log::info!(target: "test-target", "{}", EXPECTED_LOG_MESSAGE);
}
}
#[test]
fn prefix_in_log_lines() {
let re = regex::Regex::new(&format!(
r"^\d{{4}}-\d{{2}}-\d{{2}} \d{{2}}:\d{{2}}:\d{{2}} \[{}\] {}$",
EXPECTED_NODE_NAME, EXPECTED_LOG_MESSAGE,
))
.unwrap();
let executable = env::current_exe().unwrap();
let output = Command::new(executable)
.env("ENABLE_LOGGING", "1")
.args(&["--nocapture", "prefix_in_log_lines_entrypoint"])
.output()
.unwrap();
let output = String::from_utf8(output.stderr).unwrap();
assert!(re.is_match(output.trim()), "Expected:\n{}\nGot:\n{}", re, output);
}
/// This is not an actual test, it is used by the `prefix_in_log_lines` test.
/// The given test will call the test executable and only execute this one test that
/// only prints a log line prefixed by the node name `EXPECTED_NODE_NAME`.
#[test]
fn prefix_in_log_lines_entrypoint() {
if env::var("ENABLE_LOGGING").is_ok() {
let _guard = init_logger("");
prefix_in_log_lines_process();
}
}
#[crate::logging::prefix_logs_with(EXPECTED_NODE_NAME)]
fn prefix_in_log_lines_process() {
log::info!("{}", EXPECTED_LOG_MESSAGE);
}
/// This is not an actual test, it is used by the `do_not_write_with_colors_on_tty` test.
/// The given test will call the test executable and only execute this one test that
/// only prints a log line with some colors in it.
#[test]
fn do_not_write_with_colors_on_tty_entrypoint() {
if env::var("ENABLE_LOGGING").is_ok() {
let _guard = init_logger("");
log::info!("{}", console::style(EXPECTED_LOG_MESSAGE).yellow());
}
}
#[test]
fn do_not_write_with_colors_on_tty() {
let re = regex::Regex::new(&format!(
r"^\d{{4}}-\d{{2}}-\d{{2}} \d{{2}}:\d{{2}}:\d{{2}} {}$",
EXPECTED_LOG_MESSAGE,
))
.unwrap();
let executable = env::current_exe().unwrap();
let output = Command::new(executable)
.env("ENABLE_LOGGING", "1")
.args(&["--nocapture", "do_not_write_with_colors_on_tty_entrypoint"])
.output()
.unwrap();
let output = String::from_utf8(output.stderr).unwrap();
assert!(re.is_match(output.trim()), "Expected:\n{}\nGot:\n{}", re, output);
}
#[test]
fn log_max_level_is_set_properly() {
fn run_test(rust_log: Option<String>, tracing_targets: Option<String>) -> String {
let executable = env::current_exe().unwrap();
let mut command = Command::new(executable);
command
.env("PRINT_MAX_LOG_LEVEL", "1")
.args(&["--nocapture", "log_max_level_is_set_properly"]);
if let Some(rust_log) = rust_log {
command.env("RUST_LOG", rust_log);
}
if let Some(tracing_targets) = tracing_targets {
command.env("TRACING_TARGETS", tracing_targets);
}
let output = command.output().unwrap();
dbg!(String::from_utf8(output.stderr)).unwrap()
}
if env::var("PRINT_MAX_LOG_LEVEL").is_ok() {
let mut builder = LoggerBuilder::new("");
if let Ok(targets) = env::var("TRACING_TARGETS") {
builder.with_profiling(crate::TracingReceiver::Log, targets);
}
builder.init().unwrap();
eprint!("MAX_LOG_LEVEL={:?}", log::max_level());
} else {
assert_eq!("MAX_LOG_LEVEL=Info", run_test(None, None));
assert_eq!("MAX_LOG_LEVEL=Trace", run_test(Some("test=trace".into()), None));
assert_eq!("MAX_LOG_LEVEL=Debug", run_test(Some("test=debug".into()), None));
assert_eq!("MAX_LOG_LEVEL=Trace", run_test(None, Some("test=info".into())));
}
}
// This creates a bunch of threads and makes sure they start executing
// a given callback almost exactly at the same time.
fn run_on_many_threads(thread_count: usize, callback: impl Fn(usize) + 'static + Send + Clone) {
let started_count = Arc::new(AtomicUsize::new(0));
let barrier = Arc::new(AtomicBool::new(false));
let threads: Vec<_> = (0..thread_count)
.map(|nth_thread| {
let started_count = started_count.clone();
let barrier = barrier.clone();
let callback = callback.clone();
std::thread::spawn(move || {
started_count.fetch_add(1, Ordering::SeqCst);
while !barrier.load(Ordering::SeqCst) {
std::thread::yield_now();
}
callback(nth_thread);
})
})
.collect();
while started_count.load(Ordering::SeqCst) != thread_count {
std::thread::yield_now();
}
barrier.store(true, Ordering::SeqCst);
for thread in threads {
if let Err(error) = thread.join() {
println!("error: failed to join thread: {:?}", error);
unsafe { libc::abort() }
}
}
}
#[test]
fn parallel_logs_from_multiple_threads_are_properly_gathered() {
const THREAD_COUNT: usize = 128;
const LOGS_PER_THREAD: usize = 1024;
let output = run_test_in_another_process(
"parallel_logs_from_multiple_threads_are_properly_gathered",
|| {
let builder = LoggerBuilder::new("");
builder.init().unwrap();
run_on_many_threads(THREAD_COUNT, |nth_thread| {
for _ in 0..LOGS_PER_THREAD {
info!("Thread <<{}>>", nth_thread);
}
});
},
);
if let Some(output) = output {
let stderr = String::from_utf8(output.stderr).unwrap();
let mut count_per_thread = BTreeMap::new();
for line in stderr.split("\n") {
if let Some(index_s) = line.find("Thread <<") {
let index_s = index_s + "Thread <<".len();
let index_e = line.find(">>").unwrap();
let nth_thread: usize = line[index_s..index_e].parse().unwrap();
*count_per_thread.entry(nth_thread).or_insert(0) += 1;
}
}
assert_eq!(count_per_thread.len(), THREAD_COUNT);
for (_, count) in count_per_thread {
assert_eq!(count, LOGS_PER_THREAD);
}
}
}
#[test]
fn huge_single_line_log_is_properly_printed_out() {
let mut line = String::new();
line.push_str("$$START$$");
for n in 0..16 * 1024 * 1024 {
let ch = b'a' + (n as u8 % (b'z' - b'a'));
line.push(char::from(ch));
}
line.push_str("$$END$$");
let output =
run_test_in_another_process("huge_single_line_log_is_properly_printed_out", || {
let builder = LoggerBuilder::new("");
builder.init().unwrap();
info!("{}", line);
});
if let Some(output) = output {
let stderr = String::from_utf8(output.stderr).unwrap();
assert!(stderr.contains(&line));
}
}
}
@@ -0,0 +1,228 @@
// This file is part of Bizinikiwi.
// Copyright (C) Parity Technologies (UK) Ltd.
// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
//! This module contains a buffered semi-asynchronous stderr writer.
//!
//! Depending on how we were started writing to stderr can take a surprisingly long time.
//!
//! If the other side takes their sweet sweet time reading whatever we send them then writing
//! to stderr might block for a long time, since it is effectively a synchronous operation.
//! And every time we write to stderr we need to grab a global lock, which affects every thread
//! which also tries to log something at the same time.
//!
//! Of course we *will* be ultimately limited by how fast the recipient can ingest our logs,
//! but it's not like logging is the only thing we're doing. And we still can't entirely
//! avoid the problem of multiple threads contending for the same lock. (Well, technically
//! we could employ something like a lock-free circular buffer, but that might be like
//! killing a fly with a sledgehammer considering the complexity involved; this is only
//! a logger after all.)
//!
//! But we can try to make things a little better. We can offload actually writing to stderr
//! to another thread and flush the logs in bulk instead of doing it per-line, which should
//! reduce the amount of CPU time we waste on making syscalls and on spinning waiting for locks.
//!
//! How much this helps depends on a multitude of factors, including the hardware we're running on,
//! how much we're logging, from how many threads, which exact set of threads are logging, to what
//! stderr is actually connected to (is it a terminal emulator? a file? an UDP socket?), etc.
//!
//! In general this can reduce the real time execution time as much as 75% in certain cases, or it
//! can make absolutely no difference in others.
use parking_lot::{Condvar, Mutex, Once};
use std::{
io::Write,
sync::atomic::{AtomicBool, Ordering},
time::Duration,
};
use tracing::{Level, Metadata};
/// How many bytes of buffered logs will trigger an async flush on another thread?
const ASYNC_FLUSH_THRESHOLD: usize = 16 * 1024;
/// How many bytes of buffered logs will trigger a sync flush on the current thread?
const SYNC_FLUSH_THRESHOLD: usize = 768 * 1024;
/// How many bytes can be buffered at maximum?
const EMERGENCY_FLUSH_THRESHOLD: usize = 2 * 1024 * 1024;
/// If there isn't enough printed out this is how often the logs will be automatically flushed.
const AUTOFLUSH_EVERY: Duration = Duration::from_millis(50);
/// The least serious level at which a synchronous flush will be triggered.
const SYNC_FLUSH_LEVEL_THRESHOLD: Level = Level::ERROR;
/// The amount of time we'll block until the buffer is fully flushed on exit.
///
/// This should be completely unnecessary in normal circumstances.
const ON_EXIT_FLUSH_TIMEOUT: Duration = Duration::from_secs(5);
/// A global buffer to which we'll append all of our logs before flushing them out to stderr.
static BUFFER: Mutex<Vec<u8>> = parking_lot::const_mutex(Vec::new());
/// A spare buffer which we'll swap with the main buffer on each flush to minimize lock contention.
static SPARE_BUFFER: Mutex<Vec<u8>> = parking_lot::const_mutex(Vec::new());
/// A conditional variable used to forcefully trigger asynchronous flushes.
static ASYNC_FLUSH_CONDVAR: Condvar = Condvar::new();
static ENABLE_ASYNC_LOGGING: AtomicBool = AtomicBool::new(true);
fn flush_logs(mut buffer: parking_lot::lock_api::MutexGuard<parking_lot::RawMutex, Vec<u8>>) {
let mut spare_buffer = SPARE_BUFFER.lock();
std::mem::swap(&mut *spare_buffer, &mut *buffer);
std::mem::drop(buffer);
let stderr = std::io::stderr();
let mut stderr_lock = stderr.lock();
let _ = stderr_lock.write_all(&spare_buffer);
std::mem::drop(stderr_lock);
spare_buffer.clear();
}
fn log_autoflush_thread() {
let mut buffer = BUFFER.lock();
loop {
ASYNC_FLUSH_CONDVAR.wait_for(&mut buffer, AUTOFLUSH_EVERY);
loop {
flush_logs(buffer);
buffer = BUFFER.lock();
if buffer.len() >= ASYNC_FLUSH_THRESHOLD {
// While we were busy flushing we picked up enough logs to do another flush.
continue;
} else {
break;
}
}
}
}
#[cold]
fn initialize() {
std::thread::Builder::new()
.name("log-autoflush".to_owned())
.spawn(log_autoflush_thread)
.expect("thread spawning doesn't normally fail; qed");
// SAFETY: This is safe since we pass a valid pointer to `atexit`.
let errcode = unsafe { libc::atexit(on_exit) };
assert_eq!(errcode, 0, "atexit failed while setting up the logger: {}", errcode);
}
extern "C" fn on_exit() {
ENABLE_ASYNC_LOGGING.store(false, Ordering::SeqCst);
if let Some(buffer) = BUFFER.try_lock_for(ON_EXIT_FLUSH_TIMEOUT) {
flush_logs(buffer);
}
}
/// A drop-in replacement for [`std::io::stderr`] for use anywhere
/// a [`tracing_subscriber::fmt::MakeWriter`] is accepted.
pub struct MakeStderrWriter {
// A dummy field so that the structure is not publicly constructible.
_dummy: (),
}
impl Default for MakeStderrWriter {
fn default() -> Self {
static ONCE: Once = Once::new();
ONCE.call_once(initialize);
MakeStderrWriter { _dummy: () }
}
}
impl tracing_subscriber::fmt::MakeWriter<'_> for MakeStderrWriter {
type Writer = StderrWriter;
fn make_writer(&self) -> Self::Writer {
StderrWriter::new(false)
}
// The `tracing-subscriber` crate calls this for every line logged.
fn make_writer_for(&self, meta: &Metadata<'_>) -> Self::Writer {
StderrWriter::new(*meta.level() <= SYNC_FLUSH_LEVEL_THRESHOLD)
}
}
pub struct StderrWriter {
buffer: Option<parking_lot::lock_api::MutexGuard<'static, parking_lot::RawMutex, Vec<u8>>>,
sync_flush_on_drop: bool,
original_len: usize,
}
impl StderrWriter {
fn new(mut sync_flush_on_drop: bool) -> Self {
if !ENABLE_ASYNC_LOGGING.load(Ordering::Relaxed) {
sync_flush_on_drop = true;
}
// This lock isn't as expensive as it might look, since this is only called once the full
// line to be logged is already serialized into a thread-local buffer inside of the
// `tracing-subscriber` crate, and basically the only thing we'll do when holding this lock
// is to copy that over to our global shared buffer in one go in `Write::write_all` and be
// immediately dropped.
let buffer = BUFFER.lock();
StderrWriter { original_len: buffer.len(), buffer: Some(buffer), sync_flush_on_drop }
}
}
#[cold]
fn emergency_flush(buffer: &mut Vec<u8>, input: &[u8]) {
let stderr = std::io::stderr();
let mut stderr_lock = stderr.lock();
let _ = stderr_lock.write_all(buffer);
buffer.clear();
let _ = stderr_lock.write_all(input);
}
impl Write for StderrWriter {
fn write(&mut self, input: &[u8]) -> Result<usize, std::io::Error> {
let buffer = self.buffer.as_mut().expect("buffer is only None after `drop`; qed");
if buffer.len() + input.len() >= EMERGENCY_FLUSH_THRESHOLD {
// Make sure we don't blow our memory budget. Normally this should never happen,
// but there are cases where we directly print out untrusted user input which
// can potentially be megabytes in size.
emergency_flush(buffer, input);
} else {
buffer.extend_from_slice(input);
}
Ok(input.len())
}
fn write_all(&mut self, input: &[u8]) -> Result<(), std::io::Error> {
self.write(input).map(|_| ())
}
fn flush(&mut self) -> Result<(), std::io::Error> {
Ok(())
}
}
impl Drop for StderrWriter {
fn drop(&mut self) {
let buf = self.buffer.take().expect("buffer is only None after `drop`; qed");
if self.sync_flush_on_drop || buf.len() >= SYNC_FLUSH_THRESHOLD {
flush_logs(buf);
} else if self.original_len < ASYNC_FLUSH_THRESHOLD && buf.len() >= ASYNC_FLUSH_THRESHOLD {
ASYNC_FLUSH_CONDVAR.notify_one();
}
}
}