Telemetry per node (#7463)

This commit is contained in:
Cecile Tonglet
2021-01-20 12:28:56 +01:00
committed by GitHub
parent 71ef82afbc
commit 970cc25cef
49 changed files with 2578 additions and 2009 deletions
@@ -0,0 +1,123 @@
// Copyright 2021 Parity Technologies (UK) Ltd.
// This file is part of Substrate.
// Substrate 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.
// Substrate 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 Substrate. If not, see <http://www.gnu.org/licenses/>.
use once_cell::sync::OnceCell;
use parking_lot::Mutex;
use tracing_subscriber::{
filter::Directive, fmt as tracing_fmt, fmt::time::ChronoLocal, layer, reload::Handle,
EnvFilter, Registry,
};
// Handle to reload the tracing log filter
static FILTER_RELOAD_HANDLE: OnceCell<Handle<EnvFilter, SCSubscriber>> = OnceCell::new();
// Directives that are defaulted to when resetting the log filter
static DEFAULT_DIRECTIVES: OnceCell<Mutex<Vec<String>>> = OnceCell::new();
// Current state of log filter
static CURRENT_DIRECTIVES: OnceCell<Mutex<Vec<String>>> = OnceCell::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());
}
/// 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,
);
}
}
}
}
env_filter = env_filter.add_directive(
"sc_tracing=trace"
.parse()
.expect("provided directive is valid"),
);
log::debug!(target: "tracing", "Reloading log filter with: {}", env_filter);
FILTER_RELOAD_HANDLE
.get()
.ok_or("No reload handle present".to_string())?
.reload(env_filter)
.map_err(|e| format!("{}", e))
}
/// Resets the log filter back to the original state when the node was started.
///
/// Includes substrate 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 `init_logger()`.
// Used in the reload `Handle`.
type SCSubscriber<
N = tracing_fmt::format::DefaultFields,
E = crate::logging::EventFormat<ChronoLocal>,
W = fn() -> std::io::Stderr,
> = layer::Layered<tracing_fmt::Layer<Registry, N, E, W>, Registry>;
@@ -0,0 +1,334 @@
// This file is part of Substrate.
// Copyright (C) 2020-2021 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 ansi_term::Colour;
use regex::Regex;
use std::fmt::{self, Write};
use tracing::{Event, Level, Subscriber};
use tracing_log::NormalizeEvent;
use tracing_subscriber::{
field::RecordFields,
fmt::{
time::{FormatTime, SystemTime},
FmtContext, FormatEvent, FormatFields,
},
layer::Context,
registry::{LookupSpan, SpanRef},
};
/// A pre-configured event formatter.
pub struct EventFormat<T = SystemTime> {
/// 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,
/// Enable ANSI terminal colors for formatted output.
pub enable_color: 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, S, N>(
&self,
ctx: CustomFmtContext<'b, S, N>,
writer: &mut dyn fmt::Write,
event: &Event,
) -> fmt::Result
where
S: Subscriber + for<'a> LookupSpan<'a>,
N: for<'a> FormatFields<'a> + 'static,
{
if event.metadata().target() == sc_telemetry::TELEMETRY_LOG_SPAN {
return Ok(());
}
let writer = &mut MaybeColorWriter::new(self.enable_color, writer);
let normalized_meta = event.normalized_metadata();
let meta = normalized_meta.as_ref().unwrap_or_else(|| event.metadata());
time::write(&self.timer, writer, self.enable_color)?;
if self.display_level {
let fmt_level = { FmtLevel::new(meta.level(), self.enable_color) };
write!(writer, "{} ", fmt_level)?;
}
if self.display_thread_name {
let current_thread = std::thread::current();
match current_thread.name() {
Some(name) => {
write!(writer, "{} ", FmtThreadName::new(name))?;
}
// fall-back to thread id when name is absent and ids are not enabled
None => {
write!(writer, "{:0>2?} ", current_thread.id())?;
}
}
}
if self.display_target {
write!(writer, "{}: ", meta.target())?;
}
// Custom code to display node name
if let Some(span) = ctx.lookup_current() {
let parents = span.parents();
for span in std::iter::once(span).chain(parents) {
let exts = span.extensions();
if let Some(prefix) = exts.get::<super::layers::Prefix>() {
write!(writer, "{}", prefix.as_str())?;
break;
}
}
}
ctx.format_fields(writer, event)?;
writeln!(writer)?;
writer.write()
}
}
// 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>,
writer: &mut dyn fmt::Write,
event: &Event,
) -> fmt::Result {
self.format_event_custom(CustomFmtContext::FmtContext(ctx), writer, event)
}
}
struct FmtLevel<'a> {
level: &'a Level,
ansi: bool,
}
impl<'a> FmtLevel<'a> {
pub(crate) fn new(level: &'a Level, ansi: bool) -> Self {
Self { level, ansi }
}
}
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 {
if self.ansi {
match *self.level {
Level::TRACE => write!(f, "{}", Colour::Purple.paint(TRACE_STR)),
Level::DEBUG => write!(f, "{}", Colour::Blue.paint(DEBUG_STR)),
Level::INFO => write!(f, "{}", Colour::Green.paint(INFO_STR)),
Level::WARN => write!(f, "{}", Colour::Yellow.paint(WARN_STR)),
Level::ERROR => write!(f, "{}", Colour::Red.paint(ERROR_STR)),
}
} else {
match *self.level {
Level::TRACE => f.pad(TRACE_STR),
Level::DEBUG => f.pad(DEBUG_STR),
Level::INFO => f.pad(INFO_STR),
Level::WARN => f.pad(WARN_STR),
Level::ERROR => f.pad(ERROR_STR),
}
}
}
}
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 ansi_term::Style;
use std::fmt;
use tracing_subscriber::fmt::time::FormatTime;
pub(crate) fn write<T>(timer: T, writer: &mut dyn fmt::Write, with_ansi: bool) -> fmt::Result
where
T: FormatTime,
{
if with_ansi {
let style = Style::new().dimmed();
write!(writer, "{}", style.prefix())?;
timer.format_time(writer)?;
write!(writer, "{}", style.suffix())?;
} else {
timer.format_time(writer)?;
}
writer.write_char(' ')?;
Ok(())
}
}
// NOTE: `FmtContext`'s fields are private. This enum allows us to make a `format_event` function
// that works with `FmtContext` or `Context` with `FormatFields`
#[allow(dead_code)]
pub(crate) enum CustomFmtContext<'a, S, N> {
FmtContext(&'a FmtContext<'a, S, N>),
ContextWithFormatFields(&'a Context<'a, S>, &'a N),
}
impl<'a, S, N> FormatFields<'a> for CustomFmtContext<'a, S, N>
where
S: Subscriber + for<'lookup> LookupSpan<'lookup>,
N: for<'writer> FormatFields<'writer> + 'static,
{
fn format_fields<R: RecordFields>(
&self,
writer: &'a mut dyn fmt::Write,
fields: R,
) -> fmt::Result {
match self {
CustomFmtContext::FmtContext(fmt_ctx) => fmt_ctx.format_fields(writer, fields),
CustomFmtContext::ContextWithFormatFields(_ctx, fmt_fields) => {
fmt_fields.format_fields(writer, fields)
}
}
}
}
// NOTE: the following code has been duplicated from tracing-subscriber
//
// https://github.com/tokio-rs/tracing/blob/2f59b32/tracing-subscriber/src/fmt/fmt_layer.rs#L788
impl<'a, S, N> CustomFmtContext<'a, S, N>
where
S: Subscriber + for<'lookup> LookupSpan<'lookup>,
N: for<'writer> FormatFields<'writer> + 'static,
{
#[inline]
pub fn lookup_current(&self) -> Option<SpanRef<'_, S>>
where
S: for<'lookup> LookupSpan<'lookup>,
{
match self {
CustomFmtContext::FmtContext(fmt_ctx) => fmt_ctx.lookup_current(),
CustomFmtContext::ContextWithFormatFields(ctx, _) => ctx.lookup_current(),
}
}
}
/// A writer that may write to `inner_writer` with colors.
///
/// This is used by [`EventFormat`] to kill colors when `enable_color` is `false`.
///
/// It is required to call [`MaybeColorWriter::write`] after all writes are done,
/// because the content of these writes is buffered and will only be written to the
/// `inner_writer` at that point.
struct MaybeColorWriter<'a> {
enable_color: bool,
buffer: String,
inner_writer: &'a mut dyn fmt::Write,
}
impl<'a> fmt::Write for MaybeColorWriter<'a> {
fn write_str(&mut self, buf: &str) -> fmt::Result {
self.buffer.push_str(buf);
Ok(())
}
}
impl<'a> MaybeColorWriter<'a> {
/// Creates a new instance.
fn new(enable_color: bool, inner_writer: &'a mut dyn fmt::Write) -> Self {
Self {
enable_color,
inner_writer,
buffer: String::new(),
}
}
/// Write the buffered content to the `inner_writer`.
fn write(&mut self) -> fmt::Result {
lazy_static::lazy_static! {
static ref RE: Regex = Regex::new("\x1b\\[[^m]+m").expect("Error initializing color regex");
}
if !self.enable_color {
let replaced = RE.replace_all(&self.buffer, "");
self.inner_writer.write_str(&replaced)
} else {
self.inner_writer.write_str(&self.buffer)
}
}
}
@@ -0,0 +1,120 @@
// This file is part of Substrate.
// Copyright (C) 2021 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::event_format::{CustomFmtContext, EventFormat};
use std::fmt;
use tracing::{Event, Level, Subscriber};
use tracing_subscriber::{
fmt::{
time::{FormatTime, SystemTime},
FormatFields,
},
layer::Context,
registry::LookupSpan,
Layer,
};
use wasm_bindgen::prelude::*;
/// A `Layer` that display logs in the browser's console.
pub struct ConsoleLogLayer<S, N = tracing_subscriber::fmt::format::DefaultFields, T = SystemTime> {
event_format: EventFormat<T>,
fmt_fields: N,
_inner: std::marker::PhantomData<S>,
}
impl<S, T> ConsoleLogLayer<S, tracing_subscriber::fmt::format::DefaultFields, T> {
/// Create a new [`ConsoleLogLayer`] using the `EventFormat` provided in argument.
pub fn new(event_format: EventFormat<T>) -> Self {
Self {
event_format,
fmt_fields: Default::default(),
_inner: std::marker::PhantomData,
}
}
}
// NOTE: the following code took inspiration from `EventFormat` (in this file)
impl<S, N, T: FormatTime> ConsoleLogLayer<S, N, T>
where
S: Subscriber + for<'a> LookupSpan<'a>,
N: for<'writer> FormatFields<'writer> + 'static,
{
fn format_event(
&self,
ctx: &Context<'_, S>,
writer: &mut dyn fmt::Write,
event: &Event,
) -> fmt::Result {
self.event_format.format_event_custom(
CustomFmtContext::ContextWithFormatFields(ctx, &self.fmt_fields),
writer,
event,
)
}
}
// NOTE: the following code took inspiration from tracing-subscriber
//
// https://github.com/tokio-rs/tracing/blob/2f59b32/tracing-subscriber/src/fmt/fmt_layer.rs#L717
impl<S, N, T> Layer<S> for ConsoleLogLayer<S, N, T>
where
S: Subscriber + for<'a> LookupSpan<'a>,
N: for<'writer> FormatFields<'writer> + 'static,
T: FormatTime + 'static,
{
fn on_event(&self, event: &Event<'_>, ctx: Context<'_, S>) {
thread_local! {
static BUF: std::cell::RefCell<String> = std::cell::RefCell::new(String::new());
}
BUF.with(|buf| {
let borrow = buf.try_borrow_mut();
let mut a;
let mut b;
let mut buf = match borrow {
Ok(buf) => {
a = buf;
&mut *a
}
_ => {
b = String::new();
&mut b
}
};
if self.format_event(&ctx, &mut buf, event).is_ok() {
if !buf.is_empty() {
let meta = event.metadata();
let level = meta.level();
// NOTE: the following code took inspiration from tracing-subscriber
//
// https://github.com/iamcodemaker/console_log/blob/f13b5d6755/src/lib.rs#L149
match *level {
Level::ERROR => web_sys::console::error_1(&JsValue::from(buf.as_str())),
Level::WARN => web_sys::console::warn_1(&JsValue::from(buf.as_str())),
Level::INFO => web_sys::console::info_1(&JsValue::from(buf.as_str())),
Level::DEBUG => web_sys::console::log_1(&JsValue::from(buf.as_str())),
Level::TRACE => web_sys::console::debug_1(&JsValue::from(buf.as_str())),
}
}
}
buf.clear();
});
}
}
@@ -0,0 +1,25 @@
// This file is part of Substrate.
// Copyright (C) 2021 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/>.
#[cfg(target_os = "unknown")]
mod console_log;
mod prefix_layer;
#[cfg(target_os = "unknown")]
pub use console_log::*;
pub use prefix_layer::*;
@@ -0,0 +1,86 @@
// This file is part of Substrate.
// Copyright (C) 2021 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 `sc_tracing::logging::prefix_logs_with!`
pub const PREFIX_LOG_SPAN: &str = "substrate-log-prefix";
/// A `Layer` that captures the prefix span ([`PREFIX_LOG_SPAN`]) which is then used by
/// [`EventFormat`] to prefix the log lines by customizable string.
///
/// See the macro `sc_cli::prefix_logs_with!` for more details.
pub struct PrefixLayer;
impl<S> Layer<S> for PrefixLayer
where
S: Subscriber + for<'a> LookupSpan<'a>,
{
fn new_span(&self, attrs: &Attributes<'_>, id: &Id, ctx: Context<'_, S>) {
let span = ctx
.span(id)
.expect("new_span has been called for this span; qed");
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()
}
}
+531
View File
@@ -0,0 +1,531 @@
// This file is part of Substrate.
// Copyright (C) 2021 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/>.
//! Substrate 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 layers;
pub use directives::*;
pub use sc_tracing_proc_macro::*;
use sc_telemetry::{ExtTransport, TelemetryWorker};
use std::io;
use tracing::Subscriber;
use tracing_subscriber::{
fmt::time::ChronoLocal,
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 layers::*;
/// Logging Result typedef.
pub type Result<T> = std::result::Result<T, Error>;
/// Logging errors.
#[derive(Debug, thiserror::Error)]
#[allow(missing_docs)]
#[non_exhaustive]
pub enum Error {
#[error(transparent)]
IoError(#[from] io::Error),
#[error(transparent)]
SetGlobalDefaultError(#[from] tracing::subscriber::SetGlobalDefaultError),
#[error(transparent)]
DirectiveParseError(#[from] tracing_subscriber::filter::ParseError),
#[error(transparent)]
SetLoggerError(#[from] tracing_log::log_tracer::SetLoggerError),
}
macro_rules! disable_log_reloading {
($builder:expr) => {{
let builder = $builder.with_filter_reloading();
let handle = builder.reload_handle();
set_reload_handle(handle);
builder
}};
}
/// Common implementation to get the subscriber.
fn get_subscriber_internal<N, E, F, W>(
pattern: &str,
max_level: Option<log::LevelFilter>,
force_colors: Option<bool>,
telemetry_buffer_size: Option<usize>,
telemetry_external_transport: Option<ExtTransport>,
builder_hook: impl Fn(
SubscriberBuilder<
format::DefaultFields,
EventFormat<ChronoLocal>,
EnvFilter,
fn() -> std::io::Stderr,
>,
) -> SubscriberBuilder<N, E, F, W>,
) -> Result<(impl Subscriber + for<'a> LookupSpan<'a>, TelemetryWorker)>
where
N: for<'writer> FormatFields<'writer> + 'static,
E: FormatEvent<Registry, N> + 'static,
W: MakeWriter + '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("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"));
if let Ok(lvl) = std::env::var("RUST_LOG") {
if lvl != "" {
env_filter = parse_user_directives(env_filter, &lvl)?;
}
}
if pattern != "" {
// We're not sure if log or tracing is available at this moment, so silently ignore the
// parse error.
env_filter = parse_user_directives(env_filter, pattern)?;
}
let max_level_hint = Layer::<FmtSubscriber>::max_level_hint(&env_filter);
let max_level = max_level.unwrap_or_else(|| match max_level_hint {
Some(tracing_subscriber::filter::LevelFilter::INFO) | None => log::LevelFilter::Info,
Some(tracing_subscriber::filter::LevelFilter::TRACE) => log::LevelFilter::Trace,
Some(tracing_subscriber::filter::LevelFilter::WARN) => log::LevelFilter::Warn,
Some(tracing_subscriber::filter::LevelFilter::ERROR) => log::LevelFilter::Error,
Some(tracing_subscriber::filter::LevelFilter::DEBUG) => log::LevelFilter::Debug,
Some(tracing_subscriber::filter::LevelFilter::OFF) => log::LevelFilter::Off,
});
tracing_log::LogTracer::builder()
.with_max_level(max_level)
.init()?;
// If we're only logging `INFO` entries then we'll use a simplified logging format.
let simple = match max_level_hint {
Some(level) if level <= tracing_subscriber::filter::LevelFilter::INFO => true,
_ => false,
};
let enable_color = force_colors.unwrap_or_else(|| atty::is(atty::Stream::Stderr));
let timer = ChronoLocal::with_format(if simple {
"%Y-%m-%d %H:%M:%S".to_string()
} else {
"%Y-%m-%d %H:%M:%S%.3f".to_string()
});
let (telemetry_layer, telemetry_worker) =
sc_telemetry::TelemetryLayer::new(telemetry_buffer_size, telemetry_external_transport)?;
let event_format = EventFormat {
timer,
display_target: !simple,
display_level: !simple,
display_thread_name: !simple,
enable_color,
};
let builder = FmtSubscriber::builder().with_env_filter(env_filter);
#[cfg(not(target_os = "unknown"))]
let builder = builder.with_writer(std::io::stderr as _);
#[cfg(target_os = "unknown")]
let builder = builder.with_writer(std::io::sink);
#[cfg(not(target_os = "unknown"))]
let builder = builder.event_format(event_format);
#[cfg(not(target_os = "unknown"))]
let builder = builder_hook(builder);
let subscriber = builder.finish().with(PrefixLayer).with(telemetry_layer);
#[cfg(target_os = "unknown")]
let subscriber = subscriber.with(ConsoleLogLayer::new(event_format));
Ok((subscriber, telemetry_worker))
}
/// A builder that is used to initialize the global logger.
pub struct GlobalLoggerBuilder {
pattern: String,
profiling: Option<(crate::TracingReceiver, String)>,
telemetry_buffer_size: Option<usize>,
telemetry_external_transport: Option<ExtTransport>,
disable_log_reloading: bool,
force_colors: Option<bool>,
}
impl GlobalLoggerBuilder {
/// Create a new [`GlobalLoggerBuilder`] which can be used to initialize the global logger.
pub fn new<S: Into<String>>(pattern: S) -> Self {
Self {
pattern: pattern.into(),
profiling: None,
telemetry_buffer_size: None,
telemetry_external_transport: None,
disable_log_reloading: false,
force_colors: None,
}
}
/// 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
}
/// Wether or not to disable log reloading.
pub fn with_log_reloading(&mut self, enabled: bool) -> &mut Self {
self.disable_log_reloading = !enabled;
self
}
/// Set a custom buffer size for the telemetry.
pub fn with_telemetry_buffer_size(&mut self, buffer_size: usize) -> &mut Self {
self.telemetry_buffer_size = Some(buffer_size);
self
}
/// Set a custom network transport (used for the telemetry).
pub fn with_transport(&mut self, transport: ExtTransport) -> &mut Self {
self.telemetry_external_transport = Some(transport);
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<TelemetryWorker> {
if let Some((tracing_receiver, profiling_targets)) = self.profiling {
// If profiling is activated, we require `trace` logging.
let max_level = Some(log::LevelFilter::Trace);
if self.disable_log_reloading {
let (subscriber, telemetry_worker) = get_subscriber_internal(
&format!("{},{},sc_tracing=trace", self.pattern, profiling_targets),
max_level,
self.force_colors,
self.telemetry_buffer_size,
self.telemetry_external_transport,
|builder| builder,
)?;
let profiling = crate::ProfilingLayer::new(tracing_receiver, &profiling_targets);
tracing::subscriber::set_global_default(subscriber.with(profiling))?;
Ok(telemetry_worker)
} else {
let (subscriber, telemetry_worker) = get_subscriber_internal(
&format!("{},{},sc_tracing=trace", self.pattern, profiling_targets),
max_level,
self.force_colors,
self.telemetry_buffer_size,
self.telemetry_external_transport,
|builder| disable_log_reloading!(builder),
)?;
let profiling = crate::ProfilingLayer::new(tracing_receiver, &profiling_targets);
tracing::subscriber::set_global_default(subscriber.with(profiling))?;
Ok(telemetry_worker)
}
} else {
if self.disable_log_reloading {
let (subscriber, telemetry_worker) = get_subscriber_internal(
&self.pattern,
None,
self.force_colors,
self.telemetry_buffer_size,
self.telemetry_external_transport,
|builder| builder,
)?;
tracing::subscriber::set_global_default(subscriber)?;
Ok(telemetry_worker)
} else {
let (subscriber, telemetry_worker) = get_subscriber_internal(
&self.pattern,
None,
self.force_colors,
self.telemetry_buffer_size,
self.telemetry_external_transport,
|builder| disable_log_reloading!(builder),
)?;
tracing::subscriber::set_global_default(subscriber)?;
Ok(telemetry_worker)
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate as sc_tracing;
use std::{env, process::Command};
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(pattern: &str) {
let _ = GlobalLoggerBuilder::new(pattern).init().unwrap();
}
fn run_in_process(test_name: &str) {
if env::var("RUN_IN_PROCESS").is_err() {
let status = Command::new(env::current_exe().unwrap())
.arg(test_name)
.env("RUN_IN_PROCESS", "true")
.status()
.unwrap();
assert!(status.success(), "process did not ended successfully");
std::process::exit(0);
}
}
#[test]
fn test_logger_filters() {
run_in_process("test_logger_filters");
let test_pattern = "afg=debug,sync=trace,client=warn,telemetry,something-with-dash=error";
init_logger(&test_pattern);
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("afg", Level::INFO));
assert!(test_filter("afg", Level::DEBUG));
assert!(!test_filter("afg", 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_pattern = "test-target=info";
let _guard = init_logger(&test_pattern);
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()),
format!("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!("{}", ansi_term::Colour::Yellow.paint(EXPECTED_LOG_MESSAGE));
}
}
#[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()),
format!("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();
String::from_utf8(output.stderr).unwrap()
}
if env::var("PRINT_MAX_LOG_LEVEL").is_ok() {
init_logger(&env::var("TRACING_TARGETS").unwrap_or_default());
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()))
);
}
}
}