<repo_name>mdk194/mem_prometheus_exporter<file_sep>/proc/proc.go
package proc

import (
	"fmt"
	"os"
	"strconv"
)

type Proc struct {
	PID int
}

// AllProcs returns a list of all currently available processes.
func AllProcs() ([]Proc, error) {
	d, err := os.Open("/proc")
	if err != nil {
		return []Proc{}, err
	}
	defer d.Close()

	names, err := d.Readdirnames(-1)
	if err != nil {
		return []Proc{}, fmt.Errorf("could not read %s: %s", d.Name(), err)
	}

	p := []Proc{}
	for _, n := range names {
		pid, err := strconv.ParseInt(n, 10, 64)
		if err != nil {
			continue
		}
		p = append(p, Proc{PID: int(pid)})
	}

	return p, nil
}
<file_sep>/main.go
package main

import (
	"net/http"

	"github.com/prometheus/client_golang/prometheus/promhttp"
	"go.uber.org/zap"
)

func main() {
	logger, _ := zap.NewProduction()
	defer logger.Sync()

	http.Handle("/metrics", promhttp.Handler())
	logger.Info("Starting exporter at :8080/metrics")
	if err := http.ListenAndServe(":8080", nil); err != nil {
		logger.Fatal("Failed to start", zap.Error(err))
	}
}
<file_sep>/go.mod
module github.com/mdk194/mem_prometheus_exporter

go 1.13

require (
	github.com/prometheus/client_golang v1.4.0
	go.uber.org/zap v1.13.0
)
<file_sep>/collector.go
package main

import (
	"fmt"
	"strconv"
	"sync"

	"github.com/prometheus/client_golang/prometheus"

	"github.com/mdk194/mem_prometheus_exporter/proc"
)

var _ prometheus.Collector = &collector{}

type collector struct {
	ProcessMemory *prometheus.Desc
	stats         func() ([]proc.ProcStatus, error)
}

func init() {
	c := newCollector(stats)
	prometheus.MustRegister(c)
}

func stats() ([]proc.ProcStatus, error) {
	procList, err := proc.AllProcs()
	if err != nil {
		return nil, fmt.Errorf("Failed to list processes %v", err)
	}

	var wg sync.WaitGroup
	var out []proc.ProcStatus
	errChan := make(chan error)
	done := make(chan interface{})

	for _, p := range procList {
		wg.Add(1)

		// Parallel read status
		go func(pid int) {
			defer wg.Done()

			ps, err := proc.NewStatus(pid, fmt.Sprintf("/proc/%d/status", pid))
			if err != nil {
				errChan <- err
			}
			out = append(out, ps)
		}(p.PID)
	}

	go func() {
		wg.Wait()
		close(done)
	}()

	select {
	case <-done:
	case err := <-errChan:
		return nil, fmt.Errorf("Failed to read status %v", err)
	}

	return out, nil
}

func newCollector(stats func() ([]proc.ProcStatus, error)) prometheus.Collector {
	return &collector{
		ProcessMemory: prometheus.NewDesc(
			"process_memory_rss_bytes",
			"Size of memory resient set size of process read from /proc/[pid]/status",
			[]string{"pid", "name"},
			nil,
		),
		stats: stats,
	}
}

// Describe implements prometheus.Collector
func (c *collector) Describe(ch chan<- *prometheus.Desc) {
	ds := []*prometheus.Desc{
		c.ProcessMemory,
	}

	for _, d := range ds {
		ch <- d
	}
}

// Collect implements prometheus.Collector
func (c *collector) Collect(ch chan<- prometheus.Metric) {
	stats, err := c.stats()
	if err != nil {
		ch <- prometheus.NewInvalidMetric(c.ProcessMemory, err)
	}

	for _, s := range stats {
		ch <- prometheus.MustNewConstMetric(
			c.ProcessMemory,
			prometheus.GaugeValue,
			float64(s.VmRSS),
			strconv.Itoa(s.PID), s.Name,
		)
	}
}
<file_sep>/proc/proc_status_test.go
package proc

import (
	"io/ioutil"
	"os"
	"testing"
)

var mockStatus = `Name:	systemd
Umask:	0000
State:	S (sleeping)
Tgid:	1
Ngid:	0
Pid:	1
PPid:	0
TracerPid:	0
Uid:	0	0	0	0
Gid:	0	0	0	0
FDSize:	128
Groups:	 
NStgid:	1
NSpid:	1
NSpgid:	1
NSsid:	1
VmPeak:	  238376 kB
VmSize:	  172840 kB
VmLck:	       0 kB
VmPin:	       0 kB
VmHWM:	   10612 kB
VmRSS:	   10612 kB
RssAnon:	    2556 kB
RssFile:	    8056 kB
RssShmem:	       0 kB
VmData:	   25464 kB
VmStk:	     132 kB
VmExe:	     908 kB
VmLib:	    7996 kB
VmPTE:	      96 kB
VmSwap:	       0 kB
HugetlbPages:	       0 kB
CoreDumping:	0
Threads:	1
SigQ:	0/62811
SigPnd:	0000000000000000
ShdPnd:	0000000000000000
SigBlk:	7be3c0fe28014a03
SigIgn:	0000000000001000
SigCgt:	00000001800004ec
CapInh:	0000000000000000
CapPrm:	0000003fffffffff
CapEff:	0000003fffffffff
CapBnd:	0000003fffffffff
CapAmb:	0000000000000000
NoNewPrivs:	0
Seccomp:	0
Speculation_Store_Bypass:	thread vulnerable
Cpus_allowed:	ff
Cpus_allowed_list:	0-7
Mems_allowed:	00000001
Mems_allowed_list:	0
voluntary_ctxt_switches:	120965
nonvoluntary_ctxt_switches:	6455`

