Skip to content

Commit b916542

Browse files
committed
add cgroup_id_map config to sync cgroup id to ebpf programs
For some usecases, it's useful for ebpf programs to filter their metrics based on cgroups. It's generally hard to ebpf program to be able to filter cgroup at runtime. This make it easier by allowing ebpf exporter to update known interesting cgroup id at runtime via a shared BPF map.
1 parent c409de2 commit b916542

File tree

10 files changed

+214
-23
lines changed

10 files changed

+214
-23
lines changed

.vscode/config-schema.yaml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,16 @@ properties:
7171
type: number
7272
labels:
7373
$ref: "#/definitions/labels"
74+
cgroup_id_map:
75+
type: object
76+
additionalProperties: false
77+
properties:
78+
name:
79+
type: string
80+
regexps:
81+
type: array
82+
items:
83+
type: string
7484
tracing:
7585
type: object
7686
additionalProperties: false

cgroup/fanotify.go

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -63,9 +63,8 @@ func newFanotifyMonitor(path string) (*fanotifyMonitor, error) {
6363
}
6464

6565
go func() {
66-
if err := m.readFanotifyLoop(); err != nil {
67-
log.Fatalf("Error running fanotify loop: %v", err)
68-
}
66+
err := m.readFanotifyLoop()
67+
log.Fatalf("Fanotify loop terminated with err:%v", err)
6968
}()
7069

7170
return m, nil
@@ -199,7 +198,7 @@ func (m *fanotifyMonitor) Resolve(id int) string {
199198
return m.observer.lookup(id)
200199
}
201200

