Skip to content

Commit 7ccd523

Browse files
committed
Add support for complex counter values
This allows having a single map value to support multiple counters: ivan@vm:~$ curl -s http://localhost:9435/metrics | fgrep block_rq # HELP ebpf_exporter_block_rq_completed_bytes_total Total number of bytes served by block requests completions # TYPE ebpf_exporter_block_rq_completed_bytes_total counter ebpf_exporter_block_rq_completed_bytes_total{device="nvme0n1"} 258048 ebpf_exporter_block_rq_completed_bytes_total{device="nvme1n1"} 966656 # HELP ebpf_exporter_block_rq_completions_total Total number of block request completions # TYPE ebpf_exporter_block_rq_completions_total counter ebpf_exporter_block_rq_completions_total{device="nvme0n1"} 35 ebpf_exporter_block_rq_completions_total{device="nvme1n1"} 103 ebpf_exporter_ebpf_program_info{config="complex-value",id="72",program="block_rq_complete",tag="34d2100b313409cd"} 1
1 parent d1b9802 commit 7ccd523

File tree

4 files changed

+148
-20
lines changed

4 files changed

+148
-20
lines changed

config/config.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ type Counter struct {
4040
PerfEventArray bool `yaml:"perf_event_array"`
4141
FlushInterval time.Duration `yaml:"flush_interval"`
4242
Labels []Label `yaml:"labels"`
43+
Values []Value `yaml:"values"`
4344
}
4445

4546
// Histogram is a metric defining prometheus histogram
@@ -70,6 +71,12 @@ type Decoder struct {
7071
AllowUnknown bool `yaml:"allow_unknown"`
7172
}
7273

74+
// Value describes a metric in when it's split across multiple u64
75+
type Value struct {
76+
Name string `yaml:"name"`
77+
Help string `yaml:"help"`
78+
}
79+
7380
// HistogramBucketType is an enum to define how to interpret histogram
7481
type HistogramBucketType string
7582

examples/complex-value.bpf.c

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
#include <vmlinux.h>
2+
#include <bpf/bpf_helpers.h>
3+
#include <bpf/bpf_core_read.h>
4+
#include <bpf/bpf_tracing.h>
5+
6+
#define MKDEV(ma, mi) ((mi & 0xff) | (ma << 8) | ((mi & ~0xff) << 12))
7+
8+
/**
9+
* commit d152c682f03c ("block: add an explicit ->disk backpointer to the
10+
* request_queue") and commit f3fa33acca9f ("block: remove the ->rq_disk
11+
* field in struct request") make some changes to `struct request` and
12+
* `struct request_queue`. Now, to get the `struct gendisk *` field in a CO-RE
13+
* way, we need both `struct request` and `struct request_queue`.
14+
* see:
15+
* https://github.com/torvalds/linux/commit/d152c682f03c
16+
* https://github.com/torvalds/linux/commit/f3fa33acca9f
17+
*/
18+
struct request_queue___x {
19+
struct gendisk *disk;
20+
} __attribute__((preserve_access_index));
21+
22+
struct request___x {
23+
struct request_queue___x *q;
24+
struct gendisk *rq_disk;
25+
} __attribute__((preserve_access_index));
26+
27+
struct key_t {
28+
u32 dev;
29+
};
30+
31+
struct value_t {
32+
u64 count;
33+
u64 bytes;
34+
};
35+
36+
struct {
37+
__uint(type, BPF_MAP_TYPE_HASH);
38+
__uint(max_entries, 1024);
39+
__type(key, struct key_t);
40+
__type(value, struct value_t);
41+
} block_rq_completions SEC(".maps");
42+
43+
static __always_inline struct gendisk *get_disk(void *request)
44+
{
45+
struct request___x *r = request;
46+
47+
if (bpf_core_field_exists(r->rq_disk))
48+
return r->rq_disk;
49+
return r->q->disk;
50+
}
51+
52+
static struct value_t *get_value(void *map, struct key_t *key)
53+
54+
{
55+
struct value_t *value = bpf_map_lookup_elem(map, key);
56+
if (!value) {
57+
struct value_t zero = { .count = 0, .bytes = 0 };
58+
bpf_map_update_elem(map, key, &zero, BPF_NOEXIST);
59+
value = bpf_map_lookup_elem(map, key);
60+
if (!value) {
61+
return NULL;
62+
}
63+
}
64+
65+
return value;
66+
}
67+
68+
SEC("tp_btf/block_rq_complete")
69+
int BPF_PROG(block_rq_complete, struct request *rq, blk_status_t error, unsigned int nr_bytes)
70+
{
71+
struct gendisk *disk = get_disk(rq);
72+
struct key_t key = { .dev = disk ? MKDEV(disk->major, disk->first_minor) : 0 };
73+
struct value_t *value = get_value(&block_rq_completions, &key);
74+
75+
if (!value) {
76+
return 0;
77+
}
78+
79+
__sync_fetch_and_add(&value->count, 1);
80+
__sync_fetch_and_add(&value->bytes, nr_bytes);
81+
82+
return 0;
83+
}
84+
85+
char LICENSE[] SEC("license") = "GPL";

examples/complex-value.yaml

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
metrics:
2+
counters:
3+
- name: block_rq_completions
4+
help: Block request completions split into count and bytes
5+
labels:
6+
- name: device
7+
size: 4
8+
decoders:
9+
- name: majorminor
10+
values:
11+
- name: block_rq_completions_total
12+
help: Total number of block request completions
13+
- name: block_rq_completed_bytes_total
14+
help: Total number of bytes served by block requests completions

exporter/exporter.go

Lines changed: 42 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -229,7 +229,13 @@ func (e *Exporter) Describe(ch chan<- *prometheus.Desc) {
229229
e.perfEventArrayCollectors = append(e.perfEventArrayCollectors, perfSink)
230230
}
231231

232-
addDescs(cfg.Name, counter.Name, counter.Help, counter.Labels)
232+
if counter.Values != nil {
233+
for _, value := range counter.Values {
234+
addDescs(cfg.Name, value.Name, value.Help, counter.Labels)
235+
}
236+
} else {
237+
addDescs(cfg.Name, counter.Name, counter.Help, counter.Labels)
238+
}
233239
}
234240

235241
for _, histogram := range cfg.Metrics.Histograms {
@@ -298,10 +304,14 @@ func (e *Exporter) collectCounters(ch chan<- prometheus.Metric) {
298304

299305
aggregatedMapValues := aggregateMapValues(mapValues)
300306

301-
desc := e.descs[cfg.Name][counter.Name]
302-
303307
for _, metricValue := range aggregatedMapValues {
304-
ch <- prometheus.MustNewConstMetric(desc, prometheus.CounterValue, metricValue.value, metricValue.labels...)
308+
if counter.Values != nil {
309+
for i, value := range counter.Values {
310+
ch <- prometheus.MustNewConstMetric(e.descs[cfg.Name][value.Name], prometheus.CounterValue, metricValue.value[i], metricValue.labels...)
311+
}
312+
} else {
313+
ch <- prometheus.MustNewConstMetric(e.descs[cfg.Name][counter.Name], prometheus.CounterValue, metricValue.value[0], metricValue.labels...)
314+
}
305315
}
306316
}
307317
}
@@ -351,7 +361,7 @@ func (e *Exporter) collectHistograms(ch chan<- prometheus.Metric) {
351361
break
352362
}
353363

354-
histograms[key].buckets[float64(leUint)] = uint64(metricValue.value)
364+
histograms[key].buckets[float64(leUint)] = uint64(metricValue.value[0])
355365
}
356366

