diff --git a/book/changelog.md b/book/changelog.md index eeb9fdf6..79801297 100644 --- a/book/changelog.md +++ b/book/changelog.md @@ -261,9 +261,10 @@ This will be the first release of Grapevine since it was forked from Conduit ([!46](https://gitlab.computer.surgery/matrix/grapevine/-/merge_requests/46)) 10. Recognize the `!admin` prefix to invoke admin commands. ([!45](https://gitlab.computer.surgery/matrix/grapevine/-/merge_requests/45)) -11. Add the `set-tracing-filter` admin command to change log/metrics/flame +11. Add the `tracing-filter` admin command to view and change log/metrics/flame filters dynamically at runtime. - ([!49](https://gitlab.computer.surgery/matrix/grapevine/-/merge_requests/49)) + ([!49](https://gitlab.computer.surgery/matrix/grapevine/-/merge_requests/49), + [!164](https://gitlab.computer.surgery/matrix/grapevine/-/merge_requests/164)) 12. Add more configuration options. ([!49](https://gitlab.computer.surgery/matrix/grapevine/-/merge_requests/49)) * `observability.traces.filter`: The `tracing` filter to use for diff --git a/src/config/env_filter_clone.rs b/src/config/env_filter_clone.rs index 9e8983b7..c4325fc2 100644 --- a/src/config/env_filter_clone.rs +++ b/src/config/env_filter_clone.rs @@ -15,7 +15,7 @@ use tracing_subscriber::EnvFilter; /// Use [`FromStr`] or [`Deserialize`] to construct this type, then [`From`] or /// [`Into`] to convert it into an [`EnvFilter`] when needed. #[derive(Debug, Clone)] -pub(crate) struct EnvFilterClone(String); +pub(crate) struct EnvFilterClone(pub(crate) String); impl FromStr for EnvFilterClone { type Err = ::Err; diff --git a/src/observability.rs b/src/observability.rs index 0dfb3b55..44e06c91 100644 --- a/src/observability.rs +++ b/src/observability.rs @@ -21,6 +21,7 @@ use opentelemetry_sdk::{ Resource, }; use strum::{AsRefStr, IntoStaticStr}; +use thiserror::Error; use tokio::time::Instant; use tracing::{subscriber::SetGlobalDefaultError, Span}; use tracing_flame::{FlameLayer, FlushGuard}; @@ -78,9 +79,67 @@ impl ReloadHandle for reload::Handle { } } -/// A type-erased [reload handle][reload::Handle] for an [`EnvFilter`]. -pub(crate) type FilterReloadHandle = - Box + Send + Sync>; +/// Error returned from [`FilterReloadHandle::set_filter()`] +#[allow(clippy::missing_docs_in_private_items)] +#[derive(Debug, Error)] +pub(crate) enum SetFilterError { + #[error("invalid filter string")] + InvalidFilter(#[from] tracing_subscriber::filter::ParseError), + #[error("failed to reload filter layer")] + Reload(#[from] reload::Error), +} + +/// A wrapper around a tracing filter [reload handle][reload::Handle] that +/// remembers the filter string that was last set. +pub(crate) struct FilterReloadHandle { + /// The actual [`reload::Handle`] that can be used to modify the filter + /// [`Layer`] + inner: Box + Send + Sync>, + /// Filter string that was last applied to `inner` + current_filter: String, + /// Filter string that was initially loaded from the configuration + initial_filter: String, +} + +impl FilterReloadHandle { + /// Creates a new [`FilterReloadHandle`] from a filter string, returning the + /// filter layer itself and the handle that can be used to modify it. + pub(crate) fn new( + filter: EnvFilterClone, + ) -> (impl tracing_subscriber::layer::Filter, Self) { + let (layer, handle) = reload::Layer::new(EnvFilter::from(&filter)); + let handle = Self { + inner: Box::new(handle), + current_filter: filter.0.clone(), + initial_filter: filter.0, + }; + (layer, handle) + } + + /// Sets the filter string for the linked filter layer. Can fail if the + /// filter string is invalid or when the link to the layer has been + /// broken. + pub(crate) fn set_filter( + &mut self, + filter: String, + ) -> Result<(), SetFilterError> { + self.inner.reload(filter.parse()?)?; + self.current_filter = filter; + Ok(()) + } + + /// Returns the filter string that the underlying filter layer is currently + /// configured for. + pub(crate) fn get_filter(&self) -> &str { + &self.current_filter + } + + /// Returns the filter string that the underlying filter layer was + /// initialized with. + pub(crate) fn get_initial_filter(&self) -> &str { + &self.initial_filter + } +} /// Collection of [`FilterReloadHandle`]s, allowing the filters for tracing /// backends to be changed dynamically. Handles may be [`None`] if the backend @@ -153,9 +212,9 @@ where return Ok((None, None, None)); } - let (filter, handle) = reload::Layer::new(EnvFilter::from(filter)); + let (filter, handle) = FilterReloadHandle::new(filter.clone()); let (layer, data) = init()?; - Ok((Some(layer.with_filter(filter)), Some(Box::new(handle)), Some(data))) + Ok((Some(layer.with_filter(filter)), Some(handle), Some(data))) } /// Initialize observability diff --git a/src/service/admin.rs b/src/service/admin.rs index eb453239..5e3ee770 100644 --- a/src/service/admin.rs +++ b/src/service/admin.rs @@ -1,6 +1,6 @@ use std::{collections::BTreeMap, fmt::Write, sync::Arc, time::Instant}; -use clap::{Parser, ValueEnum}; +use clap::{Parser, Subcommand, ValueEnum}; use regex::Regex; use ruma::{ api::appservice::Registration, @@ -205,10 +205,41 @@ enum AdminCommand { VerifyJson, /// Dynamically change a tracing backend's filter string - SetTracingFilter { + TracingFilter { + #[command(subcommand)] + cmd: TracingFilterCommand, + }, +} + +#[derive(Debug, Subcommand)] +enum TracingFilterCommand { + Get { + backend: TracingBackend, + }, + Set { backend: TracingBackend, filter: String, }, + Reset { + backend: TracingBackend, + }, +} + +impl TracingFilterCommand { + fn backend(&self) -> &TracingBackend { + match self { + TracingFilterCommand::Get { + backend, + } + | TracingFilterCommand::Set { + backend, + .. + } + | TracingFilterCommand::Reset { + backend, + } => backend, + } + } } #[derive(Debug)] @@ -1167,9 +1198,8 @@ impl Service { ) } } - AdminCommand::SetTracingFilter { - backend, - filter, + AdminCommand::TracingFilter { + cmd, } => { let Some(handles) = &services().globals.reload_handles else { return Ok(RoomMessageEventContent::text_plain( @@ -1177,7 +1207,7 @@ impl Service { )); }; let mut handles = handles.write().await; - let handle = match backend { + let handle = match cmd.backend() { TracingBackend::Log => &mut handles.log, TracingBackend::Flame => &mut handles.flame, TracingBackend::Traces => &mut handles.traces, @@ -1187,15 +1217,27 @@ impl Service { "Backend is disabled", )); }; - let filter = match filter.parse() { - Ok(filter) => filter, - Err(e) => { + + let filter = match cmd { + TracingFilterCommand::Set { + filter, + .. + } => filter, + TracingFilterCommand::Reset { + .. + } => handle.get_initial_filter().to_owned(), + TracingFilterCommand::Get { + .. + } => { return Ok(RoomMessageEventContent::text_plain( - format!("Invalid filter string: {e}"), + format!( + "Current filter string: {}", + handle.get_filter() + ), )); } }; - if let Err(e) = handle.reload(filter) { + if let Err(e) = handle.set_filter(filter) { return Ok(RoomMessageEventContent::text_plain(format!( "Failed to reload filter: {e}" )));