Repository: elastic/go-sysinfo (go)

Issue:
LoadAverage() Where is this used?
I'm looking at using your library to provide cross-platform (Linux, BSD and Darwin) System Information, but going through the docs and codebase I don't see where `.LoadAverage()` is ever used or the interface.

Is this just missing?

Pull request description:
Feat/load average implementation
This should close #120 

As I said on the issue, I had some problems for the `darwin` implementation, since I had to do it from a VM. I tested it without CGO (`CGO_ENABLED=0`) and still worked.

The way the load is parsed seems a bit tricky, although it's the standard way of doing so for BSD systems using Golang. I tried parsing the values the same way you did on others `SysctlRaw` without any luck (I'd appreciate an explanation if you know the reason, I'm still pretty new to Golang)

If anything needs some changes, please let me know!

Linux host: Fedora 36 Workstation, Kernel 5.19.15-201
Darwin host: macOS Catalina, Version 10.15.7

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 84c02a9..5e6ca24 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -13,7 +13,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 
 ### Changed
 
-- Remove custom sysctl implementation and partial cgo requirement 
+- Remove custom sysctl implementation and partial cgo requirement
+- Changes on the `Host` and `LoadAverage` interfaces, now implemented by default on Linux and Darwin platforms. [#140](https://github.com/elastic/go-sysinfo/pull/140)
 
 ### Deprecated
 
diff --git a/README.md b/README.md
index 444de0d..409432c 100644
--- a/README.md
+++ b/README.md
@@ -35,6 +35,7 @@ that are implemented.
 | `Info()`         | x      | x     | x       | x   |
 | `Memory()`       | x      | x     | x       | x   |
 | `CPUTimer`       | x      | x     | x       | x   |
+| `LoadAverage`    | x      | x     |         |     |
 | `VMStat`         |        | x     |         |     |
 | `NetworkCounters`|        | x     |         |     |
 
diff --git a/go.mod b/go.mod
index 536235c..3116420 100644
--- a/go.mod
+++ b/go.mod
@@ -5,7 +5,7 @@ go 1.17
 require (
 	github.com/elastic/go-windows v1.0.0
 	github.com/joeshaw/multierror v0.0.0-20140124173710-69b34d4ec901
-	github.com/prometheus/procfs v0.0.0-20190425082905-87a4384529e0
+	github.com/prometheus/procfs v0.8.0
 	github.com/stretchr/testify v1.3.0
 	golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a
 	howett.net/plist v0.0.0-20181124034731-591f970eefbb
diff --git a/go.sum b/go.sum
index 43db4ec..66a1db2 100644
--- a/go.sum
+++ b/go.sum
@@ -2,6 +2,8 @@ github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8
 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/elastic/go-windows v1.0.0 h1:qLURgZFkkrYyTTkvYpsZIgf83AUsdIHfvlJaqaZ7aSY=
 github.com/elastic/go-windows v1.0.0/go.mod h1:TsU0Nrp7/y3+VwE82FoZF8gC/XFg/Elz6CcloAxnPgU=
+github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg=
+github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
 github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
 github.com/joeshaw/multierror v0.0.0-20140124173710-69b34d4ec901 h1:rp+c0RAYOWj8l6qbCUTSiRLG/iKnW3K3/QfPPuSsBt4=
 github.com/joeshaw/multierror v0.0.0-20140124173710-69b34d4ec901/go.mod h1:Z86h9688Y0wesXCyonoVr47MasHilkuLMqGhRZ4Hpak=
@@ -12,12 +14,12 @@ github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
 github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
 github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
-github.com/prometheus/procfs v0.0.0-20190425082905-87a4384529e0 h1:c8R11WC8m7KNMkTv/0+Be8vvwo4I3/Ut9AC2FW8fX3U=
-github.com/prometheus/procfs v0.0.0-20190425082905-87a4384529e0/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
+github.com/prometheus/procfs v0.8.0 h1:ODq8ZFEaYeCaZOJlZZdJA2AbQR98dSHSM1KW/You5mo=
+github.com/prometheus/procfs v0.8.0/go.mod h1:z7EfXMXOkbkqb9IINtpCn86r/to3BnA0uaxHdg830/4=
 github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
 github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
 github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
-golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20220601150217-0de741cfad7f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a h1:dGzPydgVsqGcTRVwiLJ1jVbufYwmzD3LfVPLKsKg+0k=
 golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
diff --git a/providers/darwin/host_darwin.go b/providers/darwin/host_darwin.go
index 8f4602c..7d23d01 100644
--- a/providers/darwin/host_darwin.go
+++ b/providers/darwin/host_darwin.go
@@ -139,6 +139,21 @@ func (h *host) Memory() (*types.HostMemoryInfo, error) {
 	return &mem, nil
 }
 
+func (h *host) LoadAverage() (*types.LoadAverageInfo, error) {
+	load, err := getLoadAverage()
+	if err != nil {
+		return nil, fmt.Errorf("failed to get loadavg: %w", err)
+	}
+
+	scale := float64(load.scale)
+
+	return &types.LoadAverageInfo{
+		One:     float64(load.load[0]) / scale,
+		Five:    float64(load.load[1]) / scale,
+		Fifteen: float64(load.load[2]) / scale,
+	}, nil
+}
+
 func newHost() (*host, error) {
 	h := &host{}
 	r := &reader{}
diff --git a/providers/darwin/load_average_darwin.go b/providers/darwin/load_average_darwin.go
new file mode 100644
index 0000000..3232208
--- /dev/null
+++ b/providers/darwin/load_average_darwin.go
@@ -0,0 +1,45 @@
+// 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.
+
+//go:build amd64 || arm64
+// +build amd64 arm64
+
+package darwin
+
+import (
+	"unsafe"
+
+	"golang.org/x/sys/unix"
+)
+
+const loadAverage = "vm.loadavg"
+
+type loadAvg struct {
+	load  [3]uint32
+	scale int
+}
+
+func getLoadAverage() (*loadAvg, error) {
+	data, err := unix.SysctlRaw(loadAverage)
+	if err != nil {
+		return nil, err
+	}
+
+	load := *(*loadAvg)(unsafe.Pointer((&data[0])))
+
+	return &load, nil
+}
diff --git a/providers/darwin/process_darwin.go b/providers/darwin/process_darwin.go
index e335f98..b63367e 100644
--- a/providers/darwin/process_darwin.go
+++ b/providers/darwin/process_darwin.go
@@ -46,7 +46,9 @@ func (s darwinSystem) Processes() ([]types.Process, error) {
 			continue
 		}
 
-		processes = append(processes, &process{pid: int(pid)})
+		processes = append(processes, &process{
+			pid: int(pid),
+		})
 	}
 
 	return processes, nil
@@ -118,18 +120,23 @@ func (p *process) Info() (types.ProcessInfo, error) {
 }
 
 func (p *process) User() (types.UserInfo, error) {
-	var task procTaskAllInfo
-	if err := getProcTaskAllInfo(p.pid, &task); err != nil {
+	kproc, err := unix.SysctlKinfoProc("kern.proc.pid", p.pid)
+	if err != nil {
 		return types.UserInfo{}, err
 	}
 
+	egid := ""
+	if len(kproc.Eproc.Ucred.Groups) > 0 {
+		egid = strconv.Itoa(int(kproc.Eproc.Ucred.Groups[0]))
+	}
+
 	return types.UserInfo{
-		UID:  strconv.Itoa(int(task.Pbsd.Pbi_ruid)),
-		EUID: strconv.Itoa(int(task.Pbsd.Pbi_uid)),
-		SUID: strconv.Itoa(int(task.Pbsd.Pbi_svuid)),
-		GID:  strconv.Itoa(int(task.Pbsd.Pbi_rgid)),
-		EGID: strconv.Itoa(int(task.Pbsd.Pbi_gid)),
-		SGID: strconv.Itoa(int(task.Pbsd.Pbi_svgid)),
+		UID:  strconv.Itoa(int(kproc.Eproc.Pcred.P_ruid)),
+		EUID: strconv.Itoa(int(kproc.Eproc.Ucred.Uid)),
+		SUID: strconv.Itoa(int(kproc.Eproc.Pcred.P_svuid)),
+		GID:  strconv.Itoa(int(kproc.Eproc.Pcred.P_rgid)),
+		SGID: strconv.Itoa(int(kproc.Eproc.Pcred.P_svgid)),
+		EGID: egid,
 	}, nil
 }
 
diff --git a/providers/linux/host_linux.go b/providers/linux/host_linux.go
index 4898078..9adb5cb 100644
--- a/providers/linux/host_linux.go
+++ b/providers/linux/host_linux.go
@@ -82,6 +82,20 @@ func (h *host) VMStat() (*types.VMStatInfo, error) {
 	return parseVMStat(content)
 }
 
+// LoadAverage reports data from /proc/loadavg on linux.
+func (h *host) LoadAverage() (*types.LoadAverageInfo, error) {
+	loadAvg, err := h.procFS.LoadAvg()
+	if err != nil {
+		return nil, err
+	}
+
+	return &types.LoadAverageInfo{
+		One:     loadAvg.Load1,
+		Five:    loadAvg.Load5,
+		Fifteen: loadAvg.Load15,
+	}, nil
+}
+
 // NetworkCounters reports data from /proc/net on linux
 func (h *host) NetworkCounters() (*types.NetworkCountersInfo, error) {
 	snmpRaw, err := ioutil.ReadFile(h.procFS.path("net/snmp"))
diff --git a/types/host.go b/types/host.go
index 49e6960..d2911ee 100644
--- a/types/host.go
+++ b/types/host.go
@@ -98,7 +98,7 @@ type OSInfo struct {
 // LoadAverage is the interface that wraps the LoadAverage method.
 // LoadAverage returns load info on the host
 type LoadAverage interface {
-	LoadAverage() LoadAverageInfo
+	LoadAverage() (*LoadAverageInfo, error)
 }
 
 // LoadAverageInfo contains load statistics


Test patch:
diff --git a/providers/darwin/load_average_darwin_test.go b/providers/darwin/load_average_darwin_test.go
new file mode 100644
index 0000000..cad8ffa
--- /dev/null
+++ b/providers/darwin/load_average_darwin_test.go
@@ -0,0 +1,30 @@
+// 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 darwin
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestGetLoadAverage(t *testing.T) {
+	a, err := getLoadAverage()
+	assert.NoError(t, err)
+	assert.NotEmpty(t, a)
+}
diff --git a/providers/darwin/process_darwin_test.go b/providers/darwin/process_darwin_test.go
index 3b1e341..1d2dfc5 100644
--- a/providers/darwin/process_darwin_test.go
+++ b/providers/darwin/process_darwin_test.go
@@ -115,5 +115,15 @@ func TestProcesses(t *testing.T) {
 		t.Fatal("empty exec")
 	}
 
+	u, err := ps[0].User()
+	require.NoError(t, err)
+
+	require.NotEmpty(t, u.UID)
+	require.NotEmpty(t, u.EUID)
+	require.NotEmpty(t, u.SUID)
+	require.NotEmpty(t, u.GID)
+	require.NotEmpty(t, u.EGID)
+	require.NotEmpty(t, u.SGID)
+
 	t.Log(ps[0].Info())
 }
diff --git a/providers/linux/host_linux_test.go b/providers/linux/host_linux_test.go
index fb36e95..6884b4e 100644
--- a/providers/linux/host_linux_test.go
+++ b/providers/linux/host_linux_test.go
@@ -72,6 +72,23 @@ func TestHostVMStat(t *testing.T) {
 	t.Log(string(data))
 }
 
+func TestHostLoadAverage(t *testing.T) {
+	host, err := newLinuxSystem("testdata/ubuntu1710").Host()
+	if err != nil {
+		t.Fatal(err)
+	}
+	s, err := host.(types.LoadAverage).LoadAverage()
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	data, err := json.Marshal(s)
+	if err != nil {
+		t.Fatal(err)
+	}
+	t.Log(string(data))
+}
+
 func TestHostNetworkCounters(t *testing.T) {
 	host, err := newLinuxSystem("testdata/fedora30").Host()
 	if err != nil {
diff --git a/providers/linux/testdata/ubuntu1710/proc/loadavg b/providers/linux/testdata/ubuntu1710/proc/loadavg
new file mode 100644
index 0000000..81ad54e
--- /dev/null
+++ b/providers/linux/testdata/ubuntu1710/proc/loadavg
@@ -0,0 +1,1 @@
+1.55 1.74 1.73 2/1318 46377
diff --git a/system_test.go b/system_test.go
index 675de88..63ae68d 100644
--- a/system_test.go
+++ b/system_test.go
@@ -241,10 +241,22 @@ func TestHost(t *testing.T) {
 	info := host.Info()
 	assert.NotZero(t, info)
 
+	output := map[string]interface{}{}
+	output["host.info"] = info
+
+	if v, ok := host.(types.LoadAverage); ok {
+		loadAvg, err := v.LoadAverage()
+		if err != nil {
+			t.Fatal(err)
+		}
+		output["host.loadavg"] = loadAvg
+	}
+
 	memory, err := host.Memory()
 	if err != nil {
 		t.Fatal(err)
 	}
+	output["host.memory"] = memory
 
 	cpu, err := host.CPUTime()
 	if errors.Is(err, types.ErrNotImplemented) {
@@ -255,12 +267,9 @@ func TestHost(t *testing.T) {
 	if err != nil {
 		t.Fatal(err)
 	}
+	output["host.cpu"] = cpu
 
-	logAsJSON(t, map[string]interface{}{
-		"host.info":   info,
-		"host.memory": memory,
-		"host.cpu":    cpu,
-	})
+	logAsJSON(t, output)
 }
 
 func logAsJSON(t testing.TB, v interface{}) {