smartmontools

package module
v0.4.1 Latest Latest
Warning

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

Go to latest
Published: May 28, 2026 License: GPL-3.0 Imports: 11 Imported by: 0

README

smartmontools-go

A Go library that interfaces with smartmontools to monitor and manage storage device health using S.M.A.R.T. (Self-Monitoring, Analysis, and Reporting Technology) data.

CI Coverage Status CodeFactor Stable Release Prerelease

Features

  • 🔍 Device Scanning: Automatically detect available storage devices
  • 👀 Drive Discovery: DiscoverDevices probes each drive's optimal protocol and reports SMART readability
  • 💻 NAS Platform Support: Automatic smartctl discovery across Synology DSM, QNAP, FreeBSD/TrueNAS, macOS, and standard Linux
  • 💚 Health Monitoring: Check device health status using SMART data
  • 🏥 SMART Health Flags: Full exit code bit decomposition (ExecBits / HealthBits) via ExitCodeInfo
  • 📊 SMART Attributes: Read and parse detailed SMART attributes
    • 🔋 Wear Level: Normalized wear-level percentage for SSDs and NVMe drives via WearLevelPercent()
    • 🌡️ Temperature Monitoring: Track device temperature
  • ⚙️ Self-Tests: Initiate and monitor SMART self-tests
  • 🔧 Device Information: Retrieve model, serial number, firmware version, and more
  • 🔌 USB Bridge Support: Automatic fallback for unknown USB bridges with embedded device database

Prerequisites

This library requires smartctl (part of smartmontools) to be installed on your system.

Minimum supported version: smartctl 7.0 (for JSON -j output).

Linux
# Debian/Ubuntu
sudo apt-get install smartmontools

# RHEL/CentOS/Fedora
sudo yum install smartmontools

# Arch Linux
sudo pacman -S smartmontools
macOS
brew install smartmontools
Windows

Download and install from smartmontools.org

LibBackend prerequisites (optional, Linux/macOS only)

LibBackend does not require smartctl. Instead it loads a pre-built smartmon wrapper shared library at runtime. The library is available for:

  • macOS (Apple silicon): ships pre-built with the module at backends/lib/sdk/libsmartmon_go.dylib
  • Linux / other platforms: build the wrapper once from the repository root:
scripts/setup-lib-backend.sh

The script downloads the correct libsmartmon.a static library from dianlight/smartmontools-sdk releases and compiles the thin C++ wrapper in backends/lib/csrc/ into backends/lib/sdk/libsmartmon_go.so.

Point the backend at the compiled library via the SMARTMON_LIB_PATH environment variable or libbackend.WithLibraryPath(...). See Library Resolution Order below.

Installation

go get github.com/dianlight/smartmontools-go

To use LibBackend also import the backend sub-package in your code:

import libbackend "github.com/dianlight/smartmontools-go/backends/lib"

To use CompareBackend:

import comparebackend "github.com/dianlight/smartmontools-go/backends/compare"

Usage

Basic Example
package main

import (
    "fmt"
    "log"

    "github.com/dianlight/smartmontools-go"
)

func main() {
    // Create a new client
    client, err := smartmontools.NewClient()
    if err != nil {
        log.Fatalf("Failed to create client: %v", err)
    }

    // Scan for devices
    devices, err := client.ScanDevices()
    if err != nil {
        log.Fatalf("Failed to scan devices: %v", err)
    }

    for _, device := range devices {
        fmt.Printf("Device: %s (type: %s)\n", device.Name, device.Type)
        
        // Check health
        healthy, err := client.CheckHealth(device.Name)
        if err != nil {
            log.Printf("Failed to check health: %v", err)
            continue
        }
        
        if healthy {
            fmt.Println("  Health: PASSED ✓")
        } else {
            fmt.Println("  Health: FAILED ✗")
        }
    }
}
Getting SMART Information
// Get detailed SMART information
smartInfo, err := client.GetSMARTInfo("/dev/sda")
if err != nil {
    log.Fatalf("Failed to get SMART info: %v", err)
}

fmt.Printf("Model: %s\n", smartInfo.ModelName)
fmt.Printf("Serial: %s\n", smartInfo.SerialNumber)
fmt.Printf("Temperature: %d°C\n", smartInfo.Temperature.Current)
fmt.Printf("Power On Hours: %d\n", smartInfo.PowerOnTime.Hours)

// Access SMART attributes
if smartInfo.AtaSmartData != nil {
    for _, attr := range smartInfo.AtaSmartData.Table {
        fmt.Printf("Attribute %d (%s): %d\n", attr.ID, attr.Name, attr.Value)
    }
}
Running Self-Tests
// Run a short self-test
err := client.RunSelfTest("/dev/sda", "short")
if err != nil {
    log.Fatalf("Failed to run self-test: %v", err)
}

// Available test types: "short", "long", "conveyance", "offline"
Custom smartctl Path
// If smartctl is not in PATH or you want to use a specific binary
client, err := smartmontools.NewClient(smartmontools.WithSmartctlPath("/usr/local/sbin/smartctl"))
if err != nil {
    log.Fatalf("Failed to create client: %v", err)
}

NAS / embedded platforms: NewClient automatically searches 11 platform-specific locations (Synology DSM, QNAP Entware/QPKG, FreeBSD/TrueNAS, macOS Homebrew, NixOS, …) when smartctl is not found in PATH. WithSmartctlPath always takes precedence.

Drive Discovery

DiscoverDevices scans all available drives, probes each with its auto-detected protocol, and transparently attempts an SAT fallback for drives that cannot be read with their native protocol (common with USB-to-SATA bridges).

results, err := client.DiscoverDevices(context.Background())
if err != nil {
    log.Fatalf("Discovery failed: %v", err)
}

