diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index bf3f1b51..f8251868 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -89,6 +89,11 @@ jobs: - name: Install libelf-dev run: sudo apt-get install -y libelf-dev + - name: Update pci.ids + run: | + curl -o /tmp/pci.ids https://raw.githubusercontent.com/pciutils/pciids/master/pci.ids + sudo mv /tmp/pci.ids /usr/share/misc/pci.ids + - name: Build run: make build diff --git a/README.md b/README.md index 48569c3e..d4a72911 100644 --- a/README.md +++ b/README.md @@ -555,6 +555,28 @@ you need to wrap it with `KPROBE_REGS_IP_FIX()` from `regs-ip.bpf.h`. With major-minor decoder you can turn kernel's combined u32 view of major and minor device numbers into a device name in `/dev`. +### `pci_vendor` + +With `pci_vendor` decoder you can transform PCI vendor IDs like 0x8086 +into human readable vendor names like `Intel Corporation`. + +### `pci_device` + +With `pci_vendor` decoder you can transform PCI vendor IDs like 0x80861000 +into human readable names like `82542 Gigabit Ethernet Controller (Fiber)`. + +Note that the you need to concatenate vendor and device id together for this. + +### `pci_class` + +With `pci_class` decoder you can transform PCI class ID (the lowest byte) into +the class name like `Network controller`. + +### `pci_subclass` + +With `pci_subclass` decoder you can transform PCI subclass (two lowest bytes) +into the subclass name like `Ethernet controller`. + #### `regexp` Regexp decoder takes list of strings from `regexp` configuration key diff --git a/decoder/decoder.go b/decoder/decoder.go index 730cd768..6e1fe15a 100644 --- a/decoder/decoder.go +++ b/decoder/decoder.go @@ -28,16 +28,20 @@ type Set struct { func NewSet() *Set { return &Set{ decoders: map[string]Decoder{ - "cgroup": &CGroup{}, - "ksym": &KSym{}, - "majorminor": &MajorMinor{}, - "regexp": &Regexp{}, - "static_map": &StaticMap{}, - "string": &String{}, - "dname": &Dname{}, - "uint": &UInt{}, - "inet_ip": &InetIP{}, - "syscall": &Syscall{}, + "cgroup": &CGroup{}, + "ksym": &KSym{}, + "majorminor": &MajorMinor{}, + "regexp": &Regexp{}, + "static_map": &StaticMap{}, + "string": &String{}, + "dname": &Dname{}, + "uint": &UInt{}, + "inet_ip": &InetIP{}, + "pci_vendor": &PCIVendor{}, + "pci_device": &PCIDevice{}, + "pci_class": &PCIClass{}, + "pci_subclass": &PCISubClass{}, + "syscall": &Syscall{}, }, } } diff --git a/decoder/pci.go b/decoder/pci.go new file mode 100644 index 00000000..691eb1e4 --- /dev/null +++ b/decoder/pci.go @@ -0,0 +1,27 @@ +package decoder + +import ( + "log" + "os" + + "github.com/jaypipes/pcidb" +) + +const pciIdsPath = "/usr/share/misc/pci.ids" +const missingPciIdsText = "missing pci.ids db" + +var pci *pcidb.PCIDB + +func init() { + if _, err := os.Stat(pciIdsPath); err != nil { + log.Printf("PCI DB path %q is not accessible: %v", pciIdsPath, err) + return + } + + db, err := pcidb.New() + if err != nil { + log.Fatalf("Error initializing PCI DB: %v", err) + } + + pci = db +} diff --git a/decoder/pci_class.go b/decoder/pci_class.go new file mode 100644 index 00000000..0c2fe74a --- /dev/null +++ b/decoder/pci_class.go @@ -0,0 +1,31 @@ +package decoder + +import ( + "fmt" + "strconv" + + "github.com/cloudflare/ebpf_exporter/v2/config" +) + +// PCIClass is a decoder that transforms PCI class id into a name +type PCIClass struct{} + +// Decode transforms PCI class id into a name +func (d *PCIClass) Decode(in []byte, conf config.Decoder) ([]byte, error) { + if pci == nil { + return []byte(missingPciIdsText), nil + } + + num, err := strconv.Atoi(string(in)) + if err != nil { + return nil, err + } + + key := fmt.Sprintf("%02x", num) + + if device, ok := pci.Classes[key]; ok { + return []byte(device.Name), nil + } else { + return []byte(fmt.Sprintf("unknown pci class: 0x%s", key)), nil + } +} diff --git a/decoder/pci_class_test.go b/decoder/pci_class_test.go new file mode 100644 index 00000000..0078c50e --- /dev/null +++ b/decoder/pci_class_test.go @@ -0,0 +1,82 @@ +package decoder + +import ( + "bytes" + "testing" + + "github.com/cloudflare/ebpf_exporter/v2/config" +) + +func TestPCIClassDecoderMissing(t *testing.T) { + if pci != nil { + t.Skip("PCI DB is available") + } + + cases := [][]byte{ + []byte("1"), + []byte("2"), + []byte("6"), + } + + for _, c := range cases { + d := &PCIClass{} + + out, err := d.Decode(c, config.Decoder{}) + if err != nil { + t.Errorf("Error decoding %#v: %v", c, err) + } + + if !bytes.Equal(out, []byte(missingPciIdsText)) { + t.Errorf("Expected %q, got %s", missingPciIdsText, out) + } + } +} + +func TestPCIClassDecoderPresent(t *testing.T) { + if pci == nil { + t.Skip("PCI DB is not available") + } + + cases := []struct { + in []byte + out []byte + }{ + { + in: []byte("1"), + out: []byte("Mass storage controller"), + }, + { + in: []byte("2"), + out: []byte("Network controller"), + }, + { + in: []byte("3"), + out: []byte("Display controller"), + }, + { + in: []byte("6"), + out: []byte("Bridge"), + }, + { + in: []byte("12"), + out: []byte("Serial bus controller"), + }, + { + in: []byte("253"), + out: []byte("unknown pci class: 0xfd"), + }, + } + + for _, c := range cases { + d := &PCIClass{} + + out, err := d.Decode(c.in, config.Decoder{}) + if err != nil { + t.Errorf("Error decoding %#v: %v", c.in, err) + } + + if !bytes.Equal(out, c.out) { + t.Errorf("Expected %q, got %q", c.out, out) + } + } +} diff --git a/decoder/pci_device.go b/decoder/pci_device.go new file mode 100644 index 00000000..7d2aca3a --- /dev/null +++ b/decoder/pci_device.go @@ -0,0 +1,31 @@ +package decoder + +import ( + "fmt" + "strconv" + + "github.com/cloudflare/ebpf_exporter/v2/config" +) + +// PCIDevice2 is a decoder that transforms PCI device id into a name +type PCIDevice struct{} + +// Decode transforms PCI device id into a name +func (d *PCIDevice) Decode(in []byte, conf config.Decoder) ([]byte, error) { + if pci == nil { + return []byte(missingPciIdsText), nil + } + + num, err := strconv.Atoi(string(in)) + if err != nil { + return nil, err + } + + key := fmt.Sprintf("%04x", num) + + if device, ok := pci.Products[key]; ok { + return []byte(device.Name), nil + } else { + return []byte(fmt.Sprintf("unknown pci device: 0x%s", key)), nil + } +} diff --git a/decoder/pci_device_test.go b/decoder/pci_device_test.go new file mode 100644 index 00000000..f31955a3 --- /dev/null +++ b/decoder/pci_device_test.go @@ -0,0 +1,86 @@ +package decoder + +import ( + "bytes" + "testing" + + "github.com/cloudflare/ebpf_exporter/v2/config" +) + +func TestPCIDeviceDecoderMissing(t *testing.T) { + if pci != nil { + t.Skip("PCI DB is available") + } + + cases := [][]byte{ + []byte("2156269568"), // 0x80861000 + []byte("268596191"), // 0x100273df + []byte("282994436"), // 0x10de2704 + } + + for _, c := range cases { + d := &PCIDevice{} + + out, err := d.Decode(c, config.Decoder{}) + if err != nil { + t.Errorf("Error decoding %#v: %v", c, err) + } + + if !bytes.Equal(out, []byte(missingPciIdsText)) { + t.Errorf("Expected %q, got %s", missingPciIdsText, out) + } + } +} + +func TestPCIDeviceDecoderPresent(t *testing.T) { + if pci == nil { + t.Skip("PCI DB is not available") + } + + cases := []struct { + in []byte + out []byte + }{ + { + in: []byte("2156269568"), // 0x80861000 + out: []byte("82542 Gigabit Ethernet Controller (Fiber)"), + }, + { + in: []byte("268596191"), // 0x100273df + out: []byte("Navi 22 [Radeon RX 6700/6700 XT/6750 XT / 6800M/6850M XT]"), + }, + { + in: []byte("282994436"), // 0x10de2704 + out: []byte("AD103 [GeForce RTX 4080]"), + }, + { + in: []byte("364056607"), // 0x15b3101f + out: []byte("MT2894 Family [ConnectX-6 Lx]"), + }, + { + in: []byte("340633610"), // 0x144da80a + out: []byte("NVMe SSD Controller PM9A1/PM9A3/980PRO"), + }, + { + in: []byte("350492180"), // 0x14e41614 + out: []byte("BCM57454 NetXtreme-E 10Gb/25Gb/40Gb/50Gb/100Gb Ethernet"), + }, + { + in: []byte("3735928559"), // 0xdeadbeef + out: []byte("unknown pci device: 0xdeadbeef"), + }, + } + + for _, c := range cases { + d := &PCIDevice{} + + out, err := d.Decode(c.in, config.Decoder{}) + if err != nil { + t.Errorf("Error decoding %#v: %v", c.in, err) + } + + if !bytes.Equal(out, c.out) { + t.Errorf("Expected %q, got %q", c.out, out) + } + } +} diff --git a/decoder/pci_subclass.go b/decoder/pci_subclass.go new file mode 100644 index 00000000..654e68ea --- /dev/null +++ b/decoder/pci_subclass.go @@ -0,0 +1,38 @@ +package decoder + +import ( + "fmt" + "strconv" + + "github.com/cloudflare/ebpf_exporter/v2/config" +) + +// PCISubClass is a decoder that transforms PCI class id into a name +type PCISubClass struct{} + +// Decode transforms PCI class id into a name +func (d *PCISubClass) Decode(in []byte, conf config.Decoder) ([]byte, error) { + if pci == nil { + return []byte(missingPciIdsText), nil + } + + num, err := strconv.Atoi(string(in)) + if err != nil { + return nil, err + } + + classID := fmt.Sprintf("%02x", num>>8) + + if class, ok := pci.Classes[classID]; ok { + subclassID := fmt.Sprintf("%02x", num&0xff) + for _, subclass := range class.Subclasses { + if subclass.ID == subclassID { + return []byte(subclass.Name), nil + } + } + + return []byte(fmt.Sprintf("unknown pci subclass: 0x%s (class 0x%s)", subclassID, classID)), nil + } else { + return []byte(fmt.Sprintf("unknown pci class: 0x%s", classID)), nil + } +} diff --git a/decoder/pci_subclass_test.go b/decoder/pci_subclass_test.go new file mode 100644 index 00000000..349a2137 --- /dev/null +++ b/decoder/pci_subclass_test.go @@ -0,0 +1,98 @@ +package decoder + +import ( + "bytes" + "testing" + + "github.com/cloudflare/ebpf_exporter/v2/config" +) + +func TestPCISubClassDecoderMissing(t *testing.T) { + if pci != nil { + t.Skip("PCI DB is available") + } + + cases := [][]byte{ + []byte("5"), + []byte("264"), + []byte("512"), + } + + for _, c := range cases { + d := &PCISubClass{} + + out, err := d.Decode(c, config.Decoder{}) + if err != nil { + t.Errorf("Error decoding %#v: %v", c, err) + } + + if !bytes.Equal(out, []byte(missingPciIdsText)) { + t.Errorf("Expected %q, got %s", missingPciIdsText, out) + } + } +} + +func TestPCISubClassDecoderPresent(t *testing.T) { + if pci == nil { + t.Skip("PCI DB is not available") + } + + cases := []struct { + in []byte + out []byte + }{ + { + in: []byte("5"), // 0x0005 + out: []byte("Image coprocessor"), + }, + { + in: []byte("264"), // 0x0108 + out: []byte("Non-Volatile memory controller"), + }, + { + in: []byte("512"), // 0x0200 + out: []byte("Ethernet controller"), + }, + { + in: []byte("770"), // 0x0302 + out: []byte("3D controller"), + }, + { + in: []byte("3075"), // 0x0c03 + out: []byte("USB controller"), + }, + { + in: []byte("1536"), // 0x0600 + out: []byte("Host bridge"), + }, + { + in: []byte("1540"), // 0x0604 + out: []byte("PCI bridge"), + }, + { + in: []byte("64768"), // 0xfd00 + out: []byte("unknown pci class: 0xfd"), + }, + { + in: []byte("267"), // 0x010b + out: []byte("unknown pci subclass: 0x0b (class 0x01)"), + }, + { + in: []byte("3"), // 0x0003 + out: []byte("unknown pci subclass: 0x03 (class 0x00)"), + }, + } + + for _, c := range cases { + d := &PCISubClass{} + + out, err := d.Decode(c.in, config.Decoder{}) + if err != nil { + t.Errorf("Error decoding %#v: %v", c.in, err) + } + + if !bytes.Equal(out, c.out) { + t.Errorf("Expected %q, got %q", c.out, out) + } + } +} diff --git a/decoder/pci_vendor.go b/decoder/pci_vendor.go new file mode 100644 index 00000000..5554b7da --- /dev/null +++ b/decoder/pci_vendor.go @@ -0,0 +1,31 @@ +package decoder + +import ( + "fmt" + "strconv" + + "github.com/cloudflare/ebpf_exporter/v2/config" +) + +// PCIVendor is a decoder that transforms PCI vendor id into a name +type PCIVendor struct{} + +// Decode transforms PCI vendor id into a name +func (d *PCIVendor) Decode(in []byte, conf config.Decoder) ([]byte, error) { + if pci == nil { + return []byte(missingPciIdsText), nil + } + + num, err := strconv.Atoi(string(in)) + if err != nil { + return nil, err + } + + key := fmt.Sprintf("%02x", num) + + if vendor, ok := pci.Vendors[key]; ok { + return []byte(vendor.Name), nil + } else { + return []byte(fmt.Sprintf("unknown pci vendor: 0x%s", key)), nil + } +} diff --git a/decoder/pci_vendor_test.go b/decoder/pci_vendor_test.go new file mode 100644 index 00000000..8bad695c --- /dev/null +++ b/decoder/pci_vendor_test.go @@ -0,0 +1,86 @@ +package decoder + +import ( + "bytes" + "testing" + + "github.com/cloudflare/ebpf_exporter/v2/config" +) + +func TestPCIVendorDecoderMissing(t *testing.T) { + if pci != nil { + t.Skip("PCI DB is available") + } + + cases := [][]byte{ + []byte("32902"), // 0x8086 + []byte("4098"), // 0x1002 + []byte("4318"), // 0x10de + } + + for _, c := range cases { + d := &PCIVendor{} + + out, err := d.Decode(c, config.Decoder{}) + if err != nil { + t.Errorf("Error decoding %#v: %v", c, err) + } + + if !bytes.Equal(out, []byte(missingPciIdsText)) { + t.Errorf("Expected %q, got %s", missingPciIdsText, out) + } + } +} + +func TestPCIVendorDecoderPresent(t *testing.T) { + if pci == nil { + t.Skip("PCI DB is not available") + } + + cases := []struct { + in []byte + out []byte + }{ + { + in: []byte("32902"), // 0x8086 + out: []byte("Intel Corporation"), + }, + { + in: []byte("4098"), // 0x1002 + out: []byte("Advanced Micro Devices, Inc. [AMD/ATI]"), + }, + { + in: []byte("4318"), // 0x10de + out: []byte("NVIDIA Corporation"), + }, + { + in: []byte("5555"), // 0x15b3 + out: []byte("Mellanox Technologies"), + }, + { + in: []byte("5197"), // 0x144d + out: []byte("Samsung Electronics Co Ltd"), + }, + { + in: []byte("5348"), // 0x14e4 + out: []byte("Broadcom Inc. and subsidiaries"), + }, + { + in: []byte("48879"), // 0xbeef + out: []byte("unknown pci vendor: 0xbeef"), + }, + } + + for _, c := range cases { + d := &PCIVendor{} + + out, err := d.Decode(c.in, config.Decoder{}) + if err != nil { + t.Errorf("Error decoding %#v: %v", c.in, err) + } + + if !bytes.Equal(out, c.out) { + t.Errorf("Expected %q, got %q", c.out, out) + } + } +} diff --git a/examples/pci.bpf.c b/examples/pci.bpf.c new file mode 100644 index 00000000..8d99ed07 --- /dev/null +++ b/examples/pci.bpf.c @@ -0,0 +1,53 @@ +#include +#include +#include +#include "maps.bpf.h" + +struct pci_key_t { + u32 vendor; + u32 device; + u8 class; + u16 subclass; +}; + +struct { + __uint(type, BPF_MAP_TYPE_LRU_HASH); + __uint(max_entries, 128); + __type(key, struct pci_key_t); + __type(value, u64); +} pci_user_read_config_ops_total SEC(".maps"); + +static int handle(struct pci_dev *dev) +{ + u16 vendor = BPF_CORE_READ(dev, vendor); + u16 device = BPF_CORE_READ(dev, device); + u32 class = BPF_CORE_READ(dev, class); + + struct pci_key_t key = { + .vendor = vendor, .device = (vendor << 16) + device, .class = class >> 16, .subclass = class >> 8 + }; + + increment_map(&pci_user_read_config_ops_total, &key, 1); + + return 0; +} + +SEC("kprobe/pci_user_read_config_byte") +int BPF_PROG(pci_user_read_config_byte, struct pci_dev *dev) +{ + return handle(dev); +} + +SEC("kprobe/pci_user_read_config_word") +int BPF_PROG(pci_user_read_config_word, struct pci_dev *dev) +{ + return handle(dev); +} + +SEC("kprobe/pci_user_read_config_dword") +int BPF_PROG(pci_user_read_config_dword, struct pci_dev *dev) +{ + return handle(dev); +} + +char LICENSE[] SEC("license") = "GPL"; diff --git a/examples/pci.yaml b/examples/pci.yaml new file mode 100644 index 00000000..37af1980 --- /dev/null +++ b/examples/pci.yaml @@ -0,0 +1,25 @@ +metrics: + counters: + - name: pci_user_read_config_ops_total + help: The number of operations reading pci configs + labels: + - name: pci_vendor + size: 4 + decoders: + - name: uint + - name: pci_vendor + - name: pci_device + size: 4 + decoders: + - name: uint + - name: pci_device + - name: pci_class + size: 2 + decoders: + - name: uint + - name: pci_class + - name: pci_subclass + size: 2 + decoders: + - name: uint + - name: pci_subclass diff --git a/go.mod b/go.mod index 2dd7d771..c802dc9c 100644 --- a/go.mod +++ b/go.mod @@ -7,6 +7,7 @@ require ( github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf github.com/elastic/go-perf v0.0.0-20191212140718-9c656876f595 github.com/iovisor/gobpf v0.2.0 + github.com/jaypipes/pcidb v1.0.0 github.com/prometheus/client_golang v1.16.0 github.com/prometheus/common v0.44.0 golang.org/x/sys v0.10.0 @@ -23,6 +24,7 @@ require ( github.com/golang/protobuf v1.5.3 // indirect github.com/kr/text v0.2.0 // indirect github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect + github.com/mitchellh/go-homedir v1.0.0 // indirect github.com/prometheus/client_model v0.4.0 // indirect github.com/prometheus/procfs v0.10.1 // indirect google.golang.org/protobuf v1.30.0 // indirect diff --git a/go.sum b/go.sum index 5eb975a6..8f4d7a9f 100644 --- a/go.sum +++ b/go.sum @@ -23,11 +23,15 @@ github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/iovisor/gobpf v0.2.0 h1:34xkQxft+35GagXBk3n23eqhm0v7q0ejeVirb8sqEOQ= github.com/iovisor/gobpf v0.2.0/go.mod h1:WSY9Jj5RhdgC3ci1QaacvbFdQ8cbrEjrpiZbLHLt2s4= +github.com/jaypipes/pcidb v1.0.0 h1:vtZIfkiCUE42oYbJS0TAq9XSfSmcsgo9IdxSm9qzYU8= +github.com/jaypipes/pcidb v1.0.0/go.mod h1:TnYUvqhPBzCKnH34KrIX22kAeEbDCSRJ9cqLRCuNDfk= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo= github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= +github.com/mitchellh/go-homedir v1.0.0 h1:vKb8ShqSby24Yrqr/yDYkuFz8d0WUjys40rvnGC8aR0= +github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/prometheus/client_golang v1.16.0 h1:yk/hx9hDbrGHovbci4BY+pRMfSuuat626eFsHb7tmT8=