mirror of
https://github.com/prometheus/node_exporter.git
synced 2024-12-28 15:09:45 -08:00
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:
parent
d98335cbf0
commit
91ddafdb33
|
@ -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
268
collector/logind_linux.go
Normal 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),
|
||||
}
|
||||
}
|
102
collector/logind_linux_test.go
Normal file
102
collector/logind_linux_test.go
Normal 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)
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue