diff --git a/README.md b/README.md index c53d33d0..97cf1b86 100644 --- a/README.md +++ b/README.md @@ -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_ diff --git a/collector/logind_linux.go b/collector/logind_linux.go new file mode 100644 index 00000000..e7172f1b --- /dev/null +++ b/collector/logind_linux.go @@ -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), + } +} diff --git a/collector/logind_linux_test.go b/collector/logind_linux_test.go new file mode 100644 index 00000000..7c941583 --- /dev/null +++ b/collector/logind_linux_test.go @@ -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) + } +}