for _, r := range results {
    fmt.Printf("Device:   %s\n", r.DevicePath)
    fmt.Printf("Protocol: %s\n", r.DetectedProtocol)
    fmt.Printf("Readable: %v\n", r.SMARTReadable)
    if r.SATFallbackRequired {
        fmt.Println("  (SAT fallback was required)")
    }
    if r.SMARTReadable {
        fmt.Printf("  Model:  %s\n", r.Model)
        fmt.Printf("  Serial: %s\n", r.Serial)
    }
}
Exit Code Information

When smartctl exits with a non-zero status, SMARTInfo.ExitCodeInfo is populated with a breakdown of the exit bits:

info, err := client.GetSMARTInfo(ctx, "/dev/sda")
if err != nil {
    log.Fatalf("Failed to get SMART info: %v", err)
}

if info.ExitCodeInfo != nil {
    // Bits 0–2: execution failures (device open failed, SMART command failed, …)
    if info.ExitCodeInfo.ExecBits != 0 {
        fmt.Printf("Execution failure bits: 0x%02x\n", info.ExitCodeInfo.ExecBits)
    }
    // Bits 3–7: SMART health flags (disk failing, pre-failure attributes, …)
    if info.ExitCodeInfo.HealthBits != 0 {
        hb := info.ExitCodeInfo.HealthBits
        fmt.Printf("SMART health bits: 0x%02x\n", hb)
        fmt.Printf("  Disk failing:        %v\n", hb&0x08 != 0)
        fmt.Printf("  Pre-failure attrs:   %v\n", hb&0x10 != 0)
        fmt.Printf("  Past prefail:        %v\n", hb&0x20 != 0)
        fmt.Printf("  Error log entries:   %v\n", hb&0x40 != 0)
        fmt.Printf("  Self-test failures:  %v\n", hb&0x80 != 0)
    }
}
Wear Level

SMARTInfo.WearLevelPercent() returns a normalized 0–100 value representing the percentage of drive life used (0 = new, 100 = worn out), or nil for HDDs and drives where no wear data is available:

info, err := client.GetSMARTInfo(ctx, "/dev/sda")
if err != nil {
    log.Fatalf("Failed to get SMART info: %v", err)
}

if wear := info.WearLevelPercent(); wear != nil {
    fmt.Printf("Wear level: %d%%\n", *wear)
} else {
    fmt.Println("Wear level: N/A (HDD or unsupported drive)")
}

The source used depends on the drive type:

Drive type Source
NVMe nvme_smart_health_information_log.percentage_used
SSD (ATA) Attr 231 (SSD Life Left) → 177 (Wear Leveling Count) → 173 (SSD Life Used)
HDD nil
Logging

The library uses the tlog package for structured logging.

Default behavior:

  • When you call smartmontools.NewClient() without a WithLogHandler option, the client creates a debug-level *tlog.Logger (via tlog.NewLoggerWithLevel(tlog.LevelDebug)) so that diagnostic output (command execution, fallbacks, warnings) is available.
  • You can adjust the global log level at runtime using tlog.SetLevelFromString("info") or tlog.SetLevel(tlog.LevelInfo). Levels include: trace, debug, info, notice, warn, error, fatal.
  • All internal logging is key/value structured. Expensive debug operations are guarded; if you perform your own heavy debug logging, first check with tlog.IsLevelEnabled(tlog.LevelDebug).

Override the logger for a specific client instance:

import (
    "context"
    "log"
    "github.com/dianlight/smartmontools-go"
    "github.com/dianlight/tlog"
)

func main() {
    // Create a custom logger which only logs WARN and above.
    customLogger := tlog.NewLoggerWithLevel(tlog.LevelWarn)

    client, err := smartmontools.NewClient(
        smartmontools.WithLogHandler(customLogger),
    )
    if err != nil {
        log.Fatalf("Failed to create client: %v", err)
    }

    // Example global level change (applies to package-level functions too)
    if err := tlog.SetLevelFromString("info"); err != nil {
        tlog.Warn("Failed to set log level", "error", err)
    }

    devices, err := client.ScanDevices(context.Background())
    if err != nil {
        tlog.Error("Scan failed", "error", err)
        return
    }
    tlog.Info("Scan complete", "count", len(devices))
}

If you need a logger instance with a different minimum level temporarily (without changing globals), use:

traceLogger := tlog.WithLevel(tlog.LevelTrace) // returns *slog.Logger for ad-hoc usage
traceLogger.Log(context.Background(), tlog.LevelTrace, "Detailed trace")

For code interacting with the client, prefer passing a *tlog.Logger via WithLogHandler. For ad-hoc logging outside the client lifecycle, use the package-level helpers (tlog.Info, tlog.DebugContext, etc.).

Graceful shutdown of callback processor (if you registered callbacks):

defer tlog.Shutdown() // Ensures queued callback events are processed before exit
Custom Default Context
// Set a default context that will be used when methods are called with nil context
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()

client, err := smartmontools.NewClient(smartmontools.WithContext(ctx))
if err != nil {
    log.Fatalf("Failed to create client: %v", err)
}

// Now when calling with nil context, the client will use the configured default
info, err := client.GetSMARTInfo(nil, "/dev/sda") // Uses the 30s timeout context
Combining Options
// Combine multiple options
client, err := smartmontools.NewClient(
    smartmontools.WithSmartctlPath("/usr/local/sbin/smartctl"),
    smartmontools.WithLogHandler(logger),
    smartmontools.WithContext(ctx),
)
if err != nil {
    log.Fatalf("Failed to create client: %v", err)
}
LibBackend (Linux/macOS — no subprocess)

