Skip to content

Commit 84f28ba

Browse files
WVerlaekona-agent
andcommitted
Add OpenTelemetry tracing for individual Go tests
Parse go test -json output during the test phase to create spans for each test. Test spans are children of the package span, showing test parallelism and duration in build traces. Co-authored-by: Ona <no-reply@ona.com>
1 parent 8206b4e commit 84f28ba

File tree

4 files changed

+734
-1
lines changed

4 files changed

+734
-1
lines changed

pkg/leeway/build.go

Lines changed: 55 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1362,7 +1362,13 @@ func executeBuildPhase(buildctx *buildContext, p *Package, builddir string, bld
13621362

13631363
log.WithField("phase", phase).WithField("package", p.FullName()).WithField("commands", bld.Commands[phase]).Debug("running commands")
13641364

1365-
err := executeCommandsForPackage(buildctx, p, builddir, cmds)
1365+
var err error
1366+
// Use TestExecutor for test phase if available (enables per-test tracing)
1367+
if phase == PackageBuildPhaseTest && bld.TestExecutor != nil {
1368+
err = bld.TestExecutor(buildctx, p, builddir)
1369+
} else {
1370+
err = executeCommandsForPackage(buildctx, p, builddir, cmds)
1371+
}
13661372
pkgRep.phaseDone[phase] = time.Now()
13671373

13681374
return err
@@ -1521,6 +1527,11 @@ type packageBuild struct {
15211527
// It's used for post-build processing that needs to happen regardless of provenance settings,
15221528
// such as Docker image extraction.
15231529
PostProcess func(buildCtx *buildContext, pkg *Package, buildDir string) error
1530+
1531+
// TestExecutor is an optional function that executes tests with tracing support.
1532+
// If set, it will be used instead of the standard command execution for the test phase.
1533+
// This allows Go packages to create spans for individual tests.
1534+
TestExecutor func(buildctx *buildContext, p *Package, builddir string) error
15241535
}
15251536

15261537
type testCoverageFunc func() (coverage, funcsWithoutTest, funcsWithTest int, err error)
@@ -2103,6 +2114,7 @@ func (p *Package) buildGo(buildctx *buildContext, wd, result string) (res *packa
21032114
}
21042115
}
21052116
var reportCoverage testCoverageFunc
2117+
var testExecutor func(buildctx *buildContext, p *Package, builddir string) error
21062118
if !cfg.DontTest && !buildctx.DontTest {
21072119
testCommand := []string{goCommand, "test"}
21082120
if log.IsLevelEnabled(log.DebugLevel) {
@@ -2122,6 +2134,9 @@ func (p *Package) buildGo(buildctx *buildContext, wd, result string) (res *packa
21222134
testCommand = append(testCommand, "./...")
21232135

21242136
commands[PackageBuildPhaseTest] = append(commands[PackageBuildPhaseTest], testCommand)
2137+
2138+
// Create test executor for tracing individual tests
2139+
testExecutor = createGoTestExecutor(testCommand)
21252140
}
21262141

21272142
var buildCmd []string
@@ -2160,9 +2175,48 @@ func (p *Package) buildGo(buildctx *buildContext, wd, result string) (res *packa
21602175
return &packageBuild{
21612176
Commands: commands,
21622177
TestCoverage: reportCoverage,
2178+
TestExecutor: testExecutor,
21632179
}, nil
21642180
}
21652181

2182+
// createGoTestExecutor creates a test executor function that runs go test with JSON output
2183+
// and creates OpenTelemetry spans for each individual test when a TestTracingReporter is available.
2184+
func createGoTestExecutor(testCommand []string) func(buildctx *buildContext, p *Package, builddir string) error {
2185+
return func(buildctx *buildContext, p *Package, builddir string) error {
2186+
// Check if the reporter supports test tracing
2187+
tracingReporter, ok := buildctx.Reporter.(TestTracingReporter)
2188+
if !ok {
2189+
// Fall back to standard execution without tracing
2190+
return executeCommandsForPackage(buildctx, p, builddir, [][]string{testCommand})
2191+
}
2192+
2193+
// Get the test tracer from the reporter
2194+
tracer := tracingReporter.GetGoTestTracer(p)
2195+
if tracer == nil {
2196+
// Tracer not available, fall back to standard execution
2197+
return executeCommandsForPackage(buildctx, p, builddir, [][]string{testCommand})
2198+
}
2199+
2200+
// Set up environment
2201+
env := append(os.Environ(), p.Environment...)
2202+
env = append(env, fmt.Sprintf("%s=%s", EnvvarWorkspaceRoot, p.C.W.Origin))
2203+
2204+
// Export SOURCE_DATE_EPOCH for reproducible builds
2205+
mtime, err := p.getDeterministicMtime()
2206+
if err == nil {
2207+
env = append(env, fmt.Sprintf("SOURCE_DATE_EPOCH=%d", mtime))
2208+
}
2209+
env = append(env, "DOCKER_BUILDKIT=1")
2210+
2211+
// Create output writer that reports to the build reporter
2212+
outputWriter := &reporterStream{R: buildctx.Reporter, P: p, IsErr: false}
2213+
2214+
// Run go test with JSON output and create spans for each test
2215+
log.WithField("package", p.FullName()).WithField("command", strings.Join(testCommand, " ")).Debug("running go test with tracing")
2216+
return tracer.RunGoTest(context.Background(), env, builddir, testCommand, outputWriter)
2217+
}
2218+
}
2219+
21662220
func collectGoTestCoverage(covfile string) testCoverageFunc {
21672221
return func() (coverage, funcsWithoutTest, funcsWithTest int, err error) {
21682222
// We need to collect the coverage for all packages in the module.

0 commit comments

Comments
 (0)