Skip to content

Commit 8bb6fc6

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 8bb6fc6

File tree

2 files changed

+101
-2
lines changed

2 files changed

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

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)