Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WIP: Add more testing + memory stats from cgroups #10

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
58 commits
Select commit Hold shift + click to select a range
c6abe3d
Change socket refused from gauge to counter
hamiltont Jan 30, 2020
36d4121
Fix counter-or-gauge metric documentation
hamiltont Jan 30, 2020
d4c73f7
Markdown cleanup
hamiltont Jan 30, 2020
2351bdb
Initial test script based on node_exporter
hamiltont Jan 30, 2020
7515a97
Allow http.Shutdown call for testing
hamiltont Jan 30, 2020
abb9906
Cleanup test suite and use mainTest by default
hamiltont Jan 30, 2020
5a700f6
Ensure tests run server at proper address
hamiltont Jan 30, 2020
0ad0613
Fix testing bugs - run main & avoid hang on --version
hamiltont Jan 30, 2020
ee24c67
First circleCI image
hamiltont Jan 30, 2020
324b61f
Testing artifacts and machine builds
hamiltont Jan 31, 2020
b1152d1
Add workflow
hamiltont Jan 31, 2020
161855d
Stop using container at all. Start using BASH_ENV
hamiltont Jan 31, 2020
a70a1e6
Stop attempting bashrc hacks
hamiltont Jan 31, 2020
1f9768e
Stop trying to use circleCI
hamiltont Jan 31, 2020
8f9e075
Initial travis CI file
hamiltont Jan 31, 2020
26df7d2
Workaround bug in golangci-lint
hamiltont Jan 31, 2020
7c9b7c8
Fix bug found by ci linting :-)
hamiltont Jan 31, 2020
cd0bfae
Added codecov.io
hamiltont Jan 31, 2020
10a20d3
Define CI matrix directly (no job include)
hamiltont Jan 31, 2020
4804ae2
Update test flags to work with codecov
hamiltont Jan 31, 2020
13ca4f3
Including integration tests into coverage counts
hamiltont Jan 31, 2020
4c9259f
Listing units in travis-ci
hamiltont Jan 31, 2020
3bb0ed0
Log args as debug to aid in testing
hamiltont Jan 31, 2020
4060136
Testing
hamiltont Jan 31, 2020
d0e740e
New go-acc version to work with go.mod
hamiltont Feb 1, 2020
44c22fa
Debugging - add go list
hamiltont Feb 1, 2020
17652d4
Document todo regarding integration test coverage
hamiltont Feb 1, 2020
62fe927
Enabling cgroup accounting on Travis-CI
hamiltont Feb 1, 2020
37f6d68
Fixing typo
hamiltont Feb 1, 2020
1a6ae16
Documenting requirements for CPUAccounting
hamiltont Feb 1, 2020
6499bbc
On Travis-CI, enable CPUAccounting by default
hamiltont Feb 1, 2020
83c718e
Testing multiple OS builds
hamiltont Feb 1, 2020
0549675
Adding fixtures for testing
hamiltont Feb 1, 2020
9806a99
Rewrite cgroups.go to allow unit testing
hamiltont Feb 1, 2020
1a0c29c
Ignore jetbrains idea files
hamiltont Feb 1, 2020
7abd3fe
Stop including integration tests in coverage count
hamiltont Feb 1, 2020
9191e17
Move cgroup work to dedicated package
hamiltont Feb 1, 2020
64eaf3e
Add first systemd.go test unit
hamiltont Feb 1, 2020
8aba2ba
Pull cpuacct controller logic into separate file
hamiltont Feb 1, 2020
dc57119
Tiny bit more testing
hamiltont Feb 1, 2020
e438c3f
Better code organization
hamiltont Feb 1, 2020
5b0ba07
Read memory.stat from cgroup
hamiltont Feb 2, 2020
b93d38d
Linting
hamiltont Feb 2, 2020
fd79e1d
Add memory stats to systemd_exporter
hamiltont Feb 2, 2020
f760287
Enable memory accounting in travis CI
hamiltont Feb 2, 2020
2e51219
Fix bug caused by typo
hamiltont Feb 2, 2020
4fa6131
Switch unit handling on parsed type
hamiltont Feb 3, 2020
a02aeed
Generalize code relying on cgroups
hamiltont Feb 3, 2020
026ea0f
Split cgroup metrics from dbus metrics
hamiltont Feb 3, 2020
5eadce7
Stop reading CPUAccounting
hamiltont Feb 3, 2020
d71f59a
Remove special-casing in generic collect unit CPU
hamiltont Feb 3, 2020
29df5f7
Add 3 more memory metrics
hamiltont Feb 3, 2020
6dff430
Ensure fields have units declared
hamiltont Feb 3, 2020
0ec9941
Merge branch 'fixup/metric-types' into feat/add-circleci
hamiltont Feb 3, 2020
8b7bd76
Add tests for complex 'systemd mount mode' logic
hamiltont Feb 5, 2020
4447073
Fix linting issues
hamiltont Feb 5, 2020
6884b2a
Accept linting suggestion
hamiltont Feb 5, 2020
7eec03e
cgroup: Export multiple new symbols
hamiltont Feb 24, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
/systemd_exporter
/bin/golangci-lint
.idea
coverage.txt
5 changes: 5 additions & 0 deletions .golangci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
issues:
exclude:
- "not declared by package utf8"
- "unicode/utf8/utf8.go"