func TestProcStatus(t *testing.T) {
	tmpfile, err := ioutil.TempFile("", "mockStatus")
	if err != nil {
		t.Fatal(err)
	}
	defer os.Remove(tmpfile.Name()) // clean up

	if _, err := tmpfile.Write([]byte(mockStatus)); err != nil {
		t.Fatal(err)
	}
	if err := tmpfile.Close(); err != nil {
		t.Fatal(err)
	}

	s, err := NewStatus(1, tmpfile.Name())
	if err != nil {
		t.Fatal(err)
	}

	for _, test := range []struct {
		name string
		want int
		have int
	}{
		{name: "Pid", want: 1, have: s.PID},
		{name: "Tgid", want: 1, have: s.TGID},
		{name: "VmPeak", want: 238376 * 1024, have: int(s.VmPeak)},
		{name: "VmSize", want: 172840 * 1024, have: int(s.VmSize)},
		{name: "VmLck", want: 0 * 1024, have: int(s.VmLck)},
		{name: "VmPin", want: 0 * 1024, have: int(s.VmPin)},
		{name: "VmHWM", want: 10612 * 1024, have: int(s.VmHWM)},
		{name: "VmRSS", want: 10612 * 1024, have: int(s.VmRSS)},
		{name: "RssAnon", want: 2556 * 1024, have: int(s.RssAnon)},
		{name: "RssFile", want: 8056 * 1024, have: int(s.RssFile)},
		{name: "RssShmem", want: 0 * 1024, have: int(s.RssShmem)},
		{name: "VmData", want: 25464 * 1024, have: int(s.VmData)},
		{name: "VmStk", want: 132 * 1024, have: int(s.VmStk)},
		{name: "VmExe", want: 908 * 1024, have: int(s.VmExe)},
		{name: "VmLib", want: 7996 * 1024, have: int(s.VmLib)},
		{name: "VmPTE", want: 96 * 1024, have: int(s.VmPTE)},
		{name: "VmPMD", want: 0 * 1024, have: int(s.VmPMD)},
		{name: "VmSwap", want: 0 * 1024, have: int(s.VmSwap)},
		{name: "HugetlbPages", want: 0 * 1024, have: int(s.HugetlbPages)},
		{name: "VoluntaryCtxtSwitches", want: 120965, have: int(s.VoluntaryCtxtSwitches)},
		{name: "NonVoluntaryCtxtSwitches", want: 6455, have: int(s.NonVoluntaryCtxtSwitches)},
	} {
		if test.want != test.have {
			t.Errorf("want %s %d, have %d", test.name, test.want, test.have)
		}
	}
}
<file_sep>/Makefile
SVC=mem_prometheus_exporter

LDFLAGS=-ldflags '-s -w -extldflags "-static"'

.PHONY: default
default: bin

.PHONY: test
test:
	GO111MODULE=on go test ./... -count=1

.PHONY: bin
bin: test
	GO111MODULE=on CGO_ENABLED=0 GOARCH=amd64 GOOS=linux go build ${LDFLAGS} -o bin/${SVC}-amd64-linux

.PHONY: install
install: test
	go install ./...

.PHONY: clean
	rm -rf bin
<file_sep>/README.md
This is an example of prometheus exporter that:
- list current running processes in linux host
- read /proc/[pid]/status
- export VM RSS of those processes to prometheus format at :8080/metrics


# Build
Run:

    make


# Run

    go run *.go
    curl localhost:8080/metrics

Should have output like below:

    # HELP process_memory_rss_bytes Size of memory resient set size of process read from /proc/[pid]/status
    # TYPE process_memory_rss_bytes gauge
    process_memory_rss_bytes{name="(sd-pam)",pid="758"} 2.92864e+06
    process_memory_rss_bytes{name="NetworkManager",pid="691"} 2.0226048e+07
    process_memory_rss_bytes{name="Xorg",pid="801"} 2.7267072e+07
    process_memory_rss_bytes{name="acpi_thermal_pm",pid="127"} 0
    process_memory_rss_bytes{name="at-spi-bus-laun",pid="834"} 5.967872e+06
    process_memory_rss_bytes{name="at-spi2-registr",pid="932"} 5.976064e+06
    process_memory_rss_bytes{name="auditd",pid="59"} 0
    process_memory_rss_bytes{name="avahi-daemon",pid="688"} 4.067328e+06
    process_memory_rss_bytes{name="avahi-daemon",pid="699"} 348160
    process_memory_rss_bytes{name="blockd",pid="70"} 0
    process_memory_rss_bytes{name="bluetoothd",pid="689"} 5.967872e+06

