-
Notifications
You must be signed in to change notification settings - Fork 4
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
Changes from all commits
1d94f2c
b99706b
f0f1ee6
2fd260f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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); | ||
} | ||
} | ||
} |
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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) | ||
} |
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; |
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, | ||
|
@@ -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;) => { | ||
|
@@ -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<()> { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. then |
||
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, | ||
} | ||
|
@@ -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, | ||
|
@@ -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(¤t) { | ||
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> { | ||
|
@@ -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, | ||
|
@@ -357,6 +423,7 @@ mod tests { | |
|
||
let (server, listener) = new_server_and_listener( | ||
mock_client, | ||
MockReloadHandle, | ||
active_prompt, | ||
tx_actioned_prompts, | ||
socket_path.clone(), | ||
|
@@ -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(); | ||
|
There was a problem hiding this comment.
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