1use std::{pin::Pin, sync::Arc, time::Duration};
2
3use async_trait::async_trait;
4use futures::{Stream, StreamExt as _};
5use saluki_api::{
6 extract::{Query, State},
7 response::IntoResponse,
8 routing::{post, Router},
9 APIHandler, DynamicRoute, EndpointType, StatusCode,
10};
11use saluki_common::{logging::parse_filter_directives, sync::shutdown::ShutdownHandle};
12use saluki_core::runtime::{state::DataspaceRegistry, InitializationError, Supervisable, SupervisorFuture};
13use saluki_error::{generic_error, GenericError};
14use serde::Deserialize;
15use tokio::{
16 pin, select,
17 sync::{mpsc, Mutex},
18 time::sleep,
19};
20use tracing::{debug, error, info, warn};
21use tracing_subscriber::{filter::Targets, reload::Handle, Registry};
22
23use super::LogLevel;
24
25#[derive(Deserialize)]
26struct OverrideQueryParams {
27 time_secs: u64,
28}
29
30enum LoggingOverrideAction {
32 Override { duration: Duration, filter: Targets },
37
38 Reset,
40
41 UpdateBase(Targets),
46}
47
48#[derive(Clone)]
54pub struct LoggingOverrideController {
55 tx: mpsc::Sender<LoggingOverrideAction>,
56}
57
58impl LoggingOverrideController {
59 pub(crate) async fn override_for(&self, duration: Duration, filter: Targets) -> Result<(), GenericError> {
61 self.send(LoggingOverrideAction::Override { duration, filter }).await
62 }
63
64 pub(crate) async fn reset(&self) -> Result<(), GenericError> {
66 self.send(LoggingOverrideAction::Reset).await
67 }
68
69 pub async fn update_base(&self, filter: Targets) -> Result<(), GenericError> {
73 self.send(LoggingOverrideAction::UpdateBase(filter)).await
74 }
75
76 async fn send(&self, action: LoggingOverrideAction) -> Result<(), GenericError> {
77 self.tx
78 .send(action)
79 .await
80 .map_err(|_| generic_error!("logging override worker is no longer running"))
81 }
82}
83
84#[derive(Clone)]
86pub struct LoggingHandlerState {
87 controller: LoggingOverrideController,
88}
89
90pub struct LoggingAPIHandler {
102 state: LoggingHandlerState,
103}
104
105impl LoggingAPIHandler {
106 fn new(controller: LoggingOverrideController) -> Self {
108 Self {
109 state: LoggingHandlerState { controller },
110 }
111 }
112
113 async fn override_handler(
114 State(state): State<LoggingHandlerState>, params: Query<OverrideQueryParams>, body: String,
115 ) -> impl IntoResponse {
116 const MAXIMUM_OVERRIDE_LENGTH_SECS: u64 = 600;
118 if params.time_secs > MAXIMUM_OVERRIDE_LENGTH_SECS {
119 return (
120 StatusCode::BAD_REQUEST,
121 format!(
122 "override time cannot be greater than {} seconds",
123 MAXIMUM_OVERRIDE_LENGTH_SECS
124 ),
125 );
126 }
127
128 let duration = Duration::from_secs(params.time_secs);
130 let new_filter = match parse_filter_directives(&body) {
131 Ok(filter) => filter,
132 Err(e) => {
133 return (
134 StatusCode::BAD_REQUEST,
135 format!("failed to parse override filter: {}", e),
136 )
137 }
138 };
139
140 let _ = state.controller.override_for(duration, new_filter).await;
142
143 (StatusCode::OK, "acknowledged".to_string())
144 }
145
146 async fn reset_handler(State(state): State<LoggingHandlerState>) {
147 let _ = state.controller.reset().await;
149 }
150}
151
152impl APIHandler for LoggingAPIHandler {
153 type State = LoggingHandlerState;
154
155 fn generate_initial_state(&self) -> Self::State {
156 self.state.clone()
157 }
158
159 fn generate_routes(&self) -> Router<Self::State> {
160 Router::new()
161 .route("/logging/override", post(Self::override_handler))
162 .route("/logging/reset", post(Self::reset_handler))
163 }
164}
165
166pub struct LoggingOverrideWorker {
171 handler: LoggingAPIHandler,
172 state: Arc<Mutex<LoggingOverrideWorkerState>>,
173}
174
175struct LoggingOverrideWorkerState {
176 reload_handle: Handle<Targets, Registry>,
177 rx: mpsc::Receiver<LoggingOverrideAction>,
178}
179
180impl LoggingOverrideWorker {
181 pub(super) fn new(reload_handle: Handle<Targets, Registry>) -> (Self, LoggingOverrideController) {
187 let (tx, rx) = mpsc::channel(1);
188 let controller = LoggingOverrideController { tx };
189 let handler = LoggingAPIHandler::new(controller.clone());
190 let worker = Self {
191 handler,
192 state: Arc::new(Mutex::new(LoggingOverrideWorkerState { reload_handle, rx })),
193 };
194
195 (worker, controller)
196 }
197}
198
199#[async_trait]
200impl Supervisable for LoggingOverrideWorker {
201 fn name(&self) -> &str {
202 "level-override-processor"
203 }
204
205 async fn initialize(&self, process_shutdown: ShutdownHandle) -> Result<SupervisorFuture, InitializationError> {
206 let mut state = self.state.clone().lock_owned().await;
207 let logging_route = DynamicRoute::http(EndpointType::Privileged, &self.handler);
208
209 Ok(Box::pin(async move {
210 DataspaceRegistry::try_current()
211 .ok_or_else(|| generic_error!("Dataspace not available."))?
212 .assert(logging_route, "logging-api");
213
214 process_override_actions(&mut state, process_shutdown).await;
215 Ok(())
216 }))
217 }
218}
219
220async fn process_override_actions(state: &mut LoggingOverrideWorkerState, process_shutdown: ShutdownHandle) {
221 let mut base_filter = match state.reload_handle.clone_current() {
225 Some(filter) => filter,
226 None => {
227 error!("Logging subsystem is in an indeterminate state; dynamic log filtering will not be available.");
228
229 process_shutdown.await;
230 return;
231 }
232 };
233
234 let mut override_active = false;
235 let override_timeout = sleep(Duration::from_secs(3600));
236
237 pin!(override_timeout, process_shutdown);
238
239 loop {
240 select! {
241 _ = &mut process_shutdown => break,
242 maybe_action = state.rx.recv() => match maybe_action {
243 Some(LoggingOverrideAction::Override { duration, filter }) => {
244 info!(directives = %filter, "Overriding existing log filtering directives for {} seconds...", duration.as_secs());
245
246 match state.reload_handle.reload(filter) {
247 Ok(()) => {
248 override_active = true;
251 override_timeout.as_mut().reset(tokio::time::Instant::now() + duration);
252 },
253 Err(e) => error!(error = %e, "Failed to override log filtering directives."),
254 }
255 },
256
257 Some(LoggingOverrideAction::Reset) => {
258 override_timeout.as_mut().reset(tokio::time::Instant::now());
261 },
262
263 Some(LoggingOverrideAction::UpdateBase(new_base)) => {
264 if new_base == base_filter {
267 continue;
268 }
269
270 base_filter = new_base;
271
272 if override_active {
273 info!(
276 directives = %base_filter,
277 "Updated base log filtering directives; application deferred until active override expires."
278 );
279 } else {
280 let new_directives = base_filter.to_string();
281 match state.reload_handle.reload(base_filter.clone()) {
282 Ok(()) => info!(directives = %new_directives, "Updated base log filtering directives."),
283 Err(e) => error!(error = %e, "Failed to update base log filtering directives."),
284 }
285 }
286 },
287
288 None => break,
290 },
291 _ = &mut override_timeout => {
292 if override_active {
297 override_active = false;
298
299 let restore_directives = base_filter.to_string();
300 if let Err(e) = state.reload_handle.reload(base_filter.clone()) {
301 error!(error = %e, "Failed to reset log filtering directives.");
302 }
303
304 info!(directives = %restore_directives, "Restored base log filtering directives.");
305 }
306
307 override_timeout.as_mut().reset(tokio::time::Instant::now() + Duration::from_secs(3600));
308 }
309 }
310 }
311
312 if let Err(e) = state.reload_handle.reload(base_filter) {
314 error!(error = %e, "Failed to reset log filtering directives before override handler shutdown.");
315 }
316}
317
318type LogLevelStream = Pin<Box<dyn Stream<Item = LogLevel> + Send>>;
319
320pub struct DynamicLogLevelWorker {
331 levels: Arc<Mutex<LogLevelStream>>,
332 controller: LoggingOverrideController,
333}
334
335impl DynamicLogLevelWorker {
336 pub fn new<S>(levels: S, controller: LoggingOverrideController) -> Self
338 where
339 S: Stream<Item = LogLevel> + Send + 'static,
340 {
341 Self {
342 levels: Arc::new(Mutex::new(Box::pin(levels))),
343 controller,
344 }
345 }
346}
347
348#[async_trait]
349impl Supervisable for DynamicLogLevelWorker {
350 fn name(&self) -> &str {
351 "dynamic-log-level"
352 }
353
354 async fn initialize(&self, process_shutdown: ShutdownHandle) -> Result<SupervisorFuture, InitializationError> {
355 let mut levels = self.levels.clone().lock_owned().await;
356 let controller = self.controller.clone();
357
358 Ok(Box::pin(async move {
359 pin!(process_shutdown);
360
361 debug!("Dynamic log level worker started.");
362
363 loop {
364 select! {
365 _ = &mut process_shutdown => break,
366 maybe_level = levels.next() => match maybe_level {
367 Some(log_level) => {
368 if let Err(e) = controller.update_base(log_level.as_targets()).await {
369 warn!(error = %e, %log_level, "Failed to apply updated log level.");
370 }
371 }
372 None => {
373 debug!("Log level stream ended. No further log level changes will be applied.");
374 (&mut process_shutdown).await;
375 break;
376 }
377 },
378 }
379 }
380
381 debug!("Dynamic log level worker stopped.");
382
383 Ok(())
384 }))
385 }
386}
387
388#[cfg(test)]
389mod tests {
390 use std::time::Duration;
391
392 use tokio::{sync::mpsc, time::sleep};
393 use tracing_subscriber::{reload, Registry};
394
395 use super::*;
396
397 fn filter(directives: &str) -> Targets {
398 parse_filter_directives(directives).expect("valid filter directives")
399 }
400
401 fn current_filter(handle: &reload::Handle<Targets, Registry>) -> String {
402 handle
403 .clone_current()
404 .expect("reload layer should be alive")
405 .to_string()
406 }
407
408 async fn wait_for_filter(handle: &reload::Handle<Targets, Registry>, expected: &str) {
409 let deadline = tokio::time::Instant::now() + Duration::from_secs(1);
410
411 loop {
412 let actual = current_filter(handle);
413 if actual == expected {
414 return;
415 }
416
417 assert!(
418 tokio::time::Instant::now() < deadline,
419 "filter did not become `{expected}`; last value was `{actual}`"
420 );
421
422 sleep(Duration::from_millis(10)).await;
423 }
424 }
425
426 fn spawn_processor(
427 base_filter: Targets,
428 ) -> (
429 LoggingOverrideController,
430 reload::Handle<Targets, Registry>,
431 tokio::task::JoinHandle<()>,
432 reload::Layer<Targets, Registry>,
433 ) {
434 let (filter_layer, reload_handle) = reload::Layer::new(base_filter);
435 let (tx, rx) = mpsc::channel(1);
436 let controller = LoggingOverrideController { tx };
437 let mut state = LoggingOverrideWorkerState {
438 reload_handle: reload_handle.clone(),
439 rx,
440 };
441 let processor = tokio::spawn(async move {
442 process_override_actions(&mut state, ShutdownHandle::noop()).await;
443 });
444
445 (controller, reload_handle, processor, filter_layer)
448 }
449
450 fn log_level(directives: &str) -> LogLevel {
451 LogLevel::try_from(directives.to_string()).expect("valid log level")
452 }
453
454 fn log_level_stream() -> (mpsc::Sender<LogLevel>, impl Stream<Item = LogLevel> + Send + 'static) {
456 let (tx, rx) = mpsc::channel(4);
457 let levels = futures::stream::unfold(rx, |mut rx| async move { rx.recv().await.map(|level| (level, rx)) });
458 (tx, levels)
459 }
460
461 async fn next_base_update(actions: &mut mpsc::Receiver<LoggingOverrideAction>) -> String {
462 let action = tokio::time::timeout(Duration::from_secs(2), actions.recv())
463 .await
464 .expect("worker should send an action")
465 .expect("worker should still be running");
466 match action {
467 LoggingOverrideAction::UpdateBase(filter) => filter.to_string(),
468 _ => panic!("worker should only update the base filter"),
469 }
470 }
471
472 #[tokio::test]
473 async fn dynamic_log_level_worker_applies_each_level_as_the_base_filter() {
474 let (tx, mut actions) = mpsc::channel(1);
475 let (levels_tx, levels) = log_level_stream();
476 let worker = DynamicLogLevelWorker::new(levels, LoggingOverrideController { tx });
477
478 let (coordinator, shutdown) = ShutdownHandle::paired();
479 let run = tokio::spawn(worker.initialize(shutdown).await.expect("worker initializes"));
480
481 for directives in ["saluki=debug", "saluki=warn"] {
482 levels_tx.send(log_level(directives)).await.unwrap();
483 assert_eq!(next_base_update(&mut actions).await, directives);
484 }
485
486 coordinator.shutdown();
487 run.await.expect("worker task completes").expect("worker exits cleanly");
488 }
489
490 #[tokio::test]
491 async fn dynamic_log_level_worker_waits_for_shutdown_after_the_stream_ends() {
492 let (tx, _actions) = mpsc::channel(1);
493 let worker = DynamicLogLevelWorker::new(futures::stream::empty(), LoggingOverrideController { tx });
494
495 let (coordinator, shutdown) = ShutdownHandle::paired();
496 let mut run = tokio::spawn(worker.initialize(shutdown).await.expect("worker initializes"));
497
498 assert!(tokio::time::timeout(Duration::from_millis(100), &mut run)
501 .await
502 .is_err());
503
504 coordinator.shutdown();
505 run.await.expect("worker task completes").expect("worker exits cleanly");
506 }
507
508 #[tokio::test]
509 async fn dynamic_log_level_worker_keeps_the_stream_across_restarts() {
510 let (tx, mut actions) = mpsc::channel(1);
511 let (levels_tx, levels) = log_level_stream();
512 let worker = DynamicLogLevelWorker::new(levels, LoggingOverrideController { tx });
513
514 for directives in ["saluki=debug", "saluki=warn"] {
516 let (coordinator, shutdown) = ShutdownHandle::paired();
517 let run = tokio::spawn(worker.initialize(shutdown).await.expect("worker initializes"));
518
519 levels_tx.send(log_level(directives)).await.unwrap();
520 assert_eq!(next_base_update(&mut actions).await, directives);
521
522 coordinator.shutdown();
523 run.await.expect("worker task completes").expect("worker exits cleanly");
524 }
525 }
526
527 #[tokio::test]
528 async fn reset_restores_current_base_filter() {
529 let (controller, reload_handle, processor, _filter_layer) = spawn_processor(filter("agent_data_plane=info"));
530
531 controller
532 .override_for(Duration::from_secs(60), filter("hyper=warn"))
533 .await
534 .expect("send override");
535 wait_for_filter(&reload_handle, "hyper=warn").await;
536
537 controller
538 .update_base(filter("agent_data_plane=debug"))
539 .await
540 .expect("send update_base");
541 controller.reset().await.expect("send reset");
542 wait_for_filter(&reload_handle, "agent_data_plane=debug").await;
543
544 drop(controller);
545 processor.await.expect("override processor should exit cleanly");
546 }
547
548 #[tokio::test]
549 async fn override_expiration_restores_current_base_filter() {
550 let (controller, reload_handle, processor, _filter_layer) = spawn_processor(filter("agent_data_plane=info"));
551
552 controller
553 .override_for(Duration::from_millis(100), filter("hyper=warn"))
554 .await
555 .expect("send override");
556 wait_for_filter(&reload_handle, "hyper=warn").await;
557
558 controller
559 .update_base(filter("agent_data_plane=warn"))
560 .await
561 .expect("send update_base");
562 wait_for_filter(&reload_handle, "agent_data_plane=warn").await;
563
564 drop(controller);
565 processor.await.expect("override processor should exit cleanly");
566 }
567
568 #[tokio::test]
569 async fn update_base_applies_immediately_when_no_override_active() {
570 let (controller, reload_handle, processor, _filter_layer) = spawn_processor(filter("agent_data_plane=info"));
571
572 controller
573 .update_base(filter("agent_data_plane=debug"))
574 .await
575 .expect("send update_base");
576 wait_for_filter(&reload_handle, "agent_data_plane=debug").await;
577
578 drop(controller);
579 processor.await.expect("override processor should exit cleanly");
580 }
581
582 #[tokio::test]
583 async fn override_handler_rejects_span_and_field_filters() {
584 let (tx, mut rx) = mpsc::channel(1);
585 let state = LoggingHandlerState {
586 controller: LoggingOverrideController { tx },
587 };
588
589 let response = LoggingAPIHandler::override_handler(
590 State(state),
591 Query(OverrideQueryParams { time_secs: 60 }),
592 "saluki[span]=debug".to_string(),
593 )
594 .await
595 .into_response();
596
597 assert_eq!(response.status(), StatusCode::BAD_REQUEST);
598 assert!(rx.try_recv().is_err(), "a rejected override must not reach the worker");
599 }
600}