LibBackend loads libsmartmon_go.so (Linux) or libsmartmon_go.dylib (macOS) at runtime via puregono CGO, no child process spawned. It exposes the same Client API as ExecBackend.

Build the wrapper library first (see LibBackend prerequisites), then wire it in with WithBackend:

import (
    smartmontools "github.com/dianlight/smartmontools-go"
    libbackend "github.com/dianlight/smartmontools-go/backends/lib"
)

lib, err := libbackend.New(
    libbackend.WithLibraryPath("backends/lib/sdk/libsmartmon_go.dylib"), // or .so on Linux
)
if err != nil {
    log.Fatalf("Failed to load lib backend: %v", err)
}
defer lib.Close()

client, err := smartmontools.NewClient(smartmontools.WithBackend(lib))
Library Resolution Order

When WithLibraryPath is not set, libbackend.New() resolves the library in this order:

  1. SMARTMON_LIB_PATH environment variable (falls back to step 3 if the file is missing)
  2. Dynamic linker (LD_LIBRARY_PATH / DYLD_LIBRARY_PATH / rpath)
  3. Well-known absolute paths: /usr/local/lib, /opt/homebrew/lib, …
# macOS — using the pre-built library shipped with the module
SMARTMON_LIB_PATH=backends/lib/sdk/libsmartmon_go.dylib go run .

# Linux — after running scripts/setup-lib-backend.sh
SMARTMON_LIB_PATH=backends/lib/sdk/libsmartmon_go.so go run .
CompareBackend

CompareBackend runs two (or more) backends in parallel for every request. The first backend is the master — its result is always returned to the caller. Additional backends run in shadow mode; any discrepancy or error they produce is written to the logger.

Intended use: validate a new backend implementation against the battle-tested ExecBackend before switching.

import (
    smartmontools "github.com/dianlight/smartmontools-go"
    comparebackend "github.com/dianlight/smartmontools-go/backends/compare"
    execbackend "github.com/dianlight/smartmontools-go/backends/exec"
    libbackend "github.com/dianlight/smartmontools-go/backends/lib"
)

exec, err := execbackend.New()
if err != nil {
    log.Fatalf("Failed to create exec backend: %v", err)
}

lib, err := libbackend.New() // requires SMARTMON_LIB_PATH or system library
if err != nil {
    log.Fatalf("Failed to load lib backend: %v", err)
}
defer lib.Close()

compare, err := comparebackend.NewCompareBackend(
    []smartmontools.Backend{exec, lib}, // exec = master, lib = shadow
)
if err != nil {
    log.Fatalf("Failed to create compare backend: %v", err)
}
defer compare.Close()

client, err := smartmontools.NewClient(smartmontools.WithBackend(compare))
// All calls go to exec (master). lib runs in parallel silently.
// Discrepancies → "compare: result mismatch" warning in the logger.

Note: Do not pair two identical ExecBackend instances. Both would hit the same physical device simultaneously, causing device contention and spurious errors.

USB Bridge Support

The library includes automatic support for USB storage devices that use unknown USB bridges. When smartctl reports an "Unknown USB bridge" error, the library:

  1. Checks embedded database: Looks up the USB vendor:product ID in the embedded standard drivedb.h from smartmontools
  2. Automatic fallback: If found, uses the known device type; otherwise falls back to -d sat
  3. Caches results: Remembers successful device types for faster future access
client, _ := smartmontools.NewClient()

// Works automatically with USB bridges, even if unknown to smartctl
info, err := client.GetSMARTInfo("/dev/disk/by-id/usb-Intenso_Memory_Center-0:0")
if err != nil {
    log.Fatalf("Failed to get SMART info: %v", err)
}

fmt.Printf("Model: %s\n", info.ModelName)
fmt.Printf("Health: %v\n", info.SmartStatus.Passed)

The embedded database is the official smartmontools drivedb.h which contains USB bridge definitions from the upstream project. See docs/drivedb.md for details.

Efficient SMART Monitoring (Avoiding Periodic Disk Access)

When building monitoring applications that periodically check SMART status, it's important to avoid unnecessary disk I/O that can wake disks from standby mode. This is especially important for:

  • Home NAS systems with idle disk spindown
  • Battery-powered devices
  • Systems with multiple drives where periodic access causes audible noise

❌ Inefficient approach (wakes disk every check):

// DON'T: This queries the disk on every call
ticker := time.NewTicker(10 * time.Second)
for range ticker.C {
    // Error handling omitted for brevity in this anti-pattern example
    support, _ := client.IsSMARTSupported(ctx, "/dev/sda") // Disk access!
    if support.Enabled {
        // ... monitor SMART data
    }
}

✅ Efficient approach (cache and event-driven):

// DO: Query once, cache the result, update on events
type DiskMonitor struct {
    client     smartmontools.SmartClient
    smartCache map[string]*smartmontools.SMARTInfo
    cacheMutex sync.RWMutex
}

// Initial population or refresh after enable/disable
func (m *DiskMonitor) refreshSMARTInfo(ctx context.Context, devicePath string) error {
    info, err := m.client.GetSMARTInfo(ctx, devicePath)
    if err != nil {
        return err
    }
    
    m.cacheMutex.Lock()
    m.smartCache[devicePath] = info
    m.cacheMutex.Unlock()
    
    return nil
}

// Check SMART status without disk I/O
func (m *DiskMonitor) isSMARTEnabled(devicePath string) bool {
    m.cacheMutex.RLock()
    info, exists := m.smartCache[devicePath]
    m.cacheMutex.RUnlock()
    
    if !exists {
        return false
    }
    
    // Extract status from cached info - no disk access!
    support := m.client.GetSMARTSupportFromInfo(info)
    return support.Available && support.Enabled
}