202-
func (m *fanotifyMonitor) SubscribeCgroupChange(ch chan<- CgroupChange) error {
201+
func (m *fanotifyMonitor) SubscribeCgroupChange(ch chan<- ChangeNotification) error {
203202
return m.observer.subscribeCgroupChange(ch)
204203
}
205204

cgroup/monitor.go

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,17 +5,20 @@ import (
55
"log"
66
)
77

8-
var ErrCgroupIdMapUnsupported = errors.New("cgroup change subscription failed (fanotify not available)")
8+
// ErrCgroupIDMapUnsupported is returned when cgroup id map is not available
9+
var ErrCgroupIDMapUnsupported = errors.New("cgroup change subscription failed (fanotify not available)")
910

10-
type CgroupChange struct {
11-
Id int
11+
// ChangeNotification is the notification returned by cgroup monitor when a subscribed
12+
// cgroup has been added or removed
13+
type ChangeNotification struct {
14+
ID int
1215
Path string
1316
Remove bool
1417
}
1518

1619
type monitor interface {
1720
Resolve(id int) string
18-
SubscribeCgroupChange(chan<- CgroupChange) error
21+
SubscribeCgroupChange(ch chan<- ChangeNotification) error
1922
}
2023

2124
// Monitor resolves cgroup ids into their respective paths
@@ -45,6 +48,8 @@ func (m *Monitor) Resolve(id int) string {
4548
return m.inner.Resolve(id)
4649
}
4750

48-
func (m *Monitor) SubscribeCgroupChange(ch chan<- CgroupChange) error {
51+
// SubscribeCgroupChange receives cgroup change notifications. This requires
52+
// kernel with fanotify support for cgroup
53+
func (m *Monitor) SubscribeCgroupChange(ch chan<- ChangeNotification) error {
4954
return m.inner.SubscribeCgroupChange(ch)
5055
}

cgroup/observer.go

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -19,15 +19,15 @@ type observer struct {
1919
lock sync.Mutex
2020
inodeToPath map[int]*resolved
2121
pathToInode map[string]int
22-
cgroupChans []chan<- CgroupChange
22+
cgroupChans []chan<- ChangeNotification
2323
}
2424

2525
func newObserver(initial map[int]string) *observer {
2626
observer := observer{
2727
lock: sync.Mutex{},
2828
inodeToPath: map[int]*resolved{},
2929
pathToInode: map[string]int{},
30-
cgroupChans: []chan<- CgroupChange{},
30+
cgroupChans: []chan<- ChangeNotification{},
3131
}
3232

3333
for inode, name := range initial {
@@ -86,7 +86,7 @@ func (o *observer) add(inode int, path string) {
8686
o.inodeToPath[inode] = r
8787
o.pathToInode[path] = inode
8888
for _, ch := range o.cgroupChans {
89-
ch <- CgroupChange{
89+
ch <- ChangeNotification{
9090
ID: inode,
9191
Path: path,
9292
}
@@ -105,7 +105,7 @@ func (o *observer) remove(path string) {
105105
r := o.inodeToPath[inode]
106106
r.dead = time.Now()
107107
for _, ch := range o.cgroupChans {
108-
ch <- CgroupChange{
108+
ch <- ChangeNotification{
109109
ID: inode,
110110
Path: path,
111111
Remove: true,
@@ -129,12 +129,12 @@ func (o *observer) lookup(inode int) string {
129129
return r.path
130130
}
131131

132-
func (o *observer) subscribeCgroupChange(ch chan<- CgroupChange) error {
132+
func (o *observer) subscribeCgroupChange(ch chan<- ChangeNotification) error {
133133
o.cgroupChans = append(o.cgroupChans, ch)
134134
// send the initial cgroup mapping
135135
go func() {
136136
for path, inode := range o.pathToInode {
137-
ch <- CgroupChange{
137+
ch <- ChangeNotification{
138138
ID: inode,
139139
Path: path,
140140
}

cgroup/walker.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ func (m *walkerMonitor) Resolve(id int) string {
4545
return m.mapping[id]
4646
}
4747

48-
func (m *walkerMonitor) SubscribeCgroupChange(_ chan<- CgroupChange) error {
48+
func (m *walkerMonitor) SubscribeCgroupChange(_ chan<- ChangeNotification) error {
4949
return ErrCgroupIDMapUnsupported
5050
}
5151

config/config.go

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,12 @@ import (
1111

1212
// Config describes how to configure and extract metrics
1313
type Config struct {
14-
Name string `yaml:"name"`
15-
Metrics Metrics `yaml:"metrics"`
16-
Tracing Tracing `yaml:"tracing"`
17-
Kaddrs []string `yaml:"kaddrs"`
18-
BPFPath string
14+
Name string `yaml:"name"`
15+
Metrics Metrics `yaml:"metrics"`
16+
Tracing Tracing `yaml:"tracing"`
17+
Kaddrs []string `yaml:"kaddrs"`
18+
CgroupIDMap CgroupIDMap `yaml:"cgroup_id_map"`
19+
BPFPath string
1920
}
2021

2122
// Metrics is a collection of metrics attached to a program
@@ -45,6 +46,14 @@ type Histogram struct {
4546
Labels []Label `yaml:"labels"`
4647
}
4748

49+
// CgroupIDMap describes the cgroup that the bpf programs are interested in.
50+
// The cgroups that match the provided regexps will be available to the bpf program
51+
// as a shared map with provided name.
52+
type CgroupIDMap struct {
53+
Name string `yaml:"name"`
54+
Regexps []string `yaml:"regexps"`
55+
}
56+
4857
// Tracing is a collection of spans attached to a program
4958
type Tracing struct {
5059
Spans []Span `yaml:"spans"`

examples/cgroup_id_map.bpf.c

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
#include <vmlinux.h>
2+
#include <bpf/bpf_tracing.h>
3+
#include "maps.bpf.h"
4+
5+
struct {
6+
__uint(type, BPF_MAP_TYPE_LRU_HASH);
7+
__uint(max_entries, 1024);
8+
__type(key, u64);
9+
__type(value, u64);
10+
} cgroup_sched_migrations_total SEC(".maps");
11+
12+
struct {
13+
__uint(type, BPF_MAP_TYPE_LRU_HASH);
14+
__uint(max_entries, 1024);
15+
__type(key, u64);
16+
__type(value, u64);
17+
} cgroup_sched_migrations_not_match_total SEC(".maps");
18+
19+
struct {
20+
__uint(type, BPF_MAP_TYPE_LRU_HASH);
21+
__uint(max_entries, 1024);
22+
__type(key, u64);
23+
__type(value, u64);
24+
} cgroup_id_map SEC(".maps");
25+
26+
SEC("tp_btf/sched_migrate_task")
27+
int BPF_PROG(sched_migrate_task)
28+
{
29+
u64 *ok;
30+
u64 cgroup_id = bpf_get_current_cgroup_id();
31+
ok = bpf_map_lookup_elem(&cgroup_id_map, &cgroup_id);
32+
if (ok) {
33+
increment_map(&cgroup_sched_migrations_total, &cgroup_id, 1);
34+
} else {
35+
increment_map(&cgroup_sched_migrations_not_match_total, &cgroup_id, 1);
36+
}
37+
return 0;
38+
}
39+
40+
char LICENSE[] SEC("license") = "GPL";

examples/cgroup_id_map.yaml

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
metrics:
2+
counters:
3+
- name: cgroup_sched_migrations_total
4+
help: Number of sched:sched_migrate_task events per cgroup
5+
labels:
6+
- name: cgroup
7+
size: 8
8+
decoders:
9+
- name: uint
10+
- name: cgroup
11+
12+
- name: cgroup_sched_migrations_not_match_total
13+
help: Number of sched:sched_migrate_task events per cgroup not match cgroup id map
14+
labels:
15+
- name: cgroup
16+
size: 8
17+
decoders:
18+
- name: uint
19+
- name: cgroup
20+
21+
cgroup_id_map:
22+
name: cgroup_id_map
23+
regexps:
24+
- ^.*(system.slice/.*)$

exporter/cgroup_id_map.go

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
package exporter
2+
3+
import (
4+
"fmt"
5+
"log"
6+
"regexp"
7+
"unsafe"
8+
9+
"github.com/aquasecurity/libbpfgo"
10+
"github.com/cloudflare/ebpf_exporter/v2/cgroup"
11+
"github.com/cloudflare/ebpf_exporter/v2/config"
12+
)
13+
14+
// CgroupIDMap synchronises cgroup changes with the shared bpf map.
15+
type CgroupIDMap struct {
16+
bpfMap *libbpfgo.BPFMap
17+
ch chan cgroup.ChangeNotification
18+
cache map[string]*regexp.Regexp
19+
}
20+
21+
func newCgroupIDMap(module *libbpfgo.Module, cfg config.Config) (*CgroupIDMap, error) {
22+
m, err := module.GetMap(cfg.CgroupIDMap.Name)
23+
if err != nil {
24+
return nil, fmt.Errorf("failed to get map %q: %w", cfg.CgroupIDMap.Name, err)
25+
}
26+
27+
keySize := m.KeySize()
28+
if keySize != 8 {
29+
return nil, fmt.Errorf("key size for map %q is not expected 8 bytes (u64), it is %d bytes", cfg.CgroupIDMap.Name, keySize)
30+
}
31+
valueSize := m.ValueSize()
32+
if valueSize != 8 {
33+
return nil, fmt.Errorf("value size for map %q is not expected 8 bytes (u64), it is %d bytes", cfg.CgroupIDMap.Name, valueSize)
34+
}
35+
36+
c := &CgroupIDMap{
37+
bpfMap: m,
38+
ch: make(chan cgroup.ChangeNotification, 10),
39+
cache: map[string]*regexp.Regexp{},
40+
}
41+
42+
for _, expr := range cfg.CgroupIDMap.Regexps {
43+
if _, ok := c.cache[expr]; !ok {
44+
compiled, err := regexp.Compile(expr)
45+
if err != nil {
46+
return nil, fmt.Errorf("error compiling regexp %q: %w", expr, err)
47+
}
48+
c.cache[expr] = compiled
49+
}
50+
}
51+
52+
return c, nil
53+
}
54+
55+
func (c *CgroupIDMap) subscribe(m *cgroup.Monitor) error {
56+
return m.SubscribeCgroupChange(c.ch)
57+
}
58+
59+
func (c *CgroupIDMap) runLoop() {
60+
for update := range c.ch {
61+
if update.Remove {
62+
key := uint64(update.ID)
63+
err := c.bpfMap.DeleteKey(unsafe.Pointer(&key))
64+
log.Printf("Error deleting key from CgroupIDMap: %v", err)
65+
} else {
66+
key := uint64(update.ID)
67+
value := uint64(1)
68+
if c.checkMatch(update.Path) {
69+
err := c.bpfMap.Update(unsafe.Pointer(&key), unsafe.Pointer(&value))
70+
log.Printf("Error updating CgroupIDMap: %v", err)
71+
}
72+
}
73+
}
74+
}
75+
76+
func (c *CgroupIDMap) checkMatch(path string) bool {
77+
for _, compiled := range c.cache {
78+
if compiled.MatchString(path) {
79+
return true
80+
}
81+
}
82+
return false
83+
}

exporter/exporter.go

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import (
1515
"unsafe"
1616

1717
"github.com/aquasecurity/libbpfgo"
18+
"github.com/cloudflare/ebpf_exporter/v2/cgroup"
1819
"github.com/cloudflare/ebpf_exporter/v2/config"
1920
"github.com/cloudflare/ebpf_exporter/v2/decoder"
2021
"github.com/cloudflare/ebpf_exporter/v2/tracing"
@@ -36,8 +37,9 @@ var percpuMapTypes = map[libbpfgo.MapType]struct{}{
3637

3738
// Exporter is a ebpf_exporter instance implementing prometheus.Collector
3839
type Exporter struct {
39-
configs []config.Config
40-
modules map[string]*libbpfgo.Module
40+
configs []config.Config
41+
modules map[string]*libbpfgo.Module
42+
4143
perfEventArrayCollectors []*perfEventArraySink
4244
kaddrs map[string]uint64
4345
enabledConfigsDesc *prometheus.Desc
@@ -238,12 +240,31 @@ func (e *Exporter) attachConfig(ctx context.Context, cfg config.Config) error {
238240
return fmt.Errorf("error validating maps for config %q: %w", cfg.Name, err)
239241
}
240242

243+
// attach cgroup id map if exists
244+
if len(cfg.CgroupIDMap.Name) > 0 {
245+
if err := e.attachCgroupIDMap(module, cfg); err != nil {
246+
return err
247+
}
248+
}
249+
241250
e.attachedProgs[cfg.Name] = attachments
242251
e.modules[cfg.Name] = module
243252

244253
return nil
245254
}
246255

256+
func (e *Exporter) attachCgroupIDMap(module *libbpfgo.Module, cfg config.Config) error {
257+
cgMap, err := newCgroupIDMap(module, cfg)
258+
if err != nil {
259+
return err
260+
}
261+
if err := cgMap.subscribe(e.cgroupMonitor); err != nil {
262+
return err
263+
}
264+
go cgMap.runLoop()
265+
return nil
266+
}
267+
247268
// Detach detaches bpf programs and maps for exiting
248269
func (e *Exporter) Detach() {
249270
e.activeMutex.Lock()

0 commit comments

Comments
 (0)