Skip to content

Commit 545f930

Browse files
authoredAug 17, 2024··
Add probe loading tests across multiple kernel versions using vimto (#1005)
1 parent eb4e5d1 commit 545f930

File tree

13 files changed

+325
-26
lines changed

13 files changed

+325
-26
lines changed
 

‎.github/workflows/probe_load.yaml

+47
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
name: probe_load
2+
3+
on:
4+
push:
5+
branches: [ main ]
6+
pull_request:
7+
8+
env:
9+
go_version: '~1.22'
10+
CGO_ENABLED: '0'
11+
12+
jobs:
13+
vm-test:
14+
name: Run tests
15+
runs-on: ubuntu-latest
16+
timeout-minutes: 15
17+
strategy:
18+
fail-fast: false
19+
matrix:
20+
tag:
21+
- "stable"
22+
- "6.6"
23+
- "5.15"
24+
- "5.10"
25+
- "5.4"
26+
steps:
27+
- uses: actions/checkout@v4
28+
- name: Set up Go
29+
uses: actions/setup-go@v5
30+
with:
31+
go-version: '${{ env.go_version }}'
32+
- name: make docker-generate
33+
run: make docker-generate
34+
- name: verify output
35+
run: make check-clean-work-tree
36+
- name: Install vimto
37+
run: go install lmb.io/vimto@latest
38+
- name: Install qemu
39+
run: |
40+
sudo apt-get update && sudo apt-get install -y --no-install-recommends qemu-system-x86
41+
sudo chmod 0666 /dev/kvm
42+
- name: Test without verifier logs
43+
id: no_verifier_logs_test
44+
run: OTEL_GO_AUTO_SHOW_VERIFIER_LOG=false vimto -kernel :${{ matrix.tag }} -- go test -v -count=1 -tags=multi_kernel_test go.opentelemetry.io/auto/internal/pkg/instrumentation
45+
- name: Test with verifier logs
46+
run: OTEL_GO_AUTO_SHOW_VERIFIER_LOG=true vimto -kernel :${{ matrix.tag }} -- go test -v -count=1 -tags=multi_kernel_test go.opentelemetry.io/auto/internal/pkg/instrumentation
47+
if: always() && steps.no_verifier_logs_test.outcome == 'failure'

‎.golangci.yml

+1
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ linters-settings:
7474
files:
7575
- "!$test"
7676
- "!**/*test/*.go"
77+
- "!**/testutils/*.go"
7778
deny:
7879
- pkg: "testing"
7980
- pkg: "github.com/stretchr/testify"

‎.vimto.toml

+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
kernel="ghcr.io/cilium/ci-kernels:stable"
2+
smp="cpus=4"
3+
memory="8G"
4+
user="root"
5+
setup=[
6+
"mount -t cgroup2 -o nosuid,noexec,nodev cgroup2 /sys/fs/cgroup",
7+
"/bin/sh -c 'modprobe bpf_testmod || true'",
8+
"dmesg --clear",
9+
]
10+
teardown=[
11+
"dmesg --read-clear",
12+
]

‎internal/include/otel_types.h

+8-11
Original file line numberDiff line numberDiff line change
@@ -55,11 +55,6 @@ static __always_inline bool set_attr_value(otel_attirbute_t *attr, go_otel_attr_
5555
{
5656
u64 vtype = go_attr_value->vtype;
5757

58-
if (vtype == attr_type_invalid) {
59-
bpf_printk("Invalid attribute value type\n");
60-
return false;
61-
}
62-
6358
// Constant size values
6459
if (vtype == attr_type_bool ||
6560
vtype == attr_type_int64 ||
@@ -74,7 +69,8 @@ static __always_inline bool set_attr_value(otel_attirbute_t *attr, go_otel_attr_
7469
bpf_printk("Aattribute string value is too long\n");
7570
return false;
7671
}
77-
return get_go_string_from_user_ptr(&go_attr_value->string, attr->value, OTEL_ATTRIBUTE_VALUE_MAX_LEN);
72+
long res = bpf_probe_read_user(attr->value, go_attr_value->string.len & (OTEL_ATTRIBUTE_VALUE_MAX_LEN -1), go_attr_value->string.str);
73+
return res == 0;
7874
}
7975

8076
// TODO (#525): handle slices
@@ -83,7 +79,7 @@ static __always_inline bool set_attr_value(otel_attirbute_t *attr, go_otel_attr_
8379

8480
static __always_inline void convert_go_otel_attributes(void *attrs_buf, u64 slice_len, otel_attributes_t *enc_attrs)
8581
{
86-
if (attrs_buf == NULL || enc_attrs == NULL){
82+
if (attrs_buf == NULL){
8783
return;
8884
}
8985

@@ -100,7 +96,10 @@ static __always_inline void convert_go_otel_attributes(void *attrs_buf, u64 slic
10096
return;
10197
}
10298

103-
for (u8 go_attr_index = 0; go_attr_index < num_attrs; go_attr_index++) {
99+
for (u8 go_attr_index = 0; go_attr_index < OTEL_ATTRUBUTE_MAX_COUNT; go_attr_index++) {
100+
if (go_attr_index >= slice_len) {
101+
break;
102+
}
104103
__builtin_memset(&go_attr_value, 0, sizeof(go_otel_attr_value_t));
105104
// Read the value struct
106105
bpf_probe_read(&go_attr_value, sizeof(go_otel_attr_value_t), &go_attr[go_attr_index].value);
@@ -124,9 +123,7 @@ static __always_inline void convert_go_otel_attributes(void *attrs_buf, u64 slic
124123
break;
125124
}
126125

127-
if (!get_go_string_from_user_ptr(&go_str, enc_attrs->attrs[valid_attrs].key, OTEL_ATTRIBUTE_KEY_MAX_LEN)) {
128-
continue;
129-
}
126+
bpf_probe_read_user(enc_attrs->attrs[valid_attrs].key, go_str.len & (OTEL_ATTRIBUTE_KEY_MAX_LEN -1), go_str.str);
130127

131128
if (!set_attr_value(&enc_attrs->attrs[valid_attrs], &go_attr_value)) {
132129
continue;

‎internal/pkg/inject/consts.go

+4
Original file line numberDiff line numberDiff line change
@@ -139,3 +139,7 @@ func WithOffset(key string, id structfield.ID, ver *version.Version) Option {
139139
}
140140
return WithKeyValue(key, off.Offset)
141141
}
142+
143+
func GetLatestOffset(id structfield.ID) (structfield.OffsetKey, *version.Version) {
144+
return offsets.GetLatestOffset(id)
145+
}

‎internal/pkg/instrumentation/manager.go

+17-10
Original file line numberDiff line numberDiff line change
@@ -223,21 +223,28 @@ func (m *Manager) cleanup(target *process.TargetDetails) error {
223223
return errors.Join(err, bpffsCleanup(target))
224224
}
225225

226-
func (m *Manager) registerProbes() error {
226+
//nolint:revive // ignoring linter complaint about control flag
227+
func availableProbes(l logr.Logger, withTraceGlobal bool) []probe.Probe {
227228
insts := []probe.Probe{
228-
grpcClient.New(m.logger),
229-
grpcServer.New(m.logger),
230-
httpServer.New(m.logger),
231-
httpClient.New(m.logger),
232-
dbSql.New(m.logger),
233-
kafkaProducer.New(m.logger),
234-
kafkaConsumer.New(m.logger),
229+
grpcClient.New(l),
230+
grpcServer.New(l),
231+
httpServer.New(l),
232+
httpClient.New(l),
233+
dbSql.New(l),
234+
kafkaProducer.New(l),
235+
kafkaConsumer.New(l),
235236
}
236237

237-
if m.globalImpl {
238-
insts = append(insts, otelTraceGlobal.New(m.logger))
238+
if withTraceGlobal {
239+
insts = append(insts, otelTraceGlobal.New(l))
239240
}
240241

242+
return insts
243+
}
244+
245+
func (m *Manager) registerProbes() error {
246+
insts := availableProbes(m.logger, m.globalImpl)
247+
241248
for _, i := range insts {
242249
err := m.registerProbe(i)
243250
if err != nil {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
//go:build multi_kernel_test
2+
3+
// Copyright The OpenTelemetry Authors
4+
// SPDX-License-Identifier: Apache-2.0
5+
6+
package instrumentation
7+
8+
import (
9+
"log"
10+
"os"
11+
"testing"
12+
13+
"github.com/go-logr/stdr"
14+
"github.com/hashicorp/go-version"
15+
"github.com/stretchr/testify/assert"
16+
"go.opentelemetry.io/auto/internal/pkg/inject"
17+
"go.opentelemetry.io/auto/internal/pkg/instrumentation/testutils"
18+
"go.opentelemetry.io/auto/internal/pkg/instrumentation/utils"
19+
)
20+
21+
func TestLoadProbes(t *testing.T) {
22+
ver, _ := utils.GetLinuxKernelVersion()
23+
t.Logf("Running on kernel %s", ver.String())
24+
m := fakeManager(t)
25+
26+
probes := availableProbes(m.logger, true)
27+
assert.NotEmpty(t, probes)
28+
29+
for _, p := range probes {
30+
manifest := p.Manifest()
31+
fields := manifest.StructFields
32+
offsets := map[string]*version.Version{}
33+
for _, f := range fields {
34+
_, ver := inject.GetLatestOffset(f)
35+
if ver != nil {
36+
offsets[f.PkgPath] = ver
37+
offsets[f.ModPath] = ver
38+
}
39+
}
40+
t.Run(p.Manifest().Id.String(), func(t *testing.T) {
41+
testProbe, ok := p.(testutils.TestProbe)
42+
assert.True(t, ok)
43+
testutils.ProbesLoad(t, testProbe, offsets)
44+
})
45+
}
46+
}
47+
48+
func fakeManager(t *testing.T) *Manager {
49+
logger := stdr.New(log.New(os.Stderr, "", log.LstdFlags))
50+
logger = logger.WithName("Instrumentation")
51+
52+
m, err := NewManager(logger, nil, true, nil)
53+
assert.NoError(t, err)
54+
assert.NotNil(t, m)
55+
56+
return m
57+
}

‎internal/pkg/instrumentation/manager_test.go

+2
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
//go:build !multi_kernel_test
2+
13
// Copyright The OpenTelemetry Authors
24
// SPDX-License-Identifier: Apache-2.0
35

‎internal/pkg/instrumentation/probe/probe.go

+6-2
Original file line numberDiff line numberDiff line change
@@ -92,14 +92,18 @@ func (i *Base[BPFObj, BPFEvent]) Manifest() Manifest {
9292
return NewManifest(i.ID, structfields, symbols)
9393
}
9494

95+
func (i *Base[BPFObj, BPFEvent]) Spec() (*ebpf.CollectionSpec, error) {
96+
return i.SpecFn()
97+
}
98+
9599
// Load loads all instrumentation offsets.
96100
func (i *Base[BPFObj, BPFEvent]) Load(exec *link.Executable, td *process.TargetDetails) error {
97101
spec, err := i.SpecFn()
98102
if err != nil {
99103
return err
100104
}
101105

102-
err = i.injectConsts(td, spec)
106+
err = i.InjectConsts(td, spec)
103107
if err != nil {
104108
return err
105109
}
@@ -123,7 +127,7 @@ func (i *Base[BPFObj, BPFEvent]) Load(exec *link.Executable, td *process.TargetD
123127
return nil
124128
}
125129

126-
func (i *Base[BPFObj, BPFEvent]) injectConsts(td *process.TargetDetails, spec *ebpf.CollectionSpec) error {
130+
func (i *Base[BPFObj, BPFEvent]) InjectConsts(td *process.TargetDetails, spec *ebpf.CollectionSpec) error {
127131
opts, err := consts(i.Consts).injectOpts(td)
128132
if err != nil {
129133
return err
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
// Copyright The OpenTelemetry Authors
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
package testutils
5+
6+
import (
7+
"errors"
8+
"testing"
9+
10+
"github.com/cilium/ebpf"
11+
"github.com/cilium/ebpf/rlimit"
12+
"github.com/hashicorp/go-version"
13+
"github.com/stretchr/testify/assert"
14+
15+
"go.opentelemetry.io/auto/internal/pkg/instrumentation/bpffs"
16+
"go.opentelemetry.io/auto/internal/pkg/instrumentation/utils"
17+
"go.opentelemetry.io/auto/internal/pkg/process"
18+
)
19+
20+
var testGoVersion = version.Must(version.NewVersion("1.22.1"))
21+
22+
type TestProbe interface {
23+
Spec() (*ebpf.CollectionSpec, error)
24+
InjectConsts(td *process.TargetDetails, spec *ebpf.CollectionSpec) error
25+
}
26+
27+
func ProbesLoad(t *testing.T, p TestProbe, libs map[string]*version.Version) {
28+
err := rlimit.RemoveMemlock()
29+
if !assert.NoError(t, err) {
30+
return
31+
}
32+
33+
td := &process.TargetDetails{
34+
PID: 1,
35+
AllocationDetails: &process.AllocationDetails{
36+
StartAddr: 140434497441792,
37+
EndAddr: 140434497507328,
38+
},
39+
Libraries: map[string]*version.Version{
40+
"std": testGoVersion,
41+
},
42+
GoVersion: testGoVersion,
43+
}
44+
for k, v := range libs {
45+
td.Libraries[k] = v
46+
}
47+
48+
err = bpffs.Mount(td)
49+
if !assert.NoError(t, err) {
50+
return
51+
}
52+
defer func() {
53+
_ = bpffs.Cleanup(td)
54+
}()
55+
56+
spec, err := p.Spec()
57+
if !assert.NoError(t, err) {
58+
return
59+
}
60+
61+
// Inject the same constants as the BPF program.
62+
// It is important to inject the same constants as those that will be used in the actual run,
63+
// since From Linux 5.5 the verifier will use constants to eliminate dead code.
64+
err = p.InjectConsts(td, spec)
65+
if !assert.NoError(t, err) {
66+
return
67+
}
68+
69+
opts := ebpf.CollectionOptions{
70+
Maps: ebpf.MapOptions{
71+
PinPath: bpffs.PathForTargetApplication(td),
72+
},
73+
}
74+
75+
collectVerifierLogs := utils.ShouldShowVerifierLogs()
76+
if collectVerifierLogs {
77+
opts.Programs.LogLevel = ebpf.LogLevelStats | ebpf.LogLevelInstruction
78+
}
79+
80+
c, err := ebpf.NewCollectionWithOptions(spec, opts)
81+
if !assert.NoError(t, err) {
82+
var ve *ebpf.VerifierError
83+
if errors.As(err, &ve) && collectVerifierLogs {
84+
t.Logf("Verifier log: %-100v\n", ve)
85+
}
86+
}
87+
88+
defer func() {
89+
if c != nil {
90+
c.Close()
91+
}
92+
}()
93+
}

‎internal/pkg/instrumentation/utils/ebpf.go

+3-3
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ const (
2121
// If the environment variable OTEL_GO_AUTO_SHOW_VERIFIER_LOG is set to true, the verifier log will be printed.
2222
func InitializeEBPFCollection(spec *ebpf.CollectionSpec, opts *ebpf.CollectionOptions) (*ebpf.Collection, error) {
2323
// Getting full verifier log is expensive, so we only do it if the user explicitly asks for it.
24-
showVerifierLogs := shouldShowVerifierLogs()
24+
showVerifierLogs := ShouldShowVerifierLogs()
2525
if showVerifierLogs {
2626
opts.Programs.LogLevel = ebpf.LogLevelInstruction | ebpf.LogLevelBranch | ebpf.LogLevelStats
2727
}
@@ -37,8 +37,8 @@ func InitializeEBPFCollection(spec *ebpf.CollectionSpec, opts *ebpf.CollectionOp
3737
return c, err
3838
}
3939

40-
// shouldShowVerifierLogs returns if the user has configured verifier logs to be emitted.
41-
func shouldShowVerifierLogs() bool {
40+
// ShouldShowVerifierLogs returns if the user has configured verifier logs to be emitted.
41+
func ShouldShowVerifierLogs() bool {
4242
val, exists := os.LookupEnv(showVerifierLogEnvVar)
4343
if exists {
4444
boolVal, err := strconv.ParseBool(val)

‎internal/pkg/structfield/structfield.go

+53
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,20 @@ func (i *Index) GetOffset(id ID, ver *version.Version) (OffsetKey, bool) {
4949
return i.getOffset(id, ver)
5050
}
5151

52+
// GetLatestOffset returns the latest known offset value and version for id
53+
// contained in the Index i.
54+
func (i *Index) GetLatestOffset(id ID) (OffsetKey, *version.Version) {
55+
i.dataMu.RLock()
56+
defer i.dataMu.RUnlock()
57+
58+
offs, ok := i.get(id)
59+
if !ok {
60+
return OffsetKey{}, nil
61+
}
62+
off, ver := offs.getLatest()
63+
return off, ver.ToVersion()
64+
}
65+
5266
func (i *Index) getOffset(id ID, ver *version.Version) (OffsetKey, bool) {
5367
offs, ok := i.get(id)
5468
if !ok {
@@ -259,6 +273,23 @@ func (o *Offsets) Get(ver *version.Version) (OffsetKey, bool) {
259273
return v.offset, ok
260274
}
261275

276+
// getLatest returns the latest known offset value and version.
277+
func (o *Offsets) getLatest() (OffsetKey, verKey) {
278+
o.mu.RLock()
279+
defer o.mu.RUnlock()
280+
281+
latestVersion := verKey{}
282+
val := OffsetKey{}
283+
for verKey, ov := range o.values {
284+
if verKey.GreaterThan(latestVersion) && ov.offset.Valid {
285+
latestVersion = verKey
286+
val = ov.offset
287+
}
288+
}
289+
290+
return val, latestVersion
291+
}
292+
262293
// Put sets the offset value for ver. If an offset for ver is already known
263294
// (i.e. ver.Equal(other) == true), this will overwrite that value.
264295
func (o *Offsets) Put(ver *version.Version, offset OffsetKey) {
@@ -281,6 +312,28 @@ func (o *Offsets) Put(ver *version.Version, offset OffsetKey) {
281312
}
282313
}
283314

315+
func (v verKey) GreaterThan(other verKey) bool {
316+
if v.major != other.major {
317+
return v.major > other.major
318+
}
319+
if v.minor != other.minor {
320+
return v.minor > other.minor
321+
}
322+
if v.patch != other.patch {
323+
return v.patch > other.patch
324+
}
325+
return false
326+
}
327+
328+
func (v verKey) ToVersion() *version.Version {
329+
vs := fmt.Sprintf("%d.%d.%d", v.major, v.minor, v.patch)
330+
if v.prerelease != "" {
331+
vs += "-" + v.prerelease
332+
}
333+
ver, _ := version.NewVersion(vs)
334+
return ver
335+
}
336+
284337
func (o *Offsets) index() map[OffsetKey][]*version.Version {
285338
o.mu.RLock()
286339
defer o.mu.RUnlock()

‎internal/pkg/structfield/structfield_test.go

+22
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,10 @@ func TestOffsets(t *testing.T) {
5353
assert.True(t, ok, "did not get 1.2.1")
5454
assert.Equal(t, OffsetKey{Offset: 2, Valid: true}, off, "invalid value for 1.2.1")
5555

56+
off, ver := o.getLatest()
57+
assert.Equal(t, v121, ver.ToVersion(), "invalid version for latest")
58+
assert.Equal(t, OffsetKey{Offset: 2, Valid: true}, off, "invalid value for latest")
59+
5660
o.Put(v120, OffsetKey{Offset: 1, Valid: true})
5761
off, ok = o.Get(v120)
5862
assert.True(t, ok, "did not get 1.2.0 after reset")
@@ -123,3 +127,21 @@ func TestIndexUnmarshalJSON(t *testing.T) {
123127
require.NoError(t, json.NewDecoder(f).Decode(&got))
124128
assert.Equal(t, index, &got)
125129
}
130+
131+
func TestGetLatestOffsetFromIndex(t *testing.T) {
132+
off, ver := index.GetLatestOffset(NewID("std", "net/http", "Request", "Method"))
133+
assert.Equal(t, v130, ver, "invalid version for Request.Method")
134+
assert.Equal(t, OffsetKey{Offset: 1, Valid: true}, off, "invalid value for Request.Method")
135+
136+
off, ver = index.GetLatestOffset(NewID("std", "net/http", "Request", "URL"))
137+
assert.Equal(t, v130, ver, "invalid version for Request.URL")
138+
assert.Equal(t, OffsetKey{Offset: 2, Valid: true}, off, "invalid value for Request.URL")
139+
140+
off, ver = index.GetLatestOffset(NewID("std", "net/http", "Response", "Status"))
141+
assert.Equal(t, v120, ver, "invalid version for Response.Status")
142+
assert.Equal(t, OffsetKey{Offset: 0, Valid: true}, off, "invalid value for Response.Status")
143+
144+
off, ver = index.GetLatestOffset(NewID("google.golang.org/grpc", "google.golang.org/grpc", "ClientConn", "target"))
145+
assert.Equal(t, v120, ver, "invalid version for ClientConn.target")
146+
assert.Equal(t, OffsetKey{Offset: 0, Valid: true}, off, "invalid value for ClientConn.target")
147+
}

0 commit comments

Comments
 (0)
Please sign in to comment.