357367
if skip {
@@ -492,29 +502,33 @@ func (e *Exporter) MapsHandler(w http.ResponseWriter, r *http.Request) {
492502
}
493503

494504
func validateMaps(module *libbpfgo.Module, cfg config.Config) error {
495-
maps := []string{}
505+
sizes := map[string]int{}
496506

497507
for _, counter := range cfg.Metrics.Counters {
498508
if counter.Name != "" {
499-
maps = append(maps, counter.Name)
509+
if counter.Values != nil {
510+
sizes[counter.Name] = len(counter.Values) * 8
511+
} else {
512+
sizes[counter.Name] = 8
513+
}
500514
}
501515
}
502516

503517
for _, histogram := range cfg.Metrics.Histograms {
504518
if histogram.Name != "" {
505-
maps = append(maps, histogram.Name)
519+
sizes[histogram.Name] = 8
506520
}
507521
}
508522

509-
for _, name := range maps {
523+
for name, expected := range sizes {
510524
m, err := module.GetMap(name)
511525
if err != nil {
512526
return fmt.Errorf("failed to get map %q: %v", name, err)
513527
}
514528

515529
valueSize := m.ValueSize()
516-
if valueSize != 8 {
517-
return fmt.Errorf("value size for map %q is not expected 8 bytes (u64), it is %d bytes", name, valueSize)
530+
if valueSize != expected {
531+
return fmt.Errorf("value size for map %q is not expected %d bytes (8 bytes per u64 value), it is %d bytes", name, expected, valueSize)
518532
}
519533
}
520534

@@ -540,7 +554,9 @@ func aggregateMapValues(values []metricValue) []aggregatedMetricValue {
540554
value: value.value,
541555
}
542556
} else {
543-
existing.value += value.value
557+
for i := range existing.value {
558+
existing.value[i] += value.value[i]
559+
}
544560
}
545561
}
546562

@@ -604,17 +620,17 @@ func readMapValues(m *libbpfgo.BPFMap, labels []config.Label) ([]metricValue, er
604620
return metricValues, nil
605621
}
606622

607-
func mapValue(m *libbpfgo.BPFMap, key []byte) (float64, error) {
623+
func mapValue(m *libbpfgo.BPFMap, key []byte) ([]float64, error) {
608624
v, err := m.GetValue(unsafe.Pointer(&key[0]))
609625
if err != nil {
610-
return 0.0, err
626+
return []float64{0.0}, err
611627
}
612628

613629
return decodeValue(v), nil
614630
}
615631

616-
func mapValuePerCPU(m *libbpfgo.BPFMap, key []byte) ([]float64, error) {
617-
values := []float64{}
632+
func mapValuePerCPU(m *libbpfgo.BPFMap, key []byte) ([][]float64, error) {
633+
values := [][]float64{}
618634

619635
size := m.ValueSize()
620636
value := make([]byte, size*runtime.NumCPU())
@@ -631,8 +647,14 @@ func mapValuePerCPU(m *libbpfgo.BPFMap, key []byte) ([]float64, error) {
631647
}
632648

633649
// Assuming counter's value type is always u64
634-
func decodeValue(value []byte) float64 {
635-
return float64(util.GetHostByteOrder().Uint64(value))
650+
func decodeValue(value []byte) []float64 {
651+
values := make([]float64, len(value)/8)
652+
653+
for i := range values {
654+
values[i] = float64(util.GetHostByteOrder().Uint64(value[i*8:]))
655+
}
656+
657+
return values
636658
}
637659

638660
// metricValue is a row in a kernel map
@@ -642,13 +664,13 @@ type metricValue struct {
642664
// labels are decoded from the raw key
643665
labels []string
644666
// value is the kernel map value
645-
value float64
667+
value []float64
646668
}
647669

648670
// aggregatedMetricValue is a value after aggregation of equal label sets
649671
type aggregatedMetricValue struct {
650672
// labels are decoded from the raw key
651673
labels []string
652674
// value is the kernel map value
653-
value float64
675+
value []float64
654676
}

0 commit comments

Comments
 (0)