latest
This commit is contained in:
parent
b63bab0582
commit
2e8bbfee34
23
Makefile
23
Makefile
@ -1,16 +1,21 @@
|
|||||||
# Infer VERSION from the most recent tag; if none, use short commit hash.
|
# -------- version detection -------------------------------------------------
|
||||||
VERSION ?= $(shell git describe --tags --abbrev=0 2>/dev/null || \
|
# Use latest annotated/lightweight tag; if none, fall back to short SHA.
|
||||||
git rev-parse --short HEAD)
|
VERSION ?= $(shell git describe --tags --always --dirty=-dirty 2>/dev/null)
|
||||||
|
|
||||||
GOLDFLAGS += -s -w \
|
# -------- linker flags -------------------------------------------------------
|
||||||
-X 'git.eeqj.de/sneak/sysinfo/internal/sysinfo.Version=$(VERSION)'
|
# No inner quotes around -X value — go tool handles the spaces correctly.
|
||||||
|
LDFLAGS = -s -w -X "git.eeqj.de/sneak/sysinfo/internal/sysinfo.Version=$(VERSION)"
|
||||||
|
|
||||||
all: build
|
# -------- generic go build/install ------------------------------------------
|
||||||
|
PKG_CMD = ./cmd/sysinfo
|
||||||
|
|
||||||
build:
|
build:
|
||||||
GOFLAGS=-ldflags="$(GOLDFLAGS)" go build ./cmd/sysinfo
|
go build -ldflags '$(LDFLAGS)' $(PKG_CMD)
|
||||||
|
|
||||||
install:
|
install:
|
||||||
GOFLAGS=-ldflags="$(GOLDFLAGS)" go install ./cmd/sysinfo
|
go install -ldflags '$(LDFLAGS)' $(PKG_CMD)
|
||||||
|
|
||||||
.PHONY: all build install
|
clean:
|
||||||
|
rm -f sysinfo
|
||||||
|
|
||||||
|
.PHONY: build install clean
|
||||||
|
21
internal/sysinfo/collector.go
Normal file
21
internal/sysinfo/collector.go
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
package sysinfo
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Context is shared with every Collector.
|
||||||
|
type Context struct {
|
||||||
|
Now time.Time
|
||||||
|
Logf func(string, ...any)
|
||||||
|
Run func(string, ...string) ([]byte, error)
|
||||||
|
SafeRun func(string, ...string) string
|
||||||
|
SafeRead func(string) string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Collector produces one JSON fragment.
|
||||||
|
type Collector interface {
|
||||||
|
Key() string
|
||||||
|
Collect(*Context) (json.RawMessage, error)
|
||||||
|
}
|
67
internal/sysinfo/collector_block.go
Normal file
67
internal/sysinfo/collector_block.go
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
package sysinfo
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"path/filepath"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type BlockCollector struct{}
|
||||||
|
|
||||||
|
func (BlockCollector) Key() string { return "blockdevs" }
|
||||||
|
|
||||||
|
func (BlockCollector) Collect(c *Context) (json.RawMessage, error) {
|
||||||
|
rePU := regexp.MustCompile(`PARTUUID="([^"]+)"`)
|
||||||
|
all := map[string]any{}
|
||||||
|
|
||||||
|
links, _ := filepath.Glob("/dev/disk/by-id/*")
|
||||||
|
seen := map[string]bool{}
|
||||||
|
for _, link := range links {
|
||||||
|
base := filepath.Base(link)
|
||||||
|
target, _ := filepath.EvalSymlinks(link)
|
||||||
|
dev := filepath.Base(target)
|
||||||
|
if seen[dev] {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
seen[dev] = true
|
||||||
|
|
||||||
|
bd := map[string]any{
|
||||||
|
"timestamp": c.Now.Format(time.RFC3339),
|
||||||
|
"smartctl": c.SafeRun("smartctl", "-a", "/dev/"+dev),
|
||||||
|
"blkid": c.SafeRun("blkid", "-p", "/dev/"+dev),
|
||||||
|
}
|
||||||
|
sfd, _ := c.runCmd("sfdisk", "-J", "/dev/"+dev)
|
||||||
|
bd["sfdisk"] = json.RawMessage(sfd)
|
||||||
|
lsb, _ := c.runCmd("lsblk", "-J", "/dev/"+dev)
|
||||||
|
bd["lsblk"] = json.RawMessage(lsb)
|
||||||
|
|
||||||
|
parts := map[string]any{}
|
||||||
|
var ls struct {
|
||||||
|
Blockdevices []struct {
|
||||||
|
Children []struct{ Name string `json:"name"` } `json:"children"`
|
||||||
|
} `json:"blockdevices"`
|
||||||
|
}
|
||||||
|
_ = json.Unmarshal(lsb, &ls)
|
||||||
|
for _, d := range ls.Blockdevices {
|
||||||
|
for _, ch := range d.Children {
|
||||||
|
partdev := "/dev/" + ch.Name
|
||||||
|
blk := c.SafeRun("blkid", "-p", partdev)
|
||||||
|
m := rePU.FindStringSubmatch(blk)
|
||||||
|
if len(m) != 2 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
uuid := strings.ToLower(m[1])
|
||||||
|
pinfo := map[string]any{
|
||||||
|
"blkid": blk,
|
||||||
|
}
|
||||||
|
plsb, _ := c.runCmd("lsblk", "-J", partdev)
|
||||||
|
pinfo["lsblk"] = json.RawMessage(plsb)
|
||||||
|
parts[uuid] = pinfo
|
||||||
|
}
|
||||||
|
}
|
||||||
|
bd["partitions"] = parts
|
||||||
|
all[base] = bd
|
||||||
|
}
|
||||||
|
return json.Marshal(all)
|
||||||
|
}
|
49
internal/sysinfo/collector_network.go
Normal file
49
internal/sysinfo/collector_network.go
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
package sysinfo
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type NetworkCollector struct{}
|
||||||
|
|
||||||
|
func (NetworkCollector) Key() string { return "network" }
|
||||||
|
|
||||||
|
func (NetworkCollector) Collect(c *Context) (json.RawMessage, error) {
|
||||||
|
def, _ := c.defaultIfaceSet()
|
||||||
|
sections := map[string]any{}
|
||||||
|
|
||||||
|
nets, _ := filepath.Glob("/sys/class/net/*")
|
||||||
|
for _, n := range nets {
|
||||||
|
if strings.HasSuffix(n, "/lo") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
iface := filepath.Base(n)
|
||||||
|
mac := strings.ToLower(strings.ReplaceAll(
|
||||||
|
c.SafeRead(filepath.Join(n, "address")), ":", ""))
|
||||||
|
if mac == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
entry := map[string]any{
|
||||||
|
"iface": iface,
|
||||||
|
"timestamp": c.Now.Format(time.RFC3339),
|
||||||
|
"link": c.SafeRun("ip", "-details", "link", "show", iface),
|
||||||
|
"ethtool": c.SafeRun("ethtool", iface),
|
||||||
|
}
|
||||||
|
ipJSON, _ := c.runCmd("ip", "-j", "address", "show", iface)
|
||||||
|
entry["ip_addr"] = json.RawMessage(ipJSON)
|
||||||
|
|
||||||
|
stats, _ := c.readNetStats(iface)
|
||||||
|
entry["statistics"] = json.RawMessage(stats)
|
||||||
|
|
||||||
|
if def[iface] {
|
||||||
|
if out := c.SafeRun("curl", "--interface", iface, "-s",
|
||||||
|
"https://ipinfo.io"); json.Valid([]byte(out)) {
|
||||||
|
entry["ipinfo"] = json.RawMessage(out)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sections[mac] = entry
|
||||||
|
}
|
||||||
|
return json.Marshal(sections)
|
||||||
|
}
|
12
internal/sysinfo/collector_packages.go
Normal file
12
internal/sysinfo/collector_packages.go
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
package sysinfo
|
||||||
|
|
||||||
|
import "encoding/json"
|
||||||
|
|
||||||
|
type PackagesCollector struct{}
|
||||||
|
|
||||||
|
func (PackagesCollector) Key() string { return "packages" }
|
||||||
|
func (PackagesCollector) Collect(c *Context) (json.RawMessage, error) {
|
||||||
|
out := c.SafeRun("dpkg-query", "-W",
|
||||||
|
"-f=${Package} ${Version}\\n")
|
||||||
|
return json.Marshal(map[string]string{"dpkg": out})
|
||||||
|
}
|
12
internal/sysinfo/collector_sensors.go
Normal file
12
internal/sysinfo/collector_sensors.go
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
package sysinfo
|
||||||
|
|
||||||
|
import "encoding/json"
|
||||||
|
|
||||||
|
type SensorsCollector struct{}
|
||||||
|
|
||||||
|
func (SensorsCollector) Key() string { return "sensors" }
|
||||||
|
func (SensorsCollector) Collect(c *Context) (json.RawMessage, error) {
|
||||||
|
return json.Marshal(map[string]string{
|
||||||
|
"output": c.SafeRun("sensors"),
|
||||||
|
})
|
||||||
|
}
|
27
internal/sysinfo/collector_system.go
Normal file
27
internal/sysinfo/collector_system.go
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
package sysinfo
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type SystemCollector struct{}
|
||||||
|
|
||||||
|
func (SystemCollector) Key() string { return "system" }
|
||||||
|
|
||||||
|
func (SystemCollector) Collect(c *Context) (json.RawMessage, error) {
|
||||||
|
data := map[string]string{
|
||||||
|
"timestamp": c.Now.Format(time.RFC3339),
|
||||||
|
"uname": c.SafeRun("uname", "-a"),
|
||||||
|
"lsb_release": c.SafeRun("lsb_release", "-a"),
|
||||||
|
"cpuinfo": c.SafeRead("/proc/cpuinfo"),
|
||||||
|
"meminfo": c.SafeRead("/proc/meminfo"),
|
||||||
|
"distro": "ubuntu",
|
||||||
|
}
|
||||||
|
serial := strings.TrimSpace(
|
||||||
|
c.SafeRun("dmidecode", "-s", "system-serial-number"))
|
||||||
|
if serial != "" {
|
||||||
|
data["id"] = serial
|
||||||
|
}
|
||||||
|
return json.Marshal(data)
|
||||||
|
}
|
28
internal/sysinfo/collector_zfs.go
Normal file
28
internal/sysinfo/collector_zfs.go
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
package sysinfo
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ZFSCollector struct{}
|
||||||
|
|
||||||
|
func (ZFSCollector) Key() string { return "zfs" }
|
||||||
|
|
||||||
|
func (ZFSCollector) Collect(c *Context) (json.RawMessage, error) {
|
||||||
|
if c.SafeRun("which", "zpool") == "" {
|
||||||
|
return json.Marshal(nil) // no zfs installed
|
||||||
|
}
|
||||||
|
pools := strings.Fields(
|
||||||
|
c.SafeRun("zpool", "list", "-H", "-o", "name"))
|
||||||
|
section := map[string]any{}
|
||||||
|
for _, p := range pools {
|
||||||
|
section[p] = map[string]string{
|
||||||
|
"timestamp": c.Now.Format(time.RFC3339),
|
||||||
|
"status": c.SafeRun("zpool", "status", "-v", p),
|
||||||
|
"get": c.SafeRun("zpool", "get", "-H", "all", p),
|
||||||
|
"zfs_list": c.SafeRun("zfs", "list", "-Hp", "-r", p),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return json.Marshal(section)
|
||||||
|
}
|
@ -1,12 +1,21 @@
|
|||||||
package sysinfo
|
package sysinfo
|
||||||
|
|
||||||
// Snapshot is the top-level JSON object.
|
import "encoding/json"
|
||||||
|
|
||||||
|
// Snapshot stores timestamp + raw section blobs.
|
||||||
type Snapshot struct {
|
type Snapshot struct {
|
||||||
SnapshotTime string `json:"snapshot_time"`
|
Timestamp string `json:"snapshot_time"`
|
||||||
System *SystemData `json:"system"`
|
Sections map[string]json.RawMessage `json:"-"`
|
||||||
Sensors *SensorsData `json:"sensors,omitempty"`
|
}
|
||||||
Blockdevs map[string]*BlockDevice `json:"blockdevs,omitempty"`
|
|
||||||
Network map[string]*NetIface `json:"network,omitempty"`
|
// MarshalJSON injects raw sections into root object.
|
||||||
ZFS map[string]*ZPool `json:"zfs,omitempty"`
|
func (s *Snapshot) MarshalJSON() ([]byte, error) {
|
||||||
Packages *PackagesData `json:"packages,omitempty"`
|
obj := map[string]json.RawMessage{
|
||||||
|
"snapshot_time": json.RawMessage(
|
||||||
|
[]byte(`"` + s.Timestamp + `"`)),
|
||||||
|
}
|
||||||
|
for k, v := range s.Sections {
|
||||||
|
obj[k] = v
|
||||||
|
}
|
||||||
|
return json.Marshal(obj)
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user