This commit is contained in:
Jeffrey Paul 2025-05-01 03:11:54 -07:00
parent b63bab0582
commit 2e8bbfee34
9 changed files with 247 additions and 17 deletions

View File

@ -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

View 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)
}

View 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)
}

View 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)
}

View 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})
}

View 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"),
})
}

View 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)
}

View 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)
}

View File

@ -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)
} }