Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Test probe load with mocks #1991

Merged
merged 4 commits into from
Mar 15, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/probe_load.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,9 @@ jobs:
sudo chmod 0666 /dev/kvm
- name: Test without verifier logs
id: no_verifier_logs_test
run: OTEL_GO_AUTO_SHOW_VERIFIER_LOG=false vimto -kernel :${{ matrix.tag }} -- go test -v -count=1 -tags=ebpf_test go.opentelemetry.io/auto/internal/pkg/instrumentation
run: OTEL_GO_AUTO_SHOW_VERIFIER_LOG=false vimto -kernel :${{ matrix.tag }} -- go test -v -count=1 go.opentelemetry.io/auto/internal/pkg/instrumentation
- name: Test with verifier logs
run: OTEL_GO_AUTO_SHOW_VERIFIER_LOG=true vimto -kernel :${{ matrix.tag }} -- go test -v -count=1 -tags=ebpf_test go.opentelemetry.io/auto/internal/pkg/instrumentation
run: OTEL_GO_AUTO_SHOW_VERIFIER_LOG=true vimto -kernel :${{ matrix.tag }} -- go test -v -count=1 go.opentelemetry.io/auto/internal/pkg/instrumentation
if: always() && steps.no_verifier_logs_test.outcome == 'failure'
- name: Test eBPF sampling
run: vimto -kernel :${{ matrix.tag }} -- go test -v -count=1 -tags=ebpf_test go.opentelemetry.io/auto/internal/pkg/instrumentation/probe/sampling
177 changes: 136 additions & 41 deletions internal/pkg/instrumentation/manager_load_test.go
Original file line number Diff line number Diff line change
@@ -1,17 +1,22 @@
// Copyright The OpenTelemetry Authors
// SPDX-License-Identifier: Apache-2.0

//go:build ebpf_test

package instrumentation

