Skip to content

Commit 146a48b

Browse files
committed
Review feedback
1 parent c86c9d6 commit 146a48b

12 files changed

Lines changed: 278 additions & 185 deletions

File tree

.github/workflows/build.yml

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -37,10 +37,7 @@ jobs:
3737
with:
3838
go-version: '~1.25'
3939

40-
# The goreleaser-cross image is Debian-based and ships clang, but not
41-
# libbpf-dev or linux-libc-dev. Install them so the goreleaser
42-
# `before:` hook (make probes-bpf-all) can compile the probes BPF
43-
# objects that //go:embed bpf needs.
40+
# BPF dependencies for probes feature
4441
- name: Install BPF build dependencies
4542
run: |
4643
apt-get update

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@
66
/out
77
/bin
88
*.bpf.o
9+
# bpf2go output: .o is regenerated by `make probes-bpf`; the .go wrapper is committed.
10+
probes/probe_bpfel.o
11+
probes/probe_bpfeb.o
912
probes/bpf/*.ll
1013
TODO.md
1114
minikube-*

.goreleaser.yml

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
version: 2
22
env:
33
- CGO_ENABLED=1
4-
# Build the BPF object before goreleaser builds the Go binaries.
5-
# //go:embed bpf needs probe.bpf.o present on disk at `go build` time;
6-
# goreleaser cross-compiles Go but doesn't otherwise know about our BPF
7-
# sidecar. Requires clang + libbpf-dev + linux-libc-dev in the build
8-
# environment. One object covers every Go target arch -- our BPF code is
9-
# arch-agnostic at the bytecode level.
4+
# Regenerate the bpf2go-emitted BPF loader + object before goreleaser builds
5+
# the Go binaries. probes/probe_bpfel.go uses `//go:embed probe_bpfel.o` and
6+
# needs the .o present on disk at `go build` time; goreleaser cross-compiles
7+
# Go but doesn't otherwise know about our BPF sidecar. Requires clang +
8+
# libbpf-dev + linux-libc-dev in the build environment. One object covers
9+
# every Go target arch -- our BPF code is arch-agnostic at the bytecode level.
1010
before:
1111
hooks:
1212
- make probes-bpf

Makefile

Lines changed: 11 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@
33
GOARCH ?= $(shell go env GOARCH)
44
CLANG ?= clang
55

6-
PROBES_BPF_OBJ := probes/bpf/probe.bpf.o
6+
PROBES_BPF_GO := probes/probe_bpfel.go
7+
PROBES_BPF_OBJ := probes/probe_bpfel.o
78

89
# On Debian-based systems (including the goreleaser-cross container)
910
# `linux-libc-dev` installs arch-specific headers under
@@ -19,26 +20,22 @@ BPF_CFLAGS := $(if $(MULTIARCH),-I/usr/include/$(MULTIARCH))
1920

2021
all: crossbuild
2122

22-
# BPF compilation is host-architecture-independent: clang -target bpf emits
23-
# the same bytecode regardless of where it runs. Our BPF program doesn't
24-
# touch any arch-specific macros (PT_REGS_*, etc.), so a single object
25-
# loads on amd64 and arm64 alike -- the embedded loader picks it without
26-
# consulting runtime.GOARCH.
23+
# BPF compilation runs through cilium/ebpf's bpf2go (driven by `go generate`
24+
# via probes/gen.go). bpf2go compiles probe.bpf.c with clang, parses the
25+
# resulting BTF, and emits a Go file mirroring the C structs + a loader.
2726
#
28-
# If a future change adds PT_REGS_PARM1(ctx) or similar, the file will need
29-
# to be compiled separately per arch with the matching -D__TARGET_ARCH_*
30-
# flag; at that point this rule should grow per-target variables again.
27+
# We restrict to `-target bpfel` because parca-agent only ships amd64/arm64
28+
# (both little-endian). Our BPF program touches no arch-specific macros
29+
# (PT_REGS_*, etc.), so a single bpfel object loads on both arches.
3130
#
3231
# Requires clang + libbpf headers (libbpf-dev / libbpf-devel) + kernel UAPI
3332
# headers (linux-libc-dev / kernel-headers) on the host. No cross-toolchain
3433
# needed.
3534
probes-bpf: $(PROBES_BPF_OBJ)
3635

37-
$(PROBES_BPF_OBJ): probes/bpf/probe.bpf.c
38-
$(CLANG) -O2 -g -target bpf \
39-
$(BPF_CFLAGS) \
40-
-Wall -Werror \
41-
-c $< -o $@
36+
$(PROBES_BPF_GO) $(PROBES_BPF_OBJ): probes/bpf/probe.bpf.c probes/gen.go
37+
BPF2GO_CC=$(CLANG) BPF2GO_CFLAGS="-O2 -g -Wall -Werror $(BPF_CFLAGS)" \
38+
go generate ./probes/
4239

4340
# crossbuild produces both amd64 and arm64 release binaries via goreleaser
4441
# inside its cross container. Goreleaser doesn't invoke make, so we build

flags/flags.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -165,7 +165,7 @@ type Flags struct {
165165

166166
OTLPLogging bool `default:"false" help:"Forward parca-agent's own logrus output to the remote-store as OTLP log records (in addition to local stderr). Requires a remote-store; ignored in offline mode."`
167167

168-
ProbeConfig string `default:"" help:"Path to a YAML file declaring uprobe attachments. When set, parca-agent attaches a uprobe per matching binary and streams probe-fire events to the configured remote-store as OTLP/Arrow logs. Empty disables the feature."`
168+
ProbeConfigFile string `default:"" help:"Path to a YAML file declaring uprobe attachments. When set, parca-agent attaches a uprobe per matching binary and streams probe-fire events to the configured remote-store as OTLP/Arrow logs. Empty disables the feature."`
169169
}
170170

171171
type ExitCode int

main.go

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -444,22 +444,26 @@ func mainWithExitCode() flags.ExitCode {
444444
}
445445
}
446446

447-
if f.ProbeConfig != "" {
447+
if f.ProbeConfigFile != "" {
448448
if grpcConn == nil {
449449
return flags.Failure("--probe-config requires a remote-store; cannot be used in offline mode")
450450
}
451451
probesSvc, err := probes.Start(mainCtx, probes.StartConfig{
452-
ConfigPath: f.ProbeConfig,
452+
ConfigPath: f.ProbeConfigFile,
453453
}, parcaReporter)
454454
if err != nil {
455-
return flags.Failure("Failed to start probes: %v", err)
455+
// Probes are a non-essential feature; a bad config (typo in
456+
// probes.yaml, missing symbol, etc.) shouldn't take the whole
457+
// agent down. Log and continue without probe support.
458+
log.Errorf("probes disabled — failed to start: %v", err)
459+
} else {
460+
parcaReporter.SetProbes(probesSvc)
461+
defer func() {
462+
if err := probesSvc.Close(); err != nil {
463+
log.Warnf("probes: close: %v", err)
464+
}
465+
}()
456466
}
457-
parcaReporter.SetProbes(probesSvc)
458-
defer func() {
459-
if err := probesSvc.Close(); err != nil {
460-
log.Warnf("probes: close: %v", err)
461-
}
462-
}()
463467
}
464468

465469
includeEnvVars := libpf.Set[string]{}

probes/bpf/README.md

Lines changed: 20 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,27 @@
11
# probes BPF
22

3-
`probe.bpf.c` is the kernel-side uprobe program. The compiled output
4-
`probe.bpf.o` is produced by `make probes-bpf` from the parca-agent
5-
repository root and is git-ignored.
3+
`probe.bpf.c` is the kernel-side uprobe program. It is compiled by
4+
cilium/ebpf's `bpf2go` (driven from `../gen.go` via `go generate`), which
5+
emits `../probe_bpfel.go` (a Go mirror of the C structs plus a loader)
6+
and `../probe_bpfel.o` (the BPF bytecode, embedded into the .go file
7+
via `//go:embed`).
68

7-
A single object covers every supported Go architecture. Our BPF program
8-
does not touch any arch-specific macros (`PT_REGS_PARM1` and friends from
9-
`bpf/bpf_tracing.h`), so `clang -target bpf` emits identical bytecode
10-
regardless of host arch and regardless of which `__TARGET_ARCH_*` define
11-
might be in scope. The `runtime.GOARCH`-keyed lookup in `../loader.go`
12-
was deliberately removed to avoid pretending we ship arch-specific
13-
artifacts when we don't.
14-
15-
If a future change adds anything that branches on `__TARGET_ARCH_*` (most
16-
likely a `PT_REGS_PARMn(ctx)` to read a function argument at uprobe
17-
entry), the build will need to split per arch again:
18-
19-
- per-target `BPF_ARCH` variable in the Makefile mapping `amd64 -> x86`
20-
and `arm64 -> arm64`,
21-
- `-D__TARGET_ARCH_$(BPF_ARCH)` passed to clang,
22-
- a `probe.bpf.<GOARCH>` naming scheme,
23-
- a matching `runtime.GOARCH`-keyed embed lookup in `loader.go`.
9+
Run `make probes-bpf` from the repository root to regenerate both files.
10+
The Makefile injects `BPF2GO_CFLAGS` with the multiarch include path
11+
that Debian-derived distros need; on Fedora/RHEL `go generate ./probes/`
12+
works directly.
2413

25-
Git history has the previous form if you need to crib from it.
26-
27-
This README is committed so `//go:embed bpf` in `../loader.go` always
28-
has a valid embed target even before any BPF object has been compiled.
14+
A single object covers every supported Go architecture. Our BPF program
15+
does not touch any arch-specific macros (`PT_REGS_PARM1` and friends
16+
from `bpf/bpf_tracing.h`), so `clang -target bpf` emits identical
17+
bytecode regardless of host arch. We restrict bpf2go to `-target bpfel`
18+
because parca-agent only ships amd64/arm64 (both little-endian).
19+
20+
If a future change adds anything that branches on `__TARGET_ARCH_*`
21+
(most likely a `PT_REGS_PARMn(ctx)` to read a function argument at
22+
uprobe entry), the build will need to split per arch again. The bpf2go
23+
invocation in `../gen.go` would grow a second `-target bpfeb` variant
24+
and per-arch `__TARGET_ARCH_*` defines in `BPF2GO_CFLAGS`.
2925

3026
Build dependencies on the host:
3127

probes/bpf/probe.bpf.c

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,9 +46,13 @@ struct probe_event {
4646
__u32 tid;
4747
char comm[16];
4848
__u32 spec_id;
49-
__u32 is_main; // 1 iff tid == tgid
5049
};
5150

51+
// Force probe_event into BTF so bpf2go can mirror it as a Go struct. Without
52+
// this, the type is only referenced from a local in probe_exit and clang's
53+
// BTF emitter elides it, breaking `bpf2go -type probe_event`.
54+
const struct probe_event *_probe_event_btf_force __attribute__((unused));
55+
5256
// scope_stacks: per-tid open-scope state. LRU evicts dead threads without
5357
// requiring a sched_process_exit hook.
5458
struct {
@@ -143,7 +147,6 @@ int probe_exit(struct pt_regs *ctx)
143147
e->pid = tgid;
144148
e->tid = tid;
145149
e->spec_id = cookie_spec_id(cookie);
146-
e->is_main = (tid == tgid) ? 1 : 0;
147150
bpf_get_current_comm(&e->comm, sizeof(e->comm));
148151

149152
bpf_ringbuf_submit(e, 0);

probes/gen.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
//go:build linux
2+
3+
package probes
4+
5+
// probe_bpfel.{go,o} are generated from bpf/probe.bpf.c by cilium/ebpf's
6+
// bpf2go. The Go file is committed; the .o file is embedded into it via
7+
// `go:embed` and is also written next to it so the loader can find it.
8+
//
9+
// To regenerate after editing the C source, run `make probes-bpf` (the
10+
// Makefile injects BPF2GO_CFLAGS with the multiarch include path that
11+
// Debian-derived distros need). On distros without multiarch headers
12+
// (Fedora/RHEL), `go generate ./probes/` works directly.
13+
//
14+
// We restrict to `bpfel` because parca-agent only ships amd64 and arm64
15+
// builds (both little-endian); generating a `bpfeb` variant would add
16+
// dead bytes to the binary.
17+
18+
//go:generate go run github.com/cilium/ebpf/cmd/bpf2go -target bpfel -type probe_event probe bpf/probe.bpf.c

probes/loader.go

Lines changed: 15 additions & 102 deletions
Original file line numberDiff line numberDiff line change
@@ -3,56 +3,17 @@
33
package probes
44

55
import (
6-
"bytes"
7-
"embed"
8-
"encoding/binary"
96
"fmt"
107

11-
"github.com/cilium/ebpf"
128
"github.com/cilium/ebpf/ringbuf"
139
)
1410

15-
//go:embed bpf
16-
var bpfFS embed.FS
17-
18-
// probeEventSize is the on-the-wire size of `struct probe_event` from
19-
// probe.bpf.c. Keep the Go decoder in sync if the C struct changes.
20-
const probeEventSize = 48
21-
22-
// rawProbeEvent is the layout produced by bpf/probe.bpf.c. Decoded from
23-
// little-endian raw bytes; do not change field order without updating
24-
// the C struct.
25-
type rawProbeEvent struct {
26-
KtimeNs uint64
27-
DurationNs uint64
28-
PID uint32
29-
TID uint32
30-
Comm [16]byte
31-
SpecID uint32
32-
IsMain uint32
33-
}
34-
35-
func decodeEvent(b []byte, e *rawProbeEvent) error {
36-
if len(b) < probeEventSize {
37-
return fmt.Errorf("ringbuf record too short: %d bytes", len(b))
38-
}
39-
e.KtimeNs = binary.LittleEndian.Uint64(b[0:8])
40-
e.DurationNs = binary.LittleEndian.Uint64(b[8:16])
41-
e.PID = binary.LittleEndian.Uint32(b[16:20])
42-
e.TID = binary.LittleEndian.Uint32(b[20:24])
43-
copy(e.Comm[:], b[24:40])
44-
e.SpecID = binary.LittleEndian.Uint32(b[40:44])
45-
e.IsMain = binary.LittleEndian.Uint32(b[44:48])
46-
return nil
47-
}
48-
49-
// loadedBPF holds the resources owned by a loaded BPF program.
11+
// loadedBPF holds the resources owned by a loaded BPF program. The
12+
// `objs` struct is the bpf2go-generated container for the maps and
13+
// programs in bpf/probe.bpf.c.
5014
type loadedBPF struct {
51-
coll *ebpf.Collection
52-
progEntry *ebpf.Program
53-
progExit *ebpf.Program
54-
ringbufMap *ebpf.Map
55-
reader *ringbuf.Reader
15+
objs probeObjects
16+
reader *ringbuf.Reader
5617
}
5718

5819
func (l *loadedBPF) Close() error {
@@ -61,74 +22,26 @@ func (l *loadedBPF) Close() error {
6122
}
6223
var firstErr error
6324
if l.reader != nil {
64-
if err := l.reader.Close(); err != nil && firstErr == nil {
25+
if err := l.reader.Close(); err != nil {
6526
firstErr = err
6627
}
6728
}
68-
if l.coll != nil {
69-
l.coll.Close()
29+
if err := l.objs.Close(); err != nil && firstErr == nil {
30+
firstErr = err
7031
}
7132
return firstErr
7233
}
7334

7435
func loadBPF() (*loadedBPF, error) {
75-
blob, err := readEmbeddedBPF()
76-
if err != nil {
77-
return nil, err
78-
}
79-
80-
spec, err := ebpf.LoadCollectionSpecFromReader(bytes.NewReader(blob))
81-
if err != nil {
82-
return nil, fmt.Errorf("load BPF collection spec: %w", err)
36+
var l loadedBPF
37+
if err := loadProbeObjects(&l.objs, nil); err != nil {
38+
return nil, fmt.Errorf("load probe BPF objects: %w", err)
8339
}
84-
85-
coll, err := ebpf.NewCollection(spec)
40+
reader, err := ringbuf.NewReader(l.objs.ProbeEvents)
8641
if err != nil {
87-
return nil, fmt.Errorf("create BPF collection: %w", err)
88-
}
89-
90-
progEntry, ok := coll.Programs["probe_entry"]
91-
if !ok {
92-
coll.Close()
93-
return nil, fmt.Errorf("BPF object missing program 'probe_entry'")
94-
}
95-
progExit, ok := coll.Programs["probe_exit"]
96-
if !ok {
97-
coll.Close()
98-
return nil, fmt.Errorf("BPF object missing program 'probe_exit'")
99-
}
100-
rbMap, ok := coll.Maps["probe_events"]
101-
if !ok {
102-
coll.Close()
103-
return nil, fmt.Errorf("BPF object missing map 'probe_events'")
104-
}
105-
106-
reader, err := ringbuf.NewReader(rbMap)
107-
if err != nil {
108-
coll.Close()
42+
_ = l.objs.Close()
10943
return nil, fmt.Errorf("open ringbuf reader: %w", err)
11044
}
111-
112-
return &loadedBPF{
113-
coll: coll,
114-
progEntry: progEntry,
115-
progExit: progExit,
116-
ringbufMap: rbMap,
117-
reader: reader,
118-
}, nil
119-
}
120-
121-
func readEmbeddedBPF() ([]byte, error) {
122-
// One object covers every arch: clang -target bpf is host-independent
123-
// and our program uses no arch-specific macros (PT_REGS_*, etc.). If
124-
// that changes, this lookup will need a runtime.GOARCH-keyed suffix.
125-
const name = "bpf/probe.bpf.o"
126-
blob, err := bpfFS.ReadFile(name)
127-
if err != nil {
128-
return nil, fmt.Errorf("embedded BPF object %s not found: run `make probes-bpf` (%w)", name, err)
129-
}
130-
if len(blob) < 4 || string(blob[:4]) != "\x7fELF" {
131-
return nil, fmt.Errorf("embedded BPF object %s is not a valid ELF (size=%d): run `make probes-bpf`", name, len(blob))
132-
}
133-
return blob, nil
45+
l.reader = reader
46+
return &l, nil
13447
}

0 commit comments

Comments
 (0)