@@ -6,10 +6,12 @@ use clap_complete::Shell;
66use paths:: { CwdRelative , Daemon , DaemonAbsPath , sub_path} ;
77use std:: io:: Write as _;
88use tokio:: { net:: UnixListener , runtime:: Builder } ;
9- use tracing_subscriber:: { EnvFilter , fmt, prelude:: * } ;
109
1110use minimald:: server:: { Config , HostKey , Server } ;
1211
12+ mod logging;
13+ use logging:: { DaemonLogger , LogMode } ;
14+
1315#[ cfg( target_os = "linux" ) ]
1416use tokio_vsock:: { VMADDR_CID_ANY , VsockAddr , VsockListener } ;
1517
@@ -347,145 +349,6 @@ fn lock_held(path: &std::path::Path) -> std::io::Result<bool> {
347349 }
348350}
349351
350- /// Installs the microVM's on-volume log appender post-mount, given the log
351- /// directory. Returns the [`VolumeLogRelease`] the Shutdown RPC runs before
352- /// quiescing the volume: it reloads the file layer off and drops the worker
353- /// guard, closing the appender's fd. microVM pid-1 only.
354- ///
355- /// [`VolumeLogRelease`]: minimald::server::VolumeLogRelease
356- type LogActivator = Box <
357- dyn FnOnce ( & std:: path:: Path ) -> Result < minimald:: server:: VolumeLogRelease , MainError > + Send ,
358- > ;
359-
360- /// The daily-rotated, retention-bounded appender both minimald log paths use
361- /// (detached native daemon and microVM pid-1). Files carry a date suffix
362- /// (`minimald.log.2026-07-20`); rotation and pruning are inline (no
363- /// background thread, no partial intermediates), so the volume-log release
364- /// closes the file with a plain guard drop — nothing to join.
365- fn build_log_appender (
366- log_dir : & std:: path:: Path ,
367- ) -> Result < tracing_appender:: rolling:: RollingFileAppender , MainError > {
368- tracing_appender:: rolling:: Builder :: new ( )
369- . rotation ( tracing_appender:: rolling:: Rotation :: DAILY )
370- . filename_prefix ( "minimald.log" )
371- // Two weeks: comfortably past "what happened last week", bounded on
372- // disk.
373- . max_log_files ( 14 )
374- . build ( log_dir)
375- . map_err ( |e| MainError :: IO ( std:: io:: Error :: other ( e) , "building rotating log appender" ) )
376- }
377-
378- /// Install the tracing subscriber. Foreground processes log to stdout; a
379- /// detached native daemon (marked by [`DETACHED_ENV`]) writes only to a
380- /// daily-rotated `<state_dir>/logs/minimald.log`. The microVM's pid-1 minimald
381- /// logs to stdout (serial → host boot.log, for boot diagnosis) *and* to a
382- /// reloadable file layer that starts inert and is pointed at the state volume
383- /// once it mounts (see async_main), so `min bug` can collect the in-VM
384- /// daemon's logs.
385- ///
386- /// Returns `(guard, activator)`: the [`WorkerGuard`] for the native detached
387- /// appender (else `None`), and the microVM's [`LogActivator`] to be invoked
388- /// post-mount (else `None`). The guard must outlive the process — dropping it
389- /// flushes pending records and terminates the appender's worker thread.
390- ///
391- /// [`WorkerGuard`]: tracing_appender::non_blocking::WorkerGuard
392- #[ allow( clippy:: type_complexity) ] // the activator alias is the complexity; one site
393- fn init_tracing (
394- cli : & Cli ,
395- ) -> Result <
396- (
397- Option < tracing_appender:: non_blocking:: WorkerGuard > ,
398- Option < LogActivator > ,
399- ) ,
400- MainError ,
401- > {
402- let filter = EnvFilter :: try_from_default_env ( ) . unwrap_or_else ( |_| {
403- EnvFilter :: new ( "info" )
404- . add_directive ( "topiary=off" . parse ( ) . unwrap ( ) )
405- . add_directive ( "libcgroups=off" . parse ( ) . unwrap ( ) )
406- } ) ;
407-
408- // microVM pid-1: log to the console (serial → host boot.log) *and* to a
409- // reloadable file layer wired up once the state volume mounts (see
410- // async_main). A microVM is never `spawn_detached`'d, so this precedes the
411- // DETACHED check with no overlap.
412- if is_minimal_microvm ( ) {
413- // The file layer starts as `None` (inert — records still reach the
414- // console). `reload` lets async_main swap the on-volume appender in
415- // post-mount and the Shutdown RPC swap it back out pre-quiesce,
416- // without a hand-rolled deferred writer or a process-global hook.
417- let ( file_layer, reload_handle) = tracing_subscriber:: reload:: Layer :: new ( None ) ;
418- tracing_subscriber:: registry ( )
419- . with ( fmt:: layer ( ) . with_writer ( ot:: StdoutWriter :: new) )
420- . with ( file_layer)
421- . with ( filter)
422- . init ( ) ;
423- let activator: LogActivator = Box :: new ( move |log_dir : & std:: path:: Path | {
424- std:: fs:: create_dir_all ( log_dir)
425- . map_err ( |e| MainError :: IO ( e, "creating minimald log directory" ) ) ?;
426- let appender = build_log_appender ( log_dir) ?;
427- // lossy(false): a diagnostic log that drops records under load
428- // answers the wrong question. The cost is backpressure onto
429- // logging threads if the volume wedges — accepted, because the
430- // console layer stays independent and the volume fallback
431- // collects without the daemon.
432- let ( writer, guard) = tracing_appender:: non_blocking:: NonBlockingBuilder :: default ( )
433- . lossy ( false )
434- . finish ( appender) ;
435- reload_handle
436- . modify ( |layer| {
437- * layer = Some ( fmt:: layer ( ) . with_ansi ( false ) . with_writer ( writer) . boxed ( ) ) ;
438- } )
439- . map_err ( |e| MainError :: Other ( format ! ( "installing on-volume log layer: {e}" ) ) ) ?;
440- // The release: reload the file layer back off, then drop the guard
441- // to flush and close the on-volume file.
442- Ok ( minimald:: server:: VolumeLogRelease ( Box :: new ( move || {
443- let _ = reload_handle. modify ( |layer| * layer = None ) ;
444- drop ( guard) ;
445- } ) ) )
446- } ) ;
447- return Ok ( ( None , Some ( activator) ) ) ;
448- }
449-
450- let detached = std:: env:: var_os ( DETACHED_ENV ) . is_some ( ) ;
451- if !detached {
452- tracing_subscriber:: registry ( )
453- . with ( fmt:: layer ( ) . with_writer ( ot:: StdoutWriter :: new) )
454- . with ( filter)
455- . init ( ) ;
456- return Ok ( ( None , None ) ) ;
457- }
458-
459- // Under `<state_dir>/logs/` so `<state_dir>` itself stays
460- // dominated by the sockets, sessions, and providers it already
461- // owns. `create_dir_all` is idempotent — subsequent daemon
462- // starts don't churn.
463- let log_dir = cli
464- . minimal_state_dir ( )
465- . as_utf8_path ( )
466- . as_std_path ( )
467- . join ( "logs" ) ;
468- std:: fs:: create_dir_all ( & log_dir)
469- . map_err ( |e| MainError :: IO ( e, "creating minimald log directory" ) ) ?;
470- let appender = build_log_appender ( & log_dir) ?;
471- // lossy(false): dropped records defeat the whole point of a diagnostic
472- // log, and the native daemon's log volume is nowhere near the bound.
473- let ( writer, guard) = tracing_appender:: non_blocking:: NonBlockingBuilder :: default ( )
474- . lossy ( false )
475- . finish ( appender) ;
476- tracing_subscriber:: registry ( )
477- // ANSI colors only make sense on a terminal; a file logger
478- // just gets noise from the escape sequences.
479- . with ( fmt:: layer ( ) . with_ansi ( false ) . with_writer ( writer) )
480- . with ( filter)
481- . init ( ) ;
482- tracing:: info!(
483- log_dir = %log_dir. display( ) ,
484- "detached minimald: routing tracing output to daily-rotated log file" ,
485- ) ;
486- Ok ( ( Some ( guard) , None ) )
487- }
488-
489352async fn async_main ( ) -> Result < ( ) , MainError > {
490353 // With `networking-proxy` on, both the `ring` (workspace rustls) and the
491354 // `aws-lc-rs` (google-cloud) providers are compiled in, so rustls cannot
@@ -535,16 +398,18 @@ async fn async_main() -> Result<(), MainError> {
535398 return Ok ( ( ) ) ;
536399 }
537400
538- // Initialize tracing. Foreground runs (or the parent-side of a
539- // `--detach` re-exec) log to stdout. A child spawned by
540- // `spawn_detached` has its stdio null'd — detectable via the
541- // `MINIMALD_DETACHED` env var — so it routes tracing to a rotated log
542- // file under the state directory instead; the microVM pid-1 logs to both
543- // console and a file layer wired up below once the state volume mounts.
544- // `_log_guard` is bound at function scope so the non-blocking appender's
545- // worker survives for the daemon's entire lifetime; dropping it
546- // would flush and terminate the appender prematurely.
547- let ( _log_guard, log_activator) = init_tracing ( & cli) ?;
401+ // Install tracing. A foreground run logs to stdout only. A detached
402+ // native daemon (stdio null'd, marked by `MINIMALD_DETACHED`) and the
403+ // microVM pid-1 both log to a daily-rotated file, wired up by
404+ // `logger.activate` once the log directory is final (below): immediately
405+ // for the native daemon, after the state volume mounts for the microVM.
406+ // The activation yields a release the server state runs at shutdown.
407+ let log_mode = if is_minimal_microvm ( ) || std:: env:: var_os ( DETACHED_ENV ) . is_some ( ) {
408+ LogMode :: File
409+ } else {
410+ LogMode :: Console
411+ } ;
412+ let logger = DaemonLogger :: install ( log_mode) ?;
548413
549414 let listen_args = cli. listen_args ( ) . unwrap ( ) ;
550415
@@ -638,35 +503,38 @@ async fn async_main() -> Result<(), MainError> {
638503 return Err ( MainError :: IO ( e, "creating minimal dir" ) ) ;
639504 }
640505
641- // With the state volume mounted and state relocated onto it, point the
642- // microVM's reloadable log layer at `<state>/logs` so the in-VM daemon's
643- // runtime logs land on the persistent volume where `min bug`'s guest
644- // collector reads them. The resulting release is handed to the server
645- // state and run by the Shutdown RPC before the quiesce: the appender's
646- // write-open fd would otherwise keep the volume busy and defeat the clean
647- // unmount, leaving a dirty ext4 journal on every stop. A failure here must
648- // not wedge pid-1, so fall back to console-only.
649- let mut volume_log_release: Option < minimald:: server:: VolumeLogRelease > = None ;
650- if let Some ( activate) = log_activator {
651- let log_dir = cli
652- . minimal_state_dir ( )
653- . as_utf8_path ( )
654- . as_std_path ( )
655- . join ( "logs" ) ;
656- match activate ( & log_dir) {
657- Ok ( release) => {
658- volume_log_release = Some ( release) ;
506+ // The log directory is now final under `<state>/logs` — the native
507+ // daemon's from the start, the microVM's now that the state volume is
508+ // mounted and state relocated onto it, where `min bug`'s guest collector
509+ // reads it. Point the file log at it; the release is handed to the server
510+ // state and run at shutdown (in the microVM, before the quiesce — the
511+ // appender's write-open fd would otherwise hold the volume busy and
512+ // defeat the clean unmount). A foreground run's logger has no file and
513+ // yields `None`. A failure here must not wedge the daemon (pid-1 in the
514+ // microVM), so fall back to console-only.
515+ let log_dir = cli
516+ . minimal_state_dir ( )
517+ . as_utf8_path ( )
518+ . as_std_path ( )
519+ . join ( "logs" ) ;
520+ let log_release = match logger. activate ( & log_dir) {
521+ Ok ( release) => {
522+ if release. is_some ( ) {
659523 tracing:: info!(
660524 log_dir = %log_dir. display( ) ,
661- "microVM minimald: routing tracing output to daily-rotated log file on the data volume " ,
525+ "routing tracing output to daily-rotated log file" ,
662526 ) ;
663527 }
664- Err ( e) => tracing:: warn!(
528+ release
529+ }
530+ Err ( e) => {
531+ tracing:: warn!(
665532 error = ?e,
666- "microVM minimald: could not open the on-volume log file; continuing with console logging only" ,
667- ) ,
533+ "could not open the daemon log file; continuing with console logging only" ,
534+ ) ;
535+ None
668536 }
669- }
537+ } ;
670538
671539 // The host-key path lives under the instance dir; ensure it exists for
672540 // both the UDS and vsock paths.
@@ -834,9 +702,7 @@ async fn async_main() -> Result<(), MainError> {
834702 ) ;
835703 // TODO: When we have a daemonize command, daemonize here.
836704
837- // A UDS (native/DM2) daemon is never the microVM pid-1, so there is no
838- // on-volume log to release.
839- Server :: run ( config, listener, None )
705+ Server :: run ( config, listener, log_release)
840706 . await
841707 . map_err ( |e| MainError :: IO ( e, "serving on UDS" ) )
842708 } else {
@@ -867,7 +733,7 @@ async fn async_main() -> Result<(), MainError> {
867733 }
868734 } ;
869735
870- Server :: run ( config, listener, volume_log_release )
736+ Server :: run ( config, listener, log_release )
871737 . await
872738 . map_err ( |e| MainError :: IO ( e, "serving on guest vsock" ) )
873739 }
0 commit comments