import (
"errors"
"log/slog"
"os"
"os/exec"
"path/filepath"
"testing"

"github.com/Masterminds/semver/v3"
"github.com/cilium/ebpf"
"github.com/cilium/ebpf/rlimit"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

"go.opentelemetry.io/auto/internal/pkg/inject"
dbSql "go.opentelemetry.io/auto/internal/pkg/instrumentation/bpf/database/sql"
kafkaConsumer "go.opentelemetry.io/auto/internal/pkg/instrumentation/bpf/github.com/segmentio/kafka-go/consumer"
Expand All @@ -22,71 +27,161 @@ import (
grpcServer "go.opentelemetry.io/auto/internal/pkg/instrumentation/bpf/google.golang.org/grpc/server"
httpClient "go.opentelemetry.io/auto/internal/pkg/instrumentation/bpf/net/http/client"
httpServer "go.opentelemetry.io/auto/internal/pkg/instrumentation/bpf/net/http/server"
"go.opentelemetry.io/auto/internal/pkg/instrumentation/bpffs"
"go.opentelemetry.io/auto/internal/pkg/instrumentation/probe"
"go.opentelemetry.io/auto/internal/pkg/instrumentation/testutils"
"go.opentelemetry.io/auto/internal/pkg/instrumentation/utils"
"go.opentelemetry.io/auto/internal/pkg/process"
"go.opentelemetry.io/auto/internal/pkg/process/binary"
)

func TestLoadProbes(t *testing.T) {
if err := rlimit.RemoveMemlock(); err != nil {
t.Skip("cannot manage memory, skipping test.")
}

id := setupTestModule(t)
pid := process.ID(id)

info, err := process.NewInfo(pid, make(map[string]interface{}))
if info == nil {
t.Fatalf("failed to create process.Info: %v", err)
}
// Reset Info module information.
info.Modules = make(map[string]*semver.Version)

logger := slog.Default()
info.Allocation, err = process.Allocate(logger, pid)
if err != nil {
t.Fatalf("failed to allocate for process %d: %v", id, err)
}

ver := utils.GetLinuxKernelVersion()
require.NotNil(t, ver)
t.Logf("Running on kernel %s", ver.String())
m := fakeManager(t)

for _, p := range m.probes {
probes := []probe.Probe{
grpcClient.New(logger, ""),
grpcServer.New(logger, ""),
httpServer.New(logger, ""),
httpClient.New(logger, ""),
dbSql.New(logger, ""),
kafkaProducer.New(logger, ""),
kafkaConsumer.New(logger, ""),
autosdk.New(logger),
otelTraceGlobal.New(logger),
}

for _, p := range probes {
manifest := p.Manifest()
fields := manifest.StructFields
offsets := map[string]*semver.Version{}
for _, f := range fields {
_, ver := inject.GetLatestOffset(f)
if ver != nil {
offsets[f.PkgPath] = ver
offsets[f.ModPath] = ver
info.Modules[f.PkgPath] = ver
info.Modules[f.ModPath] = ver
}
}
t.Run(p.Manifest().ID.String(), func(t *testing.T) {
testProbe, ok := p.(testutils.TestProbe)
assert.True(t, ok)
testutils.ProbesLoad(t, testProbe, offsets)
require.Implements(t, (*TestProbe)(nil), p)
ProbesLoad(t, info, p.(TestProbe))
})
}
}

func fakeManager(t *testing.T, fnNames ...string) *Manager {
logger := slog.Default()
probes := []probe.Probe{
grpcClient.New(logger, ""),
grpcServer.New(logger, ""),
httpServer.New(logger, ""),
httpClient.New(logger, ""),
dbSql.New(logger, ""),
kafkaProducer.New(logger, ""),
kafkaConsumer.New(logger, ""),
autosdk.New(logger),
otelTraceGlobal.New(logger),
const mainGoContent = `package main

import (
"time"
)

func main() {
for {
time.Sleep(time.Hour)
}
}`

func setupTestModule(t *testing.T) int {
t.Helper()

tempDir := t.TempDir()

// Initialize a Go module
cmd := exec.Command("go", "mod", "init", "example.com/testmodule")
cmd.Dir = tempDir
if err := cmd.Run(); err != nil {
t.Fatalf("failed to initialize Go module: %v", err)
}

mainGoPath := filepath.Join(tempDir, "main.go")
if err := os.WriteFile(mainGoPath, []byte(mainGoContent), 0o600); err != nil {
t.Fatalf("failed to write main.go: %v", err)
}
ver := semver.New(1, 20, 0, "", "")
var fn []*binary.Func
for _, name := range fnNames {
fn = append(fn, &binary.Func{Name: name})

// Compile the Go program
binaryPath := filepath.Join(tempDir, "testbinary")
cmd = exec.Command("go", "build", "-o", binaryPath, mainGoPath)
cmd.Dir = tempDir
if err := cmd.Run(); err != nil {
t.Fatalf("failed to compile binary: %v", err)
}

// Run the compiled binary
cmd = exec.Command(binaryPath)
cmd.Dir = tempDir
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if err := cmd.Start(); err != nil {
t.Fatalf("failed to start binary: %v", err)
}
m := &Manager{
logger: slog.Default(),
cp: NewNoopConfigProvider(nil),
probes: make(map[probe.ID]probe.Probe),
proc: &process.Info{
ID: 1,
Functions: fn,
GoVersion: ver,
Modules: map[string]*semver.Version{},

// Ensure the process is killed when the test ends
t.Cleanup(func() {
_ = cmd.Process.Kill()
_, _ = cmd.Process.Wait()
})

// Return the process ID
return cmd.Process.Pid
}

type TestProbe interface {
Spec() (*ebpf.CollectionSpec, error)
InjectConsts(*process.Info, *ebpf.CollectionSpec) error
}

func ProbesLoad(t *testing.T, info *process.Info, p TestProbe) {
t.Helper()

require.NoError(t, bpffs.Mount(info))
t.Cleanup(func() { _ = bpffs.Cleanup(info) })

spec, err := p.Spec()
require.NoError(t, err)

// Inject the same constants as the BPF program. It is important to inject
// the same constants as those that will be used in the actual run, since
// From Linux 5.5 the verifier will use constants to eliminate dead code.
require.NoError(t, p.InjectConsts(info, spec))

opts := ebpf.CollectionOptions{
Maps: ebpf.MapOptions{
PinPath: bpffs.PathForTargetApplication(info),
},
}
for _, p := range probes {
m.probes[p.Manifest().ID] = p

collectVerifierLogs := utils.ShouldShowVerifierLogs()
if collectVerifierLogs {
opts.Programs.LogLevel = ebpf.LogLevelStats | ebpf.LogLevelInstruction
}
m.filterUnusedProbes()

return m
c, err := ebpf.NewCollectionWithOptions(spec, opts)
if !assert.NoError(t, err) {
var ve *ebpf.VerifierError
if errors.As(err, &ve) && collectVerifierLogs {
t.Logf("Verifier log: %-100v\n", ve)
}
}

if c != nil {
t.Cleanup(c.Close)
}
}
10 changes: 4 additions & 6 deletions internal/pkg/instrumentation/manager_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,18 +40,17 @@ import (

func TestProbeFiltering(t *testing.T) {
t.Run("empty target details", func(t *testing.T) {
m := fakeManager(t)
m := fakeManager()
assert.Empty(t, m.probes)
})

t.Run("only HTTP client target details", func(t *testing.T) {
m := fakeManager(t, "net/http.(*Transport).roundTrip")
m := fakeManager("net/http.(*Transport).roundTrip")
assert.Len(t, m.probes, 1) // one function, single probe
})

t.Run("HTTP server and client target details", func(t *testing.T) {
m := fakeManager(
t,
"net/http.(*Transport).roundTrip",
"net/http.serverHandler.ServeHTTP",
)
Expand All @@ -60,7 +59,6 @@ func TestProbeFiltering(t *testing.T) {

t.Run("HTTP server and client dependent function only target details", func(t *testing.T) {
m := fakeManager(
t,
// writeSubset depends on "net/http.(*Transport).roundTrip", it should be ignored without roundTrip
"net/http.Header.writeSubset",
"net/http.serverHandler.ServeHTTP",
Expand All @@ -70,7 +68,7 @@ func TestProbeFiltering(t *testing.T) {
}

func TestDependencyChecks(t *testing.T) {
m := fakeManager(t)
m := fakeManager()

t.Run("Dependent probes match", func(t *testing.T) {
syms := []probe.FunctionSymbol{
Expand Down Expand Up @@ -152,7 +150,7 @@ func TestDependencyChecks(t *testing.T) {
})
}

func fakeManager(t *testing.T, fnNames ...string) *Manager {
func fakeManager(fnNames ...string) *Manager {
logger := slog.Default()
probes := []probe.Probe{
grpcClient.New(logger, ""),
Expand Down
93 changes: 0 additions & 93 deletions internal/pkg/instrumentation/testutils/testutils.go

This file was deleted.

2 changes: 1 addition & 1 deletion internal/pkg/process/info.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ func NewInfo(id ID, relevantFuncs map[string]interface{}) (*Info, error) {

result.Functions, err = findFunctions(elfF, relevantFuncs)
if err != nil {
return nil, err
return result, err
}

result.Modules, err = findModules(goVersion, bi.Deps)
Expand Down
Loading