package main import ( "bufio" "context" "fmt" "io" "os/exec" "sort" "strings" "time" ) // The module's second long-running process (`zfs-tools events`), run by the node-engine as root. ZFS keeps // its events in memory only, so a reboot or a reload of its kernel module loses them; the follower writes // each error and change of state to its journal as ZFS emits it, where it outlives both. It writes no file // and changes nothing. It is judged by its unit (novox/hq ADR 0240), the check beside zfs_health that the // module does not run itself. // FollowArgv follows ZFS's events as they come, with their details. func FollowArgv() []string { return []string{"events", "-f", "-v"} } // FollowRetry is how long the follower waits before following again after zpool exits. const FollowRetry = 30 * time.Second // eventTime reads an event's time as `zpool events` gives it: "Oct 8 2026 21:01:55.889900112". func eventTime(s string) (time.Time, bool) { t, err := time.ParseInLocation("Jan 2 2006 15:04:05.999999999", strings.Join(strings.Fields(s), " "), time.Local) return t, err == nil } // EventLine is an event in one journal line. func EventLine(e Event) string { line := e.Time + " " + e.Class keys := make([]string, 0, len(e.Details)) for k := range e.Details { keys = append(keys, k) } sort.Strings(keys) for _, k := range keys { line += fmt.Sprintf(" %s=%s", k, e.Details[k]) } return line } // Follow reads `zpool events -f -v` and writes each notable event at or after since in a line. `-f` first // gives every event ZFS still keeps; those from before since were written by an earlier follower, or are // in zfs_events, and are not written again. func Follow(r io.Reader, out io.Writer, since time.Time) error { sc := bufio.NewScanner(r) sc.Buffer(make([]byte, 64*1024), 1024*1024) var block []string flush := func() { for _, e := range ParseEvents(strings.Join(block, "\n")) { if !Notable(e.Class) { continue } if t, ok := eventTime(e.Time); ok && t.Before(since) { continue } fmt.Fprintln(out, EventLine(e)) } block = block[:0] } for sc.Scan() { l := sc.Text() if strings.TrimSpace(l) == "" { flush() continue } block = append(block, l) } flush() return sc.Err() } // Follower runs zpool events -f until the context ends, again after FollowRetry whenever it exits. func Follower(ctx context.Context, out io.Writer) { since := time.Now() for ctx.Err() == nil { cmd := exec.CommandContext(ctx, "zpool", FollowArgv()...) stdout, err := cmd.StdoutPipe() if err == nil { err = cmd.Start() } if err == nil { err = Follow(stdout, out, since) if werr := cmd.Wait(); err == nil { err = werr } } if ctx.Err() != nil { return } fmt.Fprintf(out, "zpool events -f stopped (%v); following again in %s\n", err, FollowRetry) // What comes while it waits is still in ZFS's own list, and the next follow writes it from here. since = time.Now() select { case <-ctx.Done(): return case <-time.After(FollowRetry): } } }