pax_global_header00006660000000000000000000000064146472615370014531gustar00rootroot0000000000000052 comment=e2a9f0d26370bb14061dad7a8fce28f80371bbd9 process-exporter-0.8.3/000077500000000000000000000000001464726153700150655ustar00rootroot00000000000000process-exporter-0.8.3/.github/000077500000000000000000000000001464726153700164255ustar00rootroot00000000000000process-exporter-0.8.3/.github/workflows/000077500000000000000000000000001464726153700204625ustar00rootroot00000000000000process-exporter-0.8.3/.github/workflows/build.yml000066400000000000000000000006241464726153700223060ustar00rootroot00000000000000on: [push] jobs: build: runs-on: ubuntu-latest steps: - name: setup Go uses: actions/setup-go@v2 with: go-version: '1.22' - uses: actions/checkout@v4 with: fetch-depth: 0 - run: make style - run: make vet - run: make test - run: make BRANCH=${{ github.head_ref || github.ref_name }} build - run: make integprocess-exporter-0.8.3/.github/workflows/image.yml000066400000000000000000000026431464726153700222740ustar00rootroot00000000000000on: push: branches: [master] pull_request: jobs: image: runs-on: ubuntu-latest steps: - name: setup buildx id: buildx uses: docker/setup-buildx-action@v3 with: version: latest - name: login to docker hub if: github.event_name != 'pull_request' uses: docker/login-action@v1 with: registry: docker.io username: ${{ secrets.DOCKER_USERNAME }} password: ${{ secrets.DOCKER_PASSWORD }} - uses: actions/checkout@v4 with: fetch-depth: 0 - name: generate docker metadata id: meta uses: docker/metadata-action@v5 with: # list of Docker images to use as base name for tags images: | ncabatoff/process-exporter # generate Docker tags based on the following events/attributes tags: | type=schedule type=ref,event=branch type=ref,event=pr type=semver,pattern={{version}} type=sha - name: build docker image and, if not PR, push uses: docker/build-push-action@v5 with: file: ./Dockerfile context: . platforms: linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64 push: ${{ github.event_name != 'pull_request' }} tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }}process-exporter-0.8.3/.github/workflows/release.yml000066400000000000000000000011311464726153700226210ustar00rootroot00000000000000on: push: tags: - v[0-9].* permissions: contents: write jobs: release: runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v4 with: fetch-depth: 0 - name: setup Go uses: actions/setup-go@v4 with: go-version: '1.22' - run: make test - name: Run GoReleaser uses: goreleaser/goreleaser-action@v5 with: distribution: goreleaser version: "~> 1.25" args: release --rm-dist env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} process-exporter-0.8.3/.gitignore000066400000000000000000000001201464726153700170460ustar00rootroot00000000000000.idea /process-exporter load-generator integration-tester dist /vendor /.vscode process-exporter-0.8.3/.goreleaser.yml000066400000000000000000000030541464726153700200200ustar00rootroot00000000000000builds: - main: cmd/process-exporter/main.go binary: process-exporter ldflags: - -s -w - -X github.com/prometheus/common/version.BuildDate={{.Date}} - -X github.com/prometheus/common/version.BuildUser=goreleaser - -X github.com/prometheus/common/version.Revision={{.FullCommit}} - -X main.version={{.Version}} env: - CGO_ENABLED=0 goos: - linux goarch: - amd64 - 386 - arm - arm64 - ppc64 - ppc64le goarm: - 6 - 7 archives: - name_template: "process-exporter-{{ .Version }}.{{ .Os }}-{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}" wrap_in_directory: true checksum: name_template: checksums.txt nfpms: - homepage: https://github.com/ncabatoff/process-exporter maintainer: nick.cabatoff+procexp@gmail.com description: Prometheus exporter to report on processes running license: MIT formats: - deb - rpm bindir: /usr/bin contents: - src: packaging/process-exporter.service dst: /lib/systemd/system/process-exporter.service - src: packaging/conf/all.yaml dst: /etc/process-exporter/all.yaml type: config - src: packaging/default/process-exporter dst: /etc/default/process-exporter type: config scripts: postinstall: "packaging/scripts/postinstall.sh" postremove: "packaging/scripts/postremove.sh" preremove: "packaging/scripts/preremove.sh" release: github: owner: ncabatoff name: process-exporter draft: false prerelease: true process-exporter-0.8.3/Dockerfile000066400000000000000000000012251464726153700170570ustar00rootroot00000000000000# Start from a Debian image with the latest version of Go installed # and a workspace (GOPATH) configured at /go. FROM --platform=$BUILDPLATFORM golang:1.22 AS build ARG TARGETARCH ARG BUILDPLATFORM WORKDIR /go/src/github.com/ncabatoff/process-exporter ADD . . # Build the process-exporter command inside the container. RUN CGO_ENABLED=0 GOARCH=$TARGETARCH make build FROM scratch COPY --from=build /go/src/github.com/ncabatoff/process-exporter/process-exporter /bin/process-exporter # Run the process-exporter command by default when the container starts. ENTRYPOINT ["/bin/process-exporter"] # Document that the service listens on port 9256. EXPOSE 9256 process-exporter-0.8.3/Dockerfile.cloudbuild000066400000000000000000000001551464726153700212050ustar00rootroot00000000000000FROM scratch COPY gopath/bin/process-exporter /process-exporter ENTRYPOINT ["/process-exporter"] EXPOSE 9256 process-exporter-0.8.3/LICENSE000066400000000000000000000020641464726153700160740ustar00rootroot00000000000000The MIT License (MIT) Copyright (c) 2016 ncabatoff Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. process-exporter-0.8.3/Makefile000066400000000000000000000052231464726153700165270ustar00rootroot00000000000000pkgs = $(shell go list ./...) PREFIX ?= $(shell pwd) BIN_DIR ?= $(shell pwd) DOCKER_IMAGE_NAME ?= ncabatoff/process-exporter BRANCH ?= $(shell git rev-parse --abbrev-ref HEAD) BUILDDATE ?= $(shell date --iso-8601=seconds) BUILDUSER ?= $(shell whoami)@$(shell hostname) REVISION ?= $(shell git rev-parse HEAD) TAG_VERSION ?= $(shell git describe --tags --abbrev=0) VERSION_LDFLAGS := \ -X github.com/prometheus/common/version.Branch=$(BRANCH) \ -X github.com/prometheus/common/version.BuildDate=$(BUILDDATE) \ -X github.com/prometheus/common/version.BuildUser=$(BUILDUSER) \ -X github.com/prometheus/common/version.Revision=$(REVISION) \ -X main.version=$(TAG_VERSION) SMOKE_TEST = -config.path packaging/conf/all.yaml -once-to-stdout-delay 1s |grep -q 'namedprocess_namegroup_memory_bytes{groupname="process-exporte",memtype="virtual"}' all: format vet test build smoke style: @echo ">> checking code style" @! gofmt -d $(shell find . -name '*.go' -print) | grep '^' test: @echo ">> running short tests" go test -short $(pkgs) format: @echo ">> formatting code" go fmt $(pkgs) vet: @echo ">> vetting code" go vet $(pkgs) build: @echo ">> building code" cd cmd/process-exporter; CGO_ENABLED=0 go build -ldflags "$(VERSION_LDFLAGS)" -o ../../process-exporter -a -tags netgo smoke: @echo ">> smoke testing process-exporter" ./process-exporter $(SMOKE_TEST) integ: @echo ">> integration testing process-exporter" go build -o integration-tester cmd/integration-tester/main.go go build -o load-generator cmd/load-generator/main.go ./integration-tester -write-size-bytes 65536 install: @echo ">> installing binary" cd cmd/process-exporter; CGO_ENABLED=0 go install -a -tags netgo docker: @echo ">> building docker image" docker build -t "$(DOCKER_IMAGE_NAME):$(TAG_VERSION)" . docker rm configs docker create -v /packaging --name configs alpine:3.4 /bin/true docker cp packaging/conf configs:/packaging/conf docker run --rm --volumes-from configs "$(DOCKER_IMAGE_NAME):$(TAG_VERSION)" $(SMOKE_TEST) dockertest: docker run --rm -it -v `pwd`:/go/src/github.com/ncabatoff/process-exporter golang:1.22 make -C /go/src/github.com/ncabatoff/process-exporter test dockerinteg: docker run --rm -it -v `pwd`:/go/src/github.com/ncabatoff/process-exporter golang:1.22 make -C /go/src/github.com/ncabatoff/process-exporter build integ .PHONY: update-go-deps update-go-deps: @echo ">> updating Go dependencies" @for m in $$(go list -mod=readonly -m -f '{{ if and (not .Indirect) (not .Main)}}{{.Path}}{{end}}' all); do \ go get $$m; \ done go mod tidy .PHONY: all style format test vet build integ docker process-exporter-0.8.3/README.md000066400000000000000000000372731464726153700163600ustar00rootroot00000000000000# process-exporter Prometheus exporter that mines /proc to report on selected processes. [release]: https://github.com/ncabatoff/process-exporter/releases/latest [![Release](https://img.shields.io/github/release/ncabatoff/process-exporter.svg?style=flat-square")][release] [![Powered By: GoReleaser](https://img.shields.io/badge/powered%20by-goreleaser-green.svg?branch=master)](https://github.com/goreleaser) ![Build](https://github.com/ncabatoff/process-exporter/actions/workflows/build.yml/badge.svg) Some apps are impractical to instrument directly, either because you don't control the code or they're written in a language that isn't easy to instrument with Prometheus. We must instead resort to mining /proc. ## Installation Either grab a package for your OS from the [Releases][release] page, or install via [docker](https://hub.docker.com/r/ncabatoff/process-exporter/). ## Running Usage: ``` process-exporter [options] -config.path filename.yml ``` or via docker: ``` docker run -d --rm -p 9256:9256 --privileged -v /proc:/host/proc -v `pwd`:/config ncabatoff/process-exporter --procfs /host/proc -config.path /config/filename.yml ``` Important options (run process-exporter --help for full list): -children (default:true) makes it so that any process that otherwise isn't part of its own group becomes part of the first group found (if any) when walking the process tree upwards. In other words, resource usage of subprocesses is added to their parent's usage unless the subprocess identifies as a different group name. -threads (default:true) means that metrics will be broken down by thread name as well as group name. -recheck (default:false) means that on each scrape the process names are re-evaluated. This is disabled by default as an optimization, but since processes can choose to change their names, this may result in a process falling into the wrong group if we happen to see it for the first time before it's assumed its proper name. You can use -recheck-with-time-limit to enable this feature only for a specific duration after process starts. -procnames is intended as a quick alternative to using a config file. Details in the following section. To disable any of these options, use the `-option=false`. ## Configuration and group naming To select and group the processes to monitor, either provide command-line arguments or use a YAML configuration file. The recommended option is to use a config file via -config.path, but for convenience and backwards compatibility the -procnames/-namemapping options exist as an alternative. ### Using a config file The general format of the -config.path YAML file is a top-level `process_names` section, containing a list of name matchers: ``` process_names: - matcher1 - matcher2 ... - matcherN ``` The default config shipped with the deb/rpm packages is: ``` process_names: - name: "{{.Comm}}" cmdline: - '.+' ``` A process may only belong to one group: even if multiple items would match, the first one listed in the file wins. (Side note: to avoid confusion with the cmdline YAML element, we'll refer to the command-line arguments of a process `/proc//cmdline` as the array `argv[]`.) #### Using a config file: group name Each item in `process_names` gives a recipe for identifying and naming processes. The optional `name` tag defines a template to use to name matching processes; if not specified, `name` defaults to `{{.ExeBase}}`. Template variables available: - `{{.Comm}}` contains the basename of the original executable, i.e. 2nd field in `/proc//stat` - `{{.ExeBase}}` contains the basename of the executable - `{{.ExeFull}}` contains the fully qualified path of the executable - `{{.Username}}` contains the username of the effective user - `{{.Matches}}` map contains all the matches resulting from applying cmdline regexps - `{{.PID}}` contains the PID of the process. Note that using PID means the group will only contain a single process. - `{{.StartTime}}` contains the start time of the process. This can be useful in conjunction with PID because PIDs get reused over time. - `{{.Cgroups}}` contains (if supported) the cgroups of the process (`/proc/self/cgroup`). This is particularly useful for identifying to which container a process belongs. Using `PID` or `StartTime` is discouraged: this is almost never what you want, and is likely to result in high cardinality metrics which Prometheus will have trouble with. #### Using a config file: process selectors Each item in `process_names` must contain one or more selectors (`comm`, `exe` or `cmdline`); if more than one selector is present, they must all match. Each selector is a list of strings to match against a process's `comm`, `argv[0]`, or in the case of `cmdline`, a regexp to apply to the command line. The cmdline regexp uses the [Go syntax](https://golang.org/pkg/regexp). For `comm` and `exe`, the list of strings is an OR, meaning any process matching any of the strings will be added to the item's group. For `cmdline`, the list of regexes is an AND, meaning they all must match. Any capturing groups in a regexp must use the `?P` option to assign a name to the capture, which is used to populate `.Matches`. Performance tip: give an exe or comm clause in addition to any cmdline clause, so you avoid executing the regexp when the executable name doesn't match. ``` process_names: # comm is the second field of /proc//stat minus parens. # It is the base executable name, truncated at 15 chars. # It cannot be modified by the program, unlike exe. - comm: - bash # exe is argv[0]. If no slashes, only basename of argv[0] need match. # If exe contains slashes, argv[0] must match exactly. - exe: - postgres - /usr/local/bin/prometheus # cmdline is a list of regexps applied to argv. # Each must match, and any captures are added to the .Matches map. - name: "{{.ExeFull}}:{{.Matches.Cfgfile}}" exe: - /usr/local/bin/process-exporter cmdline: - -config.path\s+(?P\S+) ``` Here's the config I use on my home machine: ``` process_names: - comm: - chromium-browse - bash - prometheus - gvim - exe: - /sbin/upstart cmdline: - --user name: upstart:-user ``` ### Using -procnames/-namemapping instead of config.path Every name in the procnames list becomes a process group. The default name of a process is the value found in the second field of /proc//stat ("comm"), which is truncated at 15 chars. Usually this is the same as the name of the executable. If -namemapping isn't provided, every process with a comm value present in -procnames is assigned to a group based on that name, and any other processes are ignored. The -namemapping option is a comma-separated list of alternating name,regexp values. It allows assigning a name to a process based on a combination of the process name and command line. For example, using -namemapping "python2,([^/]+)\.py,java,-jar\s+([^/]+).jar" will make it so that each different python2 and java -jar invocation will be tracked with distinct metrics. Processes whose remapped name is absent from the procnames list will be ignored. On a Ubuntu Xenian machine being used as a workstation, here's a good way of tracking resource usage for a few different key user apps: process-exporter -namemapping "upstart,(--user)" \ -procnames chromium-browse,bash,gvim,prometheus,process-exporter,upstart:-user Since upstart --user is the parent process of the X11 session, this will make all apps started by the user fall into the group named "upstart:-user", unless they're one of the others named explicitly with -procnames, like gvim. ## Group Metrics There's no meaningful way to name a process that will only ever name a single process, so process-exporter assumes that every metric will be attached to a group of processes - not a [process group](https://en.wikipedia.org/wiki/Process_group) in the technical sense, just one or more processes that meet a configuration's specification of what should be monitored and how to name it. All these metrics start with `namedprocess_namegroup_` and have at minimum the label `groupname`. ### num_procs gauge Number of processes in this group. ### cpu_seconds_total counter CPU usage based on /proc/[pid]/stat fields utime(14) and stime(15) i.e. user and system time. This is similar to the node\_exporter's `node_cpu_seconds_total`. ### read_bytes_total counter Bytes read based on /proc/[pid]/io field read_bytes. The man page says > Attempt to count the number of bytes which this process really did cause to be fetched from the storage layer. This is accurate for block-backed filesystems. but I would take it with a grain of salt. As `/proc/[pid]/io` are set by the kernel as read only to the process' user (see #137), to get these values you should run `process-exporter` either as that user or as `root`. Otherwise, we can't read these values and you'll get a constant 0 in the metric. ### write_bytes_total counter Bytes written based on /proc/[pid]/io field write_bytes. As with read_bytes, somewhat dubious. May be useful for isolating which processes are doing the most I/O, but probably not measuring just how much I/O is happening. ### major_page_faults_total counter Number of major page faults based on /proc/[pid]/stat field majflt(12). ### minor_page_faults_total counter Number of minor page faults based on /proc/[pid]/stat field minflt(10). ### context_switches_total counter Number of context switches based on /proc/[pid]/status fields voluntary_ctxt_switches and nonvoluntary_ctxt_switches. The extra label `ctxswitchtype` can have two values: `voluntary` and `nonvoluntary`. ### memory_bytes gauge Number of bytes of memory used. The extra label `memtype` can have three values: *resident*: Field rss(24) from /proc/[pid]/stat, whose doc says: > This is just the pages which count toward text, data, or stack space. This does not include pages which have not been demand-loaded in, or which are swapped out. *virtual*: Field vsize(23) from /proc/[pid]/stat, virtual memory size. *swapped*: Field VmSwap from /proc/[pid]/status, translated from KB to bytes. If gathering smaps file is enabled, two additional values for `memtype` are added: *proportionalResident*: Sum of "Pss" fields from /proc/[pid]/smaps, whose doc says: > The "proportional set size" (PSS) of a process is the count of pages it has > in memory, where each page is divided by the number of processes sharing it. *proportionalSwapped*: Sum of "SwapPss" fields from /proc/[pid]/smaps ### open_filedesc gauge Number of file descriptors, based on counting how many entries are in the directory /proc/[pid]/fd. ### worst_fd_ratio gauge Worst ratio of open filedescs to filedesc limit, amongst all the procs in the group. The limit is the fd soft limit based on /proc/[pid]/limits. Normally Prometheus metrics ought to be as "basic" as possible (i.e. the raw values rather than a derived ratio), but we use a ratio here because nothing else makes sense. Suppose there are 10 procs in a given group, each with a soft limit of 4096, and one of them has 4000 open fds and the others all have 40, their total fdcount is 4360 and total soft limit is 40960, so the ratio is 1:10, but in fact one of the procs is about to run out of fds. With worst_fd_ratio we're able to know this: in the above example it would be 0.97, rather than the 0.10 you'd see if you computed sum(open_filedesc) / sum(limit_filedesc). ### oldest_start_time_seconds gauge Epoch time (seconds since 1970/1/1) at which the oldest process in the group started. This is derived from field starttime(22) from /proc/[pid]/stat, added to boot time to make it relative to epoch. ### num_threads gauge Sum of number of threads of all process in the group. Based on field num_threads(20) from /proc/[pid]/stat. ### states gauge Number of threads in the group in each of various states, based on the field state(3) from /proc/[pid]/stat. The extra label `state` can have these values: `Running`, `Sleeping`, `Waiting`, `Zombie`, `Other`. ## Group Thread Metrics Since publishing thread metrics adds a lot of overhead, use the `-threads` command-line argument to disable them, if necessary. All these metrics start with `namedprocess_namegroup_` and have at minimum the labels `groupname` and `threadname`. `threadname` is field comm(2) from /proc/[pid]/stat. Just as groupname breaks the set of processes down into groups, threadname breaks a given process group down into subgroups. ### thread_count gauge Number of threads in this thread subgroup. ### thread_cpu_seconds_total counter Same as cpu_user_seconds_total and cpu_system_seconds_total, but broken down per-thread subgroup. Unlike cpu_user_seconds_total/cpu_system_seconds_total, the label `cpumode` is used to distinguish between `user` and `system` time. ### thread_io_bytes_total counter Same as read_bytes_total and write_bytes_total, but broken down per-thread subgroup. Unlike read_bytes_total/write_bytes_total, the label `iomode` is used to distinguish between `read` and `write` bytes. ### thread_major_page_faults_total counter Same as major_page_faults_total, but broken down per-thread subgroup. ### thread_minor_page_faults_total counter Same as minor_page_faults_total, but broken down per-thread subgroup. ### thread_context_switches_total counter Same as context_switches_total, but broken down per-thread subgroup. ## Instrumentation cost process-exporter will consume CPU in proportion to the number of processes in the system and the rate at which new ones are created. The most expensive parts - applying regexps and executing templates - are only applied once per process seen, unless the command-line option -recheck is provided. If you have mostly long-running processes process-exporter overhead should be minimal: each time a scrape occurs, it will parse of /proc/$pid/stat and /proc/$pid/cmdline for every process being monitored and add a few numbers. ## Dashboards An example Grafana dashboard to view the metrics is available at https://grafana.net/dashboards/249 ## Building Requires Go 1.21 (at least) installed. ``` make ``` ## Exposing metrics through HTTPS web-config.yml ``` # Minimal TLS configuration example. Additionally, a certificate and a key file # are needed. tls_server_config: cert_file: server.crt key_file: server.key ``` Running ``` $ ./process-exporter -web.config.file web-config.yml & $ curl -sk https://localhost:9256/metrics | grep process # HELP namedprocess_scrape_errors general scrape errors: no proc metrics collected during a cycle # TYPE namedprocess_scrape_errors counter namedprocess_scrape_errors 0 # HELP namedprocess_scrape_partial_errors incremented each time a tracked proc's metrics collection fails partially, e.g. unreadable I/O stats # TYPE namedprocess_scrape_partial_errors counter namedprocess_scrape_partial_errors 0 # HELP namedprocess_scrape_procread_errors incremented each time a proc's metrics collection fails # TYPE namedprocess_scrape_procread_errors counter namedprocess_scrape_procread_errors 0 # HELP process_cpu_seconds_total Total user and system CPU time spent in seconds. # TYPE process_cpu_seconds_total counter process_cpu_seconds_total 0.21 # HELP process_exporter_build_info A metric with a constant '1' value labeled by version, revision, branch, and goversion from which process_exporter was built. # TYPE process_exporter_build_info gauge process_exporter_build_info{branch="",goversion="go1.17.3",revision="",version=""} 1 # HELP process_max_fds Maximum number of open file descriptors. # TYPE process_max_fds gauge process_max_fds 1.048576e+06 # HELP process_open_fds Number of open file descriptors. # TYPE process_open_fds gauge process_open_fds 10 ``` For further information about TLS configuration, please visit: [exporter-toolkit](https://github.com/prometheus/exporter-toolkit/blob/master/docs/web-configuration.md) process-exporter-0.8.3/cloudbuild.release.yaml000066400000000000000000000031171464726153700215200ustar00rootroot00000000000000steps: # - name: string # args: string # env: string # dir: string # id: string # waitFor: string # entrypoint: string # secretEnv: string # Setup the workspace - name: gcr.io/cloud-builders/go env: ['PROJECT_ROOT=github.com/ncabatoff/process-exporter'] args: ['env'] # Build project - name: gcr.io/cloud-builders/docker entrypoint: 'bash' args: ['-c', 'docker build -t ncabatoff/process-exporter:`echo $TAG_NAME|sed s/^v//` .'] # Login to docker hub - name: gcr.io/cloud-builders/docker entrypoint: 'bash' args: ['-c', 'docker login --username=ncabatoff --password=$$DOCKER_PASSWORD'] secretEnv: ['DOCKER_PASSWORD'] # Push to docker hub - name: gcr.io/cloud-builders/docker entrypoint: 'bash' args: ['-c', 'docker push ncabatoff/process-exporter:`echo $TAG_NAME|sed s/^v//`'] # Create github release - name: goreleaser/goreleaser entrypoint: /bin/sh dir: gopath/src/github.com env: ['GOPATH=/workspace/gopath'] args: ['-c', 'cd ncabatoff/process-exporter && git tag $TAG_NAME && /goreleaser' ] secretEnv: ['GITHUB_TOKEN'] secrets: - kmsKeyName: projects/process-exporter/locations/global/keyRings/cloudbuild/cryptoKeys/mykey secretEnv: DOCKER_PASSWORD: | CiQAeHUuEinm1h2j9mp8r0NjPw1l1bBwzDG+JHPUPf3GvtmdjXESMAD3wUauaxWrxid/zPunG67x 5+1CYedV5exh0XwQ32eu4UkniS7HHJNWBudklaG0JA== GITHUB_TOKEN: | CiQAeHUuEhEKAvfIHlUZrCgHNScm0mDKI8Z1w/N3OzDk8Ql6kAUSUQD3wUau7qRc+H7OnTUo6b2Z DKA1eMKHNg729KfHj2ZMqZXinrJloYMbZcZRXP9xv91xCq6QJB5UoFoyYDnXGdvgXC08YUstR6UB H0bwHhe1GQ== process-exporter-0.8.3/cloudbuild.yaml000066400000000000000000000022251464726153700201000ustar00rootroot00000000000000steps: # - name: string # args: string # env: string # dir: string # id: string # waitFor: string # entrypoint: string # secretEnv: string # - name: gcr.io/cloud-builders/curl # args: ['-L', '-s', '-o', 'dep', 'https://github.com/golang/dep/releases/download/v0.5.0/dep-linux-amd64'] # - name: ubuntu # args: ['chmod', '+x', 'dep'] # Setup the workspace - name: gcr.io/cloud-builders/go env: ['PROJECT_ROOT=github.com/ncabatoff/process-exporter'] args: ['env'] # Run dep in the workspace created in previous step # - name: gcr.io/cloud-builders/go # entrypoint: /bin/sh # dir: gopath/src/github.com # env: ['GOPATH=/workspace/gopath'] # args: ['-c', 'cd ncabatoff/process-exporter && /workspace/dep ensure -vendor-only' ] - name: gcr.io/cloud-builders/go entrypoint: /bin/sh dir: gopath/src/github.com env: ['GOPATH=/workspace/gopath'] args: ['-c', 'make -C ncabatoff/process-exporter style vet test build integ install' ] - name: gcr.io/cloud-builders/docker args: ['build', '--tag=gcr.io/$PROJECT_ID/process-exporter', '.', '-f', 'Dockerfile.cloudbuild'] images: ['gcr.io/$PROJECT_ID/process-exporter'] process-exporter-0.8.3/cmd/000077500000000000000000000000001464726153700156305ustar00rootroot00000000000000process-exporter-0.8.3/cmd/integration-tester/000077500000000000000000000000001464726153700214575ustar00rootroot00000000000000process-exporter-0.8.3/cmd/integration-tester/main.go000066400000000000000000000134301464726153700227330ustar00rootroot00000000000000package main import ( "bufio" "bytes" "context" "flag" "fmt" "log" "os" "os/exec" "path/filepath" "strconv" "strings" "time" ) // You wouldn't think our child could start before us, but I have observed it; maybe due to rounding? var start = time.Now().Unix() - 1 func main() { var ( flagProcessExporter = flag.String("process-exporter", "./process-exporter", "path to process-exporter") flagLoadGenerator = flag.String("load-generator", "./load-generator", "path to load-generator") flagAttempts = flag.Int("attempts", 3, "try this many times before returning failure") flagWriteSizeBytes = flag.Int("write-size-bytes", 1024*1024, "how many bytes to write each cycle") ) flag.Parse() ctx, cancel := context.WithCancel(context.Background()) defer cancel() cmdlg := exec.CommandContext(ctx, *flagLoadGenerator, "-write-size-bytes", strconv.Itoa(*flagWriteSizeBytes)) var buf = &bytes.Buffer{} cmdlg.Stdout = buf err := cmdlg.Start() if err != nil { log.Fatalf("Error launching load generator %q: %v", *flagLoadGenerator, err) } for !strings.HasPrefix(buf.String(), "ready") { time.Sleep(time.Second) } success := false for i := 0; i < *flagAttempts; i++ { comm := filepath.Base(*flagLoadGenerator) cmdpe := exec.CommandContext(ctx, *flagProcessExporter, "-once-to-stdout-delay", "20s", "-procnames", comm, "-threads=true") out, err := cmdpe.Output() if err != nil { log.Fatalf("Error launching process-exporter %q: %v", *flagProcessExporter, err) } log.Println(string(out)) results := getResults(comm, string(out)) if verify(results) { success = true break } log.Printf("try %d/%d failed", i+1, *flagAttempts) } cancel() cmdlg.Wait() if !success { os.Exit(1) } } type result struct { name string labels map[string]string value float64 } func getResults(group string, out string) map[string][]result { results := make(map[string][]result) skiplabel := fmt.Sprintf(`groupname="%s"`, group) lines := bufio.NewScanner(strings.NewReader(out)) lines.Split(bufio.ScanLines) for lines.Scan() { line := lines.Text() metric, value := "", 0.0 _, err := fmt.Sscanf(line, "namedprocess_namegroup_%s %f", &metric, &value) if err != nil { continue } pos := strings.IndexByte(metric, '{') if pos == -1 { log.Fatalf("cannot parse metric %q, no open curly found", metric) } name, labelstr := metric[:pos], metric[pos+1:] labelstr = labelstr[:len(labelstr)-1] labels := make(map[string]string) for _, kv := range strings.Split(labelstr, ",") { if kv != skiplabel { pieces := strings.SplitN(kv, "=", 2) labelname, labelvalue := pieces[0], pieces[1][1:len(pieces[1])-1] labels[labelname] = labelvalue } } results[name] = append(results[name], result{name, labels, value}) } return results } func verify(results map[string][]result) bool { success := true assertExact := func(name string, got, want float64) { if got != want { success = false log.Printf("expected %s to be %f, got %f", name, want, got) } } assertGreaterOrEqual := func(name string, got, want float64) { if got < want { success = false log.Printf("expected %s to have at least %f, got %f", name, want, got) } } assertExact("num_procs", results["num_procs"][0].value, 1) // Four locked threads plus go runtime means more than 7, but we'll say 7 to play it safe. assertGreaterOrEqual("num_threads", results["num_threads"][0].value, 7) // Our child must have started later than us. assertGreaterOrEqual("oldest_start_time_seconds", results["oldest_start_time_seconds"][0].value, float64(start)) for _, result := range results["states"] { switch state := result.labels["state"]; state { case "Other", "Zombie": assertExact("state "+state, result.value, 0) case "Running": assertGreaterOrEqual("state "+state, result.value, 2) case "Waiting": assertGreaterOrEqual("state "+state, result.value, 0) case "Sleeping": assertGreaterOrEqual("state "+state, result.value, 4) } } for _, result := range results["thread_count"] { switch tname := result.labels["threadname"]; tname { case "blocking", "sysbusy", "userbusy", "waiting": assertExact("thread_count "+tname, result.value, 1) case "main": assertGreaterOrEqual("thread_count "+tname, result.value, 3) } } for _, result := range results["thread_cpu_seconds_total"] { if result.labels["mode"] == "system" { switch tname := result.labels["threadname"]; tname { case "sysbusy", "blocking": assertGreaterOrEqual("thread_cpu_seconds_total system "+tname, result.value, 0.00001) default: assertGreaterOrEqual("thread_cpu_seconds_total system "+tname, result.value, 0) } } else if result.labels["mode"] == "user" { switch tname := result.labels["threadname"]; tname { case "userbusy": assertGreaterOrEqual("thread_cpu_seconds_total user "+tname, result.value, 0.00001) default: assertGreaterOrEqual("thread_cpu_seconds_total user "+tname, result.value, 0) } } } for _, result := range results["thread_io_bytes_total"] { tname, iomode := result.labels["threadname"], result.labels["iomode"] if iomode == "read" { continue } rname := fmt.Sprintf("%s %s %s", "thread_io_bytes_total", iomode, tname) switch tname { case "blocking", "sysbusy": assertGreaterOrEqual(rname, result.value, 0.00001) default: assertExact(rname, result.value, 0) } } otherwchan := 0.0 for _, result := range results["threads_wchan"] { switch wchan := result.labels["wchan"]; wchan { case "poll_schedule_timeout": assertGreaterOrEqual(wchan, result.value, 1) case "futex_wait_queue_me": assertGreaterOrEqual(wchan, result.value, 4) default: // The specific wchan involved for the blocking thread varies by filesystem. otherwchan++ } } // assertGreaterOrEqual("other wchan", otherwchan, 1) return success } process-exporter-0.8.3/cmd/load-generator/000077500000000000000000000000001464726153700205335ustar00rootroot00000000000000process-exporter-0.8.3/cmd/load-generator/main.go000066400000000000000000000047001464726153700220070ustar00rootroot00000000000000package main import ( "flag" "fmt" "io/ioutil" "math/rand" "runtime" "syscall" "unsafe" ) var ready = make(chan struct{}) func init() { var ( flagWaiting = flag.Int("waiting", 1, "minimum number of waiting threads") flagUserBusy = flag.Int("userbusy", 1, "minimum number of userbusy threads") flagSysBusy = flag.Int("sysbusy", 1, "minimum number of sysbusy threads") flagBlocking = flag.Int("blocking", 1, "minimum number of io blocking threads") flagWriteSizeBytes = flag.Int("write-size-bytes", 1024*1024, "how many bytes to write each cycle") ) flag.Parse() runtime.LockOSThread() for i := 0; i < *flagWaiting; i++ { go waiting() <-ready } for i := 0; i < *flagUserBusy; i++ { go userbusy() <-ready } for i := 0; i < *flagSysBusy; i++ { go diskio(false, *flagWriteSizeBytes) <-ready } for i := 0; i < *flagBlocking; i++ { go diskio(true, *flagWriteSizeBytes) <-ready } } func main() { c := make(chan struct{}) fmt.Println("ready") <-c } func setPrName(name string) error { bytes := append([]byte(name), 0) ptr := unsafe.Pointer(&bytes[0]) _, _, errno := syscall.RawSyscall6(syscall.SYS_PRCTL, syscall.PR_SET_NAME, uintptr(ptr), 0, 0, 0, 0) if errno != 0 { return syscall.Errno(errno) } return nil } func waiting() { runtime.LockOSThread() setPrName("waiting") ready <- struct{}{} c := make(chan struct{}) <-c } func userbusy() { runtime.LockOSThread() setPrName("userbusy") ready <- struct{}{} i := 1.0000001 for { i *= i } } func diskio(sync bool, writesize int) { runtime.LockOSThread() if sync { setPrName("blocking") } else { setPrName("sysbusy") } // Use random data because if we're on a filesystem that does compression like ZFS, // using zeroes is almost a no-op. b := make([]byte, writesize) _, err := rand.Read(b) if err != nil { panic("unable to get rands: " + err.Error()) } f, err := ioutil.TempFile("", "loadgen") if err != nil { panic("unable to create tempfile: " + err.Error()) } defer f.Close() sentready := false offset := int64(0) for { _, err = f.WriteAt(b, offset) if err != nil { panic("unable to write tempfile: " + err.Error()) } if sync { err = f.Sync() if err != nil { panic("unable to sync tempfile: " + err.Error()) } } _, err = f.ReadAt(b, 0) if err != nil { panic("unable to read tempfile: " + err.Error()) } if !sentready { ready <- struct{}{} sentready = true } offset++ } } process-exporter-0.8.3/cmd/process-exporter/000077500000000000000000000000001464726153700211545ustar00rootroot00000000000000process-exporter-0.8.3/cmd/process-exporter/main.go000066400000000000000000000211751464726153700224350ustar00rootroot00000000000000package main import ( "flag" "fmt" "log" "net/http" _ "net/http/pprof" "os" "regexp" "strings" "time" "github.com/ncabatoff/fakescraper" common "github.com/ncabatoff/process-exporter" "github.com/ncabatoff/process-exporter/collector" "github.com/ncabatoff/process-exporter/config" "github.com/prometheus/client_golang/prometheus" verCollector "github.com/prometheus/client_golang/prometheus/collectors/version" "github.com/prometheus/client_golang/prometheus/promhttp" "github.com/prometheus/common/promlog" promVersion "github.com/prometheus/common/version" "github.com/prometheus/exporter-toolkit/web" ) // Version is set at build time use ldflags. var version string func printManual() { fmt.Print(`Usage: process-exporter [options] -config.path filename.yml or process-exporter [options] -procnames name1,...,nameN [-namemapping k1,v1,...,kN,vN] The recommended option is to use a config file, but for convenience and backwards compatibility the -procnames/-namemapping options exist as an alternative. The -children option (default:true) makes it so that any process that otherwise isn't part of its own group becomes part of the first group found (if any) when walking the process tree upwards. In other words, resource usage of subprocesses is added to their parent's usage unless the subprocess identifies as a different group name. Command-line process selection (procnames/namemapping): Every process not in the procnames list is ignored. Otherwise, all processes found are reported on as a group based on the process name they share. Here 'process name' refers to the value found in the second field of /proc//stat, which is truncated at 15 chars. The -namemapping option allows assigning a group name based on a combination of the process name and command line. For example, using -namemapping "python2,([^/]+)\.py,java,-jar\s+([^/]+).jar" will make it so that each different python2 and java -jar invocation will be tracked with distinct metrics. Processes whose remapped name is absent from the procnames list will be ignored. Here's an example that I run on my home machine (Ubuntu Xenian): process-exporter -namemapping "upstart,(--user)" \ -procnames chromium-browse,bash,prometheus,prombench,gvim,upstart:-user Since it appears that upstart --user is the parent process of my X11 session, this will make all apps I start count against it, unless they're one of the others named explicitly with -procnames. Config file process selection (filename.yml): See README.md. ` + "\n") } type ( prefixRegex struct { prefix string regex *regexp.Regexp } nameMapperRegex struct { mapping map[string]*prefixRegex } ) func (nmr *nameMapperRegex) String() string { return fmt.Sprintf("%+v", nmr.mapping) } // Create a nameMapperRegex based on a string given as the -namemapper argument. func parseNameMapper(s string) (*nameMapperRegex, error) { mapper := make(map[string]*prefixRegex) if s == "" { return &nameMapperRegex{mapper}, nil } toks := strings.Split(s, ",") if len(toks)%2 == 1 { return nil, fmt.Errorf("bad namemapper: odd number of tokens") } for i, tok := range toks { if tok == "" { return nil, fmt.Errorf("bad namemapper: token %d is empty", i) } if i%2 == 1 { name, regexstr := toks[i-1], tok matchName := name prefix := name + ":" if r, err := regexp.Compile(regexstr); err != nil { return nil, fmt.Errorf("error compiling regexp '%s': %v", regexstr, err) } else { mapper[matchName] = &prefixRegex{prefix: prefix, regex: r} } } } return &nameMapperRegex{mapper}, nil } func (nmr *nameMapperRegex) MatchAndName(nacl common.ProcAttributes) (bool, string) { if pregex, ok := nmr.mapping[nacl.Name]; ok { if pregex == nil { return true, nacl.Name } matches := pregex.regex.FindStringSubmatch(strings.Join(nacl.Cmdline, " ")) if len(matches) > 1 { for _, matchstr := range matches[1:] { if matchstr != "" { return true, pregex.prefix + matchstr } } } } return false, "" } func init() { promVersion.Version = version prometheus.MustRegister(verCollector.NewCollector("process_exporter")) } func main() { var ( listenAddress = flag.String("web.listen-address", ":9256", "Address on which to expose metrics and web interface.") metricsPath = flag.String("web.telemetry-path", "/metrics", "Path under which to expose metrics.") onceToStdoutDelay = flag.Duration("once-to-stdout-delay", 0, "Don't bind, just wait this much time, print the metrics once to stdout, and exit") procNames = flag.String("procnames", "", "comma-separated list of process names to monitor") procfsPath = flag.String("procfs", "/proc", "path to read proc data from") nameMapping = flag.String("namemapping", "", "comma-separated list, alternating process name and capturing regex to apply to cmdline") children = flag.Bool("children", true, "if a proc is tracked, track with it any children that aren't part of their own group") threads = flag.Bool("threads", true, "report on per-threadname metrics as well") smaps = flag.Bool("gather-smaps", true, "gather metrics from smaps file, which contains proportional resident memory size") man = flag.Bool("man", false, "print manual") configPath = flag.String("config.path", "", "path to YAML config file") tlsConfigFile = flag.String("web.config.file", "", "path to YAML web config file") recheck = flag.Bool("recheck", false, "recheck process names on each scrape") recheckTimeLimit = flag.Duration("recheck-with-time-limit", 0, "recheck processes only this much time after their start, but no longer.") debug = flag.Bool("debug", false, "log debugging information to stdout") showVersion = flag.Bool("version", false, "print version information and exit") removeEmptyGroups = flag.Bool("remove-empty-groups", false, "forget process groups with no processes") ) flag.Parse() promlogConfig := &promlog.Config{} logger := promlog.New(promlogConfig) if *showVersion { fmt.Printf("%s\n", promVersion.Print("process-exporter")) os.Exit(0) } if *man { printManual() return } var matchnamer common.MatchNamer if *configPath != "" { if *nameMapping != "" || *procNames != "" { log.Fatalf("-config.path cannot be used with -namemapping or -procnames") } cfg, err := config.ReadFile(*configPath, *debug) if err != nil { log.Fatalf("error reading config file %q: %v", *configPath, err) } log.Printf("Reading metrics from %s based on %q", *procfsPath, *configPath) matchnamer = cfg.MatchNamers if *debug { log.Printf("using config matchnamer: %v", cfg.MatchNamers) } } else { namemapper, err := parseNameMapper(*nameMapping) if err != nil { log.Fatalf("Error parsing -namemapping argument '%s': %v", *nameMapping, err) } var names []string for _, s := range strings.Split(*procNames, ",") { if s != "" { if _, ok := namemapper.mapping[s]; !ok { namemapper.mapping[s] = nil } names = append(names, s) } } log.Printf("Reading metrics from %s for procnames: %v", *procfsPath, names) if *debug { log.Printf("using cmdline matchnamer: %v", namemapper) } matchnamer = namemapper } if *recheckTimeLimit != 0 { *recheck = true } pc, err := collector.NewProcessCollector( collector.ProcessCollectorOption{ ProcFSPath: *procfsPath, Children: *children, Threads: *threads, GatherSMaps: *smaps, Namer: matchnamer, Recheck: *recheck, RecheckTimeLimit: *recheckTimeLimit, Debug: *debug, RemoveEmptyGroups: *removeEmptyGroups, }, ) if err != nil { log.Fatalf("Error initializing: %v", err) } prometheus.MustRegister(pc) if *onceToStdoutDelay != 0 { // We throw away the first result because that first collection primes the pump, and // otherwise we won't see our counter metrics. This is specific to the implementation // of NamedProcessCollector.Collect(). fscraper := fakescraper.NewFakeScraper() fscraper.Scrape() time.Sleep(*onceToStdoutDelay) fmt.Print(fscraper.Scrape()) return } http.Handle(*metricsPath, promhttp.Handler()) http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { w.Write([]byte(` Named Process Exporter

Named Process Exporter

Metrics

`)) }) server := &http.Server{Addr: *listenAddress} if err := web.ListenAndServe(server, &web.FlagConfig{ WebListenAddresses: &[]string{*listenAddress}, WebConfigFile: tlsConfigFile, }, logger); err != nil { log.Fatalf("Failed to start the server: %v", err) os.Exit(1) } } process-exporter-0.8.3/collector/000077500000000000000000000000001464726153700170535ustar00rootroot00000000000000process-exporter-0.8.3/collector/process_collector.go000066400000000000000000000274061464726153700231370ustar00rootroot00000000000000package collector import ( "log" "time" common "github.com/ncabatoff/process-exporter" "github.com/ncabatoff/process-exporter/proc" "github.com/prometheus/client_golang/prometheus" ) var ( numprocsDesc = prometheus.NewDesc( "namedprocess_namegroup_num_procs", "number of processes in this group", []string{"groupname"}, nil) cpuSecsDesc = prometheus.NewDesc( "namedprocess_namegroup_cpu_seconds_total", "Cpu user usage in seconds", []string{"groupname", "mode"}, nil) readBytesDesc = prometheus.NewDesc( "namedprocess_namegroup_read_bytes_total", "number of bytes read by this group", []string{"groupname"}, nil) writeBytesDesc = prometheus.NewDesc( "namedprocess_namegroup_write_bytes_total", "number of bytes written by this group", []string{"groupname"}, nil) majorPageFaultsDesc = prometheus.NewDesc( "namedprocess_namegroup_major_page_faults_total", "Major page faults", []string{"groupname"}, nil) minorPageFaultsDesc = prometheus.NewDesc( "namedprocess_namegroup_minor_page_faults_total", "Minor page faults", []string{"groupname"}, nil) contextSwitchesDesc = prometheus.NewDesc( "namedprocess_namegroup_context_switches_total", "Context switches", []string{"groupname", "ctxswitchtype"}, nil) membytesDesc = prometheus.NewDesc( "namedprocess_namegroup_memory_bytes", "number of bytes of memory in use", []string{"groupname", "memtype"}, nil) openFDsDesc = prometheus.NewDesc( "namedprocess_namegroup_open_filedesc", "number of open file descriptors for this group", []string{"groupname"}, nil) worstFDRatioDesc = prometheus.NewDesc( "namedprocess_namegroup_worst_fd_ratio", "the worst (closest to 1) ratio between open fds and max fds among all procs in this group", []string{"groupname"}, nil) startTimeDesc = prometheus.NewDesc( "namedprocess_namegroup_oldest_start_time_seconds", "start time in seconds since 1970/01/01 of oldest process in group", []string{"groupname"}, nil) numThreadsDesc = prometheus.NewDesc( "namedprocess_namegroup_num_threads", "Number of threads", []string{"groupname"}, nil) statesDesc = prometheus.NewDesc( "namedprocess_namegroup_states", "Number of processes in states Running, Sleeping, Waiting, Zombie, or Other", []string{"groupname", "state"}, nil) scrapeErrorsDesc = prometheus.NewDesc( "namedprocess_scrape_errors", "general scrape errors: no proc metrics collected during a cycle", nil, nil) scrapeProcReadErrorsDesc = prometheus.NewDesc( "namedprocess_scrape_procread_errors", "incremented each time a proc's metrics collection fails", nil, nil) scrapePartialErrorsDesc = prometheus.NewDesc( "namedprocess_scrape_partial_errors", "incremented each time a tracked proc's metrics collection fails partially, e.g. unreadable I/O stats", nil, nil) threadWchanDesc = prometheus.NewDesc( "namedprocess_namegroup_threads_wchan", "Number of threads in this group waiting on each wchan", []string{"groupname", "wchan"}, nil) threadCountDesc = prometheus.NewDesc( "namedprocess_namegroup_thread_count", "Number of threads in this group with same threadname", []string{"groupname", "threadname"}, nil) threadCpuSecsDesc = prometheus.NewDesc( "namedprocess_namegroup_thread_cpu_seconds_total", "Cpu user/system usage in seconds", []string{"groupname", "threadname", "mode"}, nil) threadIoBytesDesc = prometheus.NewDesc( "namedprocess_namegroup_thread_io_bytes_total", "number of bytes read/written by these threads", []string{"groupname", "threadname", "iomode"}, nil) threadMajorPageFaultsDesc = prometheus.NewDesc( "namedprocess_namegroup_thread_major_page_faults_total", "Major page faults for these threads", []string{"groupname", "threadname"}, nil) threadMinorPageFaultsDesc = prometheus.NewDesc( "namedprocess_namegroup_thread_minor_page_faults_total", "Minor page faults for these threads", []string{"groupname", "threadname"}, nil) threadContextSwitchesDesc = prometheus.NewDesc( "namedprocess_namegroup_thread_context_switches_total", "Context switches for these threads", []string{"groupname", "threadname", "ctxswitchtype"}, nil) ) type ( scrapeRequest struct { results chan<- prometheus.Metric done chan struct{} } ProcessCollectorOption struct { ProcFSPath string Children bool Threads bool GatherSMaps bool Namer common.MatchNamer Recheck bool RecheckTimeLimit time.Duration Debug bool RemoveEmptyGroups bool } NamedProcessCollector struct { scrapeChan chan scrapeRequest *proc.Grouper threads bool smaps bool source proc.Source scrapeErrors int scrapeProcReadErrors int scrapePartialErrors int debug bool } ) func NewProcessCollector(options ProcessCollectorOption) (*NamedProcessCollector, error) { fs, err := proc.NewFS(options.ProcFSPath, options.Debug) if err != nil { return nil, err } fs.GatherSMaps = options.GatherSMaps p := &NamedProcessCollector{ scrapeChan: make(chan scrapeRequest), Grouper: proc.NewGrouper(options.Namer, options.Children, options.Threads, options.Recheck, options.RecheckTimeLimit, options.Debug, options.RemoveEmptyGroups), source: fs, threads: options.Threads, smaps: options.GatherSMaps, debug: options.Debug, } colErrs, _, err := p.Update(p.source.AllProcs()) if err != nil { if options.Debug { log.Print(err) } return nil, err } p.scrapePartialErrors += colErrs.Partial p.scrapeProcReadErrors += colErrs.Read go p.start() return p, nil } // Describe implements prometheus.Collector. func (p *NamedProcessCollector) Describe(ch chan<- *prometheus.Desc) { ch <- cpuSecsDesc ch <- numprocsDesc ch <- readBytesDesc ch <- writeBytesDesc ch <- membytesDesc ch <- openFDsDesc ch <- worstFDRatioDesc ch <- startTimeDesc ch <- majorPageFaultsDesc ch <- minorPageFaultsDesc ch <- contextSwitchesDesc ch <- numThreadsDesc ch <- statesDesc ch <- scrapeErrorsDesc ch <- scrapeProcReadErrorsDesc ch <- scrapePartialErrorsDesc ch <- threadWchanDesc ch <- threadCountDesc ch <- threadCpuSecsDesc ch <- threadIoBytesDesc ch <- threadMajorPageFaultsDesc ch <- threadMinorPageFaultsDesc ch <- threadContextSwitchesDesc } // Collect implements prometheus.Collector. func (p *NamedProcessCollector) Collect(ch chan<- prometheus.Metric) { req := scrapeRequest{results: ch, done: make(chan struct{})} p.scrapeChan <- req <-req.done } func (p *NamedProcessCollector) start() { for req := range p.scrapeChan { ch := req.results p.scrape(ch) req.done <- struct{}{} } } func (p *NamedProcessCollector) scrape(ch chan<- prometheus.Metric) { permErrs, groups, err := p.Update(p.source.AllProcs()) p.scrapePartialErrors += permErrs.Partial if err != nil { p.scrapeErrors++ log.Printf("error reading procs: %v", err) } else { for gname, gcounts := range groups { ch <- prometheus.MustNewConstMetric(numprocsDesc, prometheus.GaugeValue, float64(gcounts.Procs), gname) ch <- prometheus.MustNewConstMetric(membytesDesc, prometheus.GaugeValue, float64(gcounts.Memory.ResidentBytes), gname, "resident") ch <- prometheus.MustNewConstMetric(membytesDesc, prometheus.GaugeValue, float64(gcounts.Memory.VirtualBytes), gname, "virtual") ch <- prometheus.MustNewConstMetric(membytesDesc, prometheus.GaugeValue, float64(gcounts.Memory.VmSwapBytes), gname, "swapped") ch <- prometheus.MustNewConstMetric(startTimeDesc, prometheus.GaugeValue, float64(gcounts.OldestStartTime.Unix()), gname) ch <- prometheus.MustNewConstMetric(openFDsDesc, prometheus.GaugeValue, float64(gcounts.OpenFDs), gname) ch <- prometheus.MustNewConstMetric(worstFDRatioDesc, prometheus.GaugeValue, float64(gcounts.WorstFDratio), gname) ch <- prometheus.MustNewConstMetric(cpuSecsDesc, prometheus.CounterValue, gcounts.CPUUserTime, gname, "user") ch <- prometheus.MustNewConstMetric(cpuSecsDesc, prometheus.CounterValue, gcounts.CPUSystemTime, gname, "system") ch <- prometheus.MustNewConstMetric(readBytesDesc, prometheus.CounterValue, float64(gcounts.ReadBytes), gname) ch <- prometheus.MustNewConstMetric(writeBytesDesc, prometheus.CounterValue, float64(gcounts.WriteBytes), gname) ch <- prometheus.MustNewConstMetric(majorPageFaultsDesc, prometheus.CounterValue, float64(gcounts.MajorPageFaults), gname) ch <- prometheus.MustNewConstMetric(minorPageFaultsDesc, prometheus.CounterValue, float64(gcounts.MinorPageFaults), gname) ch <- prometheus.MustNewConstMetric(contextSwitchesDesc, prometheus.CounterValue, float64(gcounts.CtxSwitchVoluntary), gname, "voluntary") ch <- prometheus.MustNewConstMetric(contextSwitchesDesc, prometheus.CounterValue, float64(gcounts.CtxSwitchNonvoluntary), gname, "nonvoluntary") ch <- prometheus.MustNewConstMetric(numThreadsDesc, prometheus.GaugeValue, float64(gcounts.NumThreads), gname) ch <- prometheus.MustNewConstMetric(statesDesc, prometheus.GaugeValue, float64(gcounts.States.Running), gname, "Running") ch <- prometheus.MustNewConstMetric(statesDesc, prometheus.GaugeValue, float64(gcounts.States.Sleeping), gname, "Sleeping") ch <- prometheus.MustNewConstMetric(statesDesc, prometheus.GaugeValue, float64(gcounts.States.Waiting), gname, "Waiting") ch <- prometheus.MustNewConstMetric(statesDesc, prometheus.GaugeValue, float64(gcounts.States.Zombie), gname, "Zombie") ch <- prometheus.MustNewConstMetric(statesDesc, prometheus.GaugeValue, float64(gcounts.States.Other), gname, "Other") for wchan, count := range gcounts.Wchans { ch <- prometheus.MustNewConstMetric(threadWchanDesc, prometheus.GaugeValue, float64(count), gname, wchan) } if p.smaps { ch <- prometheus.MustNewConstMetric(membytesDesc, prometheus.GaugeValue, float64(gcounts.Memory.ProportionalBytes), gname, "proportionalResident") ch <- prometheus.MustNewConstMetric(membytesDesc, prometheus.GaugeValue, float64(gcounts.Memory.ProportionalSwapBytes), gname, "proportionalSwapped") } if p.threads { for _, thr := range gcounts.Threads { ch <- prometheus.MustNewConstMetric(threadCountDesc, prometheus.GaugeValue, float64(thr.NumThreads), gname, thr.Name) ch <- prometheus.MustNewConstMetric(threadCpuSecsDesc, prometheus.CounterValue, float64(thr.CPUUserTime), gname, thr.Name, "user") ch <- prometheus.MustNewConstMetric(threadCpuSecsDesc, prometheus.CounterValue, float64(thr.CPUSystemTime), gname, thr.Name, "system") ch <- prometheus.MustNewConstMetric(threadIoBytesDesc, prometheus.CounterValue, float64(thr.ReadBytes), gname, thr.Name, "read") ch <- prometheus.MustNewConstMetric(threadIoBytesDesc, prometheus.CounterValue, float64(thr.WriteBytes), gname, thr.Name, "write") ch <- prometheus.MustNewConstMetric(threadMajorPageFaultsDesc, prometheus.CounterValue, float64(thr.MajorPageFaults), gname, thr.Name) ch <- prometheus.MustNewConstMetric(threadMinorPageFaultsDesc, prometheus.CounterValue, float64(thr.MinorPageFaults), gname, thr.Name) ch <- prometheus.MustNewConstMetric(threadContextSwitchesDesc, prometheus.CounterValue, float64(thr.CtxSwitchVoluntary), gname, thr.Name, "voluntary") ch <- prometheus.MustNewConstMetric(threadContextSwitchesDesc, prometheus.CounterValue, float64(thr.CtxSwitchNonvoluntary), gname, thr.Name, "nonvoluntary") } } } } ch <- prometheus.MustNewConstMetric(scrapeErrorsDesc, prometheus.CounterValue, float64(p.scrapeErrors)) ch <- prometheus.MustNewConstMetric(scrapeProcReadErrorsDesc, prometheus.CounterValue, float64(p.scrapeProcReadErrors)) ch <- prometheus.MustNewConstMetric(scrapePartialErrorsDesc, prometheus.CounterValue, float64(p.scrapePartialErrors)) } process-exporter-0.8.3/common.go000066400000000000000000000006001464726153700167000ustar00rootroot00000000000000package common import ( "fmt" "time" ) type ( ProcAttributes struct { Name string Cmdline []string Cgroups []string Username string PID int StartTime time.Time } MatchNamer interface { // MatchAndName returns false if the match failed, otherwise // true and the resulting name. MatchAndName(ProcAttributes) (bool, string) fmt.Stringer } ) process-exporter-0.8.3/config/000077500000000000000000000000001464726153700163325ustar00rootroot00000000000000process-exporter-0.8.3/config/base_test.go000066400000000000000000000003071464726153700206320ustar00rootroot00000000000000package config import ( "testing" . "gopkg.in/check.v1" ) // Hook up gocheck into the "go test" runner. func Test(t *testing.T) { TestingT(t) } type MySuite struct{} var _ = Suite(&MySuite{}) process-exporter-0.8.3/config/config.go000066400000000000000000000136221464726153700201320ustar00rootroot00000000000000package config import ( "bytes" "fmt" "io/ioutil" "log" "path/filepath" "regexp" "strings" "text/template" "time" common "github.com/ncabatoff/process-exporter" "gopkg.in/yaml.v2" ) type ( Matcher interface { // Match returns empty string for no match, or the group name on success. Match(common.ProcAttributes) bool } FirstMatcher struct { matchers []common.MatchNamer } commMatcher struct { comms map[string]struct{} } exeMatcher struct { exes map[string]string } cmdlineMatcher struct { regexes []*regexp.Regexp captures map[string]string } andMatcher []Matcher templateNamer struct { template *template.Template } matchNamer struct { andMatcher templateNamer } templateParams struct { Cgroups []string Comm string ExeBase string ExeFull string Username string PID int StartTime time.Time Matches map[string]string } ) func (c *cmdlineMatcher) String() string { return fmt.Sprintf("cmdlines: %+v", c.regexes) } func (e *exeMatcher) String() string { return fmt.Sprintf("exes: %+v", e.exes) } func (c *commMatcher) String() string { var comms = make([]string, 0, len(c.comms)) for cm := range c.comms { comms = append(comms, cm) } return fmt.Sprintf("comms: %+v", comms) } func (f FirstMatcher) String() string { return fmt.Sprintf("%v", f.matchers) } func (f FirstMatcher) MatchAndName(nacl common.ProcAttributes) (bool, string) { for _, m := range f.matchers { if matched, name := m.MatchAndName(nacl); matched { return true, name } } return false, "" } func (m *matchNamer) String() string { return fmt.Sprintf("%+v", m.andMatcher) } func (m *matchNamer) MatchAndName(nacl common.ProcAttributes) (bool, string) { if !m.Match(nacl) { return false, "" } matches := make(map[string]string) for _, m := range m.andMatcher { if mc, ok := m.(*cmdlineMatcher); ok { for k, v := range mc.captures { matches[k] = v } } } exebase, exefull := nacl.Name, nacl.Name if len(nacl.Cmdline) > 0 { exefull = nacl.Cmdline[0] exebase = filepath.Base(exefull) } var buf bytes.Buffer m.template.Execute(&buf, &templateParams{ Comm: nacl.Name, Cgroups: nacl.Cgroups, ExeBase: exebase, ExeFull: exefull, Matches: matches, Username: nacl.Username, PID: nacl.PID, StartTime: nacl.StartTime, }) return true, buf.String() } func (m *commMatcher) Match(nacl common.ProcAttributes) bool { _, found := m.comms[nacl.Name] return found } func (m *exeMatcher) Match(nacl common.ProcAttributes) bool { if len(nacl.Cmdline) == 0 { return false } thisbase := filepath.Base(nacl.Cmdline[0]) fqpath, found := m.exes[thisbase] if !found { return false } if fqpath == "" { return true } return fqpath == nacl.Cmdline[0] } func (m *cmdlineMatcher) Match(nacl common.ProcAttributes) bool { for _, regex := range m.regexes { captures := regex.FindStringSubmatch(strings.Join(nacl.Cmdline, " ")) if m.captures == nil { return false } subexpNames := regex.SubexpNames() if len(subexpNames) != len(captures) { return false } for i, name := range subexpNames { m.captures[name] = captures[i] } } return true } func (m andMatcher) Match(nacl common.ProcAttributes) bool { for _, matcher := range m { if !matcher.Match(nacl) { return false } } return true } type Config struct { MatchNamers FirstMatcher } func (c *Config) UnmarshalYAML(unmarshal func(v interface{}) error) error { type ( root struct { Matchers MatcherRules `yaml:"process_names"` } ) var r root if err := unmarshal(&r); err != nil { return err } cfg, err := r.Matchers.ToConfig() if err != nil { return err } *c = *cfg return nil } type MatcherGroup struct { Name string `yaml:"name"` CommRules []string `yaml:"comm"` ExeRules []string `yaml:"exe"` CmdlineRules []string `yaml:"cmdline"` } type MatcherRules []MatcherGroup func (r MatcherRules) ToConfig() (*Config, error) { var cfg Config for _, matcher := range r { var matchers andMatcher if matcher.CommRules != nil { comms := make(map[string]struct{}) for _, c := range matcher.CommRules { comms[c] = struct{}{} } matchers = append(matchers, &commMatcher{comms}) } if matcher.ExeRules != nil { exes := make(map[string]string) for _, e := range matcher.ExeRules { if strings.Contains(e, "/") { exes[filepath.Base(e)] = e } else { exes[e] = "" } } matchers = append(matchers, &exeMatcher{exes}) } if matcher.CmdlineRules != nil { var rs []*regexp.Regexp for _, c := range matcher.CmdlineRules { r, err := regexp.Compile(c) if err != nil { return nil, fmt.Errorf("bad cmdline regex %q: %v", c, err) } rs = append(rs, r) } matchers = append(matchers, &cmdlineMatcher{ regexes: rs, captures: make(map[string]string), }) } if len(matchers) == 0 { return nil, fmt.Errorf("no matchers provided") } nametmpl := matcher.Name if nametmpl == "" { nametmpl = "{{.ExeBase}}" } tmpl := template.New("cmdname") tmpl, err := tmpl.Parse(nametmpl) if err != nil { return nil, fmt.Errorf("bad name template %q: %v", nametmpl, err) } matchNamer := &matchNamer{matchers, templateNamer{tmpl}} cfg.MatchNamers.matchers = append(cfg.MatchNamers.matchers, matchNamer) } return &cfg, nil } // ReadRecipesFile opens the named file and extracts recipes from it. func ReadFile(cfgpath string, debug bool) (*Config, error) { content, err := ioutil.ReadFile(cfgpath) if err != nil { return nil, fmt.Errorf("error reading config file %q: %v", cfgpath, err) } if debug { log.Printf("Config file %q contents:\n%s", cfgpath, content) } return GetConfig(string(content), debug) } // GetConfig extracts Config from content by parsing it as YAML. func GetConfig(content string, debug bool) (*Config, error) { var cfg Config err := yaml.Unmarshal([]byte(content), &cfg) if err != nil { return nil, err } return &cfg, nil } process-exporter-0.8.3/config/config_test.go000066400000000000000000000052321464726153700211670ustar00rootroot00000000000000package config import ( // "github.com/kylelemons/godebug/pretty" common "github.com/ncabatoff/process-exporter" . "gopkg.in/check.v1" "time" ) func (s MySuite) TestConfigBasic(c *C) { yml := ` process_names: - exe: - bash - exe: - sh - exe: - /bin/ksh ` cfg, err := GetConfig(yml, false) c.Assert(err, IsNil) c.Check(cfg.MatchNamers.matchers, HasLen, 3) bash := common.ProcAttributes{Name: "bash", Cmdline: []string{"/bin/bash"}} sh := common.ProcAttributes{Name: "sh", Cmdline: []string{"sh"}} ksh := common.ProcAttributes{Name: "ksh", Cmdline: []string{"/bin/ksh"}} found, name := cfg.MatchNamers.matchers[0].MatchAndName(bash) c.Check(found, Equals, true) c.Check(name, Equals, "bash") found, name = cfg.MatchNamers.matchers[0].MatchAndName(sh) c.Check(found, Equals, false) found, name = cfg.MatchNamers.matchers[0].MatchAndName(ksh) c.Check(found, Equals, false) found, name = cfg.MatchNamers.matchers[1].MatchAndName(bash) c.Check(found, Equals, false) found, name = cfg.MatchNamers.matchers[1].MatchAndName(sh) c.Check(found, Equals, true) c.Check(name, Equals, "sh") found, name = cfg.MatchNamers.matchers[1].MatchAndName(ksh) c.Check(found, Equals, false) found, name = cfg.MatchNamers.matchers[2].MatchAndName(bash) c.Check(found, Equals, false) found, name = cfg.MatchNamers.matchers[2].MatchAndName(sh) c.Check(found, Equals, false) found, name = cfg.MatchNamers.matchers[2].MatchAndName(ksh) c.Check(found, Equals, true) c.Check(name, Equals, "ksh") } func (s MySuite) TestConfigTemplates(c *C) { yml := ` process_names: - exe: - postmaster cmdline: - "-D\\s+.+?(?P[^/]+)(?:$|\\s)" name: "{{.ExeBase}}:{{.Matches.Path}}" - exe: - prometheus name: "{{.ExeFull}}:{{.PID}}" - comm: - cat name: "{{.StartTime}}" ` cfg, err := GetConfig(yml, false) c.Assert(err, IsNil) c.Check(cfg.MatchNamers.matchers, HasLen, 3) postgres := common.ProcAttributes{Name: "postmaster", Cmdline: []string{"/usr/bin/postmaster", "-D", "/data/pg"}} found, name := cfg.MatchNamers.matchers[0].MatchAndName(postgres) c.Check(found, Equals, true) c.Check(name, Equals, "postmaster:pg") pm := common.ProcAttributes{ Name: "prometheus", Cmdline: []string{"/usr/local/bin/prometheus"}, PID: 23, } found, name = cfg.MatchNamers.matchers[1].MatchAndName(pm) c.Check(found, Equals, true) c.Check(name, Equals, "/usr/local/bin/prometheus:23") now := time.Now() cat := common.ProcAttributes{ Name: "cat", Cmdline: []string{"/bin/cat"}, StartTime: now, } found, name = cfg.MatchNamers.matchers[2].MatchAndName(cat) c.Check(found, Equals, true) c.Check(name, Equals, now.String()) } process-exporter-0.8.3/fixtures/000077500000000000000000000000001464726153700167365ustar00rootroot00000000000000process-exporter-0.8.3/fixtures/14804/000077500000000000000000000000001464726153700174165ustar00rootroot00000000000000process-exporter-0.8.3/fixtures/14804/cgroup000066400000000000000000000001371464726153700206410ustar00rootroot000000000000000::/system.slice/docker-8dde0b0d6e919baef8d635cd9399b22639ed1e400eaec1b1cb94ff3b216cf3c3.scope process-exporter-0.8.3/fixtures/14804/cmdline000066400000000000000000000000431464726153700207510ustar00rootroot00000000000000./process-exporter-procnamesbashprocess-exporter-0.8.3/fixtures/14804/comm000066400000000000000000000000201464726153700202640ustar00rootroot00000000000000process-exporte process-exporter-0.8.3/fixtures/14804/exe000077700000000000000000000000001464726153700250242/usr/bin/process-exporterustar00rootroot00000000000000process-exporter-0.8.3/fixtures/14804/fd/000077500000000000000000000000001464726153700200075ustar00rootroot00000000000000process-exporter-0.8.3/fixtures/14804/fd/0000077700000000000000000000000001464726153700242422../../symlinktargets/abcustar00rootroot00000000000000process-exporter-0.8.3/fixtures/14804/fd/1000077700000000000000000000000001464726153700242542../../symlinktargets/defustar00rootroot00000000000000process-exporter-0.8.3/fixtures/14804/fd/10000077700000000000000000000000001464726153700244302../../symlinktargets/xyzustar00rootroot00000000000000process-exporter-0.8.3/fixtures/14804/fd/2000077700000000000000000000000001464726153700242662../../symlinktargets/ghiustar00rootroot00000000000000process-exporter-0.8.3/fixtures/14804/fd/3000077700000000000000000000000001464726153700243412../../symlinktargets/uvwustar00rootroot00000000000000process-exporter-0.8.3/fixtures/14804/io000066400000000000000000000001521464726153700177460ustar00rootroot00000000000000rchar: 1605958 wchar: 69 syscr: 5534 syscw: 1 read_bytes: 1814455 write_bytes: 0 cancelled_write_bytes: 0 process-exporter-0.8.3/fixtures/14804/limits000066400000000000000000000024531464726153700206460ustar00rootroot00000000000000Limit Soft Limit Hard Limit Units Max cpu time unlimited unlimited seconds Max file size unlimited unlimited bytes Max data size unlimited unlimited bytes Max stack size 8388608 unlimited bytes Max core file size 0 unlimited bytes Max resident set unlimited unlimited bytes Max processes 31421 31421 processes Max open files 1024 65536 files Max locked memory 65536 65536 bytes Max address space unlimited unlimited bytes Max file locks unlimited unlimited locks Max pending signals 31421 31421 signals Max msgqueue size 819200 819200 bytes Max nice priority 0 0 Max realtime priority 0 0 Max realtime timeout unlimited unlimited us process-exporter-0.8.3/fixtures/14804/stat000066400000000000000000000005071464726153700203160ustar00rootroot0000000000000014804 (process-exporte) S 10884 14804 10884 34834 14895 1077936128 1603 0 767 0 10 4 0 0 20 0 7 0 324219 17174528 1969 18446744073709551615 4194304 7971236 140736389529632 140736389529064 4564099 0 0 0 2143420159 0 0 0 17 4 0 0 2 0 0 10805248 11036864 42311680 140736389534279 140736389534314 140736389534314 140736389537765 0 process-exporter-0.8.3/fixtures/14804/status000066400000000000000000000017101464726153700206630ustar00rootroot00000000000000Name: process-exporte State: S (sleeping) Tgid: 14804 Ngid: 0 Pid: 14804 PPid: 10884 TracerPid: 0 Uid: 1000 1000 1000 1000 Gid: 1000 1000 1000 1000 FDSize: 256 Groups: 4 24 27 30 46 110 111 127 1000 NStgid: 14804 NSpid: 14804 NSpgid: 14804 NSsid: 10884 VmPeak: 16772 kB VmSize: 16772 kB VmLck: 0 kB VmPin: 0 kB VmHWM: 7876 kB VmRSS: 7876 kB VmData: 9956 kB VmStk: 132 kB VmExe: 3692 kB VmLib: 0 kB VmPTE: 48 kB VmPMD: 20 kB VmSwap: 10 kB HugetlbPages: 0 kB Threads: 7 SigQ: 0/31421 SigPnd: 0000000000000000 ShdPnd: 0000000000000000 SigBlk: 0000000000000000 SigIgn: 0000000000000000 SigCgt: fffffffe7fc1feff CapInh: 0000000000000000 CapPrm: 0000000000000000 CapEff: 0000000000000000 CapBnd: 0000003fffffffff CapAmb: 0000000000000000 Seccomp: 0 Cpus_allowed: ff Cpus_allowed_list: 0-7 Mems_allowed: 00000000,00000001 Mems_allowed_list: 0 voluntary_ctxt_switches: 72 nonvoluntary_ctxt_switches: 6 process-exporter-0.8.3/fixtures/stat000066400000000000000000000027131464726153700176370ustar00rootroot00000000000000cpu 258072 10128 55919 2163830 6946 0 2336 0 0 0 cpu0 44237 138 12166 358089 1410 0 306 0 0 0 cpu1 39583 23 11894 363839 1027 0 230 0 0 0 cpu2 44817 2670 9943 355700 1509 0 824 0 0 0 cpu3 41434 3808 6188 363646 886 0 250 0 0 0 cpu4 46320 2279 9630 356546 1342 0 312 0 0 0 cpu5 41680 1209 6096 366008 769 0 412 0 0 0 intr 16484556 45 2 0 0 0 0 0 2 1 0 0 0 4 0 0 988 219000 4 3 1601 0 0 247107 0 0 0 0 771839 691840 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 ctxt 30119844 btime 1508450329 processes 28048 procs_running 2 procs_blocked 0 softirq 5524311 18 1594113 712 780657 248302 0 24642 1420512 0 1455355 process-exporter-0.8.3/fixtures/symlinktargets/000077500000000000000000000000001464726153700220165ustar00rootroot00000000000000process-exporter-0.8.3/fixtures/symlinktargets/README000066400000000000000000000002211464726153700226710ustar00rootroot00000000000000This directory contains some empty files that are the symlinks the files in the "fd" directory point to. They are otherwise ignored by the tests process-exporter-0.8.3/fixtures/symlinktargets/abc000066400000000000000000000000001464726153700224540ustar00rootroot00000000000000process-exporter-0.8.3/fixtures/symlinktargets/def000066400000000000000000000000001464726153700224650ustar00rootroot00000000000000process-exporter-0.8.3/fixtures/symlinktargets/ghi000066400000000000000000000000001464726153700224760ustar00rootroot00000000000000process-exporter-0.8.3/fixtures/symlinktargets/uvw000066400000000000000000000000001464726153700225500ustar00rootroot00000000000000process-exporter-0.8.3/fixtures/symlinktargets/xyz000066400000000000000000000000001464726153700225610ustar00rootroot00000000000000process-exporter-0.8.3/go.mod000066400000000000000000000027011464726153700161730ustar00rootroot00000000000000module github.com/ncabatoff/process-exporter go 1.21 require ( github.com/google/go-cmp v0.6.0 github.com/ncabatoff/fakescraper v0.0.0-20201102132415-4b37ba603d65 github.com/ncabatoff/go-seq v0.0.0-20180805175032-b08ef85ed833 github.com/prometheus/client_golang v1.19.0 github.com/prometheus/common v0.52.3 github.com/prometheus/exporter-toolkit v0.11.0 github.com/prometheus/procfs v0.14.0 gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c gopkg.in/yaml.v2 v2.4.0 ) require ( github.com/beorn7/perks v1.0.1 // indirect github.com/cespare/xxhash/v2 v2.2.0 // indirect github.com/coreos/go-systemd/v22 v22.5.0 // indirect github.com/go-kit/log v0.2.1 // indirect github.com/go-logfmt/logfmt v0.5.1 // indirect github.com/golang/protobuf v1.5.3 // indirect github.com/jpillora/backoff v1.0.0 // indirect github.com/kr/pretty v0.3.1 // indirect github.com/kr/text v0.2.0 // indirect github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f // indirect github.com/prometheus/client_model v0.6.0 // indirect github.com/rogpeppe/go-internal v1.10.0 // indirect github.com/stretchr/testify v1.9.0 // indirect golang.org/x/crypto v0.21.0 // indirect golang.org/x/net v0.23.0 // indirect golang.org/x/oauth2 v0.18.0 // indirect golang.org/x/sync v0.7.0 // indirect golang.org/x/sys v0.19.0 // indirect golang.org/x/text v0.14.0 // indirect google.golang.org/appengine v1.6.7 // indirect google.golang.org/protobuf v1.33.0 // indirect ) process-exporter-0.8.3/go.sum000066400000000000000000000170151464726153700162240ustar00rootroot00000000000000github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs= github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/go-kit/log v0.2.1 h1:MRVx0/zhvdseW+Gza6N9rVzU/IVzaeE1SFI4raAhmBU= github.com/go-kit/log v0.2.1/go.mod h1:NwTd00d/i8cPZ3xOwwiv2PO5MOcx78fFErGNcVmBjv0= github.com/go-logfmt/logfmt v0.5.1 h1:otpy5pqBCBZ1ng9RQ0dPu4PN7ba75Y/aA+UpowDyNVA= github.com/go-logfmt/logfmt v0.5.1/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/jpillora/backoff v1.0.0 h1:uvFg412JmmHBHw7iwprIxkPMI+sGQ4kzOWsMeHnm2EA= github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f h1:KUppIJq7/+SVif2QVs3tOP0zanoHgBEVAwHxUSIzRqU= github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/ncabatoff/fakescraper v0.0.0-20201102132415-4b37ba603d65 h1:Og+dVkxEQNvRGU2vUKeOwYT2UJ+pEaDMWB6tIQnIh6A= github.com/ncabatoff/fakescraper v0.0.0-20201102132415-4b37ba603d65/go.mod h1:Tx6UMSMyIsjLG/VU/F6xA1+0XI+/f9o1dGJnf1l+bPg= github.com/ncabatoff/go-seq v0.0.0-20180805175032-b08ef85ed833 h1:t4WWQ9I797y7QUgeEjeXnVb+oYuEDQc6gLvrZJTYo94= github.com/ncabatoff/go-seq v0.0.0-20180805175032-b08ef85ed833/go.mod h1:0CznHmXSjMEqs5Tezj/w2emQoM41wzYM9KpDKUHPYag= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/prometheus/client_golang v1.19.0 h1:ygXvpU1AoN1MhdzckN+PyD9QJOSD4x7kmXYlnfbA6JU= github.com/prometheus/client_golang v1.19.0/go.mod h1:ZRM9uEAypZakd+q/x7+gmsvXdURP+DABIEIjnmDdp+k= github.com/prometheus/client_model v0.6.0 h1:k1v3CzpSRUTrKMppY35TLwPvxHqBu0bYgxZzqGIgaos= github.com/prometheus/client_model v0.6.0/go.mod h1:NTQHnmxFpouOD0DpvP4XujX3CdOAGQPoaGhyTchlyt8= github.com/prometheus/common v0.52.3 h1:5f8uj6ZwHSscOGNdIQg6OiZv/ybiK2CO2q2drVZAQSA= github.com/prometheus/common v0.52.3/go.mod h1:BrxBKv3FWBIGXw89Mg1AeBq7FSyRzXWI3l3e7W3RN5U= github.com/prometheus/exporter-toolkit v0.11.0 h1:yNTsuZ0aNCNFQ3aFTD2uhPOvr4iD7fdBvKPAEGkNf+g= github.com/prometheus/exporter-toolkit v0.11.0/go.mod h1:BVnENhnNecpwoTLiABx7mrPB/OLRIgN74qlQbV+FK1Q= github.com/prometheus/procfs v0.14.0 h1:Lw4VdGGoKEZilJsayHf0B+9YgLGREba2C6xr+Fdfq6s= github.com/prometheus/procfs v0.14.0/go.mod h1:XL+Iwz8k8ZabyZfMFHPiilCniixqQarAy5Mu67pHlNQ= github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA= golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/net v0.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs= golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= golang.org/x/oauth2 v0.18.0 h1:09qnuIAgzdx1XplqJvW6CQqMCtGZykZWcXzPMPUusvI= golang.org/x/oauth2 v0.18.0/go.mod h1:Wf7knwG0MPoWIMMBgFlEaSUDaKskp0dCfrlJRJXbBi8= golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o= golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= process-exporter-0.8.3/packaging/000077500000000000000000000000001464726153700170115ustar00rootroot00000000000000process-exporter-0.8.3/packaging/conf/000077500000000000000000000000001464726153700177365ustar00rootroot00000000000000process-exporter-0.8.3/packaging/conf/all.yaml000066400000000000000000000000751464726153700213740ustar00rootroot00000000000000process_names: - name: "{{.Comm}}" cmdline: - '.+'process-exporter-0.8.3/packaging/default/000077500000000000000000000000001464726153700204355ustar00rootroot00000000000000process-exporter-0.8.3/packaging/default/process-exporter000066400000000000000000000001601464726153700237010ustar00rootroot00000000000000# process-exporter startup flags OPTS='--config.path /etc/process-exporter/all.yaml --web.listen-address=:9256' process-exporter-0.8.3/packaging/process-exporter.service000066400000000000000000000003631464726153700237210ustar00rootroot00000000000000[Unit] Description=Process Exporter for Prometheus [Service] User=root Type=simple EnvironmentFile=-/etc/default/process-exporter ExecStart=/usr/bin/process-exporter $OPTS KillMode=process Restart=always [Install] WantedBy=multi-user.target process-exporter-0.8.3/packaging/scripts/000077500000000000000000000000001464726153700205005ustar00rootroot00000000000000process-exporter-0.8.3/packaging/scripts/postinstall.sh000077500000000000000000000001551464726153700234140ustar00rootroot00000000000000systemctl daemon-reload systemctl enable process-exporter.service systemctl restart process-exporter.service process-exporter-0.8.3/packaging/scripts/postremove.sh000077500000000000000000000000301464726153700232330ustar00rootroot00000000000000systemctl daemon-reload process-exporter-0.8.3/packaging/scripts/preremove.sh000077500000000000000000000001231464726153700230370ustar00rootroot00000000000000systemctl stop process-exporter.service systemctl disable process-exporter.service process-exporter-0.8.3/proc/000077500000000000000000000000001464726153700160305ustar00rootroot00000000000000process-exporter-0.8.3/proc/base_test.go000066400000000000000000000047011464726153700203320ustar00rootroot00000000000000package proc import ( "fmt" "time" common "github.com/ncabatoff/process-exporter" ) type msi map[string]int // procinfo reads the ProcIdInfo for a proc and returns it or a zero value plus // an error. func procinfo(p Proc) (IDInfo, error) { id, err := p.GetProcID() if err != nil { return IDInfo{}, err } static, err := p.GetStatic() if err != nil { return IDInfo{}, err } metrics, _, err := p.GetMetrics() if err != nil { return IDInfo{}, err } return IDInfo{id, static, metrics, nil}, nil } // read everything in the iterator func consumeIter(pi Iter) ([]IDInfo, error) { infos := []IDInfo{} for pi.Next() { info, err := procinfo(pi) if err != nil { return nil, err } infos = append(infos, info) } return infos, nil } type namer map[string]struct{} func newNamer(names ...string) namer { nr := make(namer, len(names)) for _, name := range names { nr[name] = struct{}{} } return nr } func (n namer) String() string { var ss = make([]string, 0, len(n)) for s := range n { ss = append(ss, s) } return fmt.Sprintf("%v", ss) } func (n namer) MatchAndName(nacl common.ProcAttributes) (bool, string) { if _, ok := n[nacl.Name]; ok { return true, nacl.Name } return false, "" } func newProcIDStatic(pid, ppid int, startTime uint64, name string, cmdline []string) (ID, Static) { return ID{pid, startTime}, Static{name, cmdline, []string{}, ppid, time.Unix(int64(startTime), 0).UTC(), 1000} } func newProc(pid int, name string, m Metrics) IDInfo { id, static := newProcIDStatic(pid, 0, 0, name, nil) return IDInfo{id, static, m, nil} } func newProcStart(pid int, name string, startTime uint64) IDInfo { id, static := newProcIDStatic(pid, 0, startTime, name, nil) return IDInfo{id, static, Metrics{}, nil} } func newProcParent(pid int, name string, ppid int) IDInfo { id, static := newProcIDStatic(pid, ppid, 0, name, nil) return IDInfo{id, static, Metrics{}, nil} } func piinfot(pid int, name string, c Counts, m Memory, f Filedesc, threads []Thread) IDInfo { pii := piinfo(pid, name, c, m, f, len(threads)) pii.Threads = threads return pii } func piinfo(pid int, name string, c Counts, m Memory, f Filedesc, t int) IDInfo { return piinfost(pid, name, c, m, f, t, States{}) } func piinfost(pid int, name string, c Counts, m Memory, f Filedesc, t int, s States) IDInfo { id, static := newProcIDStatic(pid, 0, 0, name, nil) return IDInfo{ ID: id, Static: static, Metrics: Metrics{c, m, f, uint64(t), s, ""}, } } process-exporter-0.8.3/proc/grouper.go000066400000000000000000000126051464726153700200460ustar00rootroot00000000000000package proc import ( "time" seq "github.com/ncabatoff/go-seq/seq" common "github.com/ncabatoff/process-exporter" ) type ( // Grouper is the top-level interface to the process metrics. All tracked // procs sharing the same group name are aggregated. Grouper struct { // groupAccum records the historical accumulation of a group so that // we can avoid ever decreasing the counts we return. groupAccum map[string]Counts tracker *Tracker threadAccum map[string]map[string]Threads debug bool removeEmptyGroups bool } // GroupByName maps group name to group metrics. GroupByName map[string]Group // Threads collects metrics for threads in a group sharing a thread name. Threads struct { Name string NumThreads int Counts } // Group describes the metrics of a single group. Group struct { Counts States Wchans map[string]int Procs int Memory OldestStartTime time.Time OpenFDs uint64 WorstFDratio float64 NumThreads uint64 Threads []Threads } ) // Returns true if x < y. Test designers should ensure they always have // a unique name/numthreads combination for each group. func lessThreads(x, y Threads) bool { return seq.Compare(x, y) < 0 } // NewGrouper creates a grouper. func NewGrouper(namer common.MatchNamer, trackChildren, trackThreads, recheck bool, recheckTimeLimit time.Duration, debug bool, removeEmptyGroups bool) *Grouper { g := Grouper{ groupAccum: make(map[string]Counts), threadAccum: make(map[string]map[string]Threads), tracker: NewTracker(namer, trackChildren, recheck, recheckTimeLimit, debug), debug: debug, removeEmptyGroups: removeEmptyGroups, } return &g } func groupadd(grp Group, ts Update) Group { var zeroTime time.Time grp.Procs++ grp.Memory.ResidentBytes += ts.Memory.ResidentBytes grp.Memory.VirtualBytes += ts.Memory.VirtualBytes grp.Memory.VmSwapBytes += ts.Memory.VmSwapBytes grp.Memory.ProportionalBytes += ts.Memory.ProportionalBytes grp.Memory.ProportionalSwapBytes += ts.Memory.ProportionalSwapBytes if ts.Filedesc.Open != -1 { grp.OpenFDs += uint64(ts.Filedesc.Open) } openratio := float64(ts.Filedesc.Open) / float64(ts.Filedesc.Limit) if grp.WorstFDratio < openratio { grp.WorstFDratio = openratio } grp.NumThreads += ts.NumThreads grp.Counts.Add(ts.Latest) grp.States.Add(ts.States) if grp.OldestStartTime == zeroTime || ts.Start.Before(grp.OldestStartTime) { grp.OldestStartTime = ts.Start } if grp.Wchans == nil { grp.Wchans = make(map[string]int) } for wchan, count := range ts.Wchans { grp.Wchans[wchan] += count } return grp } // Update asks the tracker to report on each tracked process by name. // These are aggregated by groupname, augmented by accumulated counts // from the past, and returned. Note that while the Tracker reports // only what counts have changed since last cycle, Grouper.Update // returns counts that never decrease. If removeEmptyGroups is false, // then even once the last process with name X disappears, name X will // still appear in the results with the same counts as before; of course, // all non-count metrics will be zero. func (g *Grouper) Update(iter Iter) (CollectErrors, GroupByName, error) { cerrs, tracked, err := g.tracker.Update(iter) if err != nil { return cerrs, nil, err } return cerrs, g.groups(tracked), nil } // Translate the updates into a new GroupByName and update internal history. func (g *Grouper) groups(tracked []Update) GroupByName { groups := make(GroupByName) threadsByGroup := make(map[string][]ThreadUpdate) for _, update := range tracked { groups[update.GroupName] = groupadd(groups[update.GroupName], update) if update.Threads != nil { threadsByGroup[update.GroupName] = append(threadsByGroup[update.GroupName], update.Threads...) } } // Add any accumulated counts to what was just observed, // and update the accumulators. for gname, group := range groups { if oldcounts, ok := g.groupAccum[gname]; ok { group.Counts.Add(Delta(oldcounts)) } g.groupAccum[gname] = group.Counts group.Threads = g.threads(gname, threadsByGroup[gname]) groups[gname] = group } // Now add any groups that were observed in the past but aren't running now (or delete them, if removeEmptyGroups is true). for gname, gcounts := range g.groupAccum { if _, ok := groups[gname]; !ok { if g.removeEmptyGroups { delete(g.groupAccum, gname) delete(g.threadAccum, gname) } else { groups[gname] = Group{Counts: gcounts} } } } return groups } func (g *Grouper) threads(gname string, tracked []ThreadUpdate) []Threads { if len(tracked) == 0 { delete(g.threadAccum, gname) return nil } ret := make([]Threads, 0, len(tracked)) threads := make(map[string]Threads) // First aggregate the thread metrics by thread name. for _, nc := range tracked { curthr := threads[nc.ThreadName] curthr.NumThreads++ curthr.Counts.Add(nc.Latest) curthr.Name = nc.ThreadName threads[nc.ThreadName] = curthr } // Add any accumulated counts to what was just observed, // and update the accumulators. if history := g.threadAccum[gname]; history != nil { for tname := range threads { if oldcounts, ok := history[tname]; ok { counts := threads[tname] counts.Add(Delta(oldcounts.Counts)) threads[tname] = counts } } } g.threadAccum[gname] = threads for _, thr := range threads { ret = append(ret, thr) } return ret } process-exporter-0.8.3/proc/grouper_test.go000066400000000000000000000210661464726153700211060ustar00rootroot00000000000000package proc import ( "testing" "time" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" ) type grouptest struct { grouper *Grouper procs Iter want GroupByName } //func (gt grouptest) run(c *C) { // _, err := gt.grouper.Update(gt.procs) // c.Assert(err, IsNil) // // got := gt.grouper.curgroups() // c.Check(got, DeepEquals, gt.want, Commentf("diff %s", pretty.Compare(got, gt.want))) //} func rungroup(t *testing.T, gr *Grouper, procs Iter) GroupByName { _, groups, err := gr.Update(procs) if err != nil { t.Fatalf("group.Update error: %v", err) } return groups } // TestGrouperBasic tests core Update/curgroups functionality on single-proc // groups: the grouper adds to counts and updates the other tracked metrics like // Memory. func TestGrouperBasic(t *testing.T) { p1, p2 := 1, 2 n1, n2 := "g1", "g2" starttime := time.Unix(0, 0).UTC() tests := []struct { procs []IDInfo want GroupByName }{ { []IDInfo{ piinfost(p1, n1, Counts{1, 2, 3, 4, 5, 6, 0, 0}, Memory{7, 8, 0, 0, 0}, Filedesc{4, 400}, 2, States{Other: 1}), piinfost(p2, n2, Counts{2, 3, 4, 5, 6, 7, 0, 0}, Memory{8, 9, 0, 0, 0}, Filedesc{40, 400}, 3, States{Waiting: 1}), }, GroupByName{ "g1": Group{Counts{}, States{Other: 1}, msi{}, 1, Memory{7, 8, 0, 0, 0}, starttime, 4, 0.01, 2, nil}, "g2": Group{Counts{}, States{Waiting: 1}, msi{}, 1, Memory{8, 9, 0, 0, 0}, starttime, 40, 0.1, 3, nil}, }, }, { []IDInfo{ piinfost(p1, n1, Counts{2, 3, 4, 5, 6, 7, 0, 0}, Memory{6, 7, 0, 0, 0}, Filedesc{100, 400}, 4, States{Zombie: 1}), piinfost(p2, n2, Counts{4, 5, 6, 7, 8, 9, 0, 0}, Memory{9, 8, 0, 0, 0}, Filedesc{400, 400}, 2, States{Running: 1}), }, GroupByName{ "g1": Group{Counts{1, 1, 1, 1, 1, 1, 0, 0}, States{Zombie: 1}, msi{}, 1, Memory{6, 7, 0, 0, 0}, starttime, 100, 0.25, 4, nil}, "g2": Group{Counts{2, 2, 2, 2, 2, 2, 0, 0}, States{Running: 1}, msi{}, 1, Memory{9, 8, 0, 0, 0}, starttime, 400, 1, 2, nil}, }, }, } gr := NewGrouper(newNamer(n1, n2), false, false, false, 0, false, false) for i, tc := range tests { got := rungroup(t, gr, procInfoIter(tc.procs...)) if diff := cmp.Diff(got, tc.want); diff != "" { t.Errorf("%d: curgroups differs: (-got +want)\n%s", i, diff) } } } // TestGrouperProcJoin tests the appearance of a new process in a group, // and that all procs metrics contribute to a group. func TestGrouperProcJoin(t *testing.T) { p1, p2 := 1, 2 n1, n2 := "g1", "g1" starttime := time.Unix(0, 0).UTC() tests := []struct { procs []IDInfo want GroupByName }{ { []IDInfo{ piinfo(p1, n1, Counts{1, 2, 3, 4, 5, 6, 0, 0}, Memory{3, 4, 0, 0, 0}, Filedesc{4, 400}, 2), }, GroupByName{ "g1": Group{Counts{}, States{}, msi{}, 1, Memory{3, 4, 0, 0, 0}, starttime, 4, 0.01, 2, nil}, }, }, { // The counts for pid2 won't be factored into the total yet because we only add // to counts starting with the second time we see a proc. Memory and FDs are // affected though. []IDInfo{ piinfost(p1, n1, Counts{3, 4, 5, 6, 7, 8, 0, 0}, Memory{3, 4, 0, 0, 0}, Filedesc{4, 400}, 2, States{Running: 1}), piinfost(p2, n2, Counts{1, 1, 1, 1, 1, 1, 0, 0}, Memory{1, 2, 0, 0, 0}, Filedesc{40, 400}, 3, States{Sleeping: 1}), }, GroupByName{ "g1": Group{Counts{2, 2, 2, 2, 2, 2, 0, 0}, States{Running: 1, Sleeping: 1}, msi{}, 2, Memory{4, 6, 0, 0, 0}, starttime, 44, 0.1, 5, nil}, }, }, { []IDInfo{ piinfost(p1, n1, Counts{4, 5, 6, 7, 8, 9, 0, 0}, Memory{1, 5, 0, 0, 0}, Filedesc{4, 400}, 2, States{Running: 1}), piinfost(p2, n2, Counts{2, 2, 2, 2, 2, 2, 0, 0}, Memory{2, 4, 0, 0, 0}, Filedesc{40, 400}, 3, States{Running: 1}), }, GroupByName{ "g1": Group{Counts{4, 4, 4, 4, 4, 4, 0, 0}, States{Running: 2}, msi{}, 2, Memory{3, 9, 0, 0, 0}, starttime, 44, 0.1, 5, nil}, }, }, } gr := NewGrouper(newNamer(n1), false, false, false, 0, false, false) for i, tc := range tests { got := rungroup(t, gr, procInfoIter(tc.procs...)) if diff := cmp.Diff(got, tc.want); diff != "" { t.Errorf("%d: curgroups differs: (-got +want)\n%s", i, diff) } } } // TestGrouperNonDecreasing tests the disappearance of a process. Its previous // contribution to the counts should not go away when that happens if removeEmptyGroups is false. func TestGrouperNonDecreasing(t *testing.T) { p1, p2 := 1, 2 n1, n2 := "g1", "g1" starttime := time.Unix(0, 0).UTC() tests := []struct { procs []IDInfo want GroupByName }{ { []IDInfo{ piinfo(p1, n1, Counts{3, 4, 5, 6, 7, 8, 0, 0}, Memory{3, 4, 0, 0, 0}, Filedesc{4, 400}, 2), piinfo(p2, n2, Counts{1, 1, 1, 1, 1, 1, 0, 0}, Memory{1, 2, 0, 0, 0}, Filedesc{40, 400}, 3), }, GroupByName{ "g1": Group{Counts{}, States{}, msi{}, 2, Memory{4, 6, 0, 0, 0}, starttime, 44, 0.1, 5, nil}, }, }, { []IDInfo{ piinfo(p1, n1, Counts{4, 5, 6, 7, 8, 9, 0, 0}, Memory{1, 5, 0, 0, 0}, Filedesc{4, 400}, 2), }, GroupByName{ "g1": Group{Counts{1, 1, 1, 1, 1, 1, 0, 0}, States{}, msi{}, 1, Memory{1, 5, 0, 0, 0}, starttime, 4, 0.01, 2, nil}, }, }, { []IDInfo{}, GroupByName{ "g1": Group{Counts{1, 1, 1, 1, 1, 1, 0, 0}, States{}, nil, 0, Memory{}, time.Time{}, 0, 0, 0, nil}, }, }, } gr := NewGrouper(newNamer(n1), false, false, false, 0, false, false) for i, tc := range tests { got := rungroup(t, gr, procInfoIter(tc.procs...)) if diff := cmp.Diff(got, tc.want); diff != "" { t.Errorf("%d: curgroups differs: (-got +want)\n%s", i, diff) } } } // TestGrouperNonDecreasing tests the disappearance of a process. // We want the group to disappear if removeEmptyGroups is true. func TestGrouperRemoveEmptyGroups(t *testing.T) { p1, p2 := 1, 2 n1, n2 := "g1", "g2" starttime := time.Unix(0, 0).UTC() tests := []struct { procs []IDInfo want GroupByName }{ { []IDInfo{ piinfo(p1, n1, Counts{3, 4, 5, 6, 7, 8, 0, 0}, Memory{3, 4, 0, 0, 0}, Filedesc{4, 400}, 2), piinfo(p2, n2, Counts{1, 1, 1, 1, 1, 1, 0, 0}, Memory{1, 2, 0, 0, 0}, Filedesc{40, 400}, 3), }, GroupByName{ n1: Group{Counts{}, States{}, msi{}, 1, Memory{3, 4, 0, 0, 0}, starttime, 4, 0.01, 2, nil}, n2: Group{Counts{}, States{}, msi{}, 1, Memory{1, 2, 0, 0, 0}, starttime, 40, 0.1, 3, nil}, }, }, { []IDInfo{ piinfo(p1, n1, Counts{4, 5, 6, 7, 8, 9, 0, 0}, Memory{1, 5, 0, 0, 0}, Filedesc{4, 400}, 2), }, GroupByName{ n1: Group{Counts{1, 1, 1, 1, 1, 1, 0, 0}, States{}, msi{}, 1, Memory{1, 5, 0, 0, 0}, starttime, 4, 0.01, 2, nil}, }, }, { []IDInfo{}, GroupByName{}, }, } gr := NewGrouper(newNamer(n1, n2), false, false, false, 0, false, true) for i, tc := range tests { got := rungroup(t, gr, procInfoIter(tc.procs...)) if diff := cmp.Diff(got, tc.want); diff != "" { t.Errorf("%d: curgroups differs: (-got +want)\n%s", i, diff) } } } func TestGrouperThreads(t *testing.T) { p, n, tm := 1, "g1", time.Unix(0, 0).UTC() tests := []struct { proc IDInfo want GroupByName }{ { piinfot(p, n, Counts{}, Memory{}, Filedesc{1, 1}, []Thread{ {ThreadID(ID{p, 0}), "t1", Counts{1, 2, 3, 4, 5, 6, 0, 0}, "", States{}}, {ThreadID(ID{p + 1, 0}), "t2", Counts{1, 1, 1, 1, 1, 1, 0, 0}, "", States{}}, }), GroupByName{ "g1": Group{Counts{}, States{}, msi{}, 1, Memory{}, tm, 1, 1, 2, []Threads{ Threads{"t1", 1, Counts{}}, Threads{"t2", 1, Counts{}}, }}, }, }, { piinfot(p, n, Counts{}, Memory{}, Filedesc{1, 1}, []Thread{ {ThreadID(ID{p, 0}), "t1", Counts{2, 3, 4, 5, 6, 7, 0, 0}, "", States{}}, {ThreadID(ID{p + 1, 0}), "t2", Counts{2, 2, 2, 2, 2, 2, 0, 0}, "", States{}}, {ThreadID(ID{p + 2, 0}), "t2", Counts{1, 1, 1, 1, 1, 1, 0, 0}, "", States{}}, }), GroupByName{ "g1": Group{Counts{}, States{}, msi{}, 1, Memory{}, tm, 1, 1, 3, []Threads{ Threads{"t1", 1, Counts{1, 1, 1, 1, 1, 1, 0, 0}}, Threads{"t2", 2, Counts{1, 1, 1, 1, 1, 1, 0, 0}}, }}, }, }, { piinfot(p, n, Counts{}, Memory{}, Filedesc{1, 1}, []Thread{ {ThreadID(ID{p + 1, 0}), "t2", Counts{4, 4, 4, 4, 4, 4, 0, 0}, "", States{}}, {ThreadID(ID{p + 2, 0}), "t2", Counts{2, 3, 4, 5, 6, 7, 0, 0}, "", States{}}, }), GroupByName{ "g1": Group{Counts{}, States{}, msi{}, 1, Memory{}, tm, 1, 1, 2, []Threads{ Threads{"t2", 2, Counts{4, 5, 6, 7, 8, 9, 0, 0}}, }}, }, }, } opts := cmpopts.SortSlices(lessThreads) gr := NewGrouper(newNamer(n), false, true, false, 0, false, false) for i, tc := range tests { got := rungroup(t, gr, procInfoIter(tc.proc)) if diff := cmp.Diff(got, tc.want, opts); diff != "" { t.Errorf("%d: curgroups differs: (-got +want)\n%s", i, diff) } } } process-exporter-0.8.3/proc/read.go000066400000000000000000000340561464726153700173020ustar00rootroot00000000000000package proc import ( "fmt" "os" "path/filepath" "strconv" "time" "github.com/prometheus/procfs" ) // ErrProcNotExist indicates a process couldn't be read because it doesn't exist, // typically because it disappeared while we were reading it. var ErrProcNotExist = fmt.Errorf("process does not exist") type ( // ID uniquely identifies a process. ID struct { // UNIX process id Pid int // The time the process started after system boot, the value is expressed // in clock ticks. StartTimeRel uint64 } ThreadID ID // Static contains data read from /proc/pid/* Static struct { Name string Cmdline []string Cgroups []string ParentPid int StartTime time.Time EffectiveUID int } // Counts are metric counters common to threads and processes and groups. Counts struct { CPUUserTime float64 CPUSystemTime float64 ReadBytes uint64 WriteBytes uint64 MajorPageFaults uint64 MinorPageFaults uint64 CtxSwitchVoluntary uint64 CtxSwitchNonvoluntary uint64 } // Memory describes a proc's memory usage. Memory struct { ResidentBytes uint64 VirtualBytes uint64 VmSwapBytes uint64 ProportionalBytes uint64 ProportionalSwapBytes uint64 } // Filedesc describes a proc's file descriptor usage and soft limit. Filedesc struct { // Open is the count of open file descriptors, -1 if unknown. Open int64 // Limit is the fd soft limit for the process. Limit uint64 } // States counts how many threads are in each state. States struct { Running int Sleeping int Waiting int Zombie int Other int } // Metrics contains data read from /proc/pid/* Metrics struct { Counts Memory Filedesc NumThreads uint64 States Wchan string } // Thread contains per-thread data. Thread struct { ThreadID ThreadName string Counts Wchan string States } // IDInfo groups all info for a single process. IDInfo struct { ID Static Metrics Threads []Thread } // ProcIdInfoThreads struct { // ProcIdInfo // Threads []ProcThread // } // Proc wraps the details of the underlying procfs-reading library. // Any of these methods may fail if the process has disapeared. // We try to return as much as possible rather than an error, e.g. // if some /proc files are unreadable. Proc interface { // GetPid() returns the POSIX PID (process id). They may be reused over time. GetPid() int // GetProcID() returns (pid,starttime), which can be considered a unique process id. GetProcID() (ID, error) // GetStatic() returns various details read from files under /proc//. Technically // name may not be static, but we'll pretend it is. GetStatic() (Static, error) // GetMetrics() returns various metrics read from files under /proc//. // It returns an error on complete failure. Otherwise, it returns metrics // and 0 on complete success, 1 if some (like I/O) couldn't be read. GetMetrics() (Metrics, int, error) GetStates() (States, error) GetWchan() (string, error) GetCounts() (Counts, int, error) GetThreads() ([]Thread, error) } // proccache implements the Proc interface by acting as wrapper for procfs.Proc // that caches results of some reads. proccache struct { procfs.Proc procid *ID stat *procfs.ProcStat status *procfs.ProcStatus cmdline []string cgroups []procfs.Cgroup io *procfs.ProcIO fs *FS wchan *string } proc struct { proccache } // procs is a fancier []Proc that saves on some copying. procs interface { get(int) Proc length() int } // procfsprocs implements procs using procfs. procfsprocs struct { Procs []procfs.Proc fs *FS } // Iter is an iterator over a sequence of procs. Iter interface { // Next returns true if the iterator is not exhausted. Next() bool // Close releases any resources the iterator uses. Close() error // The iterator satisfies the Proc interface. Proc } // procIterator implements the Iter interface procIterator struct { // procs is the list of Proc we're iterating over. procs // idx is the current iteration, i.e. it's an index into procs. idx int // err is set with an error when Next() fails. It is not affected by failures accessing // the current iteration variable, e.g. with GetProcId. err error // Proc is the current iteration variable, or nil if Next() has never been called or the // iterator is exhausted. Proc } // Source is a source of procs. Source interface { // AllProcs returns all the processes in this source at this moment in time. AllProcs() Iter } // FS implements Source. FS struct { procfs.FS BootTime uint64 MountPoint string GatherSMaps bool debug bool } ) func (ii IDInfo) String() string { return fmt.Sprintf("%+v:%+v", ii.ID, ii.Static) } // Add adds c2 to the counts. func (c *Counts) Add(c2 Delta) { c.CPUUserTime += c2.CPUUserTime c.CPUSystemTime += c2.CPUSystemTime c.ReadBytes += c2.ReadBytes c.WriteBytes += c2.WriteBytes c.MajorPageFaults += c2.MajorPageFaults c.MinorPageFaults += c2.MinorPageFaults c.CtxSwitchVoluntary += c2.CtxSwitchVoluntary c.CtxSwitchNonvoluntary += c2.CtxSwitchNonvoluntary } // Sub subtracts c2 from the counts. func (c Counts) Sub(c2 Counts) Delta { c.CPUUserTime -= c2.CPUUserTime c.CPUSystemTime -= c2.CPUSystemTime c.ReadBytes -= c2.ReadBytes c.WriteBytes -= c2.WriteBytes c.MajorPageFaults -= c2.MajorPageFaults c.MinorPageFaults -= c2.MinorPageFaults c.CtxSwitchVoluntary -= c2.CtxSwitchVoluntary c.CtxSwitchNonvoluntary -= c2.CtxSwitchNonvoluntary return Delta(c) } func (s *States) Add(s2 States) { s.Other += s2.Other s.Running += s2.Running s.Sleeping += s2.Sleeping s.Waiting += s2.Waiting s.Zombie += s2.Zombie } func (p IDInfo) GetThreads() ([]Thread, error) { return p.Threads, nil } // GetPid implements Proc. func (p IDInfo) GetPid() int { return p.ID.Pid } // GetProcID implements Proc. func (p IDInfo) GetProcID() (ID, error) { return p.ID, nil } // GetStatic implements Proc. func (p IDInfo) GetStatic() (Static, error) { return p.Static, nil } // GetCounts implements Proc. func (p IDInfo) GetCounts() (Counts, int, error) { return p.Metrics.Counts, 0, nil } // GetMetrics implements Proc. func (p IDInfo) GetMetrics() (Metrics, int, error) { return p.Metrics, 0, nil } // GetStates implements Proc. func (p IDInfo) GetStates() (States, error) { return p.States, nil } func (p IDInfo) GetWchan() (string, error) { return p.Wchan, nil } func (p *proccache) GetPid() int { return p.Proc.PID } func (p *proccache) getStat() (procfs.ProcStat, error) { if p.stat == nil { stat, err := p.Proc.NewStat() if err != nil { return procfs.ProcStat{}, err } p.stat = &stat } return *p.stat, nil } func (p *proccache) getStatus() (procfs.ProcStatus, error) { if p.status == nil { status, err := p.Proc.NewStatus() if err != nil { return procfs.ProcStatus{}, err } p.status = &status } return *p.status, nil } func (p *proccache) getCgroups() ([]procfs.Cgroup, error) { if p.cgroups == nil { cgroups, err := p.Proc.Cgroups() if err != nil { return nil, err } p.cgroups = cgroups } return p.cgroups, nil } // GetProcID implements Proc. func (p *proccache) GetProcID() (ID, error) { if p.procid == nil { stat, err := p.getStat() if err != nil { return ID{}, err } p.procid = &ID{Pid: p.GetPid(), StartTimeRel: stat.Starttime} } return *p.procid, nil } func (p *proccache) getCmdLine() ([]string, error) { if p.cmdline == nil { cmdline, err := p.Proc.CmdLine() if err != nil { return nil, err } p.cmdline = cmdline } return p.cmdline, nil } func (p *proccache) getWchan() (string, error) { if p.wchan == nil { wchan, err := p.Proc.Wchan() if err != nil { return "", err } p.wchan = &wchan } return *p.wchan, nil } func (p *proccache) getIo() (procfs.ProcIO, error) { if p.io == nil { io, err := p.Proc.IO() if err != nil { return procfs.ProcIO{}, err } p.io = &io } return *p.io, nil } // GetStatic returns the ProcStatic corresponding to this proc. func (p *proccache) GetStatic() (Static, error) { // /proc//cmdline is normally world-readable. cmdline, err := p.getCmdLine() if err != nil { return Static{}, err } // /proc//stat is normally world-readable. stat, err := p.getStat() if err != nil { return Static{}, err } startTime := time.Unix(int64(p.fs.BootTime), 0).UTC() startTime = startTime.Add(time.Second / userHZ * time.Duration(stat.Starttime)) // /proc//status is normally world-readable. status, err := p.getStatus() if err != nil { return Static{}, err } // /proc//cgroup(s) is normally world-readable. // However cgroups aren't always supported -> return an empty array in that // case. cgroups, err := p.getCgroups() var cgroupsStr []string if err != nil { cgroupsStr = []string{} } else { for _, c := range cgroups { cgroupsStr = append(cgroupsStr, c.Path) } } return Static{ Name: stat.Comm, Cmdline: cmdline, Cgroups: cgroupsStr, ParentPid: stat.PPID, StartTime: startTime, EffectiveUID: int(status.UIDs[1]), }, nil } func (p proc) GetCounts() (Counts, int, error) { stat, err := p.getStat() if err != nil { if err == os.ErrNotExist { err = ErrProcNotExist } return Counts{}, 0, fmt.Errorf("error reading stat file: %v", err) } status, err := p.getStatus() if err != nil { if err == os.ErrNotExist { err = ErrProcNotExist } return Counts{}, 0, fmt.Errorf("error reading status file: %v", err) } io, err := p.getIo() softerrors := 0 if err != nil { softerrors++ } return Counts{ CPUUserTime: float64(stat.UTime) / userHZ, CPUSystemTime: float64(stat.STime) / userHZ, ReadBytes: io.ReadBytes, WriteBytes: io.WriteBytes, MajorPageFaults: uint64(stat.MajFlt), MinorPageFaults: uint64(stat.MinFlt), CtxSwitchVoluntary: uint64(status.VoluntaryCtxtSwitches), CtxSwitchNonvoluntary: uint64(status.NonVoluntaryCtxtSwitches), }, softerrors, nil } func (p proc) GetWchan() (string, error) { return p.getWchan() } func (p proc) GetStates() (States, error) { stat, err := p.getStat() if err != nil { return States{}, err } var s States switch stat.State { case "R": s.Running++ case "S": s.Sleeping++ case "D": s.Waiting++ case "Z": s.Zombie++ default: s.Other++ } return s, nil } // GetMetrics returns the current metrics for the proc. The results are // not cached. func (p proc) GetMetrics() (Metrics, int, error) { counts, softerrors, err := p.GetCounts() if err != nil { return Metrics{}, 0, err } // We don't need to check for error here because p will have cached // the successful result of calling getStat in GetCounts. // Since GetMetrics isn't a pointer receiver method, our callers // won't see the effect of the caching between calls. stat, _ := p.getStat() // Ditto for states states, _ := p.GetStates() // Ditto for status status, _ := p.getStatus() numfds, err := p.Proc.FileDescriptorsLen() if err != nil { numfds = -1 softerrors |= 1 } limits, err := p.Proc.NewLimits() if err != nil { return Metrics{}, 0, err } wchan, err := p.getWchan() if err != nil { softerrors |= 1 } memory := Memory{ ResidentBytes: uint64(stat.ResidentMemory()), VirtualBytes: uint64(stat.VirtualMemory()), VmSwapBytes: uint64(status.VmSwap), } if p.proccache.fs.GatherSMaps { smaps, err := p.Proc.ProcSMapsRollup() if err != nil { softerrors |= 1 } else { memory.ProportionalBytes = smaps.Pss memory.ProportionalSwapBytes = smaps.SwapPss } } return Metrics{ Counts: counts, Memory: memory, Filedesc: Filedesc{ Open: int64(numfds), Limit: uint64(limits.OpenFiles), }, NumThreads: uint64(stat.NumThreads), States: states, Wchan: wchan, }, softerrors, nil } func (p proc) GetThreads() ([]Thread, error) { fs, err := p.fs.threadFs(p.PID) if err != nil { return nil, err } threads := []Thread{} iter := fs.AllProcs() for iter.Next() { var id ID id, err = iter.GetProcID() if err != nil { continue } var static Static static, err = iter.GetStatic() if err != nil { continue } var counts Counts counts, _, err = iter.GetCounts() if err != nil { continue } wchan, _ := iter.GetWchan() states, _ := iter.GetStates() threads = append(threads, Thread{ ThreadID: ThreadID(id), ThreadName: static.Name, Counts: counts, Wchan: wchan, States: states, }) } err = iter.Close() if err != nil { return nil, err } if len(threads) < 2 { return nil, nil } return threads, nil } // See https://github.com/prometheus/procfs/blob/master/proc_stat.go for details on userHZ. const userHZ = 100 // NewFS returns a new FS mounted under the given mountPoint. It will error // if the mount point can't be read. func NewFS(mountPoint string, debug bool) (*FS, error) { fs, err := procfs.NewFS(mountPoint) if err != nil { return nil, err } stat, err := fs.NewStat() if err != nil { return nil, err } return &FS{fs, stat.BootTime, mountPoint, false, debug}, nil } func (fs *FS) threadFs(pid int) (*FS, error) { mountPoint := filepath.Join(fs.MountPoint, strconv.Itoa(pid), "task") tfs, err := procfs.NewFS(mountPoint) if err != nil { return nil, err } return &FS{tfs, fs.BootTime, mountPoint, fs.GatherSMaps, false}, nil } // AllProcs implements Source. func (fs *FS) AllProcs() Iter { procs, err := fs.FS.AllProcs() if err != nil { err = fmt.Errorf("Error reading procs: %v", err) } return &procIterator{procs: procfsprocs{procs, fs}, err: err, idx: -1} } // get implements procs. func (p procfsprocs) get(i int) Proc { return &proc{proccache{Proc: p.Procs[i], fs: p.fs}} } // length implements procs. func (p procfsprocs) length() int { return len(p.Procs) } // Next implements Iter. func (pi *procIterator) Next() bool { pi.idx++ if pi.idx < pi.procs.length() { pi.Proc = pi.procs.get(pi.idx) } else { pi.Proc = nil } return pi.idx < pi.procs.length() } // Close implements Iter. func (pi *procIterator) Close() error { pi.Next() pi.procs = nil pi.Proc = nil return pi.err } process-exporter-0.8.3/proc/read_test.go000066400000000000000000000116751464726153700203430ustar00rootroot00000000000000package proc import ( "fmt" "os" "os/exec" "testing" "time" "github.com/google/go-cmp/cmp" ) type ( // procIDInfos implements procs using a slice of already // populated ProcIdInfo. Used for testing. procIDInfos []IDInfo ) func (p procIDInfos) get(i int) Proc { return &p[i] } func (p procIDInfos) length() int { return len(p) } func procInfoIter(ps ...IDInfo) *procIterator { return &procIterator{procs: procIDInfos(ps), idx: -1} } func allprocs(procpath string) Iter { fs, err := NewFS(procpath, false) if err != nil { cwd, _ := os.Getwd() panic("can't read " + procpath + ", cwd=" + cwd + ", err=" + fmt.Sprintf("%v", err)) } return fs.AllProcs() } func TestReadFixture(t *testing.T) { procs := allprocs("../fixtures") var pii IDInfo count := 0 for procs.Next() { count++ var err error pii, err = procinfo(procs) noerr(t, err) } err := procs.Close() noerr(t, err) if count != 1 { t.Fatalf("got %d procs, want 1", count) } wantprocid := ID{Pid: 14804, StartTimeRel: 0x4f27b} if diff := cmp.Diff(pii.ID, wantprocid); diff != "" { t.Errorf("procid differs: (-got +want)\n%s", diff) } stime, _ := time.Parse(time.RFC3339Nano, "2017-10-19T22:52:51.19Z") wantstatic := Static{ Name: "process-exporte", Cmdline: []string{"./process-exporter", "-procnames", "bash"}, Cgroups: []string{"/system.slice/docker-8dde0b0d6e919baef8d635cd9399b22639ed1e400eaec1b1cb94ff3b216cf3c3.scope"}, ParentPid: 10884, StartTime: stime, EffectiveUID: 1000, } if diff := cmp.Diff(pii.Static, wantstatic); diff != "" { t.Errorf("static differs: (-got +want)\n%s", diff) } wantmetrics := Metrics{ Counts: Counts{ CPUUserTime: 0.1, CPUSystemTime: 0.04, ReadBytes: 1814455, WriteBytes: 0, MajorPageFaults: 0x2ff, MinorPageFaults: 0x643, CtxSwitchVoluntary: 72, CtxSwitchNonvoluntary: 6, }, Memory: Memory{ ResidentBytes: 0x7b1000, VirtualBytes: 0x1061000, VmSwapBytes: 0x2800, }, Filedesc: Filedesc{ Open: 5, Limit: 0x400, }, NumThreads: 7, States: States{Sleeping: 1}, } if diff := cmp.Diff(pii.Metrics, wantmetrics); diff != "" { t.Errorf("metrics differs: (-got +want)\n%s", diff) } } func noerr(t *testing.T, err error) { if err != nil { t.Fatalf("error: %v", err) } } // Basic test of proc reading: does AllProcs return at least two procs, one of which is us. func TestAllProcs(t *testing.T) { procs := allprocs("/proc") count := 0 for procs.Next() { count++ if procs.GetPid() != os.Getpid() { continue } procid, err := procs.GetProcID() noerr(t, err) if procid.Pid != os.Getpid() { t.Errorf("got %d, want %d", procid.Pid, os.Getpid()) } static, err := procs.GetStatic() noerr(t, err) if static.ParentPid != os.Getppid() { t.Errorf("got %d, want %d", static.ParentPid, os.Getppid()) } metrics, _, err := procs.GetMetrics() noerr(t, err) if metrics.ResidentBytes == 0 { t.Errorf("got 0 bytes resident, want nonzero") } // All Go programs have multiple threads. if metrics.NumThreads < 2 { t.Errorf("got %d threads, want >1", metrics.NumThreads) } var zstates States if metrics.States == zstates { t.Errorf("got empty states") } threads, err := procs.GetThreads() if len(threads) < 2 { t.Errorf("got %d thread details, want >1", len(threads)) } } err := procs.Close() noerr(t, err) if count == 0 { t.Errorf("got %d, want 0", count) } } // Test that we can observe the absence of a child process before it spawns and after it exits, // and its presence during its lifetime. func TestAllProcsSpawn(t *testing.T) { childprocs := func() []IDInfo { found := []IDInfo{} procs := allprocs("/proc") mypid := os.Getpid() for procs.Next() { procid, err := procs.GetProcID() if err != nil { continue } static, err := procs.GetStatic() if err != nil { continue } if static.ParentPid == mypid { found = append(found, IDInfo{procid, static, Metrics{}, nil}) } } err := procs.Close() if err != nil { t.Fatalf("error closing procs iterator: %v", err) } return found } foundcat := func(procs []IDInfo) bool { for _, proc := range procs { if proc.Name == "cat" { return true } } return false } if foundcat(childprocs()) { t.Errorf("found cat before spawning it") } cmd := exec.Command("/bin/cat") wc, err := cmd.StdinPipe() noerr(t, err) err = cmd.Start() noerr(t, err) if !foundcat(childprocs()) { t.Errorf("didn't find cat after spawning it") } err = wc.Close() noerr(t, err) err = cmd.Wait() noerr(t, err) if foundcat(childprocs()) { t.Errorf("found cat after exit") } } func TestIterator(t *testing.T) { p1 := newProc(1, "p1", Metrics{}) p2 := newProc(2, "p2", Metrics{}) want := []IDInfo{p1, p2} pis := procInfoIter(want...) got, err := consumeIter(pis) noerr(t, err) if diff := cmp.Diff(got, want); diff != "" { t.Errorf("procs differs: (-got +want)\n%s", diff) } } process-exporter-0.8.3/proc/tracker.go000066400000000000000000000327071464726153700200230ustar00rootroot00000000000000package proc import ( "fmt" "log" "os/user" "strconv" "time" seq "github.com/ncabatoff/go-seq/seq" common "github.com/ncabatoff/process-exporter" ) type ( // Tracker tracks processes and records metrics. Tracker struct { // namer determines what processes to track and names them namer common.MatchNamer // tracked holds the processes are being monitored. Processes // may be blacklisted such that they no longer get tracked by // setting their value in the tracked map to nil. tracked map[ID]*trackedProc // procIds is a map from pid to ProcId. This is a convenience // to allow finding the Tracked entry of a parent process. procIds map[int]ID // firstUpdateAt is the time the first update was run. It allows to // count first usage of a process started between two Update() calls firstUpdateAt time.Time // trackChildren makes Tracker track descendants of procs the // namer wanted tracked. trackChildren bool // never ignore processes, i.e. always re-check untracked processes in case comm has changed recheck bool // limit rechecks to this much time recheckTimeLimit time.Duration username map[int]string debug bool } // Delta is an alias of Counts used to signal that its contents are not // totals, but rather the result of subtracting two totals. Delta Counts trackedThread struct { name string accum Counts latest Delta lastUpdate time.Time wchan string } // trackedProc accumulates metrics for a process, as well as // remembering an optional GroupName tag associated with it. trackedProc struct { // lastUpdate is used internally during the update cycle to find which procs have exited lastUpdate time.Time // static static Static metrics Metrics // lastaccum is the increment to the counters seen in the last update. lastaccum Delta // groupName is the tag for this proc given by the namer. groupName string threads map[ThreadID]trackedThread } // ThreadUpdate describes what's changed for a thread since the last cycle. ThreadUpdate struct { // ThreadName is the name of the thread based on field of stat. ThreadName string // Latest is how much the counts increased since last cycle. Latest Delta } // Update reports on the latest stats for a process. Update struct { // GroupName is the name given by the namer to the process. GroupName string // Latest is how much the counts increased since last cycle. Latest Delta // Memory is the current memory usage. Memory // Filedesc is the current fd usage/limit. Filedesc // Start is the time the process started. Start time.Time // NumThreads is the number of threads. NumThreads uint64 // States is how many processes are in which run state. States // Wchans is how many threads are in each non-zero wchan. Wchans map[string]int // Threads are the thread updates for this process, if the Tracker // has trackThreads==true. Threads []ThreadUpdate } // CollectErrors describes non-fatal errors found while collecting proc // metrics. CollectErrors struct { // Read is incremented every time GetMetrics() returns an error. // This means we failed to load even the basics for the process, // and not just because it disappeared on us. Read int // Partial is incremented every time we're unable to collect // some metrics (e.g. I/O) for a tracked proc, but we're still able // to get the basic stuff like cmdline and core stats. Partial int } ) func lessUpdateGroupName(x, y Update) bool { return x.GroupName < y.GroupName } func lessThreadUpdate(x, y ThreadUpdate) bool { return seq.Compare(x, y) < 0 } func lessCounts(x, y Counts) bool { return seq.Compare(x, y) < 0 } func (tp *trackedProc) getUpdate() Update { u := Update{ GroupName: tp.groupName, Latest: tp.lastaccum, Memory: tp.metrics.Memory, Filedesc: tp.metrics.Filedesc, Start: tp.static.StartTime, NumThreads: tp.metrics.NumThreads, States: tp.metrics.States, Wchans: make(map[string]int), } if tp.metrics.Wchan != "" { u.Wchans[tp.metrics.Wchan] = 1 } if len(tp.threads) > 1 { for _, tt := range tp.threads { u.Threads = append(u.Threads, ThreadUpdate{tt.name, tt.latest}) if tt.wchan != "" { u.Wchans[tt.wchan]++ } } } return u } // NewTracker creates a Tracker. func NewTracker(namer common.MatchNamer, trackChildren bool, recheck bool, recheckTimeLimit time.Duration, debug bool) *Tracker { return &Tracker{ namer: namer, tracked: make(map[ID]*trackedProc), procIds: make(map[int]ID), trackChildren: trackChildren, recheck: recheck, recheckTimeLimit: recheckTimeLimit, username: make(map[int]string), debug: debug, } } func (t *Tracker) track(groupName string, idinfo IDInfo) { tproc := trackedProc{ groupName: groupName, static: idinfo.Static, metrics: idinfo.Metrics, } if len(idinfo.Threads) > 0 { tproc.threads = make(map[ThreadID]trackedThread) for _, thr := range idinfo.Threads { tproc.threads[thr.ThreadID] = trackedThread{ thr.ThreadName, thr.Counts, Delta{}, time.Time{}, thr.Wchan} } } // If the process started while Tracker was running, all current counter happened // between the last Update() and the current Update() and should be counted. if idinfo.StartTime.After(t.firstUpdateAt) { tproc.lastaccum = Delta(tproc.metrics.Counts) } t.tracked[idinfo.ID] = &tproc } func (t *Tracker) ignore(id ID, startTime time.Time) { // only ignore ID if we didn't set recheck to true if t.recheck { if t.recheckTimeLimit == 0 { // plain -recheck with no time limit: return } if startTime.Add(t.recheckTimeLimit).After(time.Now()) { // -recheckWithTimeLimit is used and the limit is not reached yet: return } } t.tracked[id] = nil } func (tp *trackedProc) update(metrics Metrics, now time.Time, cerrs *CollectErrors, threads []Thread) { // newcounts: resource consumption since last cycle newcounts := metrics.Counts tp.lastaccum = newcounts.Sub(tp.metrics.Counts) tp.metrics = metrics tp.lastUpdate = now if len(threads) > 1 { if tp.threads == nil { tp.threads = make(map[ThreadID]trackedThread) } for _, thr := range threads { tt := trackedThread{thr.ThreadName, thr.Counts, Delta{}, now, thr.Wchan} if old, ok := tp.threads[thr.ThreadID]; ok { tt.latest, tt.accum = thr.Counts.Sub(old.accum), thr.Counts } tp.threads[thr.ThreadID] = tt } for id, tt := range tp.threads { if tt.lastUpdate != now { delete(tp.threads, id) } } } else { tp.threads = nil } } // handleProc updates the tracker if it's a known and not ignored proc. // If it's neither known nor ignored, newProc will be non-nil. // It is not an error if the process disappears while we are reading // its info out of /proc, it just means nothing will be returned and // the tracker will be unchanged. func (t *Tracker) handleProc(proc Proc, updateTime time.Time) (*IDInfo, CollectErrors) { var cerrs CollectErrors procID, err := proc.GetProcID() if err != nil { if t.debug { log.Printf("error getting proc ID for pid %+v: %v", proc.GetPid(), err) } return nil, cerrs } // Do nothing if we're ignoring this proc. last, known := t.tracked[procID] if known && last == nil { return nil, cerrs } metrics, softerrors, err := proc.GetMetrics() if err != nil { if t.debug { log.Printf("error reading metrics for %+v: %v", procID, err) } // This usually happens due to the proc having exited, i.e. // we lost the race. We don't count that as an error. if err != ErrProcNotExist { cerrs.Read++ } return nil, cerrs } var threads []Thread threads, err = proc.GetThreads() if err != nil { if t.debug { log.Printf("can't read thread metrics for %+v: %v", procID, err) } softerrors |= 1 } cerrs.Partial += softerrors if len(threads) > 0 { metrics.Counts.CtxSwitchNonvoluntary, metrics.Counts.CtxSwitchVoluntary = 0, 0 for _, thread := range threads { metrics.Counts.CtxSwitchNonvoluntary += thread.Counts.CtxSwitchNonvoluntary metrics.Counts.CtxSwitchVoluntary += thread.Counts.CtxSwitchVoluntary metrics.States.Add(thread.States) } } var newProc *IDInfo if known { last.update(metrics, updateTime, &cerrs, threads) } else { static, err := proc.GetStatic() if err != nil { if t.debug { log.Printf("error reading static details for %+v: %v", procID, err) } return nil, cerrs } newProc = &IDInfo{procID, static, metrics, threads} if t.debug { log.Printf("found new proc: %s", newProc) } // Is this a new process with the same pid as one we already know? // Then delete it from the known map, otherwise the cleanup in Update() // will remove the ProcIds entry we're creating here. if oldProcID, ok := t.procIds[procID.Pid]; ok { delete(t.tracked, oldProcID) } t.procIds[procID.Pid] = procID } return newProc, cerrs } // update scans procs and updates metrics for those which are tracked. Processes // that have gone away get removed from the Tracked map. New processes are // returned, along with the count of nonfatal errors. func (t *Tracker) update(procs Iter) ([]IDInfo, CollectErrors, error) { var newProcs []IDInfo var colErrs CollectErrors var now = time.Now() for procs.Next() { newProc, cerrs := t.handleProc(procs, now) if newProc != nil { newProcs = append(newProcs, *newProc) } colErrs.Read += cerrs.Read colErrs.Partial += cerrs.Partial } err := procs.Close() if err != nil { return nil, colErrs, fmt.Errorf("Error reading procs: %v", err) } // Rather than allocating a new map each time to detect procs that have // disappeared, we bump the last update time on those that are still // present. Then as a second pass we traverse the map looking for // stale procs and removing them. for procID, pinfo := range t.tracked { if pinfo == nil { // TODO is this a bug? we're not tracking the proc so we don't see it go away so ProcIds // and Tracked are leaking? continue } if pinfo.lastUpdate != now { delete(t.tracked, procID) delete(t.procIds, procID.Pid) } } return newProcs, colErrs, nil } // checkAncestry walks the process tree recursively towards the root, // stopping at pid 1 or upon finding a parent that's already tracked // or ignored. If we find a tracked parent track this one too; if not, // ignore this one. func (t *Tracker) checkAncestry(idinfo IDInfo, newprocs map[ID]IDInfo) string { ppid := idinfo.ParentPid pProcID := t.procIds[ppid] if pProcID.Pid < 1 { if t.debug { log.Printf("ignoring unmatched proc with no matched parent: %+v", idinfo) } // Reached root of process tree without finding a tracked parent. t.ignore(idinfo.ID, idinfo.Static.StartTime) return "" } // Is the parent already known to the tracker? if ptproc, ok := t.tracked[pProcID]; ok { if ptproc != nil { if t.debug { log.Printf("matched as %q because child of %+v: %+v", ptproc.groupName, pProcID, idinfo) } // We've found a tracked parent. t.track(ptproc.groupName, idinfo) return ptproc.groupName } // We've found an untracked parent. t.ignore(idinfo.ID, idinfo.Static.StartTime) return "" } // Is the parent another new process? if pinfoid, ok := newprocs[pProcID]; ok { if name := t.checkAncestry(pinfoid, newprocs); name != "" { if t.debug { log.Printf("matched as %q because child of %+v: %+v", name, pProcID, idinfo) } // We've found a tracked parent, which implies this entire lineage should be tracked. t.track(name, idinfo) return name } } // Parent is dead, i.e. we never saw it, or there's no tracked proc in our ancestry. if t.debug { log.Printf("ignoring unmatched proc with no matched parent: %+v", idinfo) } t.ignore(idinfo.ID, idinfo.Static.StartTime) return "" } func (t *Tracker) lookupUid(uid int) string { if name, ok := t.username[uid]; ok { return name } var name string uidstr := strconv.Itoa(uid) u, err := user.LookupId(uidstr) if err != nil { name = uidstr } else { name = u.Username } t.username[uid] = name return name } // Update modifies the tracker's internal state based on what it reads from // iter. Tracks any new procs the namer wants tracked, and updates // its metrics for existing tracked procs. Returns nonfatal errors // and the status of all tracked procs, or an error if fatal. func (t *Tracker) Update(iter Iter) (CollectErrors, []Update, error) { if t.firstUpdateAt.IsZero() { t.firstUpdateAt = time.Now() } newProcs, colErrs, err := t.update(iter) if err != nil { return colErrs, nil, err } // Step 1: track any new proc that should be tracked based on its name and cmdline. untracked := make(map[ID]IDInfo) for _, idinfo := range newProcs { nacl := common.ProcAttributes{ Name: idinfo.Name, Cmdline: idinfo.Cmdline, Cgroups: idinfo.Cgroups, Username: t.lookupUid(idinfo.EffectiveUID), PID: idinfo.Pid, StartTime: idinfo.StartTime, } wanted, gname := t.namer.MatchAndName(nacl) if wanted { if t.debug { log.Printf("matched as %q: %+v", gname, idinfo) } t.track(gname, idinfo) } else { untracked[idinfo.ID] = idinfo } } // Step 2: track any untracked new proc that should be tracked because its parent is tracked. if t.trackChildren { for _, idinfo := range untracked { if _, ok := t.tracked[idinfo.ID]; ok { // Already tracked or ignored in an earlier iteration continue } t.checkAncestry(idinfo, untracked) } } tp := []Update{} for _, tproc := range t.tracked { if tproc != nil { tp = append(tp, tproc.getUpdate()) } } return colErrs, tp, nil } process-exporter-0.8.3/proc/tracker_test.go000066400000000000000000000130321464726153700210500ustar00rootroot00000000000000package proc import ( "testing" "time" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" ) // Verify that the tracker finds and tracks or ignores procs based on the // namer, and that it can distinguish between two procs with the same pid // but different start time. func TestTrackerBasic(t *testing.T) { p1, p2, p3 := 1, 2, 3 n1, n2, n3, n4 := "g1", "g2", "g3", "g4" t1, t2, t3 := time.Unix(1, 0).UTC(), time.Unix(2, 0).UTC(), time.Unix(3, 0).UTC() tests := []struct { procs []IDInfo want []Update }{ { []IDInfo{newProcStart(p1, n1, 1), newProcStart(p3, n3, 1)}, []Update{{GroupName: n1, Start: t1, Wchans: msi{}}}, }, { // p3 (ignored) has exited and p2 has appeared []IDInfo{newProcStart(p1, n1, 1), newProcStart(p2, n2, 2)}, []Update{{GroupName: n1, Start: t1, Wchans: msi{}}, {GroupName: n2, Start: t2, Wchans: msi{}}}, }, { // p1 has exited and a new proc with a new name has taken its pid []IDInfo{newProcStart(p1, n4, 3), newProcStart(p2, n2, 2)}, []Update{{GroupName: n4, Start: t3, Wchans: msi{}}, {GroupName: n2, Start: t2, Wchans: msi{}}}, }, } // Note that n3 should not be tracked according to our namer. tr := NewTracker(newNamer(n1, n2, n4), false, false, 0, false) opts := cmpopts.SortSlices(lessUpdateGroupName) for i, tc := range tests { _, got, err := tr.Update(procInfoIter(tc.procs...)) noerr(t, err) if diff := cmp.Diff(got, tc.want, opts); diff != "" { t.Errorf("%d: update differs: (-got +want)\n%s", i, diff) } } } // TestTrackerChildren verifies that when the tracker is asked to track // children, processes not selected by the namer are still tracked if // they're children of ones that are. func TestTrackerChildren(t *testing.T) { p1, p2, p3 := 1, 2, 3 n1, n2, n3 := "g1", "g2", "g3" // In this test everything starts at time t1 for simplicity t1 := time.Unix(0, 0).UTC() tests := []struct { procs []IDInfo want []Update }{ { []IDInfo{ newProcParent(p1, n1, 0), newProcParent(p2, n2, p1), }, []Update{{GroupName: n2, Start: t1, Wchans: msi{}}}, }, { []IDInfo{ newProcParent(p1, n1, 0), newProcParent(p2, n2, p1), newProcParent(p3, n3, p2), }, []Update{{GroupName: n2, Start: t1, Wchans: msi{}}, {GroupName: n2, Start: t1, Wchans: msi{}}}, }, } // Only n2 and children of n2s should be tracked tr := NewTracker(newNamer(n2), true, false, 0, false) for i, tc := range tests { _, got, err := tr.Update(procInfoIter(tc.procs...)) noerr(t, err) if diff := cmp.Diff(got, tc.want); diff != "" { t.Errorf("%d: update differs: (-got +want)\n%s", i, diff) } } } // TestTrackerMetrics verifies that the updates returned by the tracker // match the input we're giving it. func TestTrackerMetrics(t *testing.T) { p, n, tm := 1, "g1", time.Unix(0, 0).UTC() tests := []struct { proc IDInfo want Update }{ { piinfost(p, n, Counts{1, 2, 3, 4, 5, 6, 0, 0}, Memory{7, 8, 0, 0, 0}, Filedesc{1, 10}, 9, States{Sleeping: 1}), Update{n, Delta{}, Memory{7, 8, 0, 0, 0}, Filedesc{1, 10}, tm, 9, States{Sleeping: 1}, msi{}, nil}, }, { piinfost(p, n, Counts{2, 3, 4, 5, 6, 7, 0, 0}, Memory{1, 2, 0, 0, 0}, Filedesc{2, 20}, 1, States{Running: 1}), Update{n, Delta{1, 1, 1, 1, 1, 1, 0, 0}, Memory{1, 2, 0, 0, 0}, Filedesc{2, 20}, tm, 1, States{Running: 1}, msi{}, nil}, }, } tr := NewTracker(newNamer(n), false, false, 0, false) for i, tc := range tests { _, got, err := tr.Update(procInfoIter(tc.proc)) noerr(t, err) if diff := cmp.Diff(got, []Update{tc.want}); diff != "" { t.Errorf("%d: update differs: (-got +want)\n%s", i, diff) } } } func TestTrackerThreads(t *testing.T) { p, n, tm := 1, "g1", time.Unix(0, 0).UTC() tests := []struct { proc IDInfo want Update }{ { piinfo(p, n, Counts{}, Memory{}, Filedesc{1, 1}, 1), Update{n, Delta{}, Memory{}, Filedesc{1, 1}, tm, 1, States{}, msi{}, nil}, }, { piinfot(p, n, Counts{}, Memory{}, Filedesc{1, 1}, []Thread{ {ThreadID(ID{p, 0}), "t1", Counts{1, 2, 3, 4, 5, 6, 0, 0}, "", States{}}, {ThreadID(ID{p + 1, 0}), "t2", Counts{1, 1, 1, 1, 1, 1, 0, 0}, "", States{}}, }), Update{n, Delta{}, Memory{}, Filedesc{1, 1}, tm, 2, States{}, msi{}, []ThreadUpdate{ {"t1", Delta{}}, {"t2", Delta{}}, }, }, }, { piinfot(p, n, Counts{}, Memory{}, Filedesc{1, 1}, []Thread{ {ThreadID(ID{p, 0}), "t1", Counts{2, 3, 4, 5, 6, 7, 0, 0}, "", States{}}, {ThreadID(ID{p + 1, 0}), "t2", Counts{2, 2, 2, 2, 2, 2, 0, 0}, "", States{}}, {ThreadID(ID{p + 2, 0}), "t2", Counts{1, 1, 1, 1, 1, 1, 0, 0}, "", States{}}, }), Update{n, Delta{}, Memory{}, Filedesc{1, 1}, tm, 3, States{}, msi{}, []ThreadUpdate{ {"t1", Delta{1, 1, 1, 1, 1, 1, 0, 0}}, {"t2", Delta{1, 1, 1, 1, 1, 1, 0, 0}}, {"t2", Delta{}}, }, }, }, { piinfot(p, n, Counts{}, Memory{}, Filedesc{1, 1}, []Thread{ {ThreadID(ID{p, 0}), "t1", Counts{2, 3, 4, 5, 6, 7, 0, 0}, "", States{}}, {ThreadID(ID{p + 2, 0}), "t2", Counts{1, 2, 3, 4, 5, 6, 0, 0}, "", States{}}, }), Update{n, Delta{}, Memory{}, Filedesc{1, 1}, tm, 2, States{}, msi{}, []ThreadUpdate{ {"t1", Delta{}}, {"t2", Delta{0, 1, 2, 3, 4, 5, 0, 0}}, }, }, }, } tr := NewTracker(newNamer(n), false, false, 0, false) opts := cmpopts.SortSlices(lessThreadUpdate) for i, tc := range tests { _, got, err := tr.Update(procInfoIter(tc.proc)) noerr(t, err) if diff := cmp.Diff(got, []Update{tc.want}, opts); diff != "" { t.Errorf("%d: update differs: (-got +want)\n%s, %v, %v", i, diff, got[0].Threads, tc.want.Threads) } } }