// Periodic monitoring loop
func (m *DiskMonitor) monitorLoop(ctx context.Context, devicePath string) {
    ticker := time.NewTicker(10 * time.Second)
    defer ticker.Stop()
    
    for {
        select {
        case <-ticker.C:
            // Check from cache - no disk I/O
            if !m.isSMARTEnabled(devicePath) {
                continue // Skip monitoring when SMART is disabled
            }
            
            // Only query disk when SMART is enabled
            info, err := m.client.GetSMARTInfo(ctx, devicePath)
            if err != nil {
                log.Printf("Error getting SMART info: %v", err)
                continue
            }
            
            // Update cache
            m.cacheMutex.Lock()
            m.smartCache[devicePath] = info
            m.cacheMutex.Unlock()
            
            // Process SMART data...
            
        case <-ctx.Done():
            return
        }
    }
}

// Call after enabling SMART to refresh cache
func (m *DiskMonitor) enableSMART(ctx context.Context, devicePath string) error {
    if err := m.client.EnableSMART(ctx, devicePath); err != nil {
        return err
    }
    // Refresh cache immediately after enable
    return m.refreshSMARTInfo(ctx, devicePath)
}

// Call after disabling SMART to refresh cache
func (m *DiskMonitor) disableSMART(ctx context.Context, devicePath string) error {
    if err := m.client.DisableSMART(ctx, devicePath); err != nil {
        return err
    }
    // Refresh cache immediately after disable
    return m.refreshSMARTInfo(ctx, devicePath)
}

Key principles:

  1. Call GetSMARTInfo once at startup or when SMART enable/disable state changes
  2. Cache the SMARTInfo result in your application
  3. Use GetSMARTSupportFromInfo to check SMART status from the cache (no disk I/O)
  4. Only query the disk when SMART is known to be enabled
  5. Refresh the cache after calling EnableSMART() or DisableSMART()

This approach eliminates unnecessary disk access and prevents waking disks from standby mode, resolving issues like dianlight/hassio-addons#596.

API Reference

See APIDOC.md for detailed API documentation.

Examples

See the examples directory for more detailed usage examples:

Example Description
basic Device scanning, health checking, and SMART info retrieval with the default ExecBackend
lib LibBackend via purego — same API, no subprocess spawned (Linux/macOS)
compare CompareBackend with a snapshot secondary — safe to run without two physical backends
compare-exec-lib CompareBackend with ExecBackend as master and LibBackend as shadow (Linux/macOS)

To run an example (root privileges may be required):

# ExecBackend (default)
cd examples/basic && go run .

# LibBackend (macOS arm64, pre-built library)
SMARTMON_LIB_PATH=../../backends/lib/sdk/libsmartmon_go.dylib \
  cd examples/lib && go run .

# CompareBackend: ExecBackend master + LibBackend shadow (macOS arm64)
SMARTMON_LIB_PATH=../../backends/lib/sdk/libsmartmon_go.dylib \
  cd examples/compare-exec-lib && go run .

Architecture

This library provides two backend implementations and one meta-backend:

ExecBackend LibBackend CompareBackend
Platform All Linux, macOS Any (wraps other backends)
Prerequisite smartctl binary libsmartmon_go.{so,dylib} Depends on wrapped backends
Child process ✅ (per call) Depends on wrapped backends
CGO ❌ (uses purego)
USB bridge support
Default
ExecBackend (default)

Shells out to the smartctl binary and parses its JSON output. Maximum compatibility, zero extra dependencies beyond smartmontools itself.

LibBackend (purego FFI — Linux/macOS)

Loads a pre-built smartmon wrapper shared library (libsmartmon_go.so / libsmartmon_go.dylib) at runtime using ebitengine/puregono CGO required.

// Automatic resolution (reads SMARTMON_LIB_PATH or searches system paths):
lib, err := lib.New()

// Explicit path:
lib, err := lib.New(lib.WithLibraryPath("/usr/local/lib/libsmartmon_go.so"))
CompareBackend

Runs two or more backends in parallel and logs discrepancies between their results. The first backend is always the master (its output is returned); secondary backends run as shadows.

compare, err := comparebackend.NewCompareBackend(
    []smartmontools.Backend{exec, lib},
    comparebackend.WithTLogHandler(logger),
)

📚 For a comprehensive analysis of different SMART access approaches, see our Architecture Decision Record (ADR-001), which covers:

  • Command wrapper (current default approach)
  • Direct ioctl access
  • Shared library with FFI (implemented as LibBackend)
  • Hybrid approaches

Implementation details

  • ExecBackend: locates (or is given) a smartctl binary and executes it (os/exec). Commands use --json where available and the library parses the resulting JSON output.
  • LibBackend: dlopens libsmartmon_go.so/.dylib via purego and calls functions exported by the C++ wrapper. No child process is spawned.
  • Configurable path: you can pass a custom path with NewClientWithPath(path string) if smartctl is not on PATH or you want to use a specific binary (ExecBackend only).
  • Permissions: many SMART operations require root/administrator privileges or appropriate device access. Expect permission denied errors when running without sufficient rights.
  • Error handling: the library returns errors when smartctl exits non-zero (ExecBackend), when JSON parsing fails, or when required fields are missing. Consumers should inspect errors and possibly the wrapped *exec.ExitError for diagnostics.

Example command run by ExecBackend (illustrative):

smartctl --json -a /dev/sda

This will be parsed into the library's SMARTInfo structures.

Contributing

Contributions are welcome! Please feel free to submit a Pull Request.

Roadmap

Short-term (current):

  • Stabilize the exec-based API surface (ScanDevices, GetSMARTInfo, CheckHealth, RunSelfTest).
  • Improve error messages and diagnostics when smartctl is missing, incompatible, or returns non-JSON output.
  • Add more unit tests that mock smartctl JSON output.
  • Add integration tests for LibBackend against real devices in CI.

Mid-term:

  • Implement ioctl-based device access for platforms where direct calls are preferable and safe.
  • Provide clearer compatibility matrix and CI jobs for Linux/macOS/Windows.
  • Publish pre-built libsmartmon_go binaries as release assets for common platforms.

Long-term:

  • Optimize performance and reduce process creation overhead for large-scale monitoring setups.
  • Full native Go backend (v1.0) with zero runtime dependencies.

How to help:

  • Add tests that include representative smartctl --json outputs (captured from different smartmontools versions/devices).
  • Document platform-specific permission and packaging notes (e.g., macOS notarization, Windows admin requirements).
  • Test LibBackend on additional Linux distributions and architectures.

License

This project is licensed under the GNU General Public License v3.0 - see the LICENSE file for details.

Acknowledgments

  • smartmontools — the underlying tool that makes this library possible
  • DAB-LABS/smart-sniffer — several reliability improvements in this library (multi-path binary resolution, SAT fallback, --scan-open--scan fallback, DiscoverDevices, and exit code bit decomposition) were inspired by the patterns used in the smart-sniffer agent

CI and mise

This repository uses mise for task running and a GitHub Actions workflow that runs CI on push and pull_request to main.

Common tasks:

Command Description
mise run test Run unit tests for all packages
mise run ci Run all CI checks (tidy, download, lint, test)
mise run lint Run staticcheck
mise run fmt Run gofmt on the project
mise run coverage Run tests and show coverage summary
mise run apidoc Generate API documentation (APIDOC.md)
mise run clean Remove build artifacts
mise run release [major|minor|patch] Create and push a tag — stable on main, prerelease on a branch with an open PR (requires gh)

release accepts a --dry-run flag to preview without pushing:

mise run release         # patch bump (default)
mise run release minor --dry-run

To list all available tasks run mise tasks.

Documentation

Overview

Package smartmontools provides Go bindings for interacting with smartmontools and collecting S.M.A.R.T. data from storage devices.

The root package is a thin facade over the shared domain model in the types sub-package and the default exec backend in backends/exec. NewClient creates a Client that delegates SMART operations to a pluggable Backend implementation. By default it uses ExecBackend, which shells out to the smartctl binary. Alternative backends can be supplied with WithBackend.

Features

  • Device scanning and discovery
  • SMART health status checking
  • Detailed SMART attribute reading
  • Disk type detection (SSD, HDD, NVMe, Unknown)
  • Rotation rate (RPM) information for HDDs
  • Temperature monitoring
  • Power-on time tracking
  • Self-test execution and progress monitoring
  • Device information retrieval
  • SMART support detection and management
  • Self-test availability checking
  • Standby mode detection for ATA-family devices
  • Efficient SMART monitoring with minimal disk I/O

Backend Layout

The default smartctl-backed implementation lives in github.com/dianlight/smartmontools-go/backends/exec. Shared types and interfaces live in github.com/dianlight/smartmontools-go/types; the root package re-exports them as type aliases for a flat, ergonomic API surface.

An alternative purego FFI backend lives in github.com/dianlight/smartmontools-go/backends/lib (LibBackend). It loads a pre-built smartmon wrapper shared library at runtime using ebitengine/purego, avoiding process-spawn overhead and the smartctl binary dependency.

LibBackend (D1 — SDK wrapper via purego)

LibBackend loads libsmartmon_go.so (Linux) or libsmartmon_go.dylib (macOS) at runtime. The shared library is a thin C++ wrapper that links against the pre-built libsmartmon.a static library published in github.com/dianlight/smartmontools-sdk releases.

Build the wrapper library once with the provided setup script:

scripts/setup-lib-backend.sh

The script downloads the correct SDK archive for the current platform, installs the missing smartmon_config.h, and compiles the wrapper into backends/lib/sdk/libsmartmon_go.{so,dylib}.

Library Resolution Order

New() resolves the library path in the following order:

  1. The path provided by [libbackend.WithLibraryPath].
  2. SMARTMON_LIB_PATH environment variable — if the file exists at that path it is used directly. If SMARTMON_LIB_PATH is set but the file is absent a warning is logged and the search continues to step 3. If the file exists but a library is also found in a different standard system directory a warning is logged (the configured path is still used).
  3. Standard system library paths: dynamic-linker names first (respects LD_LIBRARY_PATH / DYLD_LIBRARY_PATH / rpath), then a list of well-known absolute paths such as /usr/local/lib and /opt/homebrew/lib.

Use the LibBackend with WithBackend:

lib, err := libbackend.New(
    libbackend.WithLibraryPath("/usr/local/lib/libsmartmon_go.so"),
)
if err != nil {
    log.Fatal(err)
}
defer lib.Close()
client, err := smartmontools.NewClient(smartmontools.WithBackend(lib))

Or rely on automatic resolution via the environment variable:

// export SMARTMON_LIB_PATH=/path/to/libsmartmon_go.dylib
lib, err := libbackend.New()

Package smartmontools provides Go bindings for interfacing with smartmontools to monitor and manage storage device health using S.M.A.R.T. data.

The library wraps the smartctl command-line utility and provides a clean, idiomatic Go API for accessing SMART information from storage devices.

Index

Constants

