Skip to content

feat: per-device SMART attribute threshold overrides - #1068

Draft
Abraxos wants to merge 2 commits into
AnalogJ:masterfrom
Abraxos:feat/attribute-overrides
Draft

Abraxos wants to merge 2 commits into
AnalogJ:masterfrom
Abraxos:feat/attribute-overrides

Conversation

@Abraxos

@Abraxos Abraxos commented Aug 23, 2026

Copy link
Copy Markdown

Implements the spec in #1022: a devices block in scrutiny.yaml that lets a user set warn/fail
thresholds for a single attribute on a single device, for when Scrutiny's observed failure rates
don't match their hardware.

devices:
  - scrutiny_uuid: 106089ed-2273-54e0-b498-ec4bdfc8ca6c
    attribute_overrides:
      - protocol: ATA          # ATA, NVMe or SCSI
        attribute_id: "199"    # ATA number as a string, or the NVMe/SCSI attribute name
        warn_threshold: 100
        fail_threshold: 500

Opening as a draft: three semantic decisions are baked in that I'd like your call on before I
finish the UI, since changing them afterwards means redoing that work. Each is isolated behind a
named test, so flipping any of them is small.

Three decisions worth reviewing

1. An override replaces the observed-threshold analysis rather than layering on top of it.
Without this an attribute can never actually reach "passed": a Critical attribute that matches no
bucket falls through to Could not determine Observed Failure Rate. Concretely — a healthy ATA 188
of exactly 0 warns today, because 188's first bucket is Low: 0, High: 100 and the range test is
exclusive on Low, so 0 matches nothing. (That looks like an off-by-one worth fixing separately;
happy to open an issue.)

2. A threshold is crossed when the value is strictly worse than it, where "worse" follows the
attribute's Ideal.
Assuming "greater than" would compare backwards for ideal-high attributes —
NVMe available_spare being the obvious one, where a lower value is worse.

3. Overrides never mask the drive's own SMART verdict. An override tunes Scrutiny's heuristics;
a drive reporting WhenFailed = FAILING_NOW stays failed.

Also worth a look: thresholds are *int64 so an explicit 0 is distinguishable from "not set", and
a malformed devices block fails validation at startup rather than being dropped silently — a
silently ignored override is indistinguishable from a working one.

New status flags (AttributeStatus{Passed,Warning,Failed}Override,
DeviceStatus{Passed,Failed}Override) record that an override decided a verdict so the frontend can
explain an otherwise surprising pass or fail. DeviceStatusPassedOverride is cleared when the
device is failing for any other reason — a device isn't "passing because of an override" just
because an override cleared one attribute while another still fails.

Still to do before this is ready

  • Override badge in the detail template, and the override value in the threshold column
  • docs/ update
  • Run the frontend specs — written, but not yet executed (see below)

Testing

Tests were written before the implementation, in a deliberately staged sequence so the failures
were meaningful: tests first → compile failure; then types and signatures with no behaviour → tests
compiled and failed on assertions (expected: 0x8, actual: 0x4); then implementation → green.
That middle step matters because the no-override baselines passed at that point, independently
confirming the existing analysis before any new logic existed.

66 test functions and 61 sub-tests across the backend, 0 failures (master has 43 test
functions; this branch adds 23). The full backend suite passes, including the InfluxDB-backed
webapp/backend/pkg/web integration tests.

100% of the lines this branch changes are covered (231/231). Overall backend coverage goes from
49.5% on master to 57.4%; no package decreased. Getting there added coverage to
m20201107210306_FromPreInfluxDBSmartResultsCreatePostInfluxDBSmartResults — the pre-InfluxDB
migration helper this change touches — which had none upstream; it's now exercised for all three
protocols against an in-memory sqlite copy of the old schema.

Verified on real hardware

Test subject: a Seagate ST16000NM002C whose UltraDMA CRC Error Count (199) reads 118 after two
discrete SATA link events. Scrutiny grades that into its 70–130 bucket at 22.3% AFR — over the 20%
non-critical line — so the drive reports FAILED indefinitely, and 199 is a cumulative lifetime
counter that cannot be reset. Its media-health attributes (5, 197, 198) are all 0 and it passes
its own SMART self-assessment.

Only the thresholds were changed between runs; each was a real collection against the physical drive.

config attribute status device status recorded reason
(no override) 4 failed-scrutiny 2 failed-scrutiny Observed Failure Rate for Non-Critical Attribute is greater than 20%
warn 130 / fail 500 8 passed-override 8 passed-override Attribute is within the configured override thresholds
warn 100 / fail 500 18 warning + warning-override 0 passed Attribute is above the configured warn threshold (100)
warn 50 / fail 100 36 failed + failed-override 6 failed-scrutiny + failed-override Attribute is above the configured fail threshold (100)

A warning does not fail the device (row 3), and an override can cause a failure rather than only
clearing one (row 4) — it's a threshold mechanism, not a mute switch. A malformed devices block
refused to start, with a message naming the offending entry.

Not covered

  • The frontend specs have not been executed. Specs are written for the new status bits and for
    device-status.pipe, but not yet run. The hardware verification swapped only the Go binary into
    an existing container, so the frontend in play was stock.
  • Only ATA was verified on physical hardware. NVMe and SCSI have unit tests, including their
    ideal-high/ideal-low direction handling, but no live-drive verification — I don't have an NVMe or
    SAS device with a suitable failure to override.

AI disclosure

Written with Claude Code (Claude Opus 5), reviewed and tested by me. Per AI_POLICY.md: this
references accepted issue #1022, the tests were authored first and confirmed failing for the right
reasons before any implementation existed, and every result above was produced by running the code
— including the live-drive verification on my own hardware.

Eugene Kovalev and others added 2 commits August 22, 2026 16:25
Implements the spec in AnalogJ#1022: a `devices` block in scrutiny.yaml lets a user set
warn/fail thresholds for a single attribute on a single device, for when Scrutiny's
observed failure rates do not match their hardware.

    devices:
      - scrutiny_uuid: 106089ed-2273-54e0-b498-ec4bdfc8ca6c
        attribute_overrides:
          - protocol: ATA
            attribute_id: "199"
            warn_threshold: 100
            fail_threshold: 500

Three semantics worth reviewing explicitly, each pinned by a named test:

- An override *replaces* the observed-threshold analysis for that attribute rather
  than layering on top of it. Without this an attribute can never reach "passed":
  a critical attribute that matches no bucket falls through to a warning.
- A threshold is crossed when the value is strictly worse than it, where "worse"
  follows the attribute's Ideal. Assuming "greater than" would compare backwards
  for ideal-high attributes such as NVMe available_spare.
- Overrides never mask the drive's own SMART verdict. They tune Scrutiny's
  heuristics; a drive reporting itself as failing stays failed.

Thresholds are *int64 so that an explicit 0 is distinguishable from unset, and
misconfigured overrides fail validation at startup rather than being silently
dropped -- a silently ignored override looks identical to a working one.

New status flags (AttributeStatus{Passed,Warning,Failed}Override and
DeviceStatus{Passed,Failed}Override) record that an override decided a verdict, so
the frontend can explain an otherwise surprising pass or fail. DeviceStatusPassedOverride
is cleared when the device is failing for any other reason.

AI disclosure: written with Claude Code (Claude Opus 5). The tests were authored
first and confirmed failing for the right reasons before any implementation; every
test here passes, and the full backend suite (including the InfluxDB-backed
webapp/backend/pkg/web integration tests) is green.

Refs AnalogJ#1022

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Brings coverage of the lines this branch changes to 100% (231/231). Overall backend
coverage goes from 49.5% on master to 57.4%.

Measuring found a real gap rather than confirming a good number: the first run came in
at 87.4%, with the SCSI override path at 42.9% -- i.e. untested. Closing the rest meant
covering three things worth having tests for regardless:

- The `return err` paths when a `devices` block cannot be decoded, asserted both in
  attributeOverridesFor and in SaveSmartAttributes. The failure mode this guards against
  is a user believing a device is tuned while it is silently graded by stock analysis.
- attributeOverridesFor's uuid lookup, including case-insensitive matching, driven
  through a real config parsed from YAML rather than a stub.
- m20201107210306_FromPreInfluxDBSmartResultsCreatePostInfluxDBSmartResults, the
  pre-InfluxDB migration helper this branch touches, now exercised for ATA, NVMe and
  SCSI against an in-memory sqlite copy of the old schema. That helper had no test
  coverage upstream.

One SCSI case documents existing behaviour rather than endorsing it: the non-override
path looks SCSI attributes up in thresholds.NmveMetadata instead of ScsiMetadata, so a
SCSI-only attribute id matches nothing and its recommended threshold is never checked.
Left as-is to keep this change focused; the override path uses ScsiMetadata.

AI disclosure: written with Claude Code (Claude Opus 5), reviewed and tested by me.

Refs AnalogJ#1022

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@silentpager-rocks

Copy link
Copy Markdown

Looking forward for this change!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants