diff --git a/Makefile b/Makefile index 01135f635..a094517b3 100644 --- a/Makefile +++ b/Makefile @@ -110,11 +110,6 @@ test-junit: generate ebpf test-deps go install gotest.tools/gotestsum@latest CGO_ENABLED=1 gotestsum --junitfile $(JUNIT_OUT_DIR)/junit.xml -- $(GO_FLAGS) -tags $(GO_TAGS) ./... -# This target isn't called from CI, it doesn't work for cross compile (ie TARGET_ARCH=arm64 on -# amd64) and the CI kernel tests run them already. Useful for local testing. -sudo-golabels-test: integration-test-binaries - (cd support && sudo ./interpreter_golabels_test.test -test.v) - TESTDATA_DIRS:= \ nativeunwind/elfunwindinfo/testdata \ libpf/pfelf/testdata \ @@ -125,19 +120,19 @@ test-deps: ($(MAKE) -C "$(testdata_dir)") || exit ; \ ) -TEST_INTEGRATION_BINARY_DIRS := tracer processmanager/ebpf support interpreter/golabels/test +TEST_INTEGRATION_BINARY_DIRS := tracer processmanager/ebpf support # These binaries are named ".test" to get included into bluebox initramfs -support/golbls_1_23.test: ./interpreter/golabels/test/main.go - CGO_ENABLED=0 GOTOOLCHAIN=go1.23.7 go build -tags $(GO_TAGS),nocgo -o $@ $< +support/golbls_1_23.test: generate ebpf + CGO_ENABLED=0 GOTOOLCHAIN=go1.23.7 go test -C ./interpreter/golabels/integrationtests -c -trimpath -tags $(GO_TAGS),nocgo,integration -o $@ -support/golbls_1_24.test: ./interpreter/golabels/test/main.go - CGO_ENABLED=0 GOTOOLCHAIN=go1.24.1 go build -tags $(GO_TAGS),nocgo -o $@ $< +support/golbls_1_24.test: generate ebpf + CGO_ENABLED=0 GOTOOLCHAIN=go1.24.1 go test -C ./interpreter/golabels/integrationtests -c -trimpath -tags $(GO_TAGS),nocgo,integration -o $@ -support/golbls_cgo.test: ./interpreter/golabels/test/main-cgo.go - CGO_ENABLED=1 GOTOOLCHAIN=go1.24.1 go build -ldflags '-extldflags "-static"' -tags $(GO_TAGS),usecgo -o $@ $< +support/golbls_cgo.test: generate ebpf + CGO_ENABLED=1 GOTOOLCHAIN=go1.24.1 go test -C ./interpreter/golabels/integrationtests -c -ldflags '-extldflags "-static"' -trimpath -tags $(GO_TAGS),withcgo,integration -o $@ -integration-test-binaries: generate ebpf support/golbls_1_23.test support/golbls_1_24.test support/golbls_cgo.test +integration-test-binaries: support/golbls_1_23.test support/golbls_1_24.test support/golbls_cgo.test $(foreach test_name, $(TEST_INTEGRATION_BINARY_DIRS), \ (go test -ldflags='-extldflags=-static' -trimpath -c \ -tags $(GO_TAGS),static_build,integration \ diff --git a/interpreter/golabels/integrationtests/busy_nocgo.go b/interpreter/golabels/integrationtests/busy_nocgo.go new file mode 100644 index 000000000..2f84f753a --- /dev/null +++ b/interpreter/golabels/integrationtests/busy_nocgo.go @@ -0,0 +1,10 @@ +//go:build nocgo && linux + +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +package integrationtests // import "go.opentelemetry.io/ebpf-profiler/interpreter/golabels/integrationtests" + +//go:noinline +func busyFunc() { +} diff --git a/interpreter/golabels/integrationtests/busy_other.go b/interpreter/golabels/integrationtests/busy_other.go new file mode 100644 index 000000000..f48e1bdd1 --- /dev/null +++ b/interpreter/golabels/integrationtests/busy_other.go @@ -0,0 +1,11 @@ +//go:build !nocgo && !withcgo + +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +//nolint:lll +package integrationtests // import "go.opentelemetry.io/ebpf-profiler/interpreter/golabels/integrationtests" + +//go:noinline +func busyFunc() { +} diff --git a/interpreter/golabels/integrationtests/busy_withcgo.go b/interpreter/golabels/integrationtests/busy_withcgo.go new file mode 100644 index 000000000..cd0c466d1 --- /dev/null +++ b/interpreter/golabels/integrationtests/busy_withcgo.go @@ -0,0 +1,23 @@ +//go:build withcgo && linux + +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +package integrationtests // import "go.opentelemetry.io/ebpf-profiler/interpreter/golabels/integrationtests" + +/* +#include + +void cgofunc() { + volatile int counter = 0; + while (counter < 1000000) { + counter++; + } +} +*/ +import "C" + +//go:noinline +func busyFunc() { + C.cgofunc() +} diff --git a/interpreter/golabels/integrationtests/golabels_test.go b/interpreter/golabels/integrationtests/golabels_test.go new file mode 100644 index 000000000..e8c72bd26 --- /dev/null +++ b/interpreter/golabels/integrationtests/golabels_test.go @@ -0,0 +1,158 @@ +//go:build integration + +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +package integrationtests + +import ( + "context" + "math" + "math/rand" + "os" + "runtime/debug" + "runtime/pprof" + "strings" + "testing" + "time" + + "github.com/stretchr/testify/require" + "go.opentelemetry.io/ebpf-profiler/host" + "go.opentelemetry.io/ebpf-profiler/libpf" + "go.opentelemetry.io/ebpf-profiler/reporter" + "go.opentelemetry.io/ebpf-profiler/tracer" + tracertypes "go.opentelemetry.io/ebpf-profiler/tracer/types" +) + +type mockIntervals struct{} + +func (mockIntervals) MonitorInterval() time.Duration { return 1 * time.Second } +func (mockIntervals) TracePollInterval() time.Duration { return 250 * time.Millisecond } +func (mockIntervals) PIDCleanupInterval() time.Duration { return 1 * time.Second } + +type mockReporter struct{} + +func (mockReporter) ExecutableKnown(_ libpf.FileID) bool { return true } +func (mockReporter) ExecutableMetadata(_ *reporter.ExecutableMetadataArgs) {} + +func isRoot() bool { + return os.Geteuid() == 0 +} + +//nolint:gosec +func randomString(n int) string { + letters := []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789") + s := make([]rune, n) + for i := range s { + s[i] = letters[rand.Intn(len(letters))] + } + return string(s) +} + +func setPprofLabels(t *testing.T, ctx context.Context, cookie string, busyFunc func()) { + t.Helper() + labels := pprof.Labels( + "l1"+cookie, "label1"+randomString(16), + "l2"+cookie, "label2"+randomString(24), + "l3"+cookie, "label3"+randomString(48)) + lastUpdate := time.Now() + pprof.Do(context.TODO(), labels, func(context.Context) { + for time.Since(lastUpdate) < 10*time.Second { + // CPU go burr on purpose. + busyFunc() + if ctx.Err() != nil { + return + } + } + }) +} + +func Test_Golabels(t *testing.T) { + if !isRoot() { + t.Skip("root privileges required") + } + + buildInfo, ok := debug.ReadBuildInfo() + if !ok { + t.Fatalf("Failed to get build info") + } + + withCGO := false + for _, setting := range buildInfo.Settings { + if setting.Key == "CGO_ENABLED" { + withCGO = true + } + } + t.Logf("CGo is enabled: %t", withCGO) + + cookie := buildInfo.GoVersion + + t.Run(cookie, func(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + enabledTracers, _ := tracertypes.Parse("") + enabledTracers.Enable(tracertypes.Labels) + + trc, err := tracer.NewTracer(ctx, &tracer.Config{ + Reporter: &mockReporter{}, + Intervals: &mockIntervals{}, + IncludeTracers: enabledTracers, + SamplesPerSecond: 20, + ProbabilisticInterval: 100, + ProbabilisticThreshold: 100, + OffCPUThreshold: uint32(math.MaxUint32 / 100), + VerboseMode: true, + }) + require.NoError(t, err) + + trc.StartPIDEventProcessor(ctx) + + err = trc.AttachTracer() + require.NoError(t, err) + + t.Log("Attached tracer program") + + err = trc.EnableProfiling() + require.NoError(t, err) + + err = trc.AttachSchedMonitor() + require.NoError(t, err) + + traceCh := make(chan *host.Trace) + + err = trc.StartMapMonitors(ctx, traceCh) + require.NoError(t, err) + + go setPprofLabels(t, ctx, cookie, busyFunc) + + for trace := range traceCh { + if trace == nil { + continue + } + if len(trace.CustomLabels) > 0 { + hits := 0 + for k, v := range trace.CustomLabels { + switch k { + case "l1" + cookie: + require.Len(t, v, 22) + require.True(t, strings.HasPrefix(v, "label1")) + hits |= (1 << 0) + case "l2" + cookie: + require.Len(t, v, 30) + require.True(t, strings.HasPrefix(v, "label2")) + hits |= (1 << 1) + case "l3" + cookie: + require.Len(t, v, 47) + require.True(t, strings.HasPrefix(v, "label3")) + hits |= (1 << 2) + } + } + if hits == (1<<0 | 1<<1 | 1<<2) { + cancel() + break + } + } + } + }) +} diff --git a/interpreter/golabels/test/main-cgo.go b/interpreter/golabels/test/main-cgo.go deleted file mode 100644 index 3950f58df..000000000 --- a/interpreter/golabels/test/main-cgo.go +++ /dev/null @@ -1,62 +0,0 @@ -// Copyright The OpenTelemetry Authors -// SPDX-License-Identifier: Apache-2.0 - -//go:build usecgo -// +build usecgo - -package main - -/* -#include - -void cgofunc() { - volatile int counter = 0; - while (counter < 1000000) { - counter++; - } -} -*/ -import "C" - -import ( - "context" - "fmt" - "math/rand" - "os" - "runtime/pprof" - "time" -) - -func randomString2(n int) string { - letters := []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789") - s := make([]rune, n) - for i := range s { - s[i] = letters[rand.Intn(len(letters))] - } - return string(s) -} - -// This is a normal main program that when go build will be statically linked, this is required -// to work with qemu/bluebox testing harness. A statically linked go test built binary doesn't -// work with the go labels extractor ebpf program, not sure yet if this is a bug. -func main() { - // If first isn't subtest then we're running via bluebox init and should just exit. - if len(os.Args) != 3 || os.Args[1] != "-subtest" { - fmt.Println("PASS") - return - } - cookie := os.Args[2] - labels := pprof.Labels( - "l1"+cookie, "label1"+randomString2(16), - "l2"+cookie, "label2"+randomString2(24), - "l3"+cookie, "label3"+randomString2(48)) - lastUpdate := time.Now() - pprof.Do(context.TODO(), labels, func(context.Context) { - //nolint:revive - for time.Since(lastUpdate) < 10*time.Second { - // CPU go burr on purpose. - C.cgofunc() - } - }) - fmt.Println("PASS") -} diff --git a/interpreter/golabels/test/main.go b/interpreter/golabels/test/main.go deleted file mode 100644 index 4683bba38..000000000 --- a/interpreter/golabels/test/main.go +++ /dev/null @@ -1,50 +0,0 @@ -// Copyright The OpenTelemetry Authors -// SPDX-License-Identifier: Apache-2.0 - -//go:build nocgo -// +build nocgo - -package main - -import ( - "context" - "fmt" - "math/rand" - "os" - "runtime/pprof" - "time" -) - -//nolint:gosec -func randomString(n int) string { - letters := []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789") - s := make([]rune, n) - for i := range s { - s[i] = letters[rand.Intn(len(letters))] - } - return string(s) -} - -// This is a normal main program that when go build will be statically linked, this is required -// to work with qemu/bluebox testing harness. A statically linked go test built binary doesn't -// work with the go labels extractor ebpf program, not sure yet if this is a bug. -func main() { - // If first isn't subtest then we're running via bluebox init and should just exit. - if len(os.Args) != 3 || os.Args[1] != "-subtest" { - fmt.Println("PASS") - return - } - cookie := os.Args[2] - labels := pprof.Labels( - "l1"+cookie, "label1"+randomString(16), - "l2"+cookie, "label2"+randomString(24), - "l3"+cookie, "label3"+randomString(48)) - lastUpdate := time.Now() - pprof.Do(context.TODO(), labels, func(context.Context) { - //nolint:revive - for time.Since(lastUpdate) < 10*time.Second { - // CPU go burr on purpose. - } - }) - fmt.Println("PASS") -} diff --git a/interpreter/golabels/test/main_test.go b/interpreter/golabels/test/main_test.go deleted file mode 100644 index b62e0acbb..000000000 --- a/interpreter/golabels/test/main_test.go +++ /dev/null @@ -1,121 +0,0 @@ -// Copyright The OpenTelemetry Authors -// SPDX-License-Identifier: Apache-2.0 -package main - -import ( - "context" - "math" - "os" - "os/exec" - "strings" - "testing" - "time" - - "github.com/stretchr/testify/require" - "go.opentelemetry.io/ebpf-profiler/host" - "go.opentelemetry.io/ebpf-profiler/libpf" - "go.opentelemetry.io/ebpf-profiler/reporter" - "go.opentelemetry.io/ebpf-profiler/tracer" - tracertypes "go.opentelemetry.io/ebpf-profiler/tracer/types" -) - -type mockIntervals struct{} - -func (mockIntervals) MonitorInterval() time.Duration { return 1 * time.Second } -func (mockIntervals) TracePollInterval() time.Duration { return 250 * time.Millisecond } -func (mockIntervals) PIDCleanupInterval() time.Duration { return 1 * time.Second } - -type mockReporter struct{} - -func (mockReporter) ExecutableKnown(_ libpf.FileID) bool { return true } -func (mockReporter) ExecutableMetadata(_ *reporter.ExecutableMetadataArgs) {} - -func isRoot() bool { - return os.Geteuid() == 0 -} - -func TestGoLabels(t *testing.T) { - if !isRoot() { - t.Skip("root privileges required") - } - ctx := context.Background() - - enabledTracers, _ := tracertypes.Parse("") - enabledTracers.Enable(tracertypes.Labels) - - trc, err := tracer.NewTracer(ctx, &tracer.Config{ - Reporter: &mockReporter{}, - Intervals: &mockIntervals{}, - IncludeTracers: enabledTracers, - SamplesPerSecond: 20, - ProbabilisticInterval: 100, - ProbabilisticThreshold: 100, - OffCPUThreshold: uint32(math.MaxUint32 / 100), - VerboseMode: true, - }) - require.NoError(t, err) - - trc.StartPIDEventProcessor(ctx) - - err = trc.AttachTracer() - require.NoError(t, err) - - t.Log("Attached tracer program") - - err = trc.EnableProfiling() - require.NoError(t, err) - - err = trc.AttachSchedMonitor() - require.NoError(t, err) - - traceCh := make(chan *host.Trace) - - err = trc.StartMapMonitors(ctx, traceCh) - require.NoError(t, err) - - for _, tc := range [][]string{ - {"./golbls_1_23.test", "123"}, - {"./golbls_1_24.test", "124"}, - {"./golbls_cgo.test", "cgo"}, - } { - t.Run(tc[0], func(t *testing.T) { - // Use a separate exe for getting labels as the bpf code doesn't seem to work with - // go test static binaries at the moment, not clear if that's a problem with the bpf - // code or a bug/fact of life for static go binaries and getting g from TLS. - cookie := tc[1] - cmd := exec.Command(tc[0], "-subtest", cookie) - err := cmd.Start() - require.NoError(t, err) - - for trace := range traceCh { - if trace == nil { - continue - } - if len(trace.CustomLabels) > 0 { - hits := 0 - for k, v := range trace.CustomLabels { - switch k { - case "l1" + cookie: - require.Len(t, v, 22) - require.True(t, strings.HasPrefix(v, "label1")) - hits++ - case "l2" + cookie: - require.Len(t, v, 30) - require.True(t, strings.HasPrefix(v, "label2")) - hits++ - case "l3" + cookie: - require.Len(t, v, 47) - require.True(t, strings.HasPrefix(v, "label3")) - hits++ - } - } - if hits == 3 { - break - } - } - } - _ = cmd.Process.Signal(os.Kill) - _ = cmd.Wait() - }) - } -}