Add 'logind' exporter

logind provides a nice interface to find out about the numbers of sessions
on a system; it is used on most Linux distributions, even those which
aren't using systemd.

The exporter exposes the total number of sessions indexed by the following
attributes:

* seat
* type ("tty", "x11", ...)
* class ("user", "greeter", ...)
* remote ("true"/"false")
This commit is contained in:
Matthias Schiffer 2016-04-20 17:28:12 +02:00
parent d98335cbf0
commit 91ddafdb33
3 changed files with 371 additions and 0 deletions

View file

@ -45,6 +45,7 @@ interrupts | Exposes detailed interrupts statistics. | Linux, OpenBSD
ipvs | Exposes IPVS status from `/proc/net/ip_vs` and stats from `/proc/net/ip_vs_stats`. | Linux
ksmd | Exposes kernel and system statistics from `/sys/kernel/mm/ksm`. | Linux
lastlogin | Exposes the last time there was a login. | _any_
logind | Exposes session counts from [logind](http://www.freedesktop.org/wiki/Software/systemd/logind/). | Linux
megacli | Exposes RAID statistics from MegaCLI. | Linux
meminfo_numa | Exposes memory statistics from `/proc/meminfo_numa`. | Linux
ntp | Exposes time drift from an NTP server. | _any_

268
collector/logind_linux.go Normal file
View file

@ -0,0 +1,268 @@
// Copyright 2016 The Prometheus Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// +build !nologind
package collector
import (
"fmt"
"os"
"strconv"
"github.com/godbus/dbus"
"github.com/prometheus/client_golang/prometheus"
)
const (
logindSubsystem = "logind"
dbusObject = "org.freedesktop.login1"
dbusPath = "/org/freedesktop/login1"
)
var (
// Taken from logind as of systemd v229.
// "other" is the fallback value for unknown values (in case logind gets extended in the future).
attrRemoteValues = []string{"true", "false"}
attrTypeValues = []string{"other", "unspecified", "tty", "x11", "wayland", "mir", "web"}
attrClassValues = []string{"other", "user", "greeter", "lock-screen", "background"}
sessionsDesc = prometheus.NewDesc(
prometheus.BuildFQName(Namespace, logindSubsystem, "sessions"),
"Number of sessions registered in logind.", []string{"seat", "remote", "type", "class"}, nil,
)
)
type logindCollector struct{}
type logindDbus struct {
conn *dbus.Conn
object dbus.BusObject
}
type logindInterface interface {
listSeats() ([]string, error)
listSessions() ([]logindSessionEntry, error)
getSession(logindSessionEntry) *logindSession
}
type logindSession struct {
seat string
remote string
sessionType string
class string
}
// Struct elements must be public for the reflection magic of godbus to work.
type logindSessionEntry struct {
SessionId string
UserId uint32
UserName string
SeatId string
SessionObjectPath dbus.ObjectPath
}
type logindSeatEntry struct {
SeatId string
SeatObjectPath dbus.ObjectPath
}
func init() {
Factories["logind"] = NewLogindCollector
}
// Takes a prometheus registry and returns a new Collector exposing
// logind statistics.
func NewLogindCollector() (Collector, error) {
return &logindCollector{}, nil
}
func (lc *logindCollector) Update(ch chan<- prometheus.Metric) error {
c, err := newDbus()
if err != nil {
return fmt.Errorf("unable to connect to dbus: %s", err)
}
defer c.conn.Close()
return collectMetrics(ch, c)
}
func collectMetrics(ch chan<- prometheus.Metric, c logindInterface) error {
seats, err := c.listSeats()
if err != nil {
return fmt.Errorf("unable to get seats: %s", err)
}
sessionList, err := c.listSessions()
if err != nil {
return fmt.Errorf("unable to get sessions: %s", err)
}
sessions := make(map[logindSession]float64)
for _, s := range sessionList {
session := c.getSession(s)
if session != nil {
sessions[*session]++
}
}
for _, remote := range attrRemoteValues {
for _, sessionType := range attrTypeValues {
for _, class := range attrClassValues {
for _, seat := range seats {
count := sessions[logindSession{seat, remote, sessionType, class}]
ch <- prometheus.MustNewConstMetric(
sessionsDesc, prometheus.GaugeValue, count,
seat, remote, sessionType, class)
}
}
}
}
return nil
}
func knownStringOrOther(value string, known []string) string {
for i := range known {
if value == known[i] {
return value
}
}
return "other"
}
func newDbus() (*logindDbus, error) {
conn, err := dbus.SystemBusPrivate()
if err != nil {
return nil, err
}
methods := []dbus.Auth{dbus.AuthExternal(strconv.Itoa(os.Getuid()))}
err = conn.Auth(methods)
if err != nil {
conn.Close()
return nil, err
}
err = conn.Hello()
if err != nil {
conn.Close()
return nil, err
}
object := conn.Object(dbusObject, dbus.ObjectPath(dbusPath))
return &logindDbus{
conn: conn,
object: object,
}, nil
}
func (c *logindDbus) listSeats() ([]string, error) {
var result [][]interface{}
err := c.object.Call(dbusObject+".Manager.ListSeats", 0).Store(&result)
if err != nil {
return nil, err
}
resultInterface := make([]interface{}, len(result))
for i := range result {
resultInterface[i] = result[i]
}
seats := make([]logindSeatEntry, len(result))
seatsInterface := make([]interface{}, len(seats))
for i := range seats {
seatsInterface[i] = &seats[i]
}
err = dbus.Store(resultInterface, seatsInterface...)
if err != nil {
return nil, err
}
ret := make([]string, len(seats)+1)
for i := range seats {
ret[i] = seats[i].SeatId
}
// Always add the empty seat, which is used for remote sessions like SSH
ret[len(seats)] = ""
return ret, nil
}
func (c *logindDbus) listSessions() ([]logindSessionEntry, error) {
var result [][]interface{}
err := c.object.Call(dbusObject+".Manager.ListSessions", 0).Store(&result)
if err != nil {
return nil, err
}
resultInterface := make([]interface{}, len(result))
for i := range result {
resultInterface[i] = result[i]
}
sessions := make([]logindSessionEntry, len(result))
sessionsInterface := make([]interface{}, len(sessions))
for i := range sessions {
sessionsInterface[i] = &sessions[i]
}
err = dbus.Store(resultInterface, sessionsInterface...)
if err != nil {
return nil, err
}
return sessions, nil
}
func (c *logindDbus) getSession(session logindSessionEntry) *logindSession {
object := c.conn.Object(dbusObject, session.SessionObjectPath)
remote, err := object.GetProperty(dbusObject + ".Session.Remote")
if err != nil {
return nil
}
sessionType, err := object.GetProperty(dbusObject + ".Session.Type")
if err != nil {
return nil
}
sessionTypeStr, ok := sessionType.Value().(string)
if !ok {
return nil
}
class, err := object.GetProperty(dbusObject + ".Session.Class")
if err != nil {
return nil
}
classStr, ok := class.Value().(string)
if !ok {
return nil
}
return &logindSession{
seat: session.SeatId,
remote: remote.String(),
sessionType: knownStringOrOther(sessionTypeStr, attrTypeValues),
class: knownStringOrOther(classStr, attrClassValues),
}
}

View file

@ -0,0 +1,102 @@
// Copyright 2016 The Prometheus Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package collector
import (
"testing"
"github.com/godbus/dbus"
"github.com/prometheus/client_golang/prometheus"
)
type testLogindInterface struct{}
var testSeats = []string{"seat0", ""}
func (c *testLogindInterface) listSeats() ([]string, error) {
return testSeats, nil
}
func (c *testLogindInterface) listSessions() ([]logindSessionEntry, error) {
return []logindSessionEntry{
{
SessionId: "1",
UserId: 0,
UserName: "",
SeatId: "",
SessionObjectPath: dbus.ObjectPath("/org/freedesktop/login1/session/1"),
},
{
SessionId: "2",
UserId: 0,
UserName: "",
SeatId: "seat0",
SessionObjectPath: dbus.ObjectPath("/org/freedesktop/login1/session/2"),
},
}, nil
}
func (c *testLogindInterface) getSession(session logindSessionEntry) *logindSession {
sessions := map[dbus.ObjectPath]*logindSession{
dbus.ObjectPath("/org/freedesktop/login1/session/1"): {
seat: session.SeatId,
remote: "true",
sessionType: knownStringOrOther("tty", attrTypeValues),
class: knownStringOrOther("user", attrClassValues),
},
dbus.ObjectPath("/org/freedesktop/login1/session/2"): {
seat: session.SeatId,
remote: "false",
sessionType: knownStringOrOther("x11", attrTypeValues),
class: knownStringOrOther("greeter", attrClassValues),
},
}
return sessions[session.SessionObjectPath]
}
func TestLogindCollectorKnownStringOrOther(t *testing.T) {
known := []string{"foo", "bar"}
actual := knownStringOrOther("foo", known)
expected := "foo"
if actual != expected {
t.Errorf("knownStringOrOther failed: got %q, expected %q.", actual, expected)
}
actual = knownStringOrOther("baz", known)
expected = "other"
if actual != expected {
t.Errorf("knownStringOrOther failed: got %q, expected %q.", actual, expected)
}
}
func TestLogindCollectorCollectMetrics(t *testing.T) {
ch := make(chan prometheus.Metric)
go func() {
collectMetrics(ch, &testLogindInterface{})
close(ch)
}()
count := 0
for range ch {
count++
}
expected := len(testSeats) * len(attrRemoteValues) * len(attrTypeValues) * len(attrClassValues)
if count != expected {
t.Errorf("collectMetrics did not generate the expected number of metrics: got %d, expected %d.", count, expected)
}
}