diff --git a/.gitignore b/.gitignore index f3827eb9..462a390c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ +*~ *.iml *.swp *.o @@ -10,4 +11,7 @@ main.retry testing/ssh_config testing/ve -build/ \ No newline at end of file +build/ + +/sysinfo +/cmd/sysinfo/sysinfo diff --git a/cmd/sysinfo/main.go b/cmd/sysinfo/main.go new file mode 100644 index 00000000..bbce1deb --- /dev/null +++ b/cmd/sysinfo/main.go @@ -0,0 +1,124 @@ +package main + +import ( + "fmt" + "io" + "log" + "os" + "strings" + "text/template" + "time" + + "github.com/dustin/go-humanize" + "github.com/elastic/go-sysinfo" + "github.com/elastic/go-sysinfo/types" +) + +const ( + hostTmpl = `Host Info: + +Architecture: {{ .Info.Architecture }} +Boot Time: {{ .Info.BootTime }} ({{ .Info.BootTime | ago }} ago) +{{ with .Info.Containerized -}} +Containerized: {{ .Info.Containerized }} +{{ end -}} +Hostname: {{ .Info.Hostname }} +Unique ID: {{ .Info.UniqueID }} +OS: {{ .Info.OS }} +Kernel Version: {{ .Info.KernelVersion }} +Timezone: {{ .Info.Timezone }} (Offset: {{ .Info.TimezoneOffsetSec }}) +IP Addresses: {{ .Info.IPs | join " " }} +MAC Addresses: {{ .Info.MACs | join " " }} +` + cpuTmpl = `CPU Usage: + +User {{ .User }} +System {{ .System }} +Idle {{ .Idle }} +IOWait {{ .IOWait }} +IRQ {{ .IRQ }} +Nice {{ .Nice }} +SoftIRQ {{ .SoftIRQ }} +Steal {{ .Steal }} +` + memTmpl = `Memory Usage: + +Total {{ .Total | bytes }} +Used {{ .Used | bytes }} +Available {{ .Available | bytes }} +Free {{ .Free | bytes }} +VirtualTotal {{ .VirtualTotal | bytes }} +VirtualUsed {{ .VirtualUsed | bytes }} +VirtualFree {{ .VirtualFree | bytes }} +` + loadTmpl = `Load Average: {{ .One }} {{ .Five }} {{ .Fifteen }} +` +) + +func main() { + host, err := sysinfo.Host() + if err != nil { + log.Fatalf("error getting host info: %s", err) + } + if err := printHostInfo(os.Stdout, host); err != nil { + log.Fatalf("error printing host info: %s", err) + } +} + +func printHostInfo(w io.Writer, host types.Host) error { + if err := render(hostTmpl, host, w); err != nil { + return fmt.Errorf("error rendering host information: %w", err) + } + + cpuTimes, err := host.CPUTime() + if err != nil { + return fmt.Errorf("error getting host cpu times: %w", err) + } + fmt.Fprintln(w) + if err := render(cpuTmpl, cpuTimes, w); err != nil { + return fmt.Errorf("error rendering host cpu time: %w", err) + } + + memory, err := host.Memory() + if err != nil { + return fmt.Errorf("error getting host memory: %w", err) + } + fmt.Fprintln(w) + if err := render(memTmpl, memory, w); err != nil { + return fmt.Errorf("error rendering host memory: %w", err) + } + + load, err := host.LoadAverage() + if err != nil { + return fmt.Errorf("error getting host load: %w", err) + } + fmt.Fprintln(w) + if err := render(loadTmpl, load, w); err != nil { + return fmt.Errorf("error rendering host load: %w", err) + } + + return nil +} + +func ago(t time.Time) string { + return time.Since(t).String() +} + +func join(sep string, xs []string) string { + return strings.Join(xs, sep) +} + +func render(tmpl string, ctx interface{}, w io.Writer) error { + funcs := template.FuncMap{ + "ago": ago, + "join": join, + "bytes": humanize.Bytes, + } + + t, err := template.New("tmpl").Funcs(funcs).Parse(tmpl) + if err != nil { + return err + } + + return t.Execute(w, ctx) +} diff --git a/go.mod b/go.mod index 536235c3..67d05f63 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module github.com/elastic/go-sysinfo go 1.17 require ( + github.com/dustin/go-humanize v1.0.0 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 diff --git a/go.sum b/go.sum index 43db4ec3..10894bb9 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,7 @@ 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/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo= +github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= 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/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= diff --git a/providers/darwin/host_darwin.go b/providers/darwin/host_darwin.go index 8f4602cf..7d23d015 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 00000000..32322081 --- /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/load_average_darwin_test.go b/providers/darwin/load_average_darwin_test.go new file mode 100644 index 00000000..cad8ffaf --- /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/linux/host_linux.go b/providers/linux/host_linux.go index 4898078c..71e790bf 100644 --- a/providers/linux/host_linux.go +++ b/providers/linux/host_linux.go @@ -82,6 +82,16 @@ 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) { + content, err := ioutil.ReadFile(h.procFS.path("loadavg")) + if err != nil { + return nil, err + } + + return parseLoadAvg(content) +} + // 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/providers/linux/host_linux_test.go b/providers/linux/host_linux_test.go index fb36e95a..6884b4ec 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/load_average.go b/providers/linux/load_average.go new file mode 100644 index 00000000..fd351926 --- /dev/null +++ b/providers/linux/load_average.go @@ -0,0 +1,68 @@ +// 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 linux + +import ( + "bufio" + "bytes" + "fmt" + "reflect" + + "github.com/elastic/go-sysinfo/types" +) + +var loadAverageInfoFieldIndexToTag = make(map[int]string) + +func init() { + var loadavg types.LoadAverageInfo + val := reflect.ValueOf(loadavg) + typ := reflect.TypeOf(loadavg) + + for i := 0; i < val.NumField(); i++ { + field := typ.Field(i) + if tag := field.Tag.Get("json"); tag != "" { + loadAverageInfoFieldIndexToTag[i] = tag + } + } +} + +// parseLoadAvg parses the content of /proc/loadavg +func parseLoadAvg(content []byte) (*types.LoadAverageInfo, error) { + var loadAvg types.LoadAverageInfo + refValues := reflect.ValueOf(&loadAvg).Elem() + + s := bufio.NewScanner(bytes.NewReader(content)) + s.Split(bufio.ScanWords) + + for index := 0; index < len(loadAverageInfoFieldIndexToTag); index++ { + s.Scan() + data := s.Bytes() + val, err := parseBytesOrFloat(data) + if err != nil { + return nil, fmt.Errorf("failed to parse %v: %w", data, err) + } + + sval := refValues.Field(index) + + if sval.CanSet() { + sval.SetFloat(val) + } + } + + return &loadAvg, s.Err() +} diff --git a/providers/linux/load_average_test.go b/providers/linux/load_average_test.go new file mode 100644 index 00000000..3dd4e700 --- /dev/null +++ b/providers/linux/load_average_test.go @@ -0,0 +1,37 @@ +// 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 linux + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +var loadAvgInput = `1.55 1.74 1.73 2/1318 46377` + +func TestParseLoadAvg(t *testing.T) { + data, err := parseLoadAvg([]byte(loadAvgInput)) + if err != nil { + t.Fatal(err) + } + + assert.Equal(t, float64(1.55), data.One) + assert.Equal(t, float64(1.74), data.Five) + assert.Equal(t, float64(1.73), data.Fifteen) +} diff --git a/providers/linux/testdata/ubuntu1710/proc/loadavg b/providers/linux/testdata/ubuntu1710/proc/loadavg new file mode 100644 index 00000000..81ad54e0 --- /dev/null +++ b/providers/linux/testdata/ubuntu1710/proc/loadavg @@ -0,0 +1 @@ +1.55 1.74 1.73 2/1318 46377 diff --git a/providers/linux/util.go b/providers/linux/util.go index b8705a13..c97f1e95 100644 --- a/providers/linux/util.go +++ b/providers/linux/util.go @@ -109,3 +109,18 @@ func parseBytesOrNumber(data []byte) (uint64, error) { return num * multiplier, nil } + +func parseBytesOrFloat(data []byte) (float64, error) { + parts := bytes.Fields(data) + + if len(parts) == 0 { + return 0, errors.New("empty value") + } + + num, err := strconv.ParseFloat(string(parts[0]), 64) + if err != nil { + return 0, fmt.Errorf("failed to parse value: %w", err) + } + + return num, nil +} diff --git a/types/host.go b/types/host.go index 49e69603..2ffee7ba 100644 --- a/types/host.go +++ b/types/host.go @@ -24,6 +24,7 @@ import "time" // implementation is unable to collect all of the necessary data. type Host interface { CPUTimer + LoadAverage Info() HostInfo Memory() (*HostMemoryInfo, error) } @@ -98,7 +99,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