// Copyright 2024 The Prometheus Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package prometheus import ( "errors" "fmt" "os" "syscall" "time" "golang.org/x/sys/unix" ) // notImplementedErr is returned by stub functions that replace cgo functions, when cgo // isn't available. var notImplementedErr = fmt.Errorf("not implemented") type memoryInfo struct { vsize uint64 // Virtual memory size in bytes rss uint64 // Resident memory size in bytes } func canCollectProcess() bool { return true } func getSoftLimit(which int) (uint64, error) { rlimit := syscall.Rlimit{} if err := syscall.Getrlimit(which, &rlimit); err != nil { return 0, err } return rlimit.Cur, nil } func getOpenFileCount() (float64, error) { // Alternately, the undocumented proc_pidinfo(PROC_PIDLISTFDS) can be used to // return a list of open fds, but that requires a way to call C APIs. The // benefits, however, include fewer system calls and not failing when at the // open file soft limit. if dir, err := os.Open("/dev/fd"); err != nil { return 0.0, err } else { defer dir.Close() // Avoid ReadDir(), as it calls stat(2) on each descriptor. Not only is // that info not used, but KQUEUE descriptors fail stat(2), which causes // the whole method to fail. if names, err := dir.Readdirnames(0); err != nil { return 0.0, err } else { // Subtract 1 to ignore the open /dev/fd descriptor above. return float64(len(names) - 1), nil } } } // describe returns all descriptions of the collector for Darwin. // Ensure that this list of descriptors is kept in sync with the metrics collected // in the processCollect method. Any changes to the metrics in processCollect // (such as adding or removing metrics) should be reflected in this list of descriptors. func (c *processCollector) describe(ch chan<- *Desc) { ch <- c.cpuTotal ch <- c.openFDs ch <- c.maxFDs ch <- c.maxVsize ch <- c.startTime /* the process could be collected but not implemented yet ch <- c.rss ch <- c.vsize ch <- c.inBytes ch <- c.outBytes */ } func (c *processCollector) processCollect(ch chan<- Metric) { if procs, err := unix.SysctlKinfoProcSlice("kern.proc.pid", os.Getpid()); err == nil { if len(procs) == 1 { startTime := float64(procs[0].Proc.P_starttime.Nano() / 1e9) ch <- MustNewConstMetric(c.startTime, GaugeValue, startTime) } else { err = fmt.Errorf("sysctl() returned %d proc structs (expected 1)", len(procs)) c.reportError(ch, c.startTime, err) } } else { c.reportError(ch, c.startTime, err) } // The proc structure returned by kern.proc.pid above has an Rusage member, // but it is not filled in, so it needs to be fetched by getrusage(2). For // that call, the UTime, STime, and Maxrss members are filled out, but not // Ixrss, Idrss, or Isrss for the memory usage. Memory stats will require // access to the C API to call task_info(TASK_BASIC_INFO). rusage := unix.Rusage{} if err := unix.Getrusage(syscall.RUSAGE_SELF, &rusage); err == nil { cpuTime := time.Duration(rusage.Stime.Nano() + rusage.Utime.Nano()).Seconds() ch <- MustNewConstMetric(c.cpuTotal, CounterValue, cpuTime) } else { c.reportError(ch, c.cpuTotal, err) } if memInfo, err := getMemory(); err == nil { ch <- MustNewConstMetric(c.rss, GaugeValue, float64(memInfo.rss)) ch <- MustNewConstMetric(c.vsize, GaugeValue, float64(memInfo.vsize)) } else if !errors.Is(err, notImplementedErr) { // Don't report an error when support is not compiled in. c.reportError(ch, c.rss, err) c.reportError(ch, c.vsize, err) } if fds, err := getOpenFileCount(); err == nil { ch <- MustNewConstMetric(c.openFDs, GaugeValue, fds) } else { c.reportError(ch, c.openFDs, err) } if openFiles, err := getSoftLimit(syscall.RLIMIT_NOFILE); err == nil { ch <- MustNewConstMetric(c.maxFDs, GaugeValue, float64(openFiles)) } else { c.reportError(ch, c.maxFDs, err) } if addressSpace, err := getSoftLimit(syscall.RLIMIT_AS); err == nil { ch <- MustNewConstMetric(c.maxVsize, GaugeValue, float64(addressSpace)) } else { c.reportError(ch, c.maxVsize, err) } // TODO: socket(PF_SYSTEM) to fetch "com.apple.network.statistics" might // be able to get the per-process network send/receive counts. }