Skip to content

Commit

Permalink
Implement bpf-map-pressure-exporter
Browse files Browse the repository at this point in the history
Signed-off-by: naoki-take <[email protected]>
  • Loading branch information
tkna committed Nov 20, 2023
1 parent 8b5ea21 commit f99b90f
Show file tree
Hide file tree
Showing 18 changed files with 714 additions and 0 deletions.
2 changes: 2 additions & 0 deletions bpf-map-pressure-exporter/.dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
*
!src
13 changes: 13 additions & 0 deletions bpf-map-pressure-exporter/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# bpf-map-pressure-exporter container

# Stage1: build from source
FROM quay.io/cybozu/golang:1.21-jammy AS build
COPY src /work/src
WORKDIR /work/src
RUN CGO_ENABLED=0 go build -ldflags="-w -s" -o bpf-map-pressure-exporter

# Stage2: setup runtime container
FROM scratch
COPY --from=build /work/src/bpf-map-pressure-exporter /
EXPOSE 8080/tcp
ENTRYPOINT ["/bpf-map-pressure-exporter"]
44 changes: 44 additions & 0 deletions bpf-map-pressure-exporter/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
bpf-map-pressure-exporter
===================

`bpf-map-pressure-exporter` exposes BPF map pressure.

## Config
Default config file is `/etc/bpf-map-pressure-exporter/config.yaml`.
The target BPF maps should be specified under `mapNames`.
```
mapNames:
- cilium_ct
- ...
```

`mapNames` are interpreted as substrings of the map names and the pressure metrics for all maps including the substring are exposed.
If multiple `mapNames` are specified and some of them match the same map, only 1 metric is exposed.
Note that BPF map names are truncated to 15 characters.

Command-line options are:

| Option | Default value | Description |
| -------------- | ----------------------------------------------- | ------------------------------------ |
| `port` | `8080` | port number to export metrics |
| `config` | `/etc/bpf-map-pressure-exporter/config.yaml` | config file path |

API endpoints are:

| Path | Description |
| -------- | --------------------------- |
| /health | the path for liveness probe |
| /metrics | exporting metrics |

## Prometheus metrics

`bpf-map-pressure-exporter` exposes the following metrics.

### `bpf_map_pressure`

`bpf_map_pressure` is a gauge that indicates the BPF map pressure.

| Label | Description |
| ---------- | -------------------------- |
| `map_id` | ID of the BPF map |
| `map_name` | name of the BPF map |
1 change: 1 addition & 0 deletions bpf-map-pressure-exporter/TAG
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
0.1.0
19 changes: 19 additions & 0 deletions bpf-map-pressure-exporter/src/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
SUDO = sudo

.PHONY: all
all: check-generate test

.PHONY: check-generate
check-generate:
go mod tidy
git diff --exit-code --name-only

.PHONY: test
test:
test -z "$$(gofmt -s -l . | tee /dev/stderr)"
staticcheck ./...
test -z "$$(custom-checker -restrictpkg.packages=html/template,log ./... 2>&1 | tee /dev/stderr)"
go vet ./...
go test -c ./...
$(SUDO) ./bpf-map-pressure-exporter.test -test.v
rm -f ./bpf-map-pressure-exporter.test
40 changes: 40 additions & 0 deletions bpf-map-pressure-exporter/src/collector.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package main

import (
"strconv"

"github.com/prometheus/client_golang/prometheus"
)

type bpfMapPressureCollector struct {
describe *prometheus.Desc
fetcher IBpfMapPressureFetcher
}

func newCollector(fetcher IBpfMapPressureFetcher) *bpfMapPressureCollector {
return &bpfMapPressureCollector{
describe: prometheus.NewDesc(
"bpf_map_pressure",
"bpf map pressure",
[]string{"map_id", "map_name"},
nil,
),
fetcher: fetcher,
}
}

func (c *bpfMapPressureCollector) Describe(ch chan<- *prometheus.Desc) {
ch <- c.describe
}

func (c *bpfMapPressureCollector) Collect(ch chan<- prometheus.Metric) {
results := c.fetcher.Fetch()
for _, result := range results {
ch <- prometheus.MustNewConstMetric(
c.describe,
prometheus.GaugeValue,
result.mapPressure,
strconv.FormatUint(uint64(result.mapId), 10), result.mapName,
)
}
}
92 changes: 92 additions & 0 deletions bpf-map-pressure-exporter/src/collector_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
package main

import (
"strings"
"testing"

"github.com/prometheus/client_golang/prometheus/testutil"
"github.com/stretchr/testify/assert"
)

type mockBpfMapPressureFetcher struct {
fetchFunc func() []bpfMapPressure
}

func (f *mockBpfMapPressureFetcher) Fetch() []bpfMapPressure {
return f.fetchFunc()
}

