Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add ARP collector for Linux #540

Merged
merged 23 commits into from Apr 11, 2017
Merged

Add ARP collector for Linux #540

merged 23 commits into from Apr 11, 2017

Conversation

skottler
Copy link
Contributor

@skottler skottler commented Apr 2, 2017

This introduces a simple ARP collector with initial support for Linux. It parses /proc/net/arp and emits a count of ARP entries on a per-device basis.

/cc #535

@skottler skottler mentioned this pull request Apr 2, 2017
Copy link
Member

@grobie grobie left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, looks good in general. The consensus has been to handle procfs parsing in https://github.com/prometheus/procfs and use that implementation here though.

scanner := bufio.NewScanner(data)
entries := make(map[string]uint32)

for scanner.Scan() {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

scanner.Scan() can fail. You need to check scanner.Err() and handle that.

return parseArpEntries(file), nil
}

func parseArpEntries(data *os.File) map[string]uint32 {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The only requirement is to pass something implementing the io.Reader interface, so I'd reflect that in the function signature.

entries := make(map[string]uint32)

for scanner.Scan() {
columns := strings.Split(string(scanner.Text()), " ")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

scanner.Text() returns a string. The type conversion is unnecessary.

entries := make(map[string]uint32)

for scanner.Scan() {
columns := strings.Split(string(scanner.Text()), " ")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, check strings.Fields()

@skottler
Copy link
Contributor Author

skottler commented Apr 2, 2017

Thanks for the insanely quick feedback @grobie! 🙇‍♂️ I've adopted the changes you suggested.

The consensus has been to handle procfs parsing in https://github.com/prometheus/procfs and use that implementation here though.

This PR implements only a tiny portion of what potentially needs to get parsed from /proc/net/arp as it only covers what's needed to expose the count per device. Is it okay to add a very much incomplete API to the procfs package or would you prefer that more complete parsing support gets implemented first?

@grobie
Copy link
Member

grobie commented Apr 2, 2017 via email

entries := make(map[string]uint32)

for scanner.Scan() {
if err := scanner.Err(); err != nil {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Err() will always be nil if Scan() is not nil. Check the example in https://golang.org/pkg/bufio/#example_Scanner_custom

@skottler
Copy link
Contributor Author

skottler commented Apr 2, 2017

I'm fine with either merging the current state with a comment saying something along the lines of "if more /proc/net/arp parsing support is needed, move this to prometheus/procfs" or by adding full parsing support to procfs now.

I added a comment in that vain to the current parsing function. If you're comfortable merging this in its current state I'm happy to follow up with /proc/net/arp support in the procfs package and then to switch node_exporter to use that. Doing that now or after merge are both fine with me.

@skottler
Copy link
Contributor Author

skottler commented Apr 2, 2017

# netbsd-armv5

>> building binaries

 >   node_exporter

# github.com/prometheus/node_exporter

/usr/local/go/pkg/tool/linux_amd64/link: running arm-linux-gnueabi-gcc failed: exit status 1

/tmp/go-link-863345215/000001.o: In function `_cgo_41b2da71c790_C2func_getaddrinfo':

/usr/local/go/src/net/cgo-gcc-prolog:45: warning: Using 'getaddrinfo' in statically linked applications requires at runtime the shared libraries from the glibc version used for linking

/usr/lib/gcc-cross/arm-linux-gnueabi/6/../../../../arm-linux-gnueabi/lib/crt1.o: In function `_start':

(.text+0x34): undefined reference to `main'

collect2: error: ld returned 1 exit status

The tests look to be failing on netbsd with ARMv5. I hope that's a CI or build issue rather than actual failure because debugging that is gonna be...challenging 😂

Factories["arp"] = NewArpCollector
}

// NewArpCollector returns a new Collector exposing ARP stats.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

return &arpCollector{
count: prometheus.NewDesc(
prometheus.BuildFQName(Namespace, "arp",
"count"), "ARP entries by device",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is kind of a confusing indentation/line break combination. If a function call is broken into multiple lines, I wouldn't then expect some other argument to follow it on the same line...

How about:

count: prometheus.NewDesc(
    prometheus.BuildFQName(Namespace, "arp", "count"),
    "ARP entries by device",
    []string{"device"}, nil,
),

return entries, nil
}

// This should get extracted to the github.com/prometheus/procfs package to
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add a TODO: to the beginning of this.

@juliusv
Copy link
Member

juliusv commented Apr 2, 2017

@skottler Regarding the test errors, I don't see how you could be causing them, given that you are not introducing any architecture-specific functionality, or even cgo stuff...

for scanner.Scan() {
columns := strings.Fields(scanner.Text())

if columns[0] != "IP" {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will panic if strings.Fields returns an empty slice, so it's probably worth adding a bounds check too.

https://play.golang.org/p/09sEiI2gEN

👋 Hey Sam!

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, always bounds check first!

@skottler
Copy link
Contributor Author

skottler commented Apr 2, 2017

@juliusv I've addressed your feedback in 43c7480, c1e6b6a, and eb3934a.

@mdlayher 👋 hope you're doing well! I addressed your comment in b5ba102.

Copy link
Member

@grobie grobie left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A few more comments ;-)

func NewARPCollector() (Collector, error) {
return &arpCollector{
count: prometheus.NewDesc(
prometheus.BuildFQName(Namespace, "arp", "count"),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't include things like count in gauge metric names. I'd suggest node_arp_entries or maybe node_arp_hosts?

for scanner.Scan() {
columns := strings.Fields(scanner.Text())

if len(columns) > 0 {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry for all the nitpicking. Idiomatic go tries to reduce the amount of nesting as much as possible, so if len(columns) == 0 { continue } would be more common here. Though, I think we should actually return an error here, such line would be completely unexpected and tell us about either a broken or changed ARP file. So I suggest:

if len(columns) < 6 {
    return nil, fmt.Errorf("unexpected ARP table file")
}

// ...

// to support more complete parsing of /proc/net/arp. Instead of adding
// more fields to this function's return values it should get moved and
// changed to support each field.
func parseArpEntries(data io.Reader) (map[string]uint32, error) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

s/Arp/ARP/

}, nil
}

func getArpEntries() (map[string]uint32, error) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

s/Arp/ARP/

return fmt.Errorf("could not get ARP entries: %s", err)
}

for device, entryCount := range entries {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As a note, it's common in go to use single letter variables in such short loops. I'm fine with the descriptive names though.

@skottler
Copy link
Contributor Author

skottler commented Apr 3, 2017

@grobie thanks! I've updated this including your feedback in 532677e.

@mdlayher
Copy link
Contributor

mdlayher commented Apr 3, 2017

Seems like this would be a reasonable collector to enable by default as well. Thoughts on that from anyone?

@grobie
Copy link
Member

grobie commented Apr 3, 2017 via email

@skottler
Copy link
Contributor Author

skottler commented Apr 3, 2017

Seems like this would be a reasonable collector to enable by default as well.

Done in 5ebb24e.

Please also add it to the readme.

Done in 2a82e3d.

@grobie
Copy link
Member

grobie commented Apr 3, 2017

I believe the test failures are the result of @juliusv latest change to build Prometheus components with go1.8 by default. Maybe our build yoda @sdurrheimer can help here? :-)

Copy link
Member

@discordianfish discordianfish left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is great, thanks!

@grobie
Copy link
Member

grobie commented Apr 11, 2017

Build issues fixed in master.

@grobie grobie merged commit 6eafa51 into prometheus:master Apr 11, 2017
@skottler skottler deleted the arp branch April 11, 2017 16:48
oblitorum pushed a commit to shatteredsilicon/node_exporter that referenced this pull request Apr 9, 2024
* Implement commonalities and linux support for ARP collection

* Add ARP collector to fixtures and run as part of e2e tests

* Bubble up scanner errors

* Use single return values where it makes sense

* Add missing annotation

* Move arp_common into arp_linux

* Add license header to arp_linux.go

* Address initial feedback

* Use strings.Fields instead of strings.Split

* Deal with scanner.Err() rather than throwing away errors

* Check for scan errors in-line before interacting with the entries map

* Don't interact with potentially empty text from scan

* Check for scan errors outside the scan loop

* Add comment about moving procfs parsing

* Add more direct comment

* Update initialism style to match go style guide

* Put function args on the same line

* Add TODO in front of comment about procfs extraction

* Guard against strings.Fields returning an empty slice

* Be more defensive about ARP table format and use upcase more broadly

* Enable the ARP collector by default

* Add ARP collector to the README

* Remove 'entry'
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

5 participants