View Source
const (
	SmartAttrSSDLifeUsed       = smtypes.SmartAttrSSDLifeUsed
	SmartAttrWearLevelingCount = smtypes.SmartAttrWearLevelingCount
	SmartAttrSSDLifeLeft       = smtypes.SmartAttrSSDLifeLeft
	SmartAttrSandForceInternal = smtypes.SmartAttrSandForceInternal
	SmartAttrTotalLBAsWritten  = smtypes.SmartAttrTotalLBAsWritten
)

SMART attribute IDs for SSD detection and wear-level computation.

View Source
const DrivedbUpstreamCommit = smexec.DrivedbUpstreamCommit

DrivedbUpstreamCommit is the upstream smartmontools commit SHA from which the embedded drivedb.h was taken. It is re-exported from the exec backend.

View Source
const DrivedbUpstreamDate = smexec.DrivedbUpstreamDate

DrivedbUpstreamDate is the commit date of DrivedbUpstreamCommit in RFC 3339 format. It is re-exported from the exec backend.

Variables

This section is empty.

Functions

This section is empty.

Types

type AtaSmartData

type AtaSmartData = smtypes.AtaSmartData

AtaSmartData represents ATA SMART attributes.

type Backend added in v0.4.0

type Backend = smtypes.Backend

Backend is the pluggable execution interface for SMART operations.

type Capabilities

type Capabilities = smtypes.Capabilities

Capabilities represents SMART capabilities.

type CapabilitiesOutput

type CapabilitiesOutput = smtypes.CapabilitiesOutput

CapabilitiesOutput represents the output of smartctl -c -j.

type Client

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

Client represents a smartmontools client that delegates SMART operations to a pluggable Backend. The default backend is ExecBackend.

func (*Client) AbortSelfTest

func (c *Client) AbortSelfTest(ctx context.Context, devicePath string) error

AbortSelfTest aborts a running self-test on a device.

func (*Client) CheckHealth

func (c *Client) CheckHealth(ctx context.Context, devicePath string) (bool, error)

CheckHealth checks if a device is healthy according to SMART.

func (*Client) Close added in v0.4.0

func (c *Client) Close() error

Close releases any resources held by the active backend.

func (*Client) DisableSMART

func (c *Client) DisableSMART(ctx context.Context, devicePath string) error

DisableSMART disables SMART monitoring on a device.

func (*Client) DiscoverDevices added in v0.3.1

func (c *Client) DiscoverDevices(ctx context.Context) ([]DiscoveryResult, error)

DiscoverDevices scans all available storage devices and probes each one to determine SMART readability and protocol compatibility.

func (*Client) EnableSMART

func (c *Client) EnableSMART(ctx context.Context, devicePath string) error

EnableSMART enables SMART monitoring on a device.

func (*Client) GetAvailableSelfTests

func (c *Client) GetAvailableSelfTests(ctx context.Context, devicePath string) (*SelfTestInfo, error)

GetAvailableSelfTests returns the list of available self-test types and their durations for a device.

func (*Client) GetAvailableSelfTestsFromInfo added in v0.2.7

func (c *Client) GetAvailableSelfTestsFromInfo(smartInfo *SMARTInfo) *SelfTestInfo

GetAvailableSelfTestsFromInfo extracts available self-test types and their durations from a SMARTInfo struct without performing additional disk I/O. Applications that already hold a cached SMARTInfo (from GetSMARTInfo) should use this method instead of GetAvailableSelfTests to avoid an extra smartctl -c disk query.

Note: NVMe detection uses NvmeControllerCapabilities, which is present in -a output. The NvmeOptionalAdminCommands field (available only via -c) is not stored in SMARTInfo, so NVMe self-test capability detection may be incomplete for some controllers.

Example usage:

// Get and cache SMART info once info, err := client.GetSMARTInfo(ctx, devicePath)

if err != nil {
   return err
}

// Extract self-test types without another disk query selfTests := client.GetAvailableSelfTestsFromInfo(info)

for _, testType := range selfTests.Available {
   fmt.Printf("Test: %s (%d min)\n", testType, selfTests.Durations[testType])
}

func (*Client) GetDeviceInfo

func (c *Client) GetDeviceInfo(ctx context.Context, devicePath string) (map[string]interface{}, error)

GetDeviceInfo retrieves basic device information.

func (*Client) GetSMARTInfo

func (c *Client) GetSMARTInfo(ctx context.Context, devicePath string) (*SMARTInfo, error)

GetSMARTInfo retrieves SMART information for a device.

func (*Client) GetSMARTSupportFromInfo added in v0.2.5

func (c *Client) GetSMARTSupportFromInfo(smartInfo *SMARTInfo) *SmartSupport

GetSMARTSupportFromInfo extracts SMART support status from a SMARTInfo struct. This is a helper method that allows checking SMART availability and enablement status without performing additional disk I/O. Applications should call GetSMARTInfo once, cache the result, and use this method to check the SMART status from the cache.

This pattern avoids periodic disk access and prevents waking disks from standby mode.

Example usage:

// Get and cache SMART info once info, err := client.GetSMARTInfo(ctx, devicePath)

if err != nil {
   return err
}

// Check SMART status from cached info (no disk access) support := client.GetSMARTSupportFromInfo(info)

if support.Available && support.Enabled {
   // SMART is available and enabled
}

// After EnableSMART/DisableSMART, refresh the cache:

if err := client.EnableSMART(ctx, devicePath); err != nil {
   return err
}

// Refresh cache after state change info, err = client.GetSMARTInfo(ctx, devicePath)

if err != nil {
   return err
}

func (*Client) IsSMARTSupported

func (c *Client) IsSMARTSupported(ctx context.Context, devicePath string) (*SmartSupport, error)

IsSMARTSupported checks if SMART is supported on a device and if it's enabled.