func TestBpfMapPressureCollector(t *testing.T) {
cases := []struct {
name string
fetcher IBpfMapPressureFetcher
expect string
}{
{
name: "success",
fetcher: &mockBpfMapPressureFetcher{
fetchFunc: func() []bpfMapPressure {
return []bpfMapPressure{
{
mapId: 1,
mapName: "cilium_test_1",
mapPressure: 0.1,
},
{
mapId: 2,
mapName: "cilium_test_2",
mapPressure: 0.2,
},
}
},
},
expect: `# HELP bpf_map_pressure bpf map pressure
# TYPE bpf_map_pressure gauge
bpf_map_pressure{map_id="1",map_name="cilium_test_1"} 0.1
bpf_map_pressure{map_id="2",map_name="cilium_test_2"} 0.2
`,
},
{
name: "duplicate maps",
fetcher: &mockBpfMapPressureFetcher{
fetchFunc: func() []bpfMapPressure {
return []bpfMapPressure{
{
mapId: 1,
mapName: "cilium_test_1",
mapPressure: 0.1,
},
{
mapId: 1,
mapName: "cilium_test_2",
mapPressure: 0.1,
},
}
},
},
expect: `# HELP bpf_map_pressure bpf map pressure
# TYPE bpf_map_pressure gauge
bpf_map_pressure{map_id="1",map_name="cilium_test_1"} 0.1
bpf_map_pressure{map_id="1",map_name="cilium_test_1"} 0.1
`,
},
{
name: "no maps",
fetcher: &mockBpfMapPressureFetcher{
fetchFunc: func() []bpfMapPressure {
return []bpfMapPressure{}
},
},
expect: `# HELP bpf_map_pressure bpf map pressure
# TYPE bpf_map_pressure gauge
`,
},
}

for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
err := testutil.CollectAndCompare(newCollector(tc.fetcher), strings.NewReader(tc.expect), tc.name)
assert.NoError(t, err)
})
}
}
31 changes: 31 additions & 0 deletions bpf-map-pressure-exporter/src/config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package main

import (
"os"

"github.com/go-yaml/yaml"
)

const (
DefaultConfigPath = "/etc/bpf-map-pressure-exporter/config.yaml"
)

type Config struct {
MapNames []string `yaml:"mapNames"`
}

func loadConfig(path string) (*Config, error) {
if path == "" {
path = DefaultConfigPath
}
f, err := os.Open(path)
if err != nil {
return nil, err
}
defer f.Close()
var cfg Config
if err := yaml.NewDecoder(f).Decode(&cfg); err != nil {
return nil, err
}
return &cfg, nil
}
46 changes: 46 additions & 0 deletions bpf-map-pressure-exporter/src/config_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package main

import (
"testing"

"github.com/stretchr/testify/assert"
)

func TestLoadConfig(t *testing.T) {
cases := []struct {
name string
path string
expect *Config
expectError bool
}{
{
name: "success",
path: "testdata/config.yaml",
expect: &Config{
MapNames: []string{"hoge", "fuga", "piyo"},
},
},
{
name: "file not found",
path: "testdata/notfound.yaml",
expectError: true,
},
{
name: "invalid yaml",
path: "testdata/invalid.yaml",
expectError: true,
},
}

for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
cfg, err := loadConfig(tc.path)
if tc.expectError {
assert.Error(t, err)
return
}
assert.NoError(t, err)
assert.Equal(t, tc.expect.MapNames, cfg.MapNames)
})
}
}
82 changes: 82 additions & 0 deletions bpf-map-pressure-exporter/src/fetcher.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
package main

import (
"errors"
"os"
"strings"

"github.com/cilium/ebpf"
"github.com/cybozu-go/log"
)

type IBpfMapPressureFetcher interface {
Fetch() []bpfMapPressure
}

type bpfMapPressureFetcher struct {
mapNameStrings []string
}

type bpfMapPressure struct {
mapId uint32
mapName string
mapPressure float64
}

func newFetcher(mapNameStrings []string) *bpfMapPressureFetcher {
return &bpfMapPressureFetcher{
mapNameStrings: mapNameStrings,
}
}

func (f *bpfMapPressureFetcher) Fetch() []bpfMapPressure {
results := []bpfMapPressure{}
var id ebpf.MapID = 0
var err error
for {
id, err = ebpf.MapGetNextID(id)
if errors.Is(err, os.ErrNotExist) {
break
}
if err != nil {
_ = logger.Warn("failed to get next map id", map[string]interface{}{
"id": id,
log.FnError: err,
})
return nil
}
m, err := ebpf.NewMapFromID(id)
if err != nil {
_ = logger.Warn("failed to get map", map[string]interface{}{
"id": id,
log.FnError: err,
})
return nil
}
info, err := m.Info()
if err != nil {
_ = logger.Warn("failed to get map info", map[string]interface{}{
"id": id,
log.FnError: err,
})
return nil
}
for _, str := range f.mapNameStrings {
if strings.Contains(info.Name, str) {
itr := m.Iterate()
var key, value []byte
cnt := 0
for itr.Next(&key, &value) {
cnt++
}
results = append(results, bpfMapPressure{
mapId: uint32(id),
mapName: info.Name,
mapPressure: float64(cnt) / float64(info.MaxEntries),
})
break
}
}
}
return results
}
Loading

0 comments on commit f99b90f

Please sign in to comment.