@@ -213,9 +213,38 @@ enum Command {
213213 } ,
214214}
215215
216- fn main ( ) -> Result < ( ) > {
216+ /// Append a fatal error to the runc-compatible `--log` file so containerd can
217+ /// surface the real failure reason. Never panics: if the file cannot be opened
218+ /// the error is dropped here (it is still printed to stderr by the caller).
219+ fn write_error_log ( path : & std:: path:: Path , format : LogFormat , err : & anyhow:: Error ) {
220+ use std:: io:: Write ;
221+ // `{:#}` renders the full anyhow context chain on one line.
222+ let msg = format ! ( "{:#}" , err) ;
223+ let line = match format {
224+ // serde_json handles correct escaping of arbitrary message text.
225+ LogFormat :: Json => serde_json:: json!( { "level" : "error" , "msg" : msg } ) . to_string ( ) ,
226+ LogFormat :: Text => msg,
227+ } ;
228+ if let Ok ( mut f) = std:: fs:: OpenOptions :: new ( ) . create ( true ) . append ( true ) . open ( path) {
229+ let _ = writeln ! ( f, "{}" , line) ;
230+ }
231+ }
232+
233+ fn main ( ) {
217234 let cli = Cli :: parse ( ) ;
235+ // Capture the log destination before `cli.command` is consumed by `run`.
236+ let log = cli. log . clone ( ) ;
237+ let log_format = cli. log_format ;
238+ if let Err ( err) = run ( cli) {
239+ if let Some ( path) = log. as_deref ( ) {
240+ write_error_log ( path, log_format, & err) ;
241+ }
242+ eprintln ! ( "Error: {:#}" , err) ;
243+ std:: process:: exit ( 1 ) ;
244+ }
245+ }
218246
247+ fn run ( cli : Cli ) -> Result < ( ) > {
219248 // Resolve the state-dir root once, before any state I/O or fork, so the
220249 // supervisor child inherits the same location.
221250 state:: init_state_dir ( cli. root . as_deref ( ) . and_then ( |p| p. to_str ( ) ) ) ;
@@ -955,6 +984,29 @@ mod tests {
955984 assert_eq ! ( explicit. rootless, Some ( false ) ) ;
956985 }
957986
987+ #[ test]
988+ fn error_log_json_is_parseable_with_msg ( ) {
989+ let dir = tempfile:: tempdir ( ) . unwrap ( ) ;
990+ let path = dir. path ( ) . join ( "log.json" ) ;
991+ let err = anyhow:: anyhow!( "boom" ) . context ( "creating sandbox" ) ;
992+ write_error_log ( & path, LogFormat :: Json , & err) ;
993+ let content = std:: fs:: read_to_string ( & path) . unwrap ( ) ;
994+ let v: serde_json:: Value = serde_json:: from_str ( content. trim ( ) ) . unwrap ( ) ;
995+ assert_eq ! ( v[ "level" ] , "error" ) ;
996+ let msg = v[ "msg" ] . as_str ( ) . unwrap ( ) ;
997+ assert ! ( msg. contains( "creating sandbox" ) , "msg was: {msg}" ) ;
998+ assert ! ( msg. contains( "boom" ) , "msg was: {msg}" ) ;
999+ }
1000+
1001+ #[ test]
1002+ fn error_log_text_is_plain_message ( ) {
1003+ let dir = tempfile:: tempdir ( ) . unwrap ( ) ;
1004+ let path = dir. path ( ) . join ( "log.txt" ) ;
1005+ write_error_log ( & path, LogFormat :: Text , & anyhow:: anyhow!( "plain failure" ) ) ;
1006+ let content = std:: fs:: read_to_string ( & path) . unwrap ( ) ;
1007+ assert_eq ! ( content. trim( ) , "plain failure" ) ;
1008+ }
1009+
9581010 #[ test]
9591011 fn parse_signal_numeric ( ) {
9601012 assert_eq ! ( parse_signal( "15" ) . unwrap( ) , libc:: SIGTERM ) ;
0 commit comments