WARNING: This method performs disk I/O by calling GetSMARTInfo internally. For applications that need to check SMART status frequently (e.g., monitoring daemons), it's recommended to call GetSMARTInfo once, cache the result, and use GetSMARTSupportFromInfo to extract SMART support status from the cached data. This avoids periodic disk access and prevents waking disks from standby mode.

Preferred usage pattern for periodic monitoring:

// Initial query (performed once or when SMART status changes) info, err := client.GetSMARTInfo(ctx, devicePath)

if err != nil {
   return err
}

// Cache the info and check SMART status without disk I/O support := client.GetSMARTSupportFromInfo(info)

if !support.Enabled {
   // Skip SMART monitoring when disabled
   return
}

Only use IsSMARTSupported for one-off checks where disk access is acceptable.

func (*Client) RunSelfTest

func (c *Client) RunSelfTest(ctx context.Context, devicePath string, testType string) error

RunSelfTest initiates a SMART self-test.

func (*Client) RunSelfTestWithProgress

func (c *Client) RunSelfTestWithProgress(ctx context.Context, devicePath string, testType string, callback ProgressCallback) error

RunSelfTestWithProgress starts a SMART self-test and reports progress.

func (*Client) ScanDevices

func (c *Client) ScanDevices(ctx context.Context) ([]Device, error)

ScanDevices scans for available storage devices.

type ClientOption added in v0.1.1

type ClientOption func(*Client)

ClientOption is a function that configures a Client.

func WithBackend added in v0.4.0

func WithBackend(backend Backend) ClientOption

WithBackend sets an explicit Backend implementation, bypassing the default ExecBackend. When WithBackend is provided, options such as WithSmartctlPath and WithCommander have no effect.

func WithCommander added in v0.1.1

func WithCommander(commander Commander) ClientOption

WithCommander sets a custom commander for testing purposes. This option is only effective when using the default ExecBackend. It is silently ignored when WithBackend is also provided.

func WithContext added in v0.1.1

func WithContext(ctx context.Context) ClientOption

WithContext sets a default context to use when methods are called with nil context.

func WithLogHandler added in v0.1.1

func WithLogHandler(logger *slog.Logger) ClientOption

WithLogHandler sets a custom slog.Logger for the client.

func WithSmartctlPath added in v0.1.1

func WithSmartctlPath(path string) ClientOption

WithSmartctlPath sets a custom path to the smartctl binary. This option is only effective when using the default ExecBackend. It is silently ignored when WithBackend is also provided.

func WithTLogHandler added in v0.1.4

func WithTLogHandler(logger *tlog.Logger) ClientOption

WithTLogHandler sets a custom tlog.Logger for the client.

type Cmd

type Cmd = smtypes.Cmd

Cmd is the interface for a running command.

type Commander

type Commander = smtypes.Commander

Commander is the interface for executing OS commands.

type CompareBackend added in v0.4.0

type CompareBackend = smcompare.CompareBackend

CompareBackend is a virtual Backend that runs multiple backends in parallel, returns the first (master) backend's results, and logs discrepancies.

func NewCompareBackend added in v0.4.0

func NewCompareBackend(backends []Backend, opts ...CompareBackendOption) (*CompareBackend, error)

NewCompareBackend creates a CompareBackend with at least two backends. The first backend is the master; its results are always returned to the caller. Secondary backends run in parallel; result mismatches are logged as warnings and secondary errors are logged as errors.

type CompareBackendOption added in v0.4.0

type CompareBackendOption = smcompare.Option

CompareBackendOption configures a CompareBackend.

func WithCompareLogHandler added in v0.4.0

func WithCompareLogHandler(logger LogAdapter) CompareBackendOption

WithCompareLogHandler sets a custom LogAdapter for a CompareBackend.

func WithCompareSlogHandler added in v0.4.0

func WithCompareSlogHandler(logger *slog.Logger) CompareBackendOption

WithCompareSlogHandler sets a custom slog.Logger for a CompareBackend.

func WithCompareTLogHandler added in v0.4.0

func WithCompareTLogHandler(logger *tlog.Logger) CompareBackendOption

WithCompareTLogHandler sets a custom tlog.Logger for a CompareBackend.

type Device

type Device = smtypes.Device

Device represents a storage device.

type DiscoveryBackend added in v0.4.0

type DiscoveryBackend = smtypes.DiscoveryBackend

DiscoveryBackend extends Backend with richer device discovery details.

type DiscoveryResult added in v0.3.1

type DiscoveryResult = smtypes.DiscoveryResult

DiscoveryResult holds the outcome of probing a single device during discovery.

type ExecBackend added in v0.4.0

type ExecBackend = smexec.ExecBackend

ExecBackend is the default backend that shells out to the smartctl binary.

func NewExecBackend added in v0.4.0

func NewExecBackend(opts ...ExecBackendOption) (*ExecBackend, error)

NewExecBackend creates a new exec backend.

type ExecBackendOption added in v0.4.0

type ExecBackendOption = smexec.Option

ExecBackendOption configures an ExecBackend.

func WithExecCommander added in v0.4.0

func WithExecCommander(commander Commander) ExecBackendOption

WithExecCommander sets a custom commander for ExecBackend.

func WithExecLogHandler added in v0.4.0

func WithExecLogHandler(logger LogAdapter) ExecBackendOption

WithExecLogHandler sets a custom logger adapter for ExecBackend.

func WithExecSlogHandler added in v0.4.0

func WithExecSlogHandler(logger *slog.Logger) ExecBackendOption

WithExecSlogHandler sets a slog.Logger for ExecBackend.

func WithExecSmartctlPath added in v0.4.0

func WithExecSmartctlPath(path string) ExecBackendOption

WithExecSmartctlPath sets a custom path to the smartctl binary for ExecBackend.