34 changes: 33 additions & 1 deletion .travis.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,36 @@
language: go
# TODO Use CodeCov 'Flags' to separate coverage for integration tests and unit
# tests. Near 100% on integration is expected - these tests cast a wide net to
# catch many issues (but do not easily tell you where the issue is). Near 100%
# on unit tests is a feat of heroism - these tests identify a specific code
# chunk with an issue. We mainly care about 100% on unit tests, but 100% on
# integration is an easy win and a nice to have
#
# See https://docs.codecov.io/docs/flags
# Use go get github.com/stristr/go-acc && go-acc ./...
# Or use coverpkg=github.com/povilasv/systemd_exporter,github.com/povilasv/systemd_exporter/systemd

# This defines the script for us automatically. By default it installs
# to requested go version then runs make
language: go
go:
- "1.x"

before_script: systemd --version
os: linux

go:
- 1.x

before_script: systemd --version && systemctl list-units

after_success:
- bash <(curl -s https://codecov.io/bash)

jobs:
include:
- dist: xenial
name: xenial-229
- dist: bionic
name: bionic-237


14 changes: 6 additions & 8 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,18 @@

### **Breaking changes**

* `systemd_unit_state` label `type` has new meaning
Now shows Unit type (`service`, `scope`, etc), not Service Unit types (`simple`, `forking`, etc)
or mount unit types(`aufs`,`ext3`, etc). Service and mount types have been moved to `systemd_unit_info`
* `systemd_unit_state` label `type` has new meaning. Previously `type` contained service unit type (`simple`, `forking`, etc) or mount unit types (`aufs`, `ext3`, etc). Now `systemd_unit_state{type}` contains overall unit type (`service`, `scope`, etc) to allow easy PromQL group by clauses. Service and mount types have been moved to `systemd_unit_info`

### Changes

- [FEATURE] Read unit CPU usage from cgroup. Added `systemd_unit_cpu_seconds_total` metric. **Note** - Untested on unified hierarchy
- [FEATURE] Add `systemd_unit_info` with metainformation about units incl. subtype specific info
- [ENHANCEMENT] Added `type` label to all metrics named `systemd_unit-*` to support PromQL grouping
* [ENHANCEMENT] `systemd_unit_state` works for all unit types, not just service and mount units
* [ENHANCEMENT] Scrapes are approx 80% faster. If needed, set GOMAXPROCS to limit max concurrency
* [CHANGE] Start tracking metric cardinality in readme
* [CHANGE] Expanded default set of unit types monitored. Only device unit types are not enabled by default
* [BUGFIX] `timer_last_trigger_seconds` metric is now exported as expected for all timers
- [ENHANCEMENT] `systemd_unit_state` works for all unit types, not just service and mount units
- [ENHANCEMENT] Scrapes are approx 80% faster. If needed, set GOMAXPROCS to limit max concurrency
- [CHANGE] Start tracking metric cardinality in readme
- [CHANGE] Expanded default set of unit types monitored. Only device unit types are not enabled by default
- [BUGFIX] `timer_last_trigger_seconds` metric is now exported as expected for all timers

## 0.2.0 / 2019-03-20

Expand Down
9 changes: 7 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ BRANCH := $(shell git branch | grep \* | cut -d ' ' -f2)

LINT_FLAGS := run --deadline=120s
LINTER := ./bin/golangci-lint
TESTFLAGS := -v -cover
TEST_FLAGS := -v -cover -race -coverprofile=coverage.txt -covermode=atomic

GO111MODULE := on
all: $(LINTER) deps test lint build
Expand All @@ -23,7 +23,12 @@ deps:

.PHONY: test
test:
go test $(TESTFLAGS) ./...
ifdef TRAVIS
sudo sh -c 'echo DefaultCPUAccounting=yes >> /etc/systemd/system.conf'
sudo sh -c 'echo DefaultMemoryAccounting=yes >> /etc/systemd/system.conf'
sudo systemctl daemon-reload
endif
go test $(TEST_FLAGS) ./...

.PHONY: build
build: deps
Expand Down
8 changes: 5 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,19 +79,21 @@ Note that a number of unit types are filtered by default
| ----------------------------------------- | ----------- | -------- | ------------------------------------------------------------------ |
| systemd_exporter_build_info | Gauge | UNSTABLE | 1 per systemd-exporter |
| systemd_unit_info | Gauge | UNSTABLE | 1 per service + 1 per mount |
| systemd_unit_cpu_seconds_total | Gauge | UNSTABLE | 2 per mount/scope/slice/socket/swap {mode="system/user"} |
| systemd_unit_cpu_seconds_total | Counter | UNSTABLE | <sup>1</sup>2 per mount/scope/slice/socket/swap {mode="system/user"}|
| systemd_unit_state | Gauge | UNSTABLE | 5 per unit {state="activating/active/deactivating/failed/inactive} |
| systemd_unit_tasks_current | Gauge | UNSTABLE | 1 per service |
| systemd_unit_tasks_max | Gauge | UNSTABLE | 1 per service |
| systemd_unit_start_time_seconds | Gauge | UNSTABLE | 1 per service |
| systemd_service_restart_total | Gauge | UNSTABLE | 1 per service |
| systemd_service_restart_total | Counter | UNSTABLE | 1 per service |
| systemd_socket_accepted_connections_total | Counter | UNSTABLE | 1 per socket |
| systemd_socket_current_connections | Gauge | UNSTABLE | 1 per socket |
| systemd_socket_refused_connections_total | Gauge | UNSTABLE | 1 per socket |
| systemd_socket_refused_connections_total | Counter | UNSTABLE | 1 per socket. Requires systemd>239 |
| systemd_timer_last_trigger_seconds | Gauge | UNSTABLE | 1 per timer |
| systemd_process_resident_memory_bytes | Gauge | UNSTABLE | 1 per service |
| systemd_process_virtual_memory_bytes | Gauge | UNSTABLE | 1 per service |
| systemd_process_virtual_memory_max_bytes | Gauge | UNSTABLE | 1 per service |
| systemd_process_open_fds | Gauge | UNSTABLE | 1 per service |
| systemd_process_max_fds | Gauge | UNSTABLE | 1 per service |
| systemd_process_cpu_seconds_total | Counter | UNSTABLE | 1 per service |

<sup>1</sup>Only present for units which have systemd `CPUAccounting` enabled
49 changes: 49 additions & 0 deletions cgroup/Readme.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@

This package provides functions to retrieve control group metrics from the pseudo-filesystem `/sys/cgroup/`.

**WARNING:** This package is a work in progress. Its API may still break in backwards-incompatible ways without warnings. Use it at your own risk.

The Linux kernel supports two APIs for userspace to interact with control groups, the v1 API and the v2 API. See
[this LWN Article](https://lwn.net/Articles/679786/) or
[this kernel documentation](https://www.kernel.org/doc/html/latest/admin-guide/cgroup-v2.html#deprecated-v1-core-features)
for background on the two APIs. This package will interact with both v1 and v2 APIs.


### Focus on Systemd

This package is initially focused on reading metrics for systemd units. Therefore,
the following systemd documentation is relevant.

#### Systemd cgroup mount mode

The kernel can mount the cgroupfs in any manner it chooses. However, anyone wanting to use that cgroupfs must know
where/how it is mounted. When there was only one cgroup API, it was always mounted at `/sys/fs/cgroup`. With the
transition from v1 to v2, the mounting approach differs per-distro, with some mounting only v2, some mounting only
v1(all hierarchies), and some mounting a combination. For simplicity, this package initially focuses on the three
mount "modes" supported by systemd:

via [systemd.io](https://systemd.io/CGROUP_DELEGATION/#three-different-tree-setups-)

1. Unified — this is the simplest mode, and exposes a pure cgroup v2 logic
2. Legacy — this is the traditional cgroup v1 mode. In this mode the various controllers each get their own cgroup
file system mounted to `/sys/fs/cgroup/<controller>/`
3. Hybrid — this is a hybrid between the unified and legacy mode. It’s set up mostly like legacy

#### Systemd Supported Controllers

The initial target controllers this package aims to read from are the controllers supported by systemd. Reading from
other controllers may be supported in the future. Systemd guarantees that all v1 hierarchies are kept in sync.

Via [systemd.io](https://systemd.io/CGROUP_DELEGATION/#controller-support):

Systemd supports a number of controllers (but not all). Specifically, supported are:

on cgroup v1: cpu, cpuacct, blkio, memory, devices, pids
on cgroup v2: cpu, io, memory, pids

It is our intention to natively support all cgroup v2 controllers as they are added
to the kernel. However, regarding cgroup v1: at this point we will not add support
for any other controllers anymore. This means systemd currently does not and will
never manage the following controllers on cgroup v1: freezer, cpuset, net_cls,
perf_event, net_prio, hugetlb

175 changes: 175 additions & 0 deletions cgroup/cgroup.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
package cgroup

import (
"fmt"
"github.com/pkg/errors"
"github.com/prometheus/common/log"
"golang.org/x/sys/unix"
"os"
"path/filepath"
)

// FS is the pseudo-filesystem cgroupfs, which provides an interface to
// kernel data structures
type FS struct {
mountPoint string

// WARNING: We only read this data once at process start, systemd updates
// may require restarting systemd-exporter
cgroupUnified MountMode
}

// DefaultMountPoint is the common mount point of the cgroupfs filesystem
const DefaultMountPoint = "/sys/fs/cgroup"

// NewDefaultFS returns a new cgroup FS mounted under the default mountPoint.
// It will error if cgroup hierarchies are not laid out in a manner understood
// by systemd.
func NewDefaultFS() (FS, error) {

mode, err := cgUnifiedCached()
if err != nil || mode == MountModeUnknown {
return FS{}, fmt.Errorf("could not determine cgroupfs mount mode: %s", err)
}

return NewFS(DefaultMountPoint, mode)
}

// NewFS returns a new cgroup FS mounted under the given mountPoint. It does not check
// the provided mount mode
func NewFS(mountPoint string, mountMode MountMode) (FS, error) {
info, err := os.Stat(mountPoint)
if err != nil {
return FS{}, fmt.Errorf("could not read %s: %s", mountPoint, err)
}
if !info.IsDir() {
return FS{}, fmt.Errorf("mount point %s is not a directory", mountPoint)
}
return FS{mountPoint, mountMode}, nil
}

// path appends the given path elements to the filesystem path, adding separators
// as necessary.
func (fs FS) path(p ...string) string {
return filepath.Join(append([]string{string(fs.mountPoint)}, p...)...)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

}

// MountMode constants describe how the kernel has mounted various cgroup filesystems under /sys/fs/cgroup.
// Generally speaking, kernels using the cgroups-v1 API will have many cgroup controller hierarchies, each with
// their own fs and their own mount point. Kernels using cgroups-v2 API will only have the one unified hierarchy.
// To support back compatibility, kernels often mount both the v1 and v2 hierarchies at different points. Systemd
// has to know where the hierarchies are, so it inspects the mounts under /sys/fs/cgroup and decides what
// MountMode this kernel is using. See each constant for a description of that mode. This type corresponds to
// the unified_cache variable in systemd/src/basic/cgroup-util.c
type MountMode int8

const (
// MountModeUnknown indicates we do not recognize the mount pattern of the cgroup filesystems in /sys/fs/cgroup.
// systemd source calls this mode CGROUP_UNIFIED_UNKNOWN
MountModeUnknown MountMode = iota
// MountModeLegacy indicates both systemd and individual cgroups are using cgroup-v1 hierarchies. There is
// typically one mount point per hierarchy, and no usage of the cgroup-v2 unified hierarchy.
// systemd source calls this mode CGROUP_UNIFIED_NONE
MountModeLegacy MountMode = iota
// MountModeHybrid indicates the systemd controller is using cgroup-v2 unified hierarchy for organizing
// processes, but all other cgroups are using cgroup-v1 legacy hierarchies.
// systemd source calls this CGROUP_UNIFIED_SYSTEMD and also stores the unified_systemd_v232 flag
MountModeHybrid MountMode = iota
// MountModeUnified indicates cgroup-v2 API is in full usage and there are no cgroup-v1 hierarchies mounted.
// Non-updated programs (e.g. container orchestrators such as docker/runc) that rely on cgroup-v1 mounts will break.
// systemd source calls this CGROUP_UNIFIED_ALL
MountModeUnified MountMode = iota
)
func (c MountMode) String() string {
return [...]string{"unknown", "none", "systemd", "all"}[c]
}


// Values copied from https://github.com/torvalds/linux/blob/master/include/uapi/linux/magic.h
const (
tmpFsMagic = 0x01021994
cgroupSuperMagic = 0x27e0eb
cgroup2SuperMagic = 0x63677270
)

// cgUnifiedCached checks the filesystem types mounted under /sys/fs/cgroup to determine
// which systemd layout (legacy/hybrid/unified) is in use.
// We do not bother to track unified_systemd_v232 as our usage does not
// depend on reading the systemd hierarchy directly, we only focus on reading
// the controllers. If you care if /sys/fs/cgroup/systemd is v1 or v2 you need
// to track this
// WARNING: We cache this data once at process start. Systemd updates
// may require restarting systemd-exporter
// Equivalent to systemd cgroup-util.c#cg_unified_cached
var statfsFunc = unix.Statfs
func cgUnifiedCached() (MountMode, error) {
// if cgroupUnified != MountModeUnknown {
// return cgroupUnified, nil
// }

var fs unix.Statfs_t
err := statfsFunc("/sys/fs/cgroup/", &fs)
if err != nil {
return MountModeUnknown, errors.Wrapf(err, "failed statfs(/sys/fs/cgroup)")
}

switch fs.Type {
case cgroup2SuperMagic:
log.Debugf("Found cgroup2 on /sys/fs/cgroup/, full unified hierarchy")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As this is a package, consider not logging or allowing users to choose their own logging pkg, more info https://dave.cheney.net/2015/11/05/lets-talk-about-logging

return MountModeUnified, nil
case tmpFsMagic:
err := statfsFunc("/sys/fs/cgroup/unified/", &fs)

// Ignore err, we expect path to be missing on v232
if err == nil && fs.Type == cgroup2SuperMagic {
log.Debugf("Found cgroup2 on /sys/fs/cgroup/unified, unified hierarchy for systemd controller")
return MountModeHybrid, nil
}

err = statfsFunc("/sys/fs/cgroup/systemd/", &fs)
if err != nil {
return MountModeUnknown, errors.Wrapf(err, "failed statfs(/sys/fs/cgroup/systemd)")
}

switch fs.Type {
case cgroup2SuperMagic:
log.Debugf("Found cgroup2 on /sys/fs/cgroup/systemd, unified hierarchy for systemd controller (v232 variant)")
return MountModeHybrid, nil
case cgroupSuperMagic:
log.Debugf("Found cgroup on /sys/fs/cgroup/systemd, legacy hierarchy")
return MountModeLegacy, nil
default:
return MountModeUnknown, errors.Errorf("unknown magic number %x for fstype returned by statfs(/sys/fs/cgroup/systemd)", fs.Type)
}

default:
return MountModeUnknown, errors.Errorf("unknown magic number %x for fstype returned by statfs(/sys/fs/cgroup)", fs.Type)
}
}

// cgGetPath returns the absolute path for a specific file in a specific controller
// in the specific cgroup denoted by the passed subpath.
// Input examples: ("cpu", "/system.slice", "cpuacct.usage_all")
func (fs FS) cgGetPath(controller string, subpath string, suffix string) (string, error) {
// relevant systemd source code in cgroup-util.[h|c] specifically cg_get_path
// 2. Joins controller name with base path

if fs.cgroupUnified == MountModeUnknown {
return "", errors.Errorf("Cannot determine path with unknown mounting hierarchy")
}

// TODO Ensure controller name is valid
// TODO Convert controller name into guaranteed valid directory name
dn := controller

joined := ""
switch fs.cgroupUnified {
case MountModeLegacy, MountModeHybrid:
joined = fs.path(dn, subpath, suffix)
case MountModeUnified:
joined = fs.path(subpath, suffix)
default:
return "", errors.Errorf("unknown cgroup mount mode (e.g. unified mode) %d", fs.cgroupUnified)
}
return joined, nil
}
Loading