diff --git a/CHANGELOG.next.asciidoc b/CHANGELOG.next.asciidoc index ab8bdadbbef..8155d28ace3 100644 --- a/CHANGELOG.next.asciidoc +++ b/CHANGELOG.next.asciidoc @@ -213,6 +213,7 @@ https://github.com/elastic/beats/compare/v7.0.0-alpha2...master[Check the HEAD d - Add OpenMetrics Metricbeat module {pull}16596[16596] - Add `cloudfoundry` module to send events from Cloud Foundry. {pull}16671[16671] - Add `redisenterprise` module. {pull}16482[16482] {issue}15269[15269] +- Add system/users metricset as beta {pull}16569[16569] - Align fields to ECS and add more tests for the azure module. {issue}16024[16024] {pull}16754[16754] - Add additional cgroup fields to docker/diskio{pull}16638[16638] - Add PubSub metricset to Google Cloud Platform module {pull}15536[15536] diff --git a/NOTICE.txt b/NOTICE.txt index 4ca322a3594..68c9b8218b2 100644 --- a/NOTICE.txt +++ b/NOTICE.txt @@ -2393,6 +2393,38 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +-------------------------------------------------------------------- +Dependency: github.com/godbus/dbus +Revision: ade71ed3457e +License type (autodetected): BSD-2-Clause +./vendor/github.com/godbus/dbus/LICENSE: +-------------------------------------------------------------------- +Copyright (c) 2013, Georg Reinke (), Google +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: + +1. Redistributions of source code must retain the above copyright notice, +this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright +notice, this list of conditions and the following disclaimer in the +documentation and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED +TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + -------------------------------------------------------------------- Dependency: github.com/godbus/dbus/v5 Version: v5.0.3 diff --git a/go.mod b/go.mod index c2be718d89c..d98f1518c15 100644 --- a/go.mod +++ b/go.mod @@ -76,6 +76,7 @@ require ( github.com/go-sourcemap/sourcemap v2.1.2+incompatible // indirect github.com/go-sql-driver/mysql v1.4.1 github.com/gocarina/gocsv v0.0.0-20170324095351-ffef3ffc77be + github.com/godbus/dbus v0.0.0-20190422162347-ade71ed3457e github.com/godror/godror v0.10.4 github.com/gofrs/flock v0.7.2-0.20190320160742-5135e617513b github.com/gofrs/uuid v3.2.0+incompatible diff --git a/metricbeat/docs/fields.asciidoc b/metricbeat/docs/fields.asciidoc index 08f3c0cc848..77d62911028 100644 --- a/metricbeat/docs/fields.asciidoc +++ b/metricbeat/docs/fields.asciidoc @@ -36447,6 +36447,113 @@ format: duration -- +[float] +=== users + +Logged-in user session data + + + +*`system.users.id`*:: ++ +-- +The ID of the session + + +type: keyword + +-- + +*`system.users.seat`*:: ++ +-- +An associated logind seat + + +type: keyword + +-- + +*`system.users.path`*:: ++ +-- +The DBus object path of the session + + +type: keyword + +-- + +*`system.users.type`*:: ++ +-- +The type of the user session + + +type: keyword + +-- + +*`system.users.service`*:: ++ +-- +A session associated with the service + + +type: keyword + +-- + +*`system.users.remote`*:: ++ +-- +A bool indicating a remote session + + +type: boolean + +-- + +*`system.users.state`*:: ++ +-- +The current state of the session + + +type: keyword + +-- + +*`system.users.scope`*:: ++ +-- +The associated systemd scope + + +type: keyword + +-- + +*`system.users.leader`*:: ++ +-- +The root PID of the session + + +type: long + +-- + +*`system.users.remote_host`*:: ++ +-- +A remote host address for the session + + +type: keyword + +-- + [[exported-fields-tomcat]] == Tomcat fields diff --git a/metricbeat/docs/modules/system.asciidoc b/metricbeat/docs/modules/system.asciidoc index 74c9c06a794..1c7b49716af 100644 --- a/metricbeat/docs/modules/system.asciidoc +++ b/metricbeat/docs/modules/system.asciidoc @@ -274,6 +274,8 @@ The following metricsets are available: * <> +* <> + include::system/core.asciidoc[] include::system/cpu.asciidoc[] @@ -308,3 +310,5 @@ include::system/socket_summary.asciidoc[] include::system/uptime.asciidoc[] +include::system/users.asciidoc[] + diff --git a/metricbeat/docs/modules/system/users.asciidoc b/metricbeat/docs/modules/system/users.asciidoc new file mode 100644 index 00000000000..ab3025c02d1 --- /dev/null +++ b/metricbeat/docs/modules/system/users.asciidoc @@ -0,0 +1,23 @@ +//// +This file is generated! See scripts/mage/docs_collector.go +//// + +[[metricbeat-metricset-system-users]] +=== System users metricset + +beta[] + +include::../../../module/system/users/_meta/docs.asciidoc[] + + +==== Fields + +For a description of each field in the metricset, see the +<> section. + +Here is an example document generated by this metricset: + +[source,json] +---- +include::../../../module/system/users/_meta/data.json[] +---- diff --git a/metricbeat/docs/modules_list.asciidoc b/metricbeat/docs/modules_list.asciidoc index 30efcd5e63a..177a4206a16 100644 --- a/metricbeat/docs/modules_list.asciidoc +++ b/metricbeat/docs/modules_list.asciidoc @@ -227,7 +227,7 @@ This file is generated! See scripts/mage/docs_collector.go |<> |image:./images/icon-no.png[No prebuilt dashboards] | .1+| .1+| |<> |<> |image:./images/icon-yes.png[Prebuilt dashboards are available] | -.17+| .17+| |<> +.18+| .18+| |<> |<> |<> |<> @@ -244,6 +244,7 @@ This file is generated! See scripts/mage/docs_collector.go |<> |<> |<> +|<> beta[] |<> beta[] |image:./images/icon-yes.png[Prebuilt dashboards are available] | .4+| .4+| |<> beta[] |<> beta[] diff --git a/metricbeat/include/list_common.go b/metricbeat/include/list_common.go index 55291b93af1..2992be55031 100644 --- a/metricbeat/include/list_common.go +++ b/metricbeat/include/list_common.go @@ -152,6 +152,7 @@ import ( _ "github.com/elastic/beats/v7/metricbeat/module/system/socket" _ "github.com/elastic/beats/v7/metricbeat/module/system/socket_summary" _ "github.com/elastic/beats/v7/metricbeat/module/system/uptime" + _ "github.com/elastic/beats/v7/metricbeat/module/system/users" _ "github.com/elastic/beats/v7/metricbeat/module/traefik" _ "github.com/elastic/beats/v7/metricbeat/module/traefik/health" _ "github.com/elastic/beats/v7/metricbeat/module/uwsgi" diff --git a/metricbeat/module/system/_meta/config.yml b/metricbeat/module/system/_meta/config.yml index 65f6ffbb849..159895078c7 100644 --- a/metricbeat/module/system/_meta/config.yml +++ b/metricbeat/module/system/_meta/config.yml @@ -13,6 +13,7 @@ #- diskio #- socket #- services + #- users process.include_top_n: by_cpu: 5 # include top 5 processes by CPU by_memory: 5 # include top 5 processes by memory diff --git a/metricbeat/module/system/fields.go b/metricbeat/module/system/fields.go index 31eb04eb4e7..36a96f859eb 100644 --- a/metricbeat/module/system/fields.go +++ b/metricbeat/module/system/fields.go @@ -32,5 +32,5 @@ func init() { // AssetSystem returns asset data. // This is the base64 encoded gzipped contents of ../metricbeat/module/system. func AssetSystem() string { - return "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" + return "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" } diff --git a/metricbeat/module/system/users/_meta/data.json b/metricbeat/module/system/users/_meta/data.json new file mode 100644 index 00000000000..5b96b10ba81 --- /dev/null +++ b/metricbeat/module/system/users/_meta/data.json @@ -0,0 +1,39 @@ +{ + "@timestamp": "2017-10-12T08:05:34.853Z", + "event": { + "dataset": "system.users", + "duration": 115000, + "module": "system" + }, + "metricset": { + "name": "users", + "period": 10000 + }, + "process": { + "pid": 10786 + }, + "service": { + "type": "system" + }, + "source": { + "ip": "192.168.1.86" + }, + "system": { + "users": { + "id": 6, + "leader": 10786, + "path": "/org/freedesktop/login1/session/_36", + "remote": true, + "remote_host": "192.168.1.86", + "scope": "session-6.scope", + "seat": "", + "service": "sshd", + "state": "active", + "type": "tty" + } + }, + "user": { + "id": 1000, + "name": "alexk" + } +} \ No newline at end of file diff --git a/metricbeat/module/system/users/_meta/docs.asciidoc b/metricbeat/module/system/users/_meta/docs.asciidoc new file mode 100644 index 00000000000..596fae667a7 --- /dev/null +++ b/metricbeat/module/system/users/_meta/docs.asciidoc @@ -0,0 +1,11 @@ +The system/users metricset reports logged in users and associated sessions via dbus and logind, which is a systemd component. By default, the metricset will look in `/var/run/dbus/` for a system socket, although a new path can be selected with `DBUS_SYSTEM_BUS_ADDRESS`. + +This metricset is available on: + +- Linux + + +[float] +=== Configuration + +There are no configuration options for this metricset. diff --git a/metricbeat/module/system/users/_meta/fields.yml b/metricbeat/module/system/users/_meta/fields.yml new file mode 100644 index 00000000000..342d95629db --- /dev/null +++ b/metricbeat/module/system/users/_meta/fields.yml @@ -0,0 +1,48 @@ +- name: users + type: group + release: beta + description: > + Logged-in user session data + fields: + - name: id + type: keyword + description: > + The ID of the session + - name: seat + type: keyword + description: > + An associated logind seat + - name: path + type: keyword + description: > + The DBus object path of the session + - name: type + type: keyword + description: > + The type of the user session + - name: service + type: keyword + description: > + A session associated with the service + - name: remote + type: boolean + description: > + A bool indicating a remote session + - name: state + type: keyword + description: > + The current state of the session + - name: scope + type: keyword + description: > + The associated systemd scope + - name: leader + type: long + description: > + The root PID of the session + - name: remote_host + type: keyword + description: > + A remote host address for the session + + diff --git a/metricbeat/module/system/users/dbus.go b/metricbeat/module/system/users/dbus.go new file mode 100644 index 00000000000..03dbc9fc3a7 --- /dev/null +++ b/metricbeat/module/system/users/dbus.go @@ -0,0 +1,207 @@ +// Licensed to Elasticsearch B.V. under one or more contributor +// license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright +// ownership. Elasticsearch B.V. licenses this file to you 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 linux + +package users + +import ( + "fmt" + "os" + "strconv" + + "github.com/godbus/dbus" + "github.com/pkg/errors" +) + +const ( + loginObj = "org.freedesktop.login1" + getAll = "org.freedesktop.DBus.Properties.GetAll" + sessionList = "org.freedesktop.login1.Manager.ListSessions" +) + +// sessionInfo contains useful properties for a session +type sessionInfo struct { + Remote bool + RemoteHost string + Name string + Scope string + Service string + State string + Type string + Leader uint32 +} + +// loginSession contains basic information on a login session +type loginSession struct { + ID uint64 + UID uint32 + User string + Seat string + Path dbus.ObjectPath +} + +// initDbusConnection initializes a connection to the dbus +func initDbusConnection() (*dbus.Conn, error) { + conn, err := dbus.SystemBusPrivate() + if err != nil { + return nil, errors.Wrap(err, "error getting connection to system bus") + } + + auth := dbus.AuthExternal(strconv.Itoa(os.Getuid())) + + err = conn.Auth([]dbus.Auth{auth}) + if err != nil { + return nil, errors.Wrap(err, "error authenticating") + } + + err = conn.Hello() + if err != nil { + return nil, errors.Wrap(err, "error in Hello") + } + + return conn, nil +} + +// getSessionProps returns info on a given session pointed to by path +func getSessionProps(conn *dbus.Conn, path dbus.ObjectPath) (sessionInfo, error) { + busObj := conn.Object(loginObj, path) + + var props map[string]dbus.Variant + + err := busObj.Call(getAll, 0, "").Store(&props) + if err != nil { + return sessionInfo{}, errors.Wrap(err, "error calling DBus") + } + + return formatSessionProps(props) +} + +func formatSessionProps(props map[string]dbus.Variant) (sessionInfo, error) { + if len(props) < 8 { + return sessionInfo{}, fmt.Errorf("wrong number of fields in info: %v", props) + } + + remote, ok := props["Remote"].Value().(bool) + if !ok { + return sessionInfo{}, fmt.Errorf("failed to cast remote to bool") + } + + remoteHost, ok := props["RemoteHost"].Value().(string) + if !ok { + return sessionInfo{}, fmt.Errorf("failed to cast remote host to string") + } + + userName, ok := props["Name"].Value().(string) + if !ok { + return sessionInfo{}, fmt.Errorf("failed to cast username to string") + } + + scope, ok := props["Scope"].Value().(string) + if !ok { + return sessionInfo{}, fmt.Errorf("failed to cast scope to string") + } + + service, ok := props["Service"].Value().(string) + if !ok { + return sessionInfo{}, fmt.Errorf("failed to cast service to string") + } + + state, ok := props["State"].Value().(string) + if !ok { + return sessionInfo{}, fmt.Errorf("failed to cast state to string") + } + + sessionType, ok := props["Type"].Value().(string) + if !ok { + return sessionInfo{}, fmt.Errorf("failed to cast type to string") + } + + leader, ok := props["Leader"].Value().(uint32) + if !ok { + return sessionInfo{}, fmt.Errorf("failed to cast leader to uint32") + } + + session := sessionInfo{ + Remote: remote, + RemoteHost: remoteHost, + Name: userName, + Scope: scope, + Service: service, + State: state, + Type: sessionType, + Leader: leader, + } + + return session, nil +} + +// listSessions lists all sessions known to dbus +func listSessions(conn *dbus.Conn) ([]loginSession, error) { + busObj := conn.Object(loginObj, dbus.ObjectPath("/org/freedesktop/login1")) + var props [][]dbus.Variant + + if err := busObj.Call(sessionList, 0).Store(&props); err != nil { + return nil, errors.Wrap(err, "error calling dbus") + } + return formatSessionList(props) +} + +func formatSessionList(props [][]dbus.Variant) ([]loginSession, error) { + sessionList := make([]loginSession, len(props)) + for iter, session := range props { + if len(session) < 5 { + return nil, fmt.Errorf("wrong number of fields in session: %v", session) + } + idStr, ok := session[0].Value().(string) + if !ok { + return nil, fmt.Errorf("failed to cast user ID to string") + } + + id, err := strconv.ParseUint(idStr, 10, 32) + if err != nil { + return nil, errors.Wrap(err, "error parsing ID to int") + } + + uid, ok := session[1].Value().(uint32) + if !ok { + return nil, fmt.Errorf("failed to cast session uid to uint32") + } + user, ok := session[2].Value().(string) + if !ok { + return nil, fmt.Errorf("failed to cast session user to string") + } + seat, ok := session[3].Value().(string) + if !ok { + return nil, fmt.Errorf("failed to cast session seat to string") + } + path, ok := session[4].Value().(dbus.ObjectPath) + if !ok { + return nil, fmt.Errorf("failed to cast session path to ObjectPath") + } + newSession := loginSession{ + ID: id, + UID: uid, + User: user, + Seat: seat, + Path: path, + } + sessionList[iter] = newSession + } + + return sessionList, nil +} diff --git a/metricbeat/module/system/users/doc.go b/metricbeat/module/system/users/doc.go new file mode 100644 index 00000000000..41a0d978be4 --- /dev/null +++ b/metricbeat/module/system/users/doc.go @@ -0,0 +1,18 @@ +// Licensed to Elasticsearch B.V. under one or more contributor +// license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright +// ownership. Elasticsearch B.V. licenses this file to you 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 users diff --git a/metricbeat/module/system/users/users.go b/metricbeat/module/system/users/users.go new file mode 100644 index 00000000000..ff6ad38fa70 --- /dev/null +++ b/metricbeat/module/system/users/users.go @@ -0,0 +1,133 @@ +// Licensed to Elasticsearch B.V. under one or more contributor +// license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright +// ownership. Elasticsearch B.V. licenses this file to you 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 linux + +package users + +import ( + "net" + + "github.com/godbus/dbus" + "github.com/pkg/errors" + + "github.com/elastic/beats/v7/libbeat/common" + "github.com/elastic/beats/v7/libbeat/common/cfgwarn" + "github.com/elastic/beats/v7/metricbeat/mb" +) + +// init registers the MetricSet with the central registry as soon as the program +// starts. The New function will be called later to instantiate an instance of +// the MetricSet for each host defined in the module's configuration. After the +// MetricSet has been created then Fetch will begin to be called periodically. +func init() { + mb.Registry.MustAddMetricSet("system", "users", New) +} + +// MetricSet holds any configuration or state information. It must implement +// the mb.MetricSet interface. And this is best achieved by embedding +// mb.BaseMetricSet because it implements all of the required mb.MetricSet +// interface methods except for Fetch. +type MetricSet struct { + mb.BaseMetricSet + counter int + conn *dbus.Conn +} + +// New creates a new instance of the MetricSet. New is responsible for unpacking +// any MetricSet specific configuration options if there are any. +func New(base mb.BaseMetricSet) (mb.MetricSet, error) { + cfgwarn.Beta("The system users metricset is beta.") + + conn, err := initDbusConnection() + if err != nil { + return nil, errors.Wrap(err, "error connecting to dbus") + } + + return &MetricSet{ + BaseMetricSet: base, + counter: 1, + conn: conn, + }, nil +} + +// Fetch methods implements the data gathering and data conversion to the right +// format. It publishes the event which is then forwarded to the output. In case +// of an error set the Error field of mb.Event or simply call report.Error(). +func (m *MetricSet) Fetch(report mb.ReporterV2) error { + sessions, err := listSessions(m.conn) + if err != nil { + return errors.Wrap(err, "error listing sessions") + } + + eventMapping(m.conn, sessions, report) + + return nil +} + +// eventMapping iterates through the lists of users and sessions, combining the two +func eventMapping(conn *dbus.Conn, sessions []loginSession, report mb.ReporterV2) error { + + for _, session := range sessions { + + props, err := getSessionProps(conn, session.Path) + if err != nil { + return errors.Wrap(err, "error getting properties") + } + + event := common.MapStr{ + "id": session.ID, + "seat": session.Seat, + "path": session.Path, + "type": props.Type, + "service": props.Service, + "remote": props.Remote, + "state": props.State, + "scope": props.Scope, + "leader": props.Leader, + } + + rootEvents := common.MapStr{ + "process": common.MapStr{ + "pid": props.Leader, + }, + "user": common.MapStr{ + "name": session.User, + "id": session.UID, + }, + } + + if props.Remote { + event["remote_host"] = props.RemoteHost + if ipAddr := net.ParseIP(props.RemoteHost); ipAddr != nil { + rootEvents["source"] = common.MapStr{ + "ip": ipAddr, + } + } + } + + reported := report.Event(mb.Event{ + RootFields: rootEvents, + MetricSetFields: event, + }) + //if the channel is closed and metricbeat is shutting down, just return + if !reported { + break + } + } + return nil +} diff --git a/metricbeat/module/system/users/users_test.go b/metricbeat/module/system/users/users_test.go new file mode 100644 index 00000000000..50de07a6eee --- /dev/null +++ b/metricbeat/module/system/users/users_test.go @@ -0,0 +1,76 @@ +// Licensed to Elasticsearch B.V. under one or more contributor +// license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright +// ownership. Elasticsearch B.V. licenses this file to you 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 linux + +package users + +import ( + "testing" + + "github.com/godbus/dbus" + "github.com/stretchr/testify/assert" +) + +func TestFormatSession(t *testing.T) { + + testIn := map[string]dbus.Variant{ + "Remote": dbus.MakeVariant(true), + "RemoteHost": dbus.MakeVariant("192.168.1.1"), + "Name": dbus.MakeVariant("user"), + "Scope": dbus.MakeVariant("user-6.scope"), + "Service": dbus.MakeVariant("sshd.service"), + "State": dbus.MakeVariant("active"), + "Type": dbus.MakeVariant("remote"), + "Leader": dbus.MakeVariant(uint32(17459)), + } + + goodOut := sessionInfo{ + Remote: true, + RemoteHost: "192.168.1.1", + Name: "user", + Scope: "user-6.scope", + Service: "sshd.service", + State: "active", + Type: "remote", + Leader: 17459, + } + + output, err := formatSessionProps(testIn) + assert.NoError(t, err) + assert.Equal(t, goodOut, output) +} + +func TestFormatSessionList(t *testing.T) { + testIn := [][]dbus.Variant{ + {dbus.MakeVariant("6"), dbus.MakeVariant(uint32(1000)), dbus.MakeVariant("user"), dbus.MakeVariant(""), dbus.MakeVariant(dbus.ObjectPath("/path/to/object"))}, + } + + goodOut := []loginSession{{ + ID: uint64(6), + UID: uint32(1000), + User: "user", + Seat: "", + Path: dbus.ObjectPath("/path/to/object"), + }, + } + + output, err := formatSessionList(testIn) + assert.NoError(t, err) + assert.Equal(t, goodOut, output) + +} diff --git a/metricbeat/modules.d/system.yml b/metricbeat/modules.d/system.yml index 6351e7cba79..84fa8a86b89 100644 --- a/metricbeat/modules.d/system.yml +++ b/metricbeat/modules.d/system.yml @@ -16,6 +16,7 @@ #- diskio #- socket #- services + #- users process.include_top_n: by_cpu: 5 # include top 5 processes by CPU by_memory: 5 # include top 5 processes by memory diff --git a/vendor/github.com/godbus/dbus/.travis.yml b/vendor/github.com/godbus/dbus/.travis.yml new file mode 100644 index 00000000000..9cd57f432b0 --- /dev/null +++ b/vendor/github.com/godbus/dbus/.travis.yml @@ -0,0 +1,46 @@ +dist: precise +language: go +go_import_path: github.com/godbus/dbus +sudo: true + +go: + - 1.7.3 + - 1.8.7 + - 1.9.5 + - 1.10.1 + - tip + +env: + global: + matrix: + - TARGET=amd64 + - TARGET=arm64 + - TARGET=arm + - TARGET=386 + - TARGET=ppc64le + +matrix: + fast_finish: true + allow_failures: + - go: tip + exclude: + - go: tip + env: TARGET=arm + - go: tip + env: TARGET=arm64 + - go: tip + env: TARGET=386 + - go: tip + env: TARGET=ppc64le + +addons: + apt: + packages: + - dbus + - dbus-x11 + +before_install: + +script: + - go test -v -race ./... # Run all the tests with the race detector enabled + - go vet ./... # go vet is the official Go static analyzer diff --git a/vendor/github.com/godbus/dbus/CONTRIBUTING.md b/vendor/github.com/godbus/dbus/CONTRIBUTING.md new file mode 100644 index 00000000000..c88f9b2bdd0 --- /dev/null +++ b/vendor/github.com/godbus/dbus/CONTRIBUTING.md @@ -0,0 +1,50 @@ +# How to Contribute + +## Getting Started + +- Fork the repository on GitHub +- Read the [README](README.markdown) for build and test instructions +- Play with the project, submit bugs, submit patches! + +## Contribution Flow + +This is a rough outline of what a contributor's workflow looks like: + +- Create a topic branch from where you want to base your work (usually master). +- Make commits of logical units. +- Make sure your commit messages are in the proper format (see below). +- Push your changes to a topic branch in your fork of the repository. +- Make sure the tests pass, and add any new tests as appropriate. +- Submit a pull request to the original repository. + +Thanks for your contributions! + +### Format of the Commit Message + +We follow a rough convention for commit messages that is designed to answer two +questions: what changed and why. The subject line should feature the what and +the body of the commit should describe the why. + +``` +scripts: add the test-cluster command + +this uses tmux to setup a test cluster that you can easily kill and +start for debugging. + +Fixes #38 +``` + +The format can be described more formally as follows: + +``` +: + + + +