func WithExecTLogHandler added in v0.4.0

func WithExecTLogHandler(logger *tlog.Logger) ExecBackendOption

WithExecTLogHandler sets a tlog.Logger for ExecBackend.

type ExitCodeInfo added in v0.3.1

type ExitCodeInfo = smtypes.ExitCodeInfo

ExitCodeInfo breaks down the smartctl exit status into semantic groups.

type Flags

type Flags = smtypes.Flags

Flags represents SMART attribute flags.

type LogAdapter added in v0.4.0

type LogAdapter = smtypes.LogAdapter

LogAdapter captures the logging methods used by this package.

type Message

type Message = smtypes.Message

Message represents a message from smartctl.

type NvmeControllerCapabilities

type NvmeControllerCapabilities = smtypes.NvmeControllerCapabilities

NvmeControllerCapabilities represents NVMe controller capabilities.

type NvmeOptionalAdminCommands

type NvmeOptionalAdminCommands = smtypes.NvmeOptionalAdminCommands

NvmeOptionalAdminCommands represents NVMe optional admin commands.

type NvmeSmartHealth

type NvmeSmartHealth = smtypes.NvmeSmartHealth

NvmeSmartHealth represents NVMe SMART health information.

type NvmeSmartTestLog added in v0.2.3

type NvmeSmartTestLog = smtypes.NvmeSmartTestLog

NvmeSmartTestLog represents the NVMe self-test log.

type OfflineDataCollection

type OfflineDataCollection = smtypes.OfflineDataCollection

OfflineDataCollection represents offline data collection status.

type PollingMinutes

type PollingMinutes = smtypes.PollingMinutes

PollingMinutes represents polling minutes for different test types.

type PowerOnTime

type PowerOnTime = smtypes.PowerOnTime

PowerOnTime represents power-on time.

type ProgressCallback

type ProgressCallback = smtypes.ProgressCallback

ProgressCallback reports self-test progress.

type Raw

type Raw = smtypes.Raw

Raw represents a raw SMART attribute value.

type SMARTInfo

type SMARTInfo = smtypes.SMARTInfo

SMARTInfo represents comprehensive SMART information for a storage device.

type SelfTest

type SelfTest = smtypes.SelfTest

SelfTest represents self-test information.

type SelfTestInfo

type SelfTestInfo = smtypes.SelfTestInfo

SelfTestInfo represents available self-tests and their durations.

type SmartAttribute

type SmartAttribute = smtypes.SmartAttribute

SmartAttribute represents a single SMART attribute.

type SmartClient

type SmartClient interface {
	ScanDevices(ctx context.Context) ([]Device, error)
	GetSMARTInfo(ctx context.Context, devicePath string) (*SMARTInfo, error)
	CheckHealth(ctx context.Context, devicePath string) (bool, error)
	GetDeviceInfo(ctx context.Context, devicePath string) (map[string]interface{}, error)
	RunSelfTest(ctx context.Context, devicePath string, testType string) error
	RunSelfTestWithProgress(ctx context.Context, devicePath string, testType string, callback ProgressCallback) error
	GetAvailableSelfTests(ctx context.Context, devicePath string) (*SelfTestInfo, error)
	GetAvailableSelfTestsFromInfo(smartInfo *SMARTInfo) *SelfTestInfo
	IsSMARTSupported(ctx context.Context, devicePath string) (*SmartSupport, error)
	GetSMARTSupportFromInfo(smartInfo *SMARTInfo) *SmartSupport
	EnableSMART(ctx context.Context, devicePath string) error
	DisableSMART(ctx context.Context, devicePath string) error
	AbortSelfTest(ctx context.Context, devicePath string) error
	DiscoverDevices(ctx context.Context) ([]DiscoveryResult, error)
	Close() error
}

SmartClient interface defines the methods for interacting with smartmontools.

func NewClient

func NewClient(opts ...ClientOption) (SmartClient, error)

NewClient creates a new smartmontools client with optional configuration. If no Backend is provided via WithBackend, an ExecBackend is created using any pending exec options (e.g., from WithSmartctlPath or WithCommander). If no log handler is provided, a tlog debug-level logger is used. If no context is provided, context.Background() is used as the default.

type SmartStatus

type SmartStatus = smtypes.SmartStatus

SmartStatus represents the overall SMART health status.

type SmartSupport

type SmartSupport = smtypes.SmartSupport

SmartSupport represents SMART availability and enablement status.

type SmartctlInfo

type SmartctlInfo = smtypes.SmartctlInfo

SmartctlInfo represents smartctl metadata and messages.

type StatusField

type StatusField = smtypes.StatusField

StatusField represents a SMART status field.

type Temperature

type Temperature = smtypes.Temperature

Temperature represents device temperature.

type UserCapacity

type UserCapacity = smtypes.UserCapacity

UserCapacity represents storage device capacity information.

Directories

Path Synopsis
backends
compare
Package compare provides a virtual Backend that runs multiple backends in parallel, uses the first (master) backend's results as the response, and logs discrepancies between backends for testing and validation purposes.
Package compare provides a virtual Backend that runs multiple backends in parallel, uses the first (master) backend's results as the response, and logs discrepancies between backends for testing and validation purposes.
exec
Package smartmontools provides Go bindings for interfacing with smartmontools to monitor and manage storage device health using S.M.A.R.T. data.
Package smartmontools provides Go bindings for interfacing with smartmontools to monitor and manage storage device health using S.M.A.R.T. data.
lib
Package lib provides a Backend implementation that loads the smartmon wrapper library via purego (no CGO required).
Package lib provides a Backend implementation that loads the smartmon wrapper library via purego (no CGO required).

Jump to

Keyboard shortcuts

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