@@ -25,6 +25,7 @@ import (
2525 "github.com/in-toto/in-toto-golang/in_toto"
2626 "github.com/in-toto/in-toto-golang/in_toto/slsa_provenance/common"
2727 log "github.com/sirupsen/logrus"
28+ "go.opentelemetry.io/otel/trace"
2829 "golang.org/x/mod/modfile"
2930 "golang.org/x/sync/errgroup"
3031 "golang.org/x/sync/semaphore"
@@ -480,6 +481,7 @@ type buildOptions struct {
480481 JailedExecution bool
481482 UseFixedBuildDir bool
482483 DisableCoverage bool
484+ EnableTestTracing bool
483485 InFlightChecksums bool
484486 DockerExportToCache bool
485487 DockerExportSet bool // Track if explicitly set via CLI flag
@@ -604,6 +606,14 @@ func WithDisableCoverage(disableCoverage bool) BuildOption {
604606 }
605607}
606608
609+ // WithEnableTestTracing enables per-test OpenTelemetry span creation during Go test execution
610+ func WithEnableTestTracing (enable bool ) BuildOption {
611+ return func (opts * buildOptions ) error {
612+ opts .EnableTestTracing = enable
613+ return nil
614+ }
615+ }
616+
607617// WithInFlightChecksums enables checksumming of cache artifacts to prevent TOCTU attacks
608618func WithInFlightChecksums (enabled bool ) BuildOption {
609619 return func (opts * buildOptions ) error {
@@ -3056,29 +3066,101 @@ func executeCommandsForPackage(buildctx *buildContext, p *Package, wd string, co
30563066 if len (cmd ) == 0 {
30573067 continue // Skip empty commands
30583068 }
3059- err := run (buildctx . Reporter , p , env , wd , cmd [0 ], cmd [1 :]... )
3069+ err := run (buildctx , p , env , wd , cmd [0 ], cmd [1 :]... )
30603070 if err != nil {
30613071 return err
30623072 }
30633073 }
30643074 return nil
30653075}
30663076
3067- func run (rep Reporter , p * Package , env []string , cwd , name string , args ... string ) error {
3077+ // isGoTestCommand checks if the command is a "go test" invocation
3078+ func isGoTestCommand (name string , args []string ) bool {
3079+ if name != "go" {
3080+ return false
3081+ }
3082+ for _ , arg := range args {
3083+ if arg == "test" {
3084+ return true
3085+ }
3086+ // Stop at first non-flag argument that isn't "test"
3087+ if ! strings .HasPrefix (arg , "-" ) {
3088+ return false
3089+ }
3090+ }
3091+ return false
3092+ }
3093+
3094+ func run (buildctx * buildContext , p * Package , env []string , cwd , name string , args ... string ) error {
30683095 log .WithField ("package" , p .FullName ()).WithField ("command" , strings .Join (append ([]string {name }, args ... ), " " )).Debug ("running" )
30693096
3097+ // Check if this is a go test command and tracing is enabled
3098+ if buildctx != nil && buildctx .EnableTestTracing && isGoTestCommand (name , args ) {
3099+ if otelRep , ok := findOTelReporter (buildctx .Reporter ); ok {
3100+ if ctx := otelRep .GetPackageContext (p ); ctx != nil {
3101+ return runGoTestWithTracing (buildctx , p , env , cwd , name , args , otelRep .GetTracer (), ctx )
3102+ }
3103+ }
3104+ }
3105+
3106+ // Standard command execution
30703107 cmd := exec .Command (name , args ... )
3071- cmd .Stdout = & reporterStream {R : rep , P : p , IsErr : false }
3072- cmd .Stderr = & reporterStream {R : rep , P : p , IsErr : true }
3108+ if buildctx != nil {
3109+ cmd .Stdout = & reporterStream {R : buildctx .Reporter , P : p , IsErr : false }
3110+ cmd .Stderr = & reporterStream {R : buildctx .Reporter , P : p , IsErr : true }
3111+ }
30733112 cmd .Dir = cwd
30743113 cmd .Env = env
3075- err := cmd .Run ()
30763114
3115+ return cmd .Run ()
3116+ }
3117+
3118+ // findOTelReporter finds an OTelReporter in the reporter chain
3119+ func findOTelReporter (rep Reporter ) (* OTelReporter , bool ) {
3120+ switch r := rep .(type ) {
3121+ case * OTelReporter :
3122+ return r , true
3123+ case CompositeReporter :
3124+ for _ , inner := range r {
3125+ if otel , ok := findOTelReporter (inner ); ok {
3126+ return otel , ok
3127+ }
3128+ }
3129+ }
3130+ return nil , false
3131+ }
3132+
3133+ // runGoTestWithTracing runs go test with JSON output and creates spans for each test
3134+ func runGoTestWithTracing (buildctx * buildContext , p * Package , env []string , cwd , name string , args []string , tracer trace.Tracer , parentCtx context.Context ) error {
3135+ // Build command with -json flag
3136+ fullArgs := append ([]string {name }, args ... )
3137+ jsonArgs := ensureJSONFlag (fullArgs )
3138+
3139+ log .WithField ("package" , p .FullName ()).WithField ("command" , strings .Join (jsonArgs , " " )).Debug ("running go test with tracing" )
3140+
3141+ cmd := exec .Command (jsonArgs [0 ], jsonArgs [1 :]... )
3142+ cmd .Dir = cwd
3143+ cmd .Env = env
3144+
3145+ stdout , err := cmd .StdoutPipe ()
30773146 if err != nil {
3078- return err
3147+ return fmt . Errorf ( "failed to create stdout pipe: %w" , err )
30793148 }
3149+ cmd .Stderr = & reporterStream {R : buildctx .Reporter , P : p , IsErr : true }
30803150
3081- return nil
3151+ if err := cmd .Start (); err != nil {
3152+ return fmt .Errorf ("failed to start go test: %w" , err )
3153+ }
3154+
3155+ // Create tracer and parse output
3156+ goTracer := NewGoTestTracer (tracer , parentCtx )
3157+ outputWriter := & reporterStream {R : buildctx .Reporter , P : p , IsErr : false }
3158+
3159+ if err := goTracer .parseJSONOutput (stdout , outputWriter ); err != nil {
3160+ log .WithError (err ).Warn ("error parsing go test JSON output" )
3161+ }
3162+
3163+ return cmd .Wait ()
30823164}
30833165
30843166type reporterStream struct {
0 commit comments