Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(daemon): support dynamically setting the logging level at runtime #105

Merged
merged 4 commits into from
Sep 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion prompting-client/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@ path = "src/bin/echo.rs"
name = "prompting-client-daemon"
path = "src/bin/daemon.rs"

[[bin]]
name = "prompting-client-set-log-level"
path = "src/bin/set_log_level.rs"

[dependencies]
chrono = "0.4.38"
clap = { version = "4.5.4", features = ["derive"] }
Expand All @@ -37,6 +41,7 @@ tokio-stream = "0.1.15"
tokio = { version = "1.37.0", features = ["fs", "io-util", "macros", "net", "process", "signal", "rt-multi-thread"] }
tonic = "0.12.0"
tonic-reflection = "0.12.0"
tower = "0.4.13"
tracing = "0.1.40"
tracing-subscriber = { version = "0.3.18", features = ["env-filter"] }
tracing-journald = "0.3.0"
Expand All @@ -48,7 +53,6 @@ simple_test_case = "1.2.0"
tokio = { version = "1.37.0", features = ["process"] }
uuid = { version = "1.8.0", features = ["v4"] }
protobuf = "3.5.0"
tower = "0.4.13"
tokio-util = "0.7.11"

[build-dependencies]
Expand Down
1 change: 1 addition & 0 deletions prompting-client/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {

tonic_build::configure()
.build_server(true)
.build_client(true)
.out_dir("./src/protos")
.compile(files, include_dirs)?;

Expand Down
12 changes: 7 additions & 5 deletions prompting-client/src/bin/daemon.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
//! The daemon prompting client for apparmor prompting
use prompting_client::{daemon::run_daemon, snapd_client::SnapdSocketClient, Error, Result};
use prompting_client::{
daemon::run_daemon, log_filter, snapd_client::SnapdSocketClient, Error, Result,
DEFAULT_LOG_LEVEL,
};
use std::{env, io::stdout};
use tracing::subscriber::set_global_default;
use tracing::warn;
Expand All @@ -8,12 +11,11 @@ use tracing_subscriber::{layer::SubscriberExt, FmtSubscriber};
#[tokio::main]
async fn main() -> Result<()> {
let builder = FmtSubscriber::builder()
.with_env_filter("debug,hyper=error,h2=error")
.with_env_filter(log_filter(DEFAULT_LOG_LEVEL))
.with_writer(stdout)
.with_filter_reloading();

// TODO: (sminez) support modifying the logging level at runtime via dbus
// let reload_handle = builder.reload_handle();
let reload_handle = builder.reload_handle();
let journald_layer = tracing_journald::layer().expect("unable to open journald socket");
let subscriber = builder.finish().with(journald_layer);

Expand All @@ -37,5 +39,5 @@ async fn main() -> Result<()> {
return Ok(());
}

run_daemon(c).await
run_daemon(c, reload_handle).await
}
46 changes: 46 additions & 0 deletions prompting-client/src/bin/set_log_level.rs
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the filename could be updated to match as well

Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
//! This is a helper command for setting our logging filter within the daemon while it is running.
//! Doing this involves having to send a GRPC message to the daemon which inolves more plumbing
//! than we'd ideally like but it allows us to update the filter without having to restart the
//! daemon.
//!
//! The docs on the command itself cover how to set simple logging levels but the tracing framework
//! we are using actually supports a rich syntax for how these filters are written. As and when we
//! need to use this in anger, the docs can be found here:
//! https://docs.rs/tracing-subscriber/latest/tracing_subscriber/filter/struct.EnvFilter.html
Comment on lines +1 to +9
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, this is great!

use clap::Parser;
use prompting_client::cli_actions::set_logging_filter;
use std::process::exit;

/// Set the logging level for a running instance of the prompting client daemon.
///
/// Simple usage of this command involves specifying a level based filter for what logs are written
/// to the system journal by the prompting-client daemon. The supported levels are (in order of
/// least to most verbose):
///
/// - error
/// - warn
/// - info
/// - debug
/// - trace
///
/// The default level is "info".
Comment on lines +14 to +26
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perfect!

#[derive(Debug, Parser)]
#[clap(about, long_about = None)]
struct Args {
/// The filter to use for determining what gets logged
#[clap(short, long, value_name = "FILTER")]
filter: String,
}

#[tokio::main]
async fn main() {
let Args { filter } = Args::parse();

match set_logging_filter(filter).await {
Ok(current) => println!("logging filter set to: {current}"),
Err(e) => {
eprintln!("{e}");
exit(1);
}
}
}
36 changes: 36 additions & 0 deletions prompting-client/src/cli_actions/log_level.rs
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same

Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
use crate::{
protos::apparmor_prompting::app_armor_prompting_client::AppArmorPromptingClient, Error, Result,
SOCKET_ENV_VAR,
};
use hyper_util::rt::TokioIo;
use std::env;
use std::io;
use tokio::net::UnixStream;
use tonic::transport::{Channel, Endpoint, Uri};
use tower::service_fn;

pub async fn set_logging_filter(filter: String) -> Result<String> {
let mut client = client_from_env().await;

match client.set_logging_filter(filter).await {
Ok(resp) => Ok(resp.into_inner().current),
Err(e) => Err(Error::UnableToUpdateLogFilter {
reason: e.to_string(),
}),
}
}

async fn client_from_env() -> AppArmorPromptingClient<Channel> {
let path = env::var(SOCKET_ENV_VAR).expect("socket env var not set");

// See https://github.com/hyperium/tonic/blob/master/examples/src/uds/client.rs
let channel = Endpoint::from_static("https://not-used.com")
.connect_with_connector(service_fn(move |_: Uri| {
let path = path.clone();
async { Ok::<_, io::Error>(TokioIo::new(UnixStream::connect(path).await?)) }
}))
.await
.unwrap();

AppArmorPromptingClient::new(channel)
}
2 changes: 2 additions & 0 deletions prompting-client/src/cli_actions/mod.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
mod echo_loop;
mod log_level;
mod scripted;

pub use echo_loop::run_echo_loop;
pub use log_level::set_logging_filter;
pub use scripted::run_scripted_client_loop;
22 changes: 16 additions & 6 deletions prompting-client/src/daemon/mod.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
use crate::{
snapd_client::{PromptId, SnapMeta, SnapdSocketClient, TypedPrompt, TypedPromptReply},
Result,
Result, SOCKET_ENV_VAR,
};
use serde::{Deserialize, Serialize};
use std::{env, fs};
use std::{env, fs, sync::Arc};
use tokio::sync::mpsc::unbounded_channel;
use tokio_stream::wrappers::UnixListenerStream;
use tonic::{async_trait, transport::Server};
use tracing::{error, info};
use tracing_subscriber::{reload::Handle, EnvFilter};

mod poll;
mod server;
Expand Down Expand Up @@ -52,19 +53,28 @@ pub enum ActionedPrompt {

/// Start our backgroud polling and processing loops before dropping into running the tonic GRPC
/// server for handling incoming requestes from the Flutter UI client.
pub async fn run_daemon(c: SnapdSocketClient) -> Result<()> {
pub async fn run_daemon<L, S>(c: SnapdSocketClient, reload_handle: Handle<L, S>) -> Result<()>
where
L: From<EnvFilter> + Send + Sync + 'static,
S: 'static,
{
let (tx_prompts, rx_prompts) = unbounded_channel();
let (tx_actioned, rx_actioned) = unbounded_channel();

let mut worker = Worker::new(rx_prompts, rx_actioned, c.clone());
let active_prompt = worker.read_only_active_prompt();

let path =
env::var("PROMPTING_CLIENT_SOCKET").expect("PROMPTING_CLIENT_SOCKET env var to be set");
let path = env::var(SOCKET_ENV_VAR).expect("socket env var not set");
if let Err(e) = fs::remove_file(&path) {
error!("Failed to remove old socket file: {}. Error: {}", path, e);
}
let (server, listener) = new_server_and_listener(c.clone(), active_prompt, tx_actioned, path);
let (server, listener) = new_server_and_listener(
c.clone(),
Arc::new(reload_handle),
active_prompt,
tx_actioned,
path,
);

info!("spawning poll loop");
let poll_loop = PollLoop::new(c, tx_prompts);
Expand Down
85 changes: 76 additions & 9 deletions prompting-client/src/daemon/server.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
//! The GRPC server that handles incoming connections from client UIs.
use crate::{
daemon::{worker::ReadOnlyActivePrompt, ActionedPrompt, ReplyToPrompt},
log_filter,
protos::{
apparmor_prompting::{
self, get_current_prompt_response::Prompt, home_prompt::PatternOption,
prompt_reply::PromptReply::HomePromptReply, prompt_reply_response::PromptReplyType,
HomePatternType, MetaData, PromptReply,
HomePatternType, MetaData, PromptReply, SetLoggingFilterResponse,
},
AppArmorPrompting, AppArmorPromptingServer, GetCurrentPromptResponse, HomePrompt,
PromptReplyResponse, ResolveHomePatternTypeResponse,
Expand All @@ -20,9 +21,11 @@ use crate::{
},
Error, NO_PROMPTS_FOR_USER, PROMPT_NOT_FOUND,
};
use std::sync::Arc;
use tokio::{net::UnixListener, sync::mpsc::UnboundedSender};
use tonic::{async_trait, Code, Request, Response, Status};
use tracing::{info, warn};
use tracing_subscriber::{reload::Handle, EnvFilter};

macro_rules! map_enum {
($from:ident => $to:ident; [$($variant:ident),+]; $val:expr;) => {
Expand All @@ -42,38 +45,78 @@ macro_rules! map_enum {
};
}

pub fn new_server_and_listener<T: ReplyToPrompt + Clone>(
client: T,
pub fn new_server_and_listener<R, S>(
client: R,
reload_handle: S,
active_prompt: ReadOnlyActivePrompt,
tx_actioned_prompts: UnboundedSender<ActionedPrompt>,
socket_path: String,
) -> (AppArmorPromptingServer<Service<T>>, UnixListener) {
let service = Service::new(client.clone(), active_prompt, tx_actioned_prompts);
) -> (AppArmorPromptingServer<Service<R, S>>, UnixListener)
where
R: ReplyToPrompt + Clone,
S: SetLogFilter,
{
let service = Service::new(
client.clone(),
reload_handle,
active_prompt,
tx_actioned_prompts,
);
let listener = UnixListener::bind(&socket_path).expect("to be able to bind to our socket");

(AppArmorPromptingServer::new(service), listener)
}

pub struct Service<R>
pub trait SetLogFilter: Send + Sync + 'static {
fn set_filter(&self, filter: &str) -> crate::Result<()>;
}

impl<L, S> SetLogFilter for Arc<Handle<L, S>>
where
L: From<EnvFilter> + Send + Sync + 'static,
S: 'static,
{
fn set_filter(&self, filter: &str) -> crate::Result<()> {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

then set_filter is implemented to take a filter.

info!(?filter, "attempting to update logging filter");
let f = filter
.parse::<EnvFilter>()
.map_err(|_| Error::UnableToUpdateLogFilter {
reason: format!("{filter:?} is not a valid logging filter"),
})?;

self.reload(f).map_err(|e| Error::UnableToUpdateLogFilter {
reason: format!("failed to set logging filter: {e}"),
})?;

Ok(())
}
}

pub struct Service<R, S>
where
R: ReplyToPrompt,
S: SetLogFilter,
{
client: R,
reload_handle: S,
active_prompt: ReadOnlyActivePrompt,
tx_actioned_prompts: UnboundedSender<ActionedPrompt>,
}

impl<R> Service<R>
impl<R, S> Service<R, S>
where
R: ReplyToPrompt,
S: SetLogFilter,
{
pub fn new(
client: R,
reload_handle: S,
active_prompt: ReadOnlyActivePrompt,
tx_actioned_prompts: UnboundedSender<ActionedPrompt>,
) -> Self {
Self {
client,
reload_handle,
active_prompt,
tx_actioned_prompts,
}
Expand All @@ -87,9 +130,10 @@ where
}

#[async_trait]
impl<R> AppArmorPrompting for Service<R>
impl<R, S> AppArmorPrompting for Service<R, S>
where
R: ReplyToPrompt,
S: SetLogFilter,
{
async fn get_current_prompt(
&self,
Expand Down Expand Up @@ -165,6 +209,21 @@ where
"this endpoint is not yet implemented",
))
}

async fn set_logging_filter(
&self,
filter: Request<String>,
) -> Result<Response<SetLoggingFilterResponse>, Status> {
let current = log_filter(&filter.into_inner());

match self.reload_handle.set_filter(&current) {
Ok(_) => Ok(Response::new(SetLoggingFilterResponse { current })),
Err(e) => Err(Status::new(
Code::InvalidArgument,
format!("unable to set logging level: {e}"),
)),
}
}
}

fn map_prompt_reply(mut reply: PromptReply) -> Result<TypedPromptReply, Status> {
Expand Down Expand Up @@ -346,6 +405,13 @@ mod tests {
}
}

struct MockReloadHandle;
impl SetLogFilter for MockReloadHandle {
fn set_filter(&self, level: &str) -> crate::Result<()> {
panic!("attempt to set log level to {level}");
}
}

async fn setup_server_and_client(
mock_client: MockClient,
active_prompt: ReadOnlyActivePrompt,
Expand All @@ -357,6 +423,7 @@ mod tests {

let (server, listener) = new_server_and_listener(
mock_client,
MockReloadHandle,
active_prompt,
tx_actioned_prompts,
socket_path.clone(),
Expand All @@ -371,7 +438,7 @@ mod tests {
});

let path = socket_path.clone();
// No choice but to do this https://github.com/hyperium/tonic/blob/master/examples/src/uds/client.rs
// See https://github.com/hyperium/tonic/blob/master/examples/src/uds/client.rs
let channel = Endpoint::from_static("https://not-used.com")
.connect_with_connector(service_fn(move |_: Uri| {
let path = path.clone();
Expand Down
Loading
Loading