Skip to content

Commit 05d3912

Browse files
authored
Merge pull request #321 from gitpod-io/wv/go-test-tracing
Add OpenTelemetry tracing for individual Go tests
2 parents e0cc628 + 7813aba commit 05d3912

5 files changed

Lines changed: 821 additions & 7 deletions

File tree

cmd/build.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -229,6 +229,7 @@ func addBuildFlags(cmd *cobra.Command) {
229229
cmd.Flags().UintP("max-concurrent-tasks", "j", uint(cpus), "Limit the number of max concurrent build tasks - set to 0 to disable the limit")
230230
cmd.Flags().String("coverage-output-path", "", "Output path where test coverage file will be copied after running tests")
231231
cmd.Flags().Bool("disable-coverage", false, "Disable test coverage collection (defaults to false)")
232+
cmd.Flags().Bool("enable-test-tracing", false, "Enable per-test OpenTelemetry span creation (defaults to false)")
232233
cmd.Flags().StringToString("docker-build-options", nil, "Options passed to all 'docker build' commands")
233234
cmd.Flags().Bool("slsa-cache-verification", false, "Enable SLSA verification for cached artifacts")
234235
cmd.Flags().String("slsa-source-uri", "", "Expected source URI for SLSA verification (required when verification enabled)")
@@ -417,6 +418,7 @@ func getBuildOpts(cmd *cobra.Command) ([]leeway.BuildOption, cache.LocalCache, C
417418
}
418419

419420
disableCoverage, _ := cmd.Flags().GetBool("disable-coverage")
421+
enableTestTracing, _ := cmd.Flags().GetBool("enable-test-tracing")
420422

421423
var dockerBuildOptions leeway.DockerBuildOptions
422424
dockerBuildOptions, err = cmd.Flags().GetStringToString("docker-build-options")
@@ -482,6 +484,7 @@ func getBuildOpts(cmd *cobra.Command) ([]leeway.BuildOption, cache.LocalCache, C
482484
leeway.WithCompressionDisabled(dontCompress),
483485
leeway.WithFixedBuildDir(fixedBuildDir),
484486
leeway.WithDisableCoverage(disableCoverage),
487+
leeway.WithEnableTestTracing(enableTestTracing),
485488
leeway.WithInFlightChecksums(inFlightChecksums),
486489
leeway.WithDockerExportToCache(dockerExportToCache, dockerExportSet),
487490
leeway.WithDockerExportEnv(dockerExportEnvValue, dockerExportEnvSet),

pkg/leeway/build.go

Lines changed: 89 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -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
608618
func 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

30843166
type reporterStream struct {

0 commit comments

Comments
 (0)