From b03c2a15b39188c8cc7164682d9ccf1a2be4fdd6 Mon Sep 17 00:00:00 2001 From: Charles Hall Date: Thu, 10 Oct 2024 14:30:13 -0700 Subject: [PATCH] add observability infrastructure for cli subcmds --- src/cli.rs | 34 +++++++++++++++++++++++++++++++++- src/config.rs | 26 ++++++++++++++++++++------ src/config/env_filter_clone.rs | 17 +++++++++++++++-- src/error.rs | 3 +++ src/observability.rs | 26 +++++++++++++++++++++++++- 5 files changed, 96 insertions(+), 10 deletions(-) diff --git a/src/cli.rs b/src/cli.rs index c997ad67..f697d258 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -7,7 +7,10 @@ use std::path::PathBuf; use clap::{Parser, Subcommand}; -use crate::error; +use crate::{ + config::{default_tracing_filter, EnvFilterClone, LogFormat}, + error, observability, +}; mod serve; @@ -51,6 +54,21 @@ pub(crate) struct ConfigArg { pub(crate) config: Option, } +/// Observability arguments for CLI subcommands +#[derive(clap::Args)] +struct ObservabilityArgs { + /// Log format + #[clap(long, default_value_t = LogFormat::Full)] + log_format: LogFormat, + + /// Log filter + /// + /// For information about the syntax, see here: + /// + #[clap(long, default_value_t = default_tracing_filter())] + log_filter: EnvFilterClone, +} + #[derive(clap::Args)] pub(crate) struct ServeArgs { #[clap(flatten)] @@ -59,9 +77,23 @@ pub(crate) struct ServeArgs { impl Args { pub(crate) async fn run(self) -> Result<(), error::Main> { + if let Some((format, filter)) = self.command.cli_observability_args() { + observability::init_for_cli(format, filter.into())?; + } + match self.command { Command::Serve(args) => serve::run(args).await?, } Ok(()) } } + +impl Command { + fn cli_observability_args(&self) -> Option<(LogFormat, EnvFilterClone)> { + // All subcommands other than `serve` should return `Some`. Keep these + // match arms sorted by the enum variant name. + match self { + Command::Serve(_) => None, + } + } +} diff --git a/src/config.rs b/src/config.rs index c53f1dd5..bf08c7c1 100644 --- a/src/config.rs +++ b/src/config.rs @@ -180,20 +180,34 @@ impl Display for ListenConfig { } } -#[derive(Copy, Clone, Default, Debug, Deserialize)] +#[derive(Copy, Clone, Default, Debug, Deserialize, clap::ValueEnum)] #[serde(rename_all = "snake_case")] pub(crate) enum LogFormat { - /// Use the [`tracing_subscriber::fmt::format::Pretty`] formatter + /// Multiple lines per event, includes all information Pretty, - /// Use the [`tracing_subscriber::fmt::format::Full`] formatter + + /// One line per event, includes most information #[default] Full, - /// Use the [`tracing_subscriber::fmt::format::Compact`] formatter + + /// One line per event, includes less information Compact, - /// Use the [`tracing_subscriber::fmt::format::Json`] formatter + + /// One JSON object per line per event, includes most information Json, } +impl Display for LogFormat { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + LogFormat::Pretty => write!(f, "pretty"), + LogFormat::Full => write!(f, "full"), + LogFormat::Compact => write!(f, "compact"), + LogFormat::Json => write!(f, "json"), + } + } +} + #[derive(Clone, Debug, Deserialize)] #[serde(default)] pub(crate) struct TurnConfig { @@ -404,7 +418,7 @@ fn default_max_request_size() -> u32 { 20 * 1024 * 1024 } -fn default_tracing_filter() -> EnvFilterClone { +pub(crate) fn default_tracing_filter() -> EnvFilterClone { "info,ruma_state_res=warn" .parse() .expect("hardcoded env filter should be valid") diff --git a/src/config/env_filter_clone.rs b/src/config/env_filter_clone.rs index 183c827f..9e8983b7 100644 --- a/src/config/env_filter_clone.rs +++ b/src/config/env_filter_clone.rs @@ -5,7 +5,7 @@ //! [0]: https://github.com/tokio-rs/tracing/pull/2956 #![warn(missing_docs, clippy::missing_docs_in_private_items)] -use std::str::FromStr; +use std::{fmt, str::FromStr}; use serde::{de, Deserialize, Deserializer}; use tracing_subscriber::EnvFilter; @@ -14,7 +14,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)] +#[derive(Debug, Clone)] pub(crate) struct EnvFilterClone(String); impl FromStr for EnvFilterClone { @@ -26,6 +26,12 @@ impl FromStr for EnvFilterClone { } } +impl fmt::Display for EnvFilterClone { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.0) + } +} + impl From<&EnvFilterClone> for EnvFilter { fn from(other: &EnvFilterClone) -> Self { EnvFilter::from_str(&other.0) @@ -33,6 +39,13 @@ impl From<&EnvFilterClone> for EnvFilter { } } +impl From for EnvFilter { + fn from(other: EnvFilterClone) -> Self { + EnvFilter::from_str(&other.0) + .expect("env filter syntax should have been validated already") + } +} + impl<'de> Deserialize<'de> for EnvFilterClone { fn deserialize(deserializer: D) -> Result where diff --git a/src/error.rs b/src/error.rs index 72bcd337..e0e53436 100644 --- a/src/error.rs +++ b/src/error.rs @@ -42,6 +42,9 @@ impl fmt::Display for DisplayWithSources<'_> { pub(crate) enum Main { #[error(transparent)] ServeCommand(#[from] ServeCommand), + + #[error("failed to install global default tracing subscriber")] + SetSubscriber(#[from] tracing::subscriber::SetGlobalDefaultError), } /// Errors returned from the `serve` CLI subcommand. diff --git a/src/observability.rs b/src/observability.rs index 059ed436..f0baee1c 100644 --- a/src/observability.rs +++ b/src/observability.rs @@ -18,7 +18,7 @@ use opentelemetry_sdk::{ }; use strum::{AsRefStr, IntoStaticStr}; use tokio::time::Instant; -use tracing::Span; +use tracing::{subscriber::SetGlobalDefaultError, Span}; use tracing_flame::{FlameLayer, FlushGuard}; use tracing_opentelemetry::OtelData; use tracing_subscriber::{ @@ -469,3 +469,27 @@ pub(crate) async fn traceresponse_layer(req: Request, next: Next) -> Response { resp } + +/// Set up observability for CLI-oriented subcommands. +/// +/// Tracing spans and events will be sent to `stderr`. +pub(crate) fn init_for_cli( + log_format: LogFormat, + env_filter: EnvFilter, +) -> Result<(), SetGlobalDefaultError> { + let log_layer = + tracing_subscriber::fmt::Layer::new().with_writer(std::io::stderr); + + let log_layer = match log_format { + LogFormat::Pretty => log_layer.pretty().boxed(), + LogFormat::Full => log_layer.boxed(), + LogFormat::Compact => log_layer.compact().boxed(), + LogFormat::Json => log_layer.json().boxed(), + }; + + let log_layer = log_layer.with_filter(env_filter); + + let subscriber = Registry::default().with(log_layer); + + tracing::subscriber::set_global_default(subscriber).map_err(Into::into) +}