package main import ( "context" "fmt" "sort" "strings" ) // EventsArgv is ZFS's recent events with their details. ZFS keeps them in memory since the module loaded, // a bounded number, and gives them to root only. func EventsArgv() []string { return []string{"events", "-v"} } // Event is one ZFS event. type Event struct { Time string `json:"time"` Class string `json:"class"` // Details are the event's fields that say what and where: pool, vdev, state, error. Details map[string]string `json:"details,omitempty"` } // eventKeys are the details an event is answered with; the rest are internal identifiers and checksums. var eventKeys = map[string]bool{ "pool": true, "pool_state": true, "vdev_path": true, "vdev_type": true, "vdev_state": true, "vdev_laststate": true, "vdev_read_errors": true, "vdev_write_errors": true, "vdev_cksum_errors": true, "zio_err": true, "zio_objset": true, "zio_object": true, "history_internal_name": true, "history_internal_str": true, "vdev_ashift": true, "eid": true, "resilver_type": true, } // vdevStates are ZFS's vdev states as numbers, as events give them. var vdevStates = map[string]string{ "0x0": "UNKNOWN", "0x1": "CLOSED", "0x2": "OFFLINE", "0x3": "REMOVED", "0x4": "CANT_OPEN", "0x5": "FAULTED", "0x6": "DEGRADED", "0x7": "ONLINE", } // ParseEvents reads `zpool events -v`: a header line, then for each event a line of its time and class and // its fields indented under it, `name = value`. func ParseEvents(out string) []Event { var evs []Event for _, line := range strings.Split(out, "\n") { line = strings.TrimRight(line, "\r") if strings.TrimSpace(line) == "" { continue } if droppedLine.MatchString(strings.TrimSpace(line)) { continue } if line[0] != ' ' && line[0] != '\t' { f := strings.Fields(line) if len(f) < 2 || (f[0] == "TIME" && f[len(f)-1] == "CLASS") { continue } evs = append(evs, Event{Time: strings.Join(f[:len(f)-1], " "), Class: f[len(f)-1]}) continue } if len(evs) == 0 { continue } k, v, ok := strings.Cut(strings.TrimSpace(line), " = ") if !ok || !eventKeys[k] { continue } v = strings.Trim(strings.TrimSpace(v), `"`) if k == "vdev_state" || k == "vdev_laststate" { if s, known := vdevStates[v]; known { v = s } } e := &evs[len(evs)-1] if e.Details == nil { e.Details = map[string]string{} } e.Details[k] = v } return evs } // Notable says whether an event is an error or a change of state: every ereport (an I/O, checksum or // device error), every resource event (a device's state changed) and ZFS's own events but the history // it writes for every command and the configuration it syncs. func Notable(class string) bool { switch { case strings.HasPrefix(class, "ereport."), strings.HasPrefix(class, "resource."): return true case class == "sysevent.fs.zfs.history_event", class == "sysevent.fs.zfs.config_sync": return false case strings.HasPrefix(class, "sysevent.fs.zfs."): return true } return false } // EventsAnswer is what zfs_events answers. type EventsAnswer struct { Applicable bool `json:"applicable"` Events []Event `json:"events"` Shown int `json:"shown"` Kept int `json:"kept"` Said []string `json:"said"` } // Events are ZFS's recent errors and changes of state, newest first; with all, every event. func (m *Machine) Events(ctx context.Context, all bool, limit int) (any, error) { r, err := m.AsRoot(ctx, "zpool", EventsArgv()...) if err != nil { return notHere(err) } evs := ParseEvents(r.Out) a := EventsAnswer{Applicable: true, Events: []Event{}, Kept: len(evs), Said: []string{}} for i := len(evs) - 1; i >= 0 && len(a.Events) < limit; i-- { if all || Notable(evs[i].Class) { a.Events = append(a.Events, evs[i]) } } a.Shown = len(a.Events) for _, e := range a.Events { line := e.Time + " " + strings.TrimPrefix(strings.TrimPrefix(e.Class, "sysevent.fs.zfs."), "ereport.fs.zfs.") 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]) } a.Said = append(a.Said, line) } which := "errors and changes of state" if all { which = "events" } a.Said = append([]string{fmt.Sprintf("%d %s shown, newest first, of the %d events ZFS keeps since its module loaded", a.Shown, which, a.Kept)}, a.Said...) return a, nil }