Skip to content

Commit 5836830

Browse files
committed
process_collector: fill in most statistics on macOS
Unfortunately, the virtual memory, resident memory, and network stats will require access to undocumented C functions. I was warned off of cgo in IRC because it would then have to be enabled in a bunch of different projects that use this module, but I already was against it because that would break the ability to cross-compile. There is no interface to `dlopen` built into golang. The `github.com/ebitengine/purego` module looks promising (I can cross-compile and call these methods), but I'm currently getting unexpected results. I'll follow up with that separately if I can get it working, but hopefully this stuff is pretty uncontroversial. Tested on macOS 10.14.6 (amd64), macOS 14.6.1 (amd64), and macOS 15.0 (arm64) by spawning `/usr/bin/ulimit -a -S` and `/usr/sbin/lsof -c $my_process` from the test exporter process, and `ps -o lstart,vsize,rss,utime,stime,command` from the shell, and comparing results with the exported metrics. I can't find documentation for `RLIMIT_AS` on macOS (specifically if it's in bytes or pages). It's currently being reported back as `RLIM_INFINITY`, which seems reasonable, because I've come across reports that the value is ignored anyway[1]. The bash 3.2 code for the built-in `ulimit` divides the value reported by `getrusage(2)` by 1024 when printing, as it does for `RLIMIT_DATA`, which is documented as being bytes in `getrusage(2)`. The help for `ulimit` indicates it prints both in kbytes, so it's reasonable to assume this is already in bytes. [1] https://issues.chromium.org/issues/40581251#comment3 Signed-off-by: Matt Harbison <[email protected]>
1 parent dbf72fc commit 5836830

File tree

2 files changed

+114
-2
lines changed

2 files changed

+114
-2
lines changed
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
// Copyright 2015 The Prometheus Authors
2+
// Licensed under the Apache License, Version 2.0 (the "License");
3+
// you may not use this file except in compliance with the License.
4+
// You may obtain a copy of the License at
5+
//
6+
// http://www.apache.org/licenses/LICENSE-2.0
7+
//
8+
// Unless required by applicable law or agreed to in writing, software
9+
// distributed under the License is distributed on an "AS IS" BASIS,
10+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11+
// See the License for the specific language governing permissions and
12+
// limitations under the License.
13+
14+
//go:build darwin
15+
16+
package prometheus
17+
18+
import (
19+
"fmt"
20+
"golang.org/x/sys/unix"
21+
"os"
22+
"syscall"
23+
"time"
24+
)
25+
26+
func canCollectProcess() bool {
27+
return true
28+
}
29+
30+
func getSoftLimit(which int) (uint64, error) {
31+
rlimit := syscall.Rlimit{}
32+
33+
if err := syscall.Getrlimit(which, &rlimit); err != nil {
34+
return 0, err
35+
}
36+
37+
return rlimit.Cur, nil
38+
}
39+
40+
func getOpenFileCount() (float64, error) {
41+
// Alternately, the undocumented proc_pidinfo(PROC_PIDLISTFDS) can be used to
42+
// return a list of open fds, but that requires a way to call C APIs. The
43+
// benefits, however, include fewer system calls and not failing when at the
44+
// open file soft limit.
45+
46+
if dir, err := os.Open("/dev/fd"); err != nil {
47+
return 0.0, err
48+
} else {
49+
defer dir.Close()
50+
51+
// Avoid ReadDir(), as it calls stat(2) on each descriptor. Not only is
52+
// that info not used, but KQUEUE descriptors fail stat(2), which causes
53+
// the whole method to fail.
54+
if names, err := dir.Readdirnames(0); err != nil {
55+
return 0.0, err
56+
} else {
57+
// Subtract 1 to ignore the open /dev/fd descriptor above.
58+
return float64(len(names) - 1), nil
59+
}
60+
}
61+
}
62+
63+
func (c *processCollector) processCollect(ch chan<- Metric) {
64+
if procs, err := unix.SysctlKinfoProcSlice("kern.proc.pid", os.Getpid()); err == nil {
65+
if len(procs) == 1 {
66+
startTime := float64(procs[0].Proc.P_starttime.Nano() / 1e9)
67+
ch <- MustNewConstMetric(c.startTime, GaugeValue, startTime)
68+
} else {
69+
err = fmt.Errorf("sysctl() returned %d proc structs (expected 1)", len(procs))
70+
c.reportError(ch, c.startTime, err)
71+
}
72+
} else {
73+
c.reportError(ch, c.startTime, err)
74+
}
75+
76+
// The proc structure returned by kern.proc.pid above has an Rusage member,
77+
// but it is not filled in, so it needs to be fetched by getrusage(2). For
78+
// that call, the UTime, STime, and Maxrss members are filled out, but not
79+
// Ixrss, Idrss, or Isrss for the memory usage. Memory stats will require
80+
// access to the C API to call task_info(TASK_BASIC_INFO).
81+
rusage := unix.Rusage{}
82+
83+
if err := unix.Getrusage(syscall.RUSAGE_SELF, &rusage); err == nil {
84+
cpuTime := time.Duration(rusage.Stime.Nano() + rusage.Utime.Nano()).Seconds()
85+
ch <- MustNewConstMetric(c.cpuTotal, CounterValue, cpuTime)
86+
} else {
87+
c.reportError(ch, c.cpuTotal, err)
88+
}
89+
90+
// TODO: publish c.vsize and c.rss values
91+
92+
if fds, err := getOpenFileCount(); err == nil {
93+
ch <- MustNewConstMetric(c.openFDs, GaugeValue, fds)
94+
} else {
95+
c.reportError(ch, c.openFDs, err)
96+
}
97+
98+
if openFiles, err := getSoftLimit(syscall.RLIMIT_NOFILE); err == nil {
99+
ch <- MustNewConstMetric(c.maxFDs, GaugeValue, float64(openFiles))
100+
} else {
101+
c.reportError(ch, c.maxFDs, err)
102+
}
103+
104+
if addressSpace, err := getSoftLimit(syscall.RLIMIT_AS); err == nil {
105+
ch <- MustNewConstMetric(c.maxVsize, GaugeValue, float64(addressSpace))
106+
} else {
107+
c.reportError(ch, c.maxVsize, err)
108+
}
109+
110+
// TODO: socket(PF_SYSTEM) to fetch "com.apple.network.statistics" might
111+
// be able to get the per-process network send/receive counts.
112+
}

prometheus/process_collector_other.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,8 @@
1111
// See the License for the specific language governing permissions and
1212
// limitations under the License.
1313

14-
//go:build !windows && !js && !wasip1
15-
// +build !windows,!js,!wasip1
14+
//go:build !windows && !js && !wasip1 && !darwin
15+
// +build !windows,!js,!wasip1,!darwin
1616

1717
package prometheus
1818

0 commit comments

Comments
 (0)