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

Implement process collector for Windows #596

Merged
merged 3 commits into from Jun 14, 2019
Merged
Changes from 1 commit
Commits
File filter...
Filter file types
Jump to…
Jump to file or symbol
Failed to load files and symbols.

Always

Just for now

@@ -16,8 +16,6 @@ package prometheus
import (
"errors"
"os"

"github.com/prometheus/procfs"
)

type processCollector struct {
@@ -126,7 +124,7 @@ func NewProcessCollector(opts ProcessCollectorOpts) Collector {
}

// Set up process metric collection if supported by the runtime.
if _, err := procfs.NewDefaultFS(); err == nil {
if canCollectProcess() {
c.collectFn = c.processCollect
} else {
c.collectFn = func(ch chan<- Metric) {
@@ -153,46 +151,6 @@ func (c *processCollector) Collect(ch chan<- Metric) {
c.collectFn(ch)
}

func (c *processCollector) processCollect(ch chan<- Metric) {
pid, err := c.pidFn()
if err != nil {
c.reportError(ch, nil, err)
return
}

p, err := procfs.NewProc(pid)
if err != nil {
c.reportError(ch, nil, err)
return
}

if stat, err := p.Stat(); err == nil {
ch <- MustNewConstMetric(c.cpuTotal, CounterValue, stat.CPUTime())
ch <- MustNewConstMetric(c.vsize, GaugeValue, float64(stat.VirtualMemory()))
ch <- MustNewConstMetric(c.rss, GaugeValue, float64(stat.ResidentMemory()))
if startTime, err := stat.StartTime(); err == nil {
ch <- MustNewConstMetric(c.startTime, GaugeValue, startTime)
} else {
c.reportError(ch, c.startTime, err)
}
} else {
c.reportError(ch, nil, err)
}

if fds, err := p.FileDescriptorsLen(); err == nil {
ch <- MustNewConstMetric(c.openFDs, GaugeValue, float64(fds))
} else {
c.reportError(ch, c.openFDs, err)
}

if limits, err := p.Limits(); err == nil {
ch <- MustNewConstMetric(c.maxFDs, GaugeValue, float64(limits.OpenFiles))
ch <- MustNewConstMetric(c.maxVsize, GaugeValue, float64(limits.AddressSpace))
} else {
c.reportError(ch, nil, err)
}
}

func (c *processCollector) reportError(ch chan<- Metric, desc *Desc, err error) {
if !c.reportErrors {
return
@@ -0,0 +1,65 @@
// Copyright 2015 The Prometheus Authors
This conversation was marked as resolved by carlpett

This comment has been minimized.

Copy link
@beorn7

beorn7 Jun 11, 2019

Member

Not that it matters a lot, but in a new file, we usually update the year to the current one.

// 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.

// +build !windows

package prometheus

import (
"github.com/prometheus/procfs"
)

func canCollectProcess() bool {
_, err := procfs.NewDefaultFS()
return err == nil
}

func (c *processCollector) processCollect(ch chan<- Metric) {
pid, err := c.pidFn()
if err != nil {
c.reportError(ch, nil, err)
return
}

p, err := procfs.NewProc(pid)
if err != nil {
c.reportError(ch, nil, err)
return
}

if stat, err := p.Stat(); err == nil {
ch <- MustNewConstMetric(c.cpuTotal, CounterValue, stat.CPUTime())
ch <- MustNewConstMetric(c.vsize, GaugeValue, float64(stat.VirtualMemory()))
ch <- MustNewConstMetric(c.rss, GaugeValue, float64(stat.ResidentMemory()))
if startTime, err := stat.StartTime(); err == nil {
ch <- MustNewConstMetric(c.startTime, GaugeValue, startTime)
} else {
c.reportError(ch, c.startTime, err)
}
} else {
c.reportError(ch, nil, err)
}

if fds, err := p.FileDescriptorsLen(); err == nil {
ch <- MustNewConstMetric(c.openFDs, GaugeValue, float64(fds))
} else {
c.reportError(ch, c.openFDs, err)
}

if limits, err := p.Limits(); err == nil {
ch <- MustNewConstMetric(c.maxFDs, GaugeValue, float64(limits.OpenFiles))
ch <- MustNewConstMetric(c.maxVsize, GaugeValue, float64(limits.AddressSpace))
} else {
c.reportError(ch, nil, err)
}
}
@@ -0,0 +1,113 @@
// Copyright 2019 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 (
"syscall"
"unsafe"

"golang.org/x/sys/windows"
)

func canCollectProcess() bool {
return true
}

var (
modpsapi = syscall.NewLazyDLL("psapi.dll")
modkernel32 = syscall.NewLazyDLL("kernel32.dll")

procGetProcessMemoryInfo = modpsapi.NewProc("GetProcessMemoryInfo")
procGetProcessHandleCount = modkernel32.NewProc("GetProcessHandleCount")
)

type processMemoryCounters struct {
// https://docs.microsoft.com/en-us/windows/desktop/api/psapi/ns-psapi-_process_memory_counters_ex
_ uint32
PageFaultCount uint32
PeakWorkingSetSize uint64
WorkingSetSize uint64
QuotaPeakPagedPoolUsage uint64
QuotaPagedPoolUsage uint64
QuotaPeakNonPagedPoolUsage uint64
QuotaNonPagedPoolUsage uint64
PagefileUsage uint64
PeakPagefileUsage uint64
PrivateUsage uint64
}

func getProcessMemoryInfo(handle windows.Handle) (processMemoryCounters, error) {
mem := processMemoryCounters{}
r1, _, err := procGetProcessMemoryInfo.Call(
uintptr(handle),
uintptr(unsafe.Pointer(&mem)),
uintptr(unsafe.Sizeof(mem)),
)
if r1 != 1 {
return mem, err
} else {
return mem, nil
}
}

func getProcessHandleCount(handle windows.Handle) (uint32, error) {
var count uint32
r1, _, err := procGetProcessHandleCount.Call(
uintptr(handle),
uintptr(unsafe.Pointer(&count)),
)
if r1 != 1 {
return 0, err
} else {
return count, nil
}
}

func (c *processCollector) processCollect(ch chan<- Metric) {
h, err := windows.GetCurrentProcess()
if err != nil {
c.reportError(ch, nil, err)
return
}

var startTime, exitTime, kernelTime, userTime windows.Filetime
err = windows.GetProcessTimes(h, &startTime, &exitTime, &kernelTime, &userTime)
if err != nil {
c.reportError(ch, nil, err)
return
}
ch <- MustNewConstMetric(c.startTime, GaugeValue, float64(startTime.Nanoseconds()/1e9))
This conversation was marked as resolved by carlpett

This comment has been minimized.

Copy link
@beorn7

beorn7 Jun 11, 2019

Member

Is there a reason not to use fileTimeToSeconds here, too?

This comment has been minimized.

Copy link
@carlpett

carlpett Jun 11, 2019

Author Contributor

Yes. For whatever reason, Filetime.Nanoseconds converts to UNIX epoch, while the actual time stored within is number of 100 ns intervals passed 🤷‍♂

This comment has been minimized.

Copy link
@beorn7

beorn7 Jun 11, 2019

Member

Ugh…

ch <- MustNewConstMetric(c.cpuTotal, CounterValue, fileTimeToSeconds(kernelTime)+fileTimeToSeconds(userTime))

mem, err := getProcessMemoryInfo(h)
if err != nil {
c.reportError(ch, nil, err)
return
}
ch <- MustNewConstMetric(c.vsize, GaugeValue, float64(mem.WorkingSetSize))

This comment has been minimized.

Copy link
@beorn7

beorn7 Jun 11, 2019

Member

Concluding from the comments below, the WorkingSetSize is more like the RSS. I have no clue how to get something like the vsize on Windows.

ch <- MustNewConstMetric(c.maxVsize, GaugeValue, float64(mem.PeakWorkingSetSize))

This comment has been minimized.

Copy link
@beorn7

beorn7 Jun 11, 2019

Member

maxVsize is the maximum possible virtual memory size, not the observed peak. Not sure if there is a way to get this from somewhere on MS Windows. Perhaps it has to be hardcoded depending on whether it's 64bit or 32bit.

This comment has been minimized.

Copy link
@carlpett

carlpett Jun 11, 2019

Author Contributor

It seems it might depend on OS version as well. I found this source https://blogs.technet.microsoft.com/markrussinovich/2008/11/17/pushing-the-limits-of-windows-virtual-memory/ where they claim 8TB on 64 bit. They have a reference table which goes to Windows 2012. On my Windows lab host, I could reserve 128TB, though.

I'll see if this is available through some API, but otherwise I don't know. Not super keen on the idea of maintaining a lookup table which is hard to know when it needs to be updated.

This comment has been minimized.

Copy link
@carlpett

carlpett Jun 11, 2019

Author Contributor

Alternatively, on Linux it seems we represent unlimited with -1. Might make sense here too?

This comment has been minimized.

Copy link
@brian-brazil

brian-brazil Jun 11, 2019

Member

We could not export it. I doubt there's many 32bit systems out there that would care (and it's a hardcoded value if they need it).

This comment has been minimized.

Copy link
@carlpett

carlpett Jun 12, 2019

Author Contributor

That's also an option, of course. But 32-bit isn't actually something we can hardcode, if I understand it correctly. The value will depend on if the OS is 32 bit or not, and if booted with a 2/2 GB or 3/1 GB split between OS and application. On a 64 bit OS, it depends on if the IMAGE_FILE_LARGE_ADDRESS_AWARE flag is set on the binary by the go compiler, which seems to be the case: https://github.com/golang/go/blob/e883d000f4ce0c47711c3a7c59df8bb2f0ec557f/src/cmd/link/internal/ld/pe.go#L785-L788

This comment has been minimized.

Copy link
@carlpett

carlpett Jun 12, 2019

Author Contributor

But again, it is not clear if those hoops are worth jumping through.

This comment has been minimized.

Copy link
@beorn7

beorn7 Jun 12, 2019

Member

In doubt, let's just not export this metric, as Brian suggested.

This comment has been minimized.

Copy link
@carlpett

carlpett Jun 13, 2019

Author Contributor

Sounds good to me. In that case, should we drop the "max fds" too? That value is pretty uninteresting (although "correct"), and I added it to try to keep parity.

This comment has been minimized.

Copy link
@beorn7

beorn7 Jun 13, 2019

Member

Yes, let's keep it as it is the technically correct value and doesn't imply any maintenance overhead to keep it correct.

ch <- MustNewConstMetric(c.rss, GaugeValue, float64(mem.PrivateUsage))

This comment has been minimized.

Copy link
@brian-brazil

brian-brazil Jun 11, 2019

Member

I think RSS is WorkingSetSize

This comment has been minimized.

Copy link
@beorn7

beorn7 Jun 11, 2019

Member

Yeah, the Windows terminology is kind of weird. WSS is something else than RSS, but apparently, when Windows says "WSS", it means "RSS".

This comment has been minimized.

Copy link
@carlpett

carlpett Jun 11, 2019

Author Contributor

Yes, that seems correct.

The "working set" of a process is the set of memory pages currently visible to the process in physical RAM memory. These pages are resident and available for an application to use without triggering a page fault


handles, err := getProcessHandleCount(h)
if err != nil {
c.reportError(ch, nil, err)
return
}
ch <- MustNewConstMetric(c.openFDs, GaugeValue, float64(handles))
ch <- MustNewConstMetric(c.maxFDs, GaugeValue, float64(16*1024*1024)) // Windows has a hard-coded max limit, not per-process
This conversation was marked as resolved by carlpett

This comment has been minimized.

Copy link
@beorn7

beorn7 Jun 11, 2019

Member

Nit: End sentence with a period.

}

func fileTimeToSeconds(ft windows.Filetime) float64 {
return float64(uint64(ft.HighDateTime)<<32+uint64(ft.LowDateTime)) / 1e7
}
@@ -0,0 +1,70 @@
// Copyright 2019 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 (
"bytes"
"os"
"regexp"
"testing"

"github.com/prometheus/common/expfmt"
)

func TestWindowsProcessCollector(t *testing.T) {
registry := NewRegistry()
if err := registry.Register(NewProcessCollector(ProcessCollectorOpts{})); err != nil {
t.Fatal(err)
}
if err := registry.Register(NewProcessCollector(ProcessCollectorOpts{
PidFn: func() (int, error) { return os.Getpid(), nil },
Namespace: "foobar",
ReportErrors: true, // No errors expected, just to see if none are reported.
})); err != nil {
t.Fatal(err)
}

mfs, err := registry.Gather()
if err != nil {
t.Fatal(err)
}

var buf bytes.Buffer
for _, mf := range mfs {
if _, err := expfmt.MetricFamilyToText(&buf, mf); err != nil {
t.Fatal(err)
}
}

for _, re := range []*regexp.Regexp{
regexp.MustCompile("\nprocess_cpu_seconds_total [0-9]"),
regexp.MustCompile("\nprocess_max_fds [1-9]"),
regexp.MustCompile("\nprocess_open_fds [1-9]"),
regexp.MustCompile("\nprocess_virtual_memory_max_bytes (-1|[1-9])"),
regexp.MustCompile("\nprocess_virtual_memory_bytes [1-9]"),
regexp.MustCompile("\nprocess_resident_memory_bytes [1-9]"),
regexp.MustCompile("\nprocess_start_time_seconds [0-9.]{10,}"),
regexp.MustCompile("\nfoobar_process_cpu_seconds_total [0-9]"),
regexp.MustCompile("\nfoobar_process_max_fds [1-9]"),
regexp.MustCompile("\nfoobar_process_open_fds [1-9]"),
regexp.MustCompile("\nfoobar_process_virtual_memory_max_bytes (-1|[1-9])"),
regexp.MustCompile("\nfoobar_process_virtual_memory_bytes [1-9]"),
regexp.MustCompile("\nfoobar_process_resident_memory_bytes [1-9]"),
regexp.MustCompile("\nfoobar_process_start_time_seconds [0-9.]{10,}"),
} {
if !re.Match(buf.Bytes()) {
t.Errorf("want body to match %s\n%s", re, buf.String())
}
}
}
ProTip! Use n and p to navigate between commits in a pull request.
You can’t perform that action at this time.