4646
4747use crate :: error:: { Error , Result } ;
4848use crate :: platform:: PlatformInfo ;
49+ use crate :: tracing_compat:: { debug, error, info, trace, warn} ;
4950use async_trait:: async_trait;
5051use std:: collections:: HashMap ;
5152use std:: ffi:: OsStr ;
5253use std:: path:: PathBuf ;
5354use std:: process:: Stdio ;
5455use std:: time:: Duration ;
5556use tokio:: process:: Command as TokioCommand ;
56- use tracing:: { debug, error, instrument, trace, warn} ;
5757
5858// Re-export all command modules
5959pub mod attach;
@@ -471,6 +471,28 @@ pub trait ComposeCommand: DockerCommand {
471471/// Default timeout for command execution (30 seconds)
472472pub const DEFAULT_COMMAND_TIMEOUT : Duration = Duration :: from_secs ( 30 ) ;
473473
474+ /// Maximum length (in bytes) of stderr snippets attached to tracing events.
475+ /// Bounded so that log sinks don't drown in large error payloads.
476+ const STDERR_LOG_SNIPPET_BYTES : usize = 512 ;
477+
478+ /// Truncate `s` to at most `STDERR_LOG_SNIPPET_BYTES`, respecting UTF-8 char
479+ /// boundaries and appending an ellipsis marker when truncation occurs.
480+ #[ cfg_attr( not( feature = "tracing" ) , allow( dead_code) ) ]
481+ fn truncate_for_log ( s : & str ) -> String {
482+ if s. len ( ) <= STDERR_LOG_SNIPPET_BYTES {
483+ return s. to_string ( ) ;
484+ }
485+ // Walk back to a char boundary <= limit.
486+ let mut end = STDERR_LOG_SNIPPET_BYTES ;
487+ while end > 0 && !s. is_char_boundary ( end) {
488+ end -= 1 ;
489+ }
490+ let mut out = String :: with_capacity ( end + 4 ) ;
491+ out. push_str ( & s[ ..end] ) ;
492+ out. push_str ( "..." ) ;
493+ out
494+ }
495+
474496/// Common functionality for executing Docker commands
475497#[ derive( Debug , Clone ) ]
476498pub struct CommandExecutor {
@@ -540,20 +562,41 @@ impl CommandExecutor {
540562 }
541563 }
542564
565+ /// Get the runtime label suitable for a tracing field (e.g. "docker",
566+ /// "podman"). Returns `None` when no platform has been detected.
567+ #[ cfg_attr( not( feature = "tracing" ) , allow( dead_code) ) ]
568+ fn tracing_platform ( & self ) -> Option < & ' static str > {
569+ use crate :: platform:: Runtime ;
570+ let runtime = self . platform_info . as_ref ( ) . map ( |p| & p. runtime ) ?;
571+ Some ( match runtime {
572+ Runtime :: Docker | Runtime :: DockerDesktop => "docker" ,
573+ Runtime :: Podman => "podman" ,
574+ Runtime :: Colima => "colima" ,
575+ Runtime :: RancherDesktop => "rancher-desktop" ,
576+ Runtime :: OrbStack => "orbstack" ,
577+ } )
578+ }
579+
543580 /// Execute a Docker command with the given arguments
544581 ///
545582 /// # Errors
546583 /// Returns an error if the Docker command fails to execute, returns a non-zero exit code,
547584 /// or times out (if a timeout is configured)
548- #[ instrument(
549- name = "docker.command" ,
550- skip( self , args) ,
551- fields(
552- command = %command_name,
553- runtime = %self . get_runtime_command( ) ,
554- timeout_secs = self . timeout. map( |t| t. as_secs( ) ) ,
585+ #[ cfg_attr(
586+ feature = "tracing" ,
587+ tracing:: instrument(
588+ name = "docker.command" ,
589+ skip( self , args) ,
590+ fields(
591+ command = %command_name,
592+ args_count = args. len( ) ,
593+ platform = self . tracing_platform( ) ,
594+ runtime = %self . get_runtime_command( ) ,
595+ timeout_secs = self . timeout. map( |t| t. as_secs( ) ) ,
596+ )
555597 )
556598 ) ]
599+ #[ cfg_attr( not( feature = "tracing" ) , allow( unused_variables) ) ]
557600 pub async fn execute_command (
558601 & self ,
559602 command_name : & str ,
@@ -570,6 +613,8 @@ impl CommandExecutor {
570613
571614 trace ! ( args = ?all_args, "executing docker command" ) ;
572615
616+ let started_at = std:: time:: Instant :: now ( ) ;
617+
573618 // Execute with or without timeout
574619 let result = if let Some ( timeout_duration) = self . timeout {
575620 self . execute_with_timeout ( & runtime_command, & all_args, timeout_duration)
@@ -578,33 +623,52 @@ impl CommandExecutor {
578623 self . execute_internal ( & runtime_command, & all_args) . await
579624 } ;
580625
626+ let duration_ms = u64:: try_from ( started_at. elapsed ( ) . as_millis ( ) ) . unwrap_or ( u64:: MAX ) ;
627+
581628 match & result {
582629 Ok ( output) => {
583- debug ! (
630+ info ! (
584631 exit_code = output. exit_code,
632+ duration_ms = duration_ms,
585633 stdout_len = output. stdout. len( ) ,
586634 stderr_len = output. stderr. len( ) ,
587- "command completed successfully "
635+ "command completed"
588636 ) ;
589637 trace ! ( stdout = %output. stdout, "command stdout" ) ;
590638 if !output. stderr . is_empty ( ) {
591639 trace ! ( stderr = %output. stderr, "command stderr" ) ;
592640 }
593641 }
594642 Err ( e) => {
595- error ! ( error = %e, "command failed" ) ;
643+ let ( exit_code, stderr_snippet) = match e {
644+ Error :: CommandFailed {
645+ exit_code, stderr, ..
646+ } => ( Some ( * exit_code) , Some ( truncate_for_log ( stderr) ) ) ,
647+ _ => ( None , None ) ,
648+ } ;
649+ warn ! (
650+ command = %command_name,
651+ exit_code = exit_code,
652+ duration_ms = duration_ms,
653+ stderr_snippet = stderr_snippet. as_deref( ) ,
654+ error = %e,
655+ "command failed"
656+ ) ;
596657 }
597658 }
598659
599660 result
600661 }
601662
602663 /// Internal method to execute a command without timeout
603- #[ instrument(
604- name = "docker.process" ,
605- skip( self , all_args) ,
606- fields(
607- full_command = %format!( "{} {}" , runtime_command, all_args. join( " " ) ) ,
664+ #[ cfg_attr(
665+ feature = "tracing" ,
666+ tracing:: instrument(
667+ name = "docker.process" ,
668+ skip( self , all_args) ,
669+ fields(
670+ full_command = %format!( "{} {}" , runtime_command, all_args. join( " " ) ) ,
671+ )
608672 )
609673 ) ]
610674 async fn execute_internal (
@@ -675,10 +739,13 @@ impl CommandExecutor {
675739 }
676740
677741 /// Execute a command with a timeout
678- #[ instrument(
679- name = "docker.timeout" ,
680- skip( self , all_args) ,
681- fields( timeout_secs = timeout_duration. as_secs( ) )
742+ #[ cfg_attr(
743+ feature = "tracing" ,
744+ tracing:: instrument(
745+ name = "docker.timeout" ,
746+ skip( self , all_args) ,
747+ fields( timeout_secs = timeout_duration. as_secs( ) )
748+ )
682749 ) ]
683750 async fn execute_with_timeout (
684751 & self ,
0 commit comments