Help us shape the future of Go! Take the 2026 Go Developer Survey

metrics

package module
v0.7.3 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 24, 2026 License: BSD-3-Clause Imports: 17 Imported by: 0

README

sysc-metrics

Read-only Linux system telemetry for Go. CPU, memory, disks, network, temperature, battery, GPU and processes, read straight from /proc and /sys with nothing but the standard library. It feeds the bar widgets and system monitor in sysc-shell.

The sysc-shell system monitor, drawn from sysc-metrics data
The sysc-shell system monitor, fed by sysc-metrics (fixture data).

Documentation site · Features · Usage · Development

Features

  • CPU: total and per-core usage, per-core frequency, and load averages
  • Memory: RAM and swap
  • Disks: capacity for every mounted filesystem, plus read/write rates and busy time per block device
  • Network: byte and packet rates per interface, with error and drop counters
  • Temperature: one CPU package reading, from k10temp, coretemp or a thermal zone, whichever the machine has
  • ThinkPad fan: RPM, driver status and the requested cooling level, from the IBM ACPI interface
  • LEDs: brightness, ceiling, firmware-change counter and write access for every device in the sysfs LED class, which is how the keyboard backlight and the lid logo light are exposed
  • Battery: charge, state, power draw and time remaining, combined across every battery and UPS
  • GPU: usage, temperature, name and VRAM for AMD, NVIDIA and Intel
  • Processes: CPU and memory per process, with identities that don't get confused by PID reuse
  • Uptime
  • Small: Linux only, standard library only, and it never starts a goroutine

Installation

Requires: Go 1.26+ on Linux.

go get github.com/Nomadcxx/sysc-metrics

Usage

Values that don't depend on time come from a single read:

mem, err := metrics.ReadMemory()
if err != nil {
	return err
}
fmt.Printf("memory: %d of %d bytes used\n", mem.Memory.UsedBytes, mem.Memory.TotalBytes)

ReadMemory, ReadFilesystems, ReadThermal, ReadFan, ReadLEDs, ReadBattery, ReadUptime and ReadGPU all work this way.

Rates need two readings, so CPU, disk, network, process and GPU usage come from a sampler. Call Sample on your own ticker. The first sample only sets a baseline, and its rates come back with Valid == false:

cpu := metrics.NewCPUSampler()
gpu := metrics.NewGPUSampler()
defer gpu.Close()

for range time.Tick(time.Second) {
	c, err := cpu.Sample()
	if err != nil {
		return err
	}
	if c.Usage.Valid {
		fmt.Printf("cpu: %.0f%%\n", c.Usage.Fraction*100)
	}

	g, _ := gpu.Sample()
	for _, card := range g.GPUs {
		if card.Usage.Valid {
			fmt.Printf("%s: %.0f%%\n", card.Name, card.Usage.Fraction*100)
		}
	}
}

On a desktop with an RTX 4060 that prints:

memory: 18722406400 of 33559834624 bytes used
AD106 [GeForce RTX 4060]: 6%
cpu: 14%
AD106 [GeForce RTX 4060]: 5%
cpu: 12%

A few rules hold everywhere:

  • Check Valid. It separates a real zero from "no reading". An idle GPU at 0% is valid; a GPU nobody can measure is not.
  • Snapshots can be partial. One unreadable mount or sensor doesn't fail the whole read. It shows up in the snapshot's Issues, and everything else is filled in.
  • A missing battery or sensor is not an error. ReadBattery reports Present == false on a desktop, and ReadThermal reports Valid == false where there is no known sensor.
  • One sampler, one caller. Samplers keep the previous reading and aren't safe for concurrent use. Give each polling loop its own. Close a GPUSampler when you stop polling, because it holds open counters.

How often to poll, what to cache, and how to show the numbers is up to you.

GPU usage

Each vendor exposes usage differently:

GPU Usage VRAM
AMD gpu_busy_percent in sysfs mem_info_vram_used / mem_info_vram_total
NVIDIA nvidia-smi, run only when an NVIDIA card is present same nvidia-smi query
Intel GPUSampler only, from the second sample on not reported

ReadGPU never reports Intel usage. GPUSampler gets it from the i915 PMU counters, which need CAP_PERFMON on the process. Without that, it falls back to the per-client DRM counters in /proc/*/fdinfo, which covers i915 and amdgpu. That fallback only sees your own processes, so if the only GPU clients belong to another user (a greeter, say), usage stays invalid rather than showing a false zero. The newer Intel xe driver isn't supported yet.

To grant CAP_PERFMON to a program that uses this library, see Intel GPU usage in sysc-shell.

What it doesn't do

No power controls, directory sizes, file indexing, SMART data, quotas, vendor tools beyond nvidia-smi, daemon, or other operating systems.

Development

go vet ./...
go test -race -count=1 ./...

On a machine with an Intel i915 GPU, check the live PMU path with:

SYSC_METRICS_INTEL_GPU_LIVE=1 go test -run TestIntelGPULive .

License

BSD-3-Clause


RAMA — terminal-native tooling for the linux desktop. More projects → · Sponsor ❤️

Documentation

Overview

Package metrics provides read-only Linux system telemetry snapshots.

Index

Constants

View Source
const (
	// FanLevelAuto hands the fan back to the BIOS thermal policy.
	FanLevelAuto = "auto"
	// FanLevelDisengaged pins the fan to full speed.
	FanLevelDisengaged = "disengaged"
	// FanLevelFullSpeed is the driver's alternate spelling for full speed.
	FanLevelFullSpeed = "full-speed"
)

Fan levels the IBM ACPI driver accepts. The driver rejects anything else, so a caller that writes to the interface must filter with ValidFanLevel first.

Variables

View Source
var ErrProcessIdentityChanged = errors.New("process identity changed")

ErrProcessIdentityChanged means a PID now identifies a different process.

Functions

func FanLevelNumber added in v0.7.3

func FanLevelNumber(level string) int

FanLevelNumber returns the numeric part of a fan level. It returns -1 for the symbolic levels, which have no fixed encoder position: "auto" tracks the BIOS thermal policy and "disengaged" runs the fan flat out.

func LEDNames added in v0.7.3

func LEDNames(snapshot LEDSnapshot) []string

LEDNames lists the kernel device names in the snapshot, in order.

func ValidFanLevel added in v0.7.3

func ValidFanLevel(level string) bool

ValidFanLevel reports whether level is one of the canonical values the IBM ACPI fan driver accepts: "auto", "disengaged", "full-speed", or one digit from 0 through 7. Callers that write to the interface must check first, because the driver takes the string verbatim and refuses other spellings.

func ValidateProcessIdentity added in v0.4.0

func ValidateProcessIdentity(identity ProcessIdentity) error

ValidateProcessIdentity confirms that identity still names the same process.

Types

type BatterySnapshot added in v0.2.0

type BatterySnapshot struct {
	CollectedAt   time.Time
	Present       bool
	Charge        float64 // 0..1
	ChargeValid   bool
	State         BatteryState
	EnergyJoules  float64
	RateWatts     float64
	RateValid     bool
	TimeRemaining time.Duration
	TimeValid     bool
	Issues        []Issue
}

BatterySnapshot is the aggregate battery observation. Present is false on a machine with no battery; that is not an error.

func ReadBattery added in v0.2.0

func ReadBattery() (BatterySnapshot, error)

ReadBattery collects the current aggregate battery. It prefers sysfs power-supply devices of type Battery or UPS. A machine with none reports Present false.

type BatteryState added in v0.2.0

type BatteryState uint8

BatteryState is the charging state of the aggregate battery.

const (
	BatteryUnknown BatteryState = iota
	BatteryCharging
	BatteryDischarging
	BatteryFull
)

type BlockDevice

type BlockDevice struct {
	Name            string
	Major           uint32
	Minor           uint32
	ReadBytes       uint64
	WriteBytes      uint64
	ReadOperations  uint64
	WriteOperations uint64
	Busy            time.Duration
	Rates           BlockRates
}

BlockDevice contains cumulative counters and derived rates for one block device.

type BlockRates

type BlockRates struct {
	ReadBytesPerSecond       float64
	WriteBytesPerSecond      float64
	ReadOperationsPerSecond  float64
	WriteOperationsPerSecond float64
	BusyFraction             float64
	Valid                    bool
}

BlockRates contains rates derived from two block-device observations. Valid distinguishes observed zero rates from unavailable derived values.

type BlockSampler

type BlockSampler struct {
	// contains filtered or unexported fields
}

BlockSampler retains previous block counters for sequential rate sampling.

func NewBlockSampler

func NewBlockSampler() *BlockSampler

NewBlockSampler returns a block sampler owned by one sequential polling caller.

func (*BlockSampler) Sample

func (s *BlockSampler) Sample() (BlockSnapshot, error)

Sample returns one block-device snapshot. The sampler is not safe for concurrent use.

type BlockSnapshot

type BlockSnapshot struct {
	CollectedAt time.Time
	Devices     []BlockDevice
	Issues      []Issue
}

BlockSnapshot contains block-device data collected at one time. Devices and Issues are caller-owned snapshot slices.

type CPUCore

type CPUCore struct {
	ID             int
	Usage          CPUUsage
	FrequencyHz    uint64
	FrequencyValid bool
}

CPUCore contains one core's usage and best-effort frequency.

type CPUSampler

type CPUSampler struct {
	// contains filtered or unexported fields
}

CPUSampler retains the previous CPU counters for sequential rate sampling.

func NewCPUSampler

func NewCPUSampler() *CPUSampler

NewCPUSampler returns a CPU sampler owned by one sequential polling caller.

func (*CPUSampler) Sample

func (s *CPUSampler) Sample() (CPUSnapshot, error)

Sample returns one CPU snapshot. The sampler is not safe for concurrent use.

type CPUSnapshot

type CPUSnapshot struct {
	CollectedAt time.Time
	Usage       CPUUsage
	Cores       []CPUCore
	Load1       float64
	Load5       float64
	Load15      float64
	LoadValid   bool
	Issues      []Issue
}

CPUSnapshot contains aggregate and per-core CPU data collected at one time. Cores and Issues are caller-owned snapshot slices.

type CPUUsage

type CPUUsage struct {
	Fraction float64
	Valid    bool
}

CPUUsage contains a utilization fraction. Valid distinguishes an observed zero from no sample.

type Capacity

type Capacity struct {
	TotalBytes     uint64
	UsedBytes      uint64
	AvailableBytes uint64
}

Capacity contains byte totals. Byte fields are uint64 values.

type FanSnapshot added in v0.7.3

type FanSnapshot struct {
	CollectedAt time.Time
	Present     bool

	// Status is the driver's raw status word, normally "enabled" or
	// "disabled".
	Status string

	// SpeedRPM is the reported tachometer reading. SpeedValid is false when
	// the driver cannot produce a number. A parked fan may report zero, which
	// is a valid reading rather than a failure.
	SpeedRPM   int
	SpeedValid bool

	// Level is the cooling level the interface currently requests: "auto",
	// "disengaged" or "0".."7".
	//
	// LevelValid is false when the driver does not accept a level at all.
	// That is the default kernel behaviour -- the thinkpad_acpi module comes
	// up with fan_control=0 -- and it is the signal that reads work but
	// writes will be refused until the module is reloaded with the option
	// set.
	Level      string
	LevelValid bool

	// Issues holds per-field parse failures. The snapshot is still returned
	// so a partial reading reaches the caller.
	Issues []Issue
}

FanSnapshot is one reading of the IBM ACPI thinkpad fan interface.

Present is false on a machine without the interface, which is not an error: desktop hardware has no such file and the caller should carry on.

func ReadFan added in v0.7.3

func ReadFan() (FanSnapshot, error)

ReadFan returns one IBM ACPI fan snapshot. A machine without the thinkpad_acpi interface reports Present false rather than an error.

type Filesystem

type Filesystem struct {
	MountID    uint64
	MountPoint string
	Source     string
	Type       string
	ReadOnly   bool
	Capacity   Capacity
}

Filesystem contains one mounted filesystem and its byte capacity.

type FilesystemSnapshot

type FilesystemSnapshot struct {
	CollectedAt time.Time
	Filesystems []Filesystem
	Issues      []Issue
}

FilesystemSnapshot contains mounted filesystem data collected at one time. Filesystems and Issues are caller-owned snapshot slices.

func ReadFilesystems

func ReadFilesystems() (FilesystemSnapshot, error)

ReadFilesystems returns one mounted-filesystem snapshot from Linux. Autofs trigger points are not reported: statfs on a trigger fires the automount, and anything mounted on top has its own mountinfo row.

type GPU added in v0.3.0

type GPU struct {
	PCIID     string
	Driver    string
	Name      string
	Usage     GPUUsage
	Celsius   float64
	TempValid bool
	// VRAM is dedicated device memory. VRAMValid is false when the driver
	// does not report both used and total; an integrated part's shared
	// system memory is never counted here.
	VRAM      Capacity
	VRAMValid bool
}

GPU is one display device.

type GPUSampler added in v0.5.0

type GPUSampler struct {
	// contains filtered or unexported fields
}

GPUSampler retains Intel i915 PMU engine counters and DRM client fdinfo baselines for sequential rate sampling. It is owned by one sequential polling caller, starts no goroutines, and owns open PMU counters until Close. ReadGPU cannot report Intel usage; a consumer that needs it must sample here and close the sampler when its polling stops.

On a machine with an NVIDIA GPU the sampler owns one long-lived nvidia-smi child that reports the rows on a loop, so a sample reads the child's output instead of forking nvidia-smi (issue #53). The child is started with the first sample that needs it and ends with Close.

Without the privilege the PMU needs, usage for a GPU still lacking it comes from walking /proc for this user's /dev/dri descriptors: one readlink per descriptor each sample, and only while some GPU lacks usage. Other users' clients are invisible to that walk, so usage can stay invalid.

func NewGPUSampler added in v0.5.0

func NewGPUSampler() *GPUSampler

NewGPUSampler returns a GPU sampler owned by one sequential polling caller. It reports the same snapshot as ReadGPU and additionally fills Intel i915 usage, and usage for any GPU the other paths leave empty, from the second sample on (PMU counters, else DRM client fdinfo).

func (*GPUSampler) Close added in v0.5.0

func (s *GPUSampler) Close() error

Close closes every open PMU counter and drops sampling state. It is idempotent; sampling after Close starts from a fresh baseline and starts a new nvidia-smi child when it needs one.

func (*GPUSampler) Sample added in v0.5.0

func (s *GPUSampler) Sample() (GPUSnapshot, error)

Sample returns one GPU snapshot. The sampler is not safe for concurrent use.

type GPUSnapshot added in v0.3.0

type GPUSnapshot struct {
	CollectedAt time.Time
	GPUs        []GPU
	Issues      []Issue
}

GPUSnapshot lists GPUs collected at one time. GPUs and Issues are caller-owned.

func ReadGPU added in v0.3.0

func ReadGPU() (GPUSnapshot, error)

ReadGPU collects usage and temperature for each discrete or integrated GPU.

type GPUUsage added in v0.3.0

type GPUUsage struct {
	Fraction float64
	Valid    bool
}

GPUUsage is a 0..1 busy fraction. Valid distinguishes a real zero from no sample.

type Issue

type Issue struct {
	Source string
	Err    error
}

Issue describes a recoverable source or entity failure in a partial snapshot.

func (Issue) Error

func (i Issue) Error() string

Error returns the source and underlying error.

func (Issue) Unwrap

func (i Issue) Unwrap() error

Unwrap returns the underlying source error.

type LED added in v0.7.3

type LED struct {
	// Name is the directory name under /sys/class/leds, which is also the
	// kernel's device name.
	Name string

	// Brightness is the current level and MaxBrightness the driver's ceiling.
	// Most backlights expose a ceiling of 1, 2 or 3 rather than 255, so a
	// caller must never assume a range.
	Brightness      int
	BrightnessValid bool
	MaxBrightness   int

	// HWChanged counts brightness changes the firmware made behind the
	// driver's back, which is how a hardware Fn-key press shows up. The
	// attribute only exists on some drivers.
	HWChanged      int
	HWChangedValid bool

	// Writable reports whether this process can open the brightness
	// attribute for writing. The probe opens and immediately closes the
	// file: writing to a sysfs attribute is what changes hardware, so the
	// open alone has no effect and the answer is exact, with none of the
	// guessing a permission-bit check would need.
	Writable bool
}

LED is one device in the sysfs class LED hierarchy, most of which the thinkpad_acpi driver publishes under the "tpacpi::" prefix.

func FindLED added in v0.7.3

func FindLED(snapshot LEDSnapshot, name string) (LED, bool)

FindLED returns the LED with the given kernel device name.

type LEDSnapshot added in v0.7.3

type LEDSnapshot struct {
	CollectedAt time.Time
	LEDs        []LED
	Issues      []Issue
}

LEDSnapshot lists the LEDs found at one time. LEDs and Issues are caller-owned snapshot slices.

func ReadLEDs added in v0.7.3

func ReadLEDs() (LEDSnapshot, error)

ReadLEDs collects one reading of every device in the sysfs LED class. A machine with no class LED directory reports an empty snapshot, not an error.

type MemorySnapshot

type MemorySnapshot struct {
	CollectedAt time.Time
	Memory      Capacity
	Swap        Capacity
}

MemorySnapshot contains memory and swap byte capacities collected at one time.

func ReadMemory

func ReadMemory() (MemorySnapshot, error)

ReadMemory returns one memory and swap snapshot from Linux.

type NetworkInterface

type NetworkInterface struct {
	Name            string
	Index           uint32
	ReceiveBytes    uint64
	ReceivePackets  uint64
	ReceiveErrors   uint64
	ReceiveDropped  uint64
	TransmitBytes   uint64
	TransmitPackets uint64
	TransmitErrors  uint64
	TransmitDropped uint64
	Rates           NetworkRates
}

NetworkInterface contains cumulative counters and derived rates for one interface.

type NetworkRates

type NetworkRates struct {
	ReceiveBytesPerSecond    float64
	TransmitBytesPerSecond   float64
	ReceivePacketsPerSecond  float64
	TransmitPacketsPerSecond float64
	Valid                    bool
}

NetworkRates contains rates derived from two network observations. Valid distinguishes observed zero rates from unavailable derived values.

type NetworkSampler

type NetworkSampler struct {
	// contains filtered or unexported fields
}

NetworkSampler retains previous interface counters for sequential rate sampling.

func NewNetworkSampler

func NewNetworkSampler() *NetworkSampler

NewNetworkSampler returns a network sampler owned by one sequential polling caller.

func (*NetworkSampler) Sample

func (s *NetworkSampler) Sample() (NetworkSnapshot, error)

Sample returns one network-interface snapshot. The sampler is not safe for concurrent use.

type NetworkSnapshot

type NetworkSnapshot struct {
	CollectedAt time.Time
	Interfaces  []NetworkInterface
	Issues      []Issue
}

NetworkSnapshot contains network-interface data collected at one time. Interfaces and Issues are caller-owned snapshot slices.

type Process added in v0.4.0

type Process struct {
	Identity      ProcessIdentity
	Name          string
	ParentPID     int
	UID           uint32
	UIDValid      bool
	ResidentBytes uint64
	ResidentValid bool
	Args          []string
	CPU           CPUUsage
}

Process contains one process observation and its best-effort derived CPU usage.

type ProcessIdentity added in v0.4.0

type ProcessIdentity struct {
	PID            int
	StartTimeTicks uint64
}

ProcessIdentity distinguishes a process from a later process that reuses its PID.

type ProcessSampler added in v0.4.0

type ProcessSampler struct {
	// contains filtered or unexported fields
}

ProcessSampler retains previous process and aggregate CPU counters for sequential rate sampling. previousTotal is the aggregate tick count read before the previous process walk. The next sample closes that window with a second read taken after its own walk, so both process stats fall inside it.

func NewProcessSampler added in v0.4.0

func NewProcessSampler() *ProcessSampler

NewProcessSampler returns a process sampler owned by one sequential polling caller.

func (*ProcessSampler) Sample added in v0.4.0

func (s *ProcessSampler) Sample() (ProcessSnapshot, error)

Sample returns one process snapshot. The sampler is not safe for concurrent use.

Aggregate ticks are read before the walk and again after every process stat. The window for this sample runs from the previous pre-walk read to this post-walk read, which covers both process observations. A saturated process read later than the opening /proc/stat therefore stays inside the fraction instead of being dropped (issue #21). A delta that still exceeds that covering window is a counter regression and stays invalid (issue #7).

type ProcessSnapshot added in v0.4.0

type ProcessSnapshot struct {
	CollectedAt time.Time
	Processes   []Process
	Issues      []Issue
}

ProcessSnapshot contains processes collected at one time. Processes, Args, and Issues are caller-owned snapshot slices.

type ThermalSnapshot added in v0.3.0

type ThermalSnapshot struct {
	CollectedAt time.Time
	Celsius     float64
	Valid       bool
	Source      string
	Issues      []Issue
}

ThermalSnapshot is one scored CPU package temperature in Celsius. Valid is false when no known sensor exists; that is not an error.

func ReadThermal added in v0.3.0

func ReadThermal() (ThermalSnapshot, error)

ReadThermal collects the current CPU package temperature.

type UptimeSnapshot

type UptimeSnapshot struct {
	CollectedAt time.Time
	Uptime      time.Duration
}

UptimeSnapshot contains uptime as a duration collected at one time.

func ReadUptime

func ReadUptime() (UptimeSnapshot, error)

ReadUptime returns one uptime snapshot from Linux.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL