Skip to content

fix(collector): honor a device type set in the config file - #1067

Merged
kaysond merged 3 commits into
AnalogJ:masterfrom
justadityaraj:fix/issue-1063-honor-configured-device-type
Aug 29, 2026
Merged

kaysond merged 3 commits into
AnalogJ:masterfrom
justadityaraj:fix/issue-1063-honor-configured-device-type

Conversation

@justadityaraj

@justadityaraj justadityaraj commented Aug 23, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #1063.

What was wrong

The scsi/ata suppression keyed off the device type's value, so it could not tell a type that came from smartctl --scan from one the user pinned in collector.yaml:

if len(device.DeviceType) > 0 && device.DeviceType != "scsi" && device.DeviceType != "ata" {
    args = append(args, "--device", device.DeviceType)
}

The suppression itself is correct and stays — the comment explains it, ata drives get scanned as scsi under docker and metadata is lost. The problem is that TransformDetectedDevices writes a config override into the same models.Device.DeviceType field the scanner writes to, so provenance is already gone by the time the guard runs.

So devices: [{device: /dev/sda, type: scsi}] produced a bare smartctl --info --json /dev/sda, smartctl auto-selected SAT, exited 2, and the drive was dropped with no device info. ValidateConfig also rejects -d inside metrics_info_args, so there is no user-side workaround.

Both call sites need it. detect.go builds --info, metrics.go builds --xall. Fixing only detect.go gets the device registered and then Collect() drops the type again — I confirmed that second drop with its own failing test.

The fix

HasDeviceTypeOverride asks the config whether the type was pinned, so suppression now applies only to a scanned type. It is a method on config.Interface, sharing one getDeviceOverride lookup with GetCommandMetricsInfoArgs and GetCommandMetricsSmartArgs, and the checked-in gomock is updated to match.

The guard reads (type != "scsi" && type != "ata") || HasDeviceTypeOverride(...), so the config is only consulted for a type that is already scsi/ata. Every other device keeps exactly the old code path, and the existing strict-gomock tests make no unexpected call.

Verification

End to end, with the real collector binary and a stub smartctl on PATH that logs its argv, against devices: [{device: /dev/sda, type: scsi}]:

before:  Executing command: smartctl --info --json /dev/sda
after:   Executing command: smartctl --info --json --device scsi /dev/sda

The "before" line is byte-for-byte the log in the issue, and the "after" line is exactly the expected behaviour it asks for.

Tests — table tests at both call sites. Against the unpatched tree they fail with:

detect:   Got: [--info --json /dev/sda]   Want: [--info --json --device scsi /dev/sda]
metrics:  Got: [--xall --json /dev/sda]   Want: [--xall --json --device scsi /dev/sda]

They cover the cases that must not change too: a scanned sat still passes through, a scanned scsi is still dropped, an override on a different device doesn't leak, and an override with no type set doesn't count. go test ./collector/... is green, go vet and gofmt -l clean.

Re-verified after the rebase

Against the rebased head, driving the real config package and the real Detect with a capturing shell and the issue's exact config (devices: [{device: /dev/sda, type: scsi}]) and scan result (/dev/sda, scanned type sat):

master 5bf0b25:  smartctl --scan --json
                 smartctl --info --json /dev/sda
this branch:     smartctl --scan --json
                 smartctl --info --json --device scsi /dev/sda

The configured scsi beats the scanned sat, which is the behaviour #803 now preserves through SmartCtlInfo.

Behavioural equivalence of the shared lookup was checked the same way: GetCommandMetricsInfoArgs and GetCommandMetricsSmartArgs return byte-identical results to master across duplicate device: entries and mixed-case paths.

go test ./collector/... fails the same five pre-existing path tests as master on a Windows host, no more and no fewer; go vet and gofmt -l are clean.

AI usage disclosure

Per AI_POLICY.md: this was written with Claude Code, used substantially — for tracing the two call sites, drafting the patch and the tests, and writing this description.

On the "fully verified with human use" requirement, precisely what I did and did not do: the fix is verified by running the actual collector-metrics binary and reading the command it executed, before and after, plus the tests above — not by reasoning about the code. I do not have the reporter's USB Seagate BUP drive, so the final smartctl exit-status-2 behaviour on that specific hardware is not something I can reproduce; what I verified is the argv scrutiny constructs, which is the part this changes. Happy to adjust if you'd rather see it done differently.

@kaysond kaysond left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for putting this together. Two requests:

  1. the method added to config.go should be an actual method, not a pkg func (e.g. func (c *Config) Has...), and we should not use strings.EqualFold, we should do an O(1) lookup on the device overrides keys, because it should be a map. (this might need some extra work, which should live in its own commit)
  2. I like adding tests, but it's way too many lines and too verbose with the struct, as LLMs like to do

@justadityaraj

Copy link
Copy Markdown
Contributor Author

Sure, doing the 1.
2- yes, took some help from Codex.

@justadityaraj

Copy link
Copy Markdown
Contributor Author

Both done, in two commits.

6d34caa is the lookup change on its own, as you suggested: the overrides are parsed once into a map keyed on the lowercased device name, HasDeviceTypeOverride is a method on the config reading that map instead of a package function, and GetDeviceOverrides still returns the slice so ordering is unchanged. Matching stays case-insensitive and a device listed twice still resolves to its first entry. The mock is regenerated for the new interface method.

c405ebd cuts the tests down — 108 lines removed against 26 added across detect_test.go and metrics_test.go. What's left is the cases that actually distinguish an unset device type from an explicitly configured one.

go vet ./collector/... and gofmt are clean; collector/pkg/config and collector/pkg/collector pass. collector/pkg/detect has four failures on this Windows host, but they reproduce identically on master — they assert /dev/sda against a path the OS renders differently.

The scsi/ata suppression keyed off the type's value, so it could not tell a
type that came from `smartctl --scan` from one the user pinned in
collector.yaml. TransformDetectedDevices writes the override into the same
models.Device.DeviceType field the scanner writes to, so provenance was already
lost by the time the guard ran.

A user setting `devices: [{device: /dev/sda, type: scsi}]` therefore got a bare
`smartctl --info --json /dev/sda`, smartctl auto-selected SAT, exited 2, and the
drive was dropped with no device information.

HasDeviceTypeOverride asks the config directly instead, so suppression now only
applies to a scanned type. The len(type) > 0 short-circuit keeps the lookup off
the path when no type is set, so existing strict-gomock tests see no extra call.

Both call sites need it: detect.go builds --info and metrics.go builds --xall.
Fixing only detect.go registers the device and then drops the type again at
collection time.

Fixes AnalogJ#1063
- Factor the repeated `strings.EqualFold` override walk into `getDeviceOverride`
- Make `HasDeviceTypeOverride` a config method reading the same lookup
- Regenerate the mock for the added interface method
- Keep matching case-insensitive, first entry for a device still wins
The table-driven cases repeated the whole config struct per row. Keep the
cases that distinguish an unset type from an explicitly configured one and
drop the rest.
@kaysond
kaysond force-pushed the fix/issue-1063-honor-configured-device-type branch from c405ebd to 6dbe074 Compare August 29, 2026 04:54
@kaysond

kaysond commented Aug 29, 2026

Copy link
Copy Markdown
Collaborator

@justadityaraj - I rebased this on some changes I merged. decided to keep things as case-insensitive, for now, since its not consistent elsewhere. can you please do a quick sanity check?

@kaysond
kaysond self-requested a review August 29, 2026 05:02
@justadityaraj

Copy link
Copy Markdown
Contributor Author

Checked it, rebase looks right. Both of your edits hold up, and one of them was load-bearing in a way I had not accounted for.

The fix still works end to end. Driving the real config package and the real Detect with a capturing shell, using the issue's exact config (devices: [{device: /dev/sda, type: scsi}]) and its scan result (/dev/sda, scanned type sat):

master 5bf0b25:  smartctl --info --json /dev/sda
6dbe074:         smartctl --info --json --device scsi /dev/sda

The first line is byte-for-byte the log in #1063. The configured scsi correctly beats the scanned sat.

The operand reorder was not cosmetic, it was needed. HasDeviceTypeOverride is a pure read, so (type != "scsi" && type != "ata") || Has(x) is result-identical to what I had. But your ordering also keeps the config off the path for every non-scsi/ata device, which is why .AnyTimes() is now required rather than papering over anything: in the megaraid,14 case the call genuinely never happens. I checked the tests are still load-bearing by reverting only the guard at both call sites to master's form:

--- FAIL: TestDetect_SmartCtlInfo_ConfiguredDeviceType/should_keep_a_configured_scsi_type
--- FAIL: TestMetricsCollector_Collect_ConfiguredDeviceType/should_keep_a_configured_scsi_type
        Want: [--xall --json --device scsi sda]
        Got:  [--xall --json sda]

The other two cases pass under that mutation, which is correct: they exist to pin the behaviour that must not change.

EqualFold over the map reads fine to me, and it is the more consistent choice next to #1073. I confirmed the extraction into getDeviceOverride is behaviour-preserving rather than assuming it: GetCommandMetricsInfoArgs and GetCommandMetricsSmartArgs return byte-identical results to master across duplicate device: entries and mixed-case paths, including that first-match-wins is unchanged.

#803 composes cleanly, and makes the metrics.go half necessary. Now that SmartCtlInfo no longer overwrites device.DeviceType with smartctl's resolved transport, a pinned scsi survives detection and arrives at Collect() still as scsi, so it needs the same guard there. Before #803 it would often have been rewritten to sat and passed through on the non-standard branch by accident.

go test ./collector/... at the head fails the same five pre-existing path tests as master on my Windows host, no more and no fewer. go vet and gofmt -l clean, all 15 checks green.

One thing worth a release note, not a code change: anyone who already has type: scsi or type: ata in their collector.yaml will start getting a --device flag they have never been sent. That is the point of the fix, but the suppression you are relaxing was added precisely because -d scsi can lose metadata on ata drives that docker detects as scsi, so it seems worth a line rather than a silent change. None of the type: examples in example.collector.yaml are scsi/ata, so the blast radius is only configs that pin one deliberately.

I also fixed the PR description: it still claimed there was no config.Interface change and no mock regeneration, which stopped being true in 6d34caa, and the short-circuit paragraph described my ordering rather than yours.

Same disclosure as before, per AI_POLICY.md: this check was done with Claude Code. The before/after commands above are real runs against the two trees, not reasoning about the code. I still do not have the reporter's USB Seagate drive, so smartctl's exit status on that specific hardware remains the one thing I cannot confirm.

@kaysond
kaysond merged commit 9aa3d84 into AnalogJ:master Aug 29, 2026
17 checks passed
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.

Scrutiny Collector Ignores Configured SCSI Device Type and Executes smartctl --device sat for USB Seagate BUP Portable Drives

2 participants