Showing preview only (4,861K chars total). Download the full file or copy to clipboard to get everything.
Repository: AnalogJ/scrutiny
Branch: master
Commit: e4c40f7e8053
Files: 504
Total size: 4.5 MB
Directory structure:
gitextract_a01nueng/
├── .devcontainer/
│ ├── docker/
│ │ └── devcontainer.json
│ ├── docker-compose.yml
│ ├── docker-rootless/
│ │ └── devcontainer.json
│ ├── podman/
│ │ └── devcontainer.json
│ └── setup.sh
├── .dockerignore
├── .gitattributes
├── .github/
│ ├── DISCUSSION_TEMPLATE/
│ │ └── issue-triage.yml
│ ├── ISSUE_TEMPLATE/
│ │ ├── config.yml
│ │ └── preapproved.md
│ └── workflows/
│ ├── ci.yaml
│ ├── docker-build.yaml
│ ├── docker-nightly.yaml
│ ├── release.yaml
│ └── sponsors.yaml
├── .gitignore
├── .golangci.yml
├── .vscode/
│ ├── launch.json
│ └── tasks.json
├── AI_POLICY.md
├── CONTRIBUTING.md
├── LICENSE
├── Makefile
├── README.md
├── REFERENCES.md
├── collector/
│ ├── cmd/
│ │ ├── collector-metrics/
│ │ │ └── collector-metrics.go
│ │ └── collector-selftest/
│ │ └── collector-selftest.go
│ └── pkg/
│ ├── collector/
│ │ ├── base.go
│ │ ├── metrics.go
│ │ ├── metrics_test.go
│ │ └── selftest.go
│ ├── common/
│ │ └── shell/
│ │ ├── factory.go
│ │ ├── interface.go
│ │ ├── local_shell.go
│ │ ├── local_shell_test.go
│ │ └── mock/
│ │ └── mock_shell.go
│ ├── config/
│ │ ├── config.go
│ │ ├── config_test.go
│ │ ├── factory.go
│ │ ├── interface.go
│ │ ├── mock/
│ │ │ └── mock_config.go
│ │ └── testdata/
│ │ ├── allow_listed_devices_present.yaml
│ │ ├── device_type_comma.yaml
│ │ ├── ignore_device.yaml
│ │ ├── invalid_commands_includes_device.yaml
│ │ ├── invalid_commands_missing_json.yaml
│ │ ├── override_commands.yaml
│ │ ├── override_device_commands.yaml
│ │ ├── raid_device.yaml
│ │ └── simple_device.yaml
│ ├── detect/
│ │ ├── detect.go
│ │ ├── detect_test.go
│ │ ├── devices_darwin.go
│ │ ├── devices_freebsd.go
│ │ ├── devices_linux.go
│ │ ├── devices_linux_test.go
│ │ ├── devices_windows.go
│ │ ├── testdata/
│ │ │ ├── smartctl_info_nvme.json
│ │ │ ├── smartctl_scan_megaraid.json
│ │ │ ├── smartctl_scan_nvme.json
│ │ │ └── smartctl_scan_simple.json
│ │ ├── wwn.go
│ │ └── wwn_test.go
│ ├── errors/
│ │ ├── errors.go
│ │ └── errors_test.go
│ └── models/
│ ├── device.go
│ ├── scan.go
│ └── scan_override.go
├── docker/
│ ├── Dockerfile
│ ├── Dockerfile.collector
│ ├── Dockerfile.smartmontools
│ ├── Dockerfile.web
│ ├── README.md
│ ├── entrypoint-collector.sh
│ ├── example.hubspoke.docker-compose.yml
│ └── example.omnibus.docker-compose.yml
├── docs/
│ ├── DOWNSAMPLING.md
│ ├── INSTALL_ANSIBLE.md
│ ├── INSTALL_HUB_SPOKE.md
│ ├── INSTALL_MANUAL.md
│ ├── INSTALL_MANUAL_WINDOWS.md
│ ├── INSTALL_NAS.md
│ ├── INSTALL_PFSENSE.md
│ ├── INSTALL_ROOTLESS_PODMAN.md
│ ├── INSTALL_SYNOLOGY_COLLECTOR.md
│ ├── INSTALL_UNRAID.md
│ ├── SUPPORTED_NAS_OS.md
│ ├── TESTERS.md
│ ├── TROUBLESHOOTING_DEVICE_COLLECTOR.md
│ ├── TROUBLESHOOTING_DOCKER.md
│ ├── TROUBLESHOOTING_INFLUXDB.md
│ ├── TROUBLESHOOTING_NOTIFICATIONS.md
│ ├── TROUBLESHOOTING_REVERSE_PROXY.md
│ ├── TROUBLESHOOTING_UDEV.md
│ └── dbdiagram.io.txt
├── example.collector.yaml
├── example.scrutiny.yaml
├── go.mod
├── go.sum
├── packagr.yml
├── rootfs/
│ └── etc/
│ ├── cont-init.d/
│ │ ├── 01-timezone
│ │ └── 50-cron-config
│ ├── cron.d/
│ │ └── scrutiny
│ └── services.d/
│ ├── collector-once/
│ │ └── run
│ ├── cron/
│ │ ├── finish
│ │ └── run
│ ├── influxdb/
│ │ └── run
│ └── scrutiny/
│ └── run
└── webapp/
├── backend/
│ ├── cmd/
│ │ └── scrutiny/
│ │ └── scrutiny.go
│ └── pkg/
│ ├── config/
│ │ ├── config.go
│ │ ├── config_test.go
│ │ ├── factory.go
│ │ ├── interface.go
│ │ └── mock/
│ │ └── mock_config.go
│ ├── constants.go
│ ├── database/
│ │ ├── interface.go
│ │ ├── migrations/
│ │ │ ├── m20201107210306/
│ │ │ │ ├── device.go
│ │ │ │ ├── smart.go
│ │ │ │ ├── smart_ata_attribute.go
│ │ │ │ ├── smart_nvme_attribute.go
│ │ │ │ └── smart_scsci_attribute.go
│ │ │ ├── m20220503120000/
│ │ │ │ └── device.go
│ │ │ ├── m20220509170100/
│ │ │ │ └── device.go
│ │ │ ├── m20220716214900/
│ │ │ │ └── setting.go
│ │ │ └── m20250221084400/
│ │ │ └── device.go
│ │ ├── mock/
│ │ │ └── mock_database.go
│ │ ├── scrutiny_repository.go
│ │ ├── scrutiny_repository_device.go
│ │ ├── scrutiny_repository_device_smart_attributes.go
│ │ ├── scrutiny_repository_migrations.go
│ │ ├── scrutiny_repository_settings.go
│ │ ├── scrutiny_repository_tasks.go
│ │ ├── scrutiny_repository_tasks_test.go
│ │ ├── scrutiny_repository_temperature.go
│ │ └── scrutiny_repository_temperature_test.go
│ ├── errors/
│ │ ├── errors.go
│ │ └── errors_test.go
│ ├── models/
│ │ ├── collector/
│ │ │ ├── smart.go
│ │ │ └── smart_test.go
│ │ ├── device.go
│ │ ├── device_summary.go
│ │ ├── measurements/
│ │ │ ├── smart.go
│ │ │ ├── smart_ata_attribute.go
│ │ │ ├── smart_attribute.go
│ │ │ ├── smart_nvme_attribute.go
│ │ │ ├── smart_scsci_attribute.go
│ │ │ ├── smart_temperature.go
│ │ │ └── smart_test.go
│ │ ├── setting_entry.go
│ │ ├── settings.go
│ │ └── testdata/
│ │ ├── helper.go
│ │ ├── smart-ata-date.json
│ │ ├── smart-ata-date2.json
│ │ ├── smart-ata-failed-scrutiny.json
│ │ ├── smart-ata-full.json
│ │ ├── smart-ata.json
│ │ ├── smart-ata2.json
│ │ ├── smart-fail.json
│ │ ├── smart-fail2.json
│ │ ├── smart-megaraid0.json
│ │ ├── smart-megaraid1.json
│ │ ├── smart-nvme-failed.json
│ │ ├── smart-nvme.json
│ │ ├── smart-nvme2.json
│ │ ├── smart-pass.json
│ │ ├── smart-raid.json
│ │ ├── smart-sat.json
│ │ ├── smart-scsi.json
│ │ └── smart-scsi2.json
│ ├── notify/
│ │ ├── notify.go
│ │ └── notify_test.go
│ ├── thresholds/
│ │ ├── ata_attribute_metadata.go
│ │ ├── nvme_attribute_metadata.go
│ │ └── scsi_attribute_metadata.go
│ ├── version/
│ │ └── version.go
│ └── web/
│ ├── handler/
│ │ ├── archive_device.go
│ │ ├── delete_device.go
│ │ ├── get_device_details.go
│ │ ├── get_devices_summary.go
│ │ ├── get_devices_summary_temp_history.go
│ │ ├── get_settings.go
│ │ ├── health_check.go
│ │ ├── register_devices.go
│ │ ├── save_settings.go
│ │ ├── send_test_notification.go
│ │ ├── unarchive_device.go
│ │ ├── upload_device_metrics.go
│ │ └── upload_device_self_tests.go
│ ├── middleware/
│ │ ├── config.go
│ │ ├── logger.go
│ │ └── repository.go
│ ├── server.go
│ ├── server_test.go
│ └── testdata/
│ ├── register-devices-req-2.json
│ ├── register-devices-req.json
│ ├── register-devices-single-req.json
│ └── upload-device-metrics-req.json
└── frontend/
├── .editorconfig
├── .gitignore
├── CREDITS
├── LICENSE.md
├── README.md
├── angular.json
├── browserslist
├── e2e/
│ ├── protractor.conf.js
│ ├── src/
│ │ ├── app.e2e-spec.ts
│ │ └── app.po.ts
│ └── tsconfig.json
├── git.version.sh
├── karma.conf.js
├── package.json
├── src/
│ ├── @treo/
│ │ ├── animations/
│ │ │ ├── defaults.ts
│ │ │ ├── expand-collapse.ts
│ │ │ ├── fade.ts
│ │ │ ├── index.ts
│ │ │ ├── public-api.ts
│ │ │ ├── shake.ts
│ │ │ ├── slide.ts
│ │ │ └── zoom.ts
│ │ ├── components/
│ │ │ ├── card/
│ │ │ │ ├── card.component.html
│ │ │ │ ├── card.component.scss
│ │ │ │ ├── card.component.ts
│ │ │ │ ├── card.module.ts
│ │ │ │ ├── index.ts
│ │ │ │ └── public-api.ts
│ │ │ ├── date-range/
│ │ │ │ ├── date-range.component.html
│ │ │ │ ├── date-range.component.scss
│ │ │ │ ├── date-range.component.ts
│ │ │ │ ├── date-range.module.ts
│ │ │ │ ├── index.ts
│ │ │ │ └── public-api.ts
│ │ │ ├── drawer/
│ │ │ │ ├── drawer.component.html
│ │ │ │ ├── drawer.component.scss
│ │ │ │ ├── drawer.component.ts
│ │ │ │ ├── drawer.module.ts
│ │ │ │ ├── drawer.service.ts
│ │ │ │ ├── drawer.types.ts
│ │ │ │ ├── index.ts
│ │ │ │ └── public-api.ts
│ │ │ ├── highlight/
│ │ │ │ ├── highlight.component.html
│ │ │ │ ├── highlight.component.scss
│ │ │ │ ├── highlight.component.ts
│ │ │ │ ├── highlight.module.ts
│ │ │ │ ├── highlight.service.ts
│ │ │ │ ├── index.ts
│ │ │ │ └── public-api.ts
│ │ │ ├── message/
│ │ │ │ ├── index.ts
│ │ │ │ ├── message.component.html
│ │ │ │ ├── message.component.scss
│ │ │ │ ├── message.component.ts
│ │ │ │ ├── message.module.ts
│ │ │ │ ├── message.service.ts
│ │ │ │ ├── message.types.ts
│ │ │ │ └── public-api.ts
│ │ │ └── navigation/
│ │ │ ├── horizontal/
│ │ │ │ ├── components/
│ │ │ │ │ ├── basic/
│ │ │ │ │ │ ├── basic.component.html
│ │ │ │ │ │ └── basic.component.ts
│ │ │ │ │ ├── branch/
│ │ │ │ │ │ ├── branch.component.html
│ │ │ │ │ │ └── branch.component.ts
│ │ │ │ │ ├── divider/
│ │ │ │ │ │ ├── divider.component.html
│ │ │ │ │ │ └── divider.component.ts
│ │ │ │ │ └── spacer/
│ │ │ │ │ ├── spacer.component.html
│ │ │ │ │ └── spacer.component.ts
│ │ │ │ ├── horizontal.component.html
│ │ │ │ ├── horizontal.component.scss
│ │ │ │ └── horizontal.component.ts
│ │ │ ├── index.ts
│ │ │ ├── navigation.module.ts
│ │ │ ├── navigation.service.ts
│ │ │ ├── navigation.types.ts
│ │ │ ├── public-api.ts
│ │ │ └── vertical/
│ │ │ ├── components/
│ │ │ │ ├── aside/
│ │ │ │ │ ├── aside.component.html
│ │ │ │ │ └── aside.component.ts
│ │ │ │ ├── basic/
│ │ │ │ │ ├── basic.component.html
│ │ │ │ │ └── basic.component.ts
│ │ │ │ ├── collapsable/
│ │ │ │ │ ├── collapsable.component.html
│ │ │ │ │ └── collapsable.component.ts
│ │ │ │ ├── divider/
│ │ │ │ │ ├── divider.component.html
│ │ │ │ │ └── divider.component.ts
│ │ │ │ ├── group/
│ │ │ │ │ ├── group.component.html
│ │ │ │ │ └── group.component.ts
│ │ │ │ └── spacer/
│ │ │ │ ├── spacer.component.html
│ │ │ │ └── spacer.component.ts
│ │ │ ├── vertical.component.html
│ │ │ ├── vertical.component.scss
│ │ │ └── vertical.component.ts
│ │ ├── directives/
│ │ │ ├── autogrow/
│ │ │ │ ├── autogrow.directive.ts
│ │ │ │ ├── autogrow.module.ts
│ │ │ │ ├── index.ts
│ │ │ │ └── public-api.ts
│ │ │ └── scrollbar/
│ │ │ ├── index.ts
│ │ │ ├── public-api.ts
│ │ │ ├── scrollbar.directive.ts
│ │ │ ├── scrollbar.interfaces.ts
│ │ │ └── scrollbar.module.ts
│ │ ├── index.ts
│ │ ├── lib/
│ │ │ └── mock-api/
│ │ │ ├── index.ts
│ │ │ ├── mock-api.interceptor.ts
│ │ │ ├── mock-api.interfaces.ts
│ │ │ ├── mock-api.module.ts
│ │ │ ├── mock-api.request-handler.ts
│ │ │ ├── mock-api.service.ts
│ │ │ └── mock-api.utils.ts
│ │ ├── pipes/
│ │ │ └── find-by-key/
│ │ │ ├── find-by-key.module.ts
│ │ │ ├── find-by-key.pipe.ts
│ │ │ ├── index.ts
│ │ │ └── public-api.ts
│ │ ├── services/
│ │ │ ├── config/
│ │ │ │ ├── config.constants.ts
│ │ │ │ ├── config.module.ts
│ │ │ │ ├── config.service.ts
│ │ │ │ ├── index.ts
│ │ │ │ └── public-api.ts
│ │ │ ├── media-watcher/
│ │ │ │ ├── index.ts
│ │ │ │ ├── media-watcher.module.ts
│ │ │ │ ├── media-watcher.service.ts
│ │ │ │ └── public-api.ts
│ │ │ └── splash-screen/
│ │ │ ├── index.ts
│ │ │ ├── public-api.ts
│ │ │ ├── splash-screen.module.ts
│ │ │ └── splash-screen.service.ts
│ │ ├── styles/
│ │ │ ├── base/
│ │ │ │ ├── _colors.scss
│ │ │ │ ├── _preflight.scss
│ │ │ │ ├── _theming.scss
│ │ │ │ └── _typography.scss
│ │ │ ├── components/
│ │ │ │ ├── _card.scss
│ │ │ │ ├── _input.scss
│ │ │ │ └── _table.scss
│ │ │ ├── layout/
│ │ │ │ └── _content.scss
│ │ │ ├── main.scss
│ │ │ ├── overrides/
│ │ │ │ ├── _angular-material.scss
│ │ │ │ ├── _highlightjs.scss
│ │ │ │ ├── _perfect-scrollbar.scss
│ │ │ │ └── _quill.scss
│ │ │ ├── treo.scss
│ │ │ ├── utilities/
│ │ │ │ ├── _breakpoints.scss
│ │ │ │ ├── _colors.scss
│ │ │ │ ├── _elevations.scss
│ │ │ │ ├── _icons.scss
│ │ │ │ ├── _keyframes.scss
│ │ │ │ └── _theming.scss
│ │ │ └── vendors/
│ │ │ ├── _angular-material.scss
│ │ │ └── _normalize.scss
│ │ ├── tailwind/
│ │ │ ├── export.css
│ │ │ ├── export.js
│ │ │ ├── exported/
│ │ │ │ ├── _variables.scss
│ │ │ │ └── variables.ts
│ │ │ └── plugins/
│ │ │ ├── index.js
│ │ │ ├── utilities/
│ │ │ │ ├── color-combinations.js
│ │ │ │ ├── color-contrasts.js
│ │ │ │ ├── icon-color.js
│ │ │ │ ├── icon-size.js
│ │ │ │ └── mirror.js
│ │ │ └── variants/
│ │ │ ├── dark-light.js
│ │ │ ├── export-box-shadow.js
│ │ │ ├── export-colors.js
│ │ │ ├── export-font-family.js
│ │ │ └── export-screens.js
│ │ ├── treo.module.ts
│ │ └── validators/
│ │ ├── index.ts
│ │ ├── public-api.ts
│ │ └── validators.ts
│ ├── app/
│ │ ├── app.component.html
│ │ ├── app.component.scss
│ │ ├── app.component.ts
│ │ ├── app.module.ts
│ │ ├── app.routing.ts
│ │ ├── core/
│ │ │ ├── config/
│ │ │ │ ├── app.config.ts
│ │ │ │ ├── scrutiny-config.module.ts
│ │ │ │ └── scrutiny-config.service.ts
│ │ │ ├── core.module.ts
│ │ │ └── models/
│ │ │ ├── device-details-response-wrapper.ts
│ │ │ ├── device-model.ts
│ │ │ ├── device-summary-model.ts
│ │ │ ├── device-summary-response-wrapper.ts
│ │ │ ├── device-summary-temp-response-wrapper.ts
│ │ │ ├── measurements/
│ │ │ │ ├── smart-attribute-model.ts
│ │ │ │ ├── smart-model.ts
│ │ │ │ └── smart-temperature-model.ts
│ │ │ └── thresholds/
│ │ │ └── attribute-metadata-model.ts
│ │ ├── data/
│ │ │ └── mock/
│ │ │ ├── device/
│ │ │ │ └── details/
│ │ │ │ ├── index.ts
│ │ │ │ ├── sda.ts
│ │ │ │ ├── sdb.ts
│ │ │ │ ├── sdc.ts
│ │ │ │ ├── sdd.ts
│ │ │ │ ├── sde.ts
│ │ │ │ └── sdf.ts
│ │ │ ├── index.ts
│ │ │ └── summary/
│ │ │ ├── data.ts
│ │ │ ├── index.ts
│ │ │ └── temp_history.ts
│ │ ├── layout/
│ │ │ ├── common/
│ │ │ │ ├── dashboard-device/
│ │ │ │ │ ├── dashboard-device.component.html
│ │ │ │ │ ├── dashboard-device.component.scss
│ │ │ │ │ ├── dashboard-device.component.spec.ts
│ │ │ │ │ ├── dashboard-device.component.ts
│ │ │ │ │ └── dashboard-device.module.ts
│ │ │ │ ├── dashboard-device-archive-dialog/
│ │ │ │ │ ├── dashboard-device-archive-dialog.component.html
│ │ │ │ │ ├── dashboard-device-archive-dialog.component.scss
│ │ │ │ │ ├── dashboard-device-archive-dialog.component.spec.ts
│ │ │ │ │ ├── dashboard-device-archive-dialog.component.ts
│ │ │ │ │ ├── dashboard-device-archive-dialog.module.ts
│ │ │ │ │ └── dashboard-device-archive-dialog.service.ts
│ │ │ │ ├── dashboard-device-delete-dialog/
│ │ │ │ │ ├── dashboard-device-delete-dialog.component.html
│ │ │ │ │ ├── dashboard-device-delete-dialog.component.scss
│ │ │ │ │ ├── dashboard-device-delete-dialog.component.spec.ts
│ │ │ │ │ ├── dashboard-device-delete-dialog.component.ts
│ │ │ │ │ ├── dashboard-device-delete-dialog.module.ts
│ │ │ │ │ └── dashboard-device-delete-dialog.service.ts
│ │ │ │ ├── dashboard-settings/
│ │ │ │ │ ├── dashboard-settings.component.html
│ │ │ │ │ ├── dashboard-settings.component.scss
│ │ │ │ │ ├── dashboard-settings.component.ts
│ │ │ │ │ └── dashboard-settings.module.ts
│ │ │ │ ├── detail-settings/
│ │ │ │ │ ├── detail-settings.component.html
│ │ │ │ │ ├── detail-settings.component.scss
│ │ │ │ │ ├── detail-settings.component.spec.ts
│ │ │ │ │ ├── detail-settings.component.ts
│ │ │ │ │ └── detail-settings.module.ts
│ │ │ │ └── search/
│ │ │ │ ├── search.component.html
│ │ │ │ ├── search.component.scss
│ │ │ │ ├── search.component.ts
│ │ │ │ └── search.module.ts
│ │ │ ├── layout.component.html
│ │ │ ├── layout.component.scss
│ │ │ ├── layout.component.ts
│ │ │ ├── layout.module.ts
│ │ │ ├── layout.types.ts
│ │ │ └── layouts/
│ │ │ ├── empty/
│ │ │ │ ├── empty.component.html
│ │ │ │ ├── empty.component.scss
│ │ │ │ ├── empty.component.ts
│ │ │ │ └── empty.module.ts
│ │ │ └── horizontal/
│ │ │ └── material/
│ │ │ ├── material.component.html
│ │ │ ├── material.component.scss
│ │ │ ├── material.component.ts
│ │ │ └── material.module.ts
│ │ ├── modules/
│ │ │ ├── dashboard/
│ │ │ │ ├── dashboard.component.html
│ │ │ │ ├── dashboard.component.scss
│ │ │ │ ├── dashboard.component.ts
│ │ │ │ ├── dashboard.module.ts
│ │ │ │ ├── dashboard.resolvers.ts
│ │ │ │ ├── dashboard.routing.ts
│ │ │ │ ├── dashboard.service.spec.ts
│ │ │ │ └── dashboard.service.ts
│ │ │ ├── detail/
│ │ │ │ ├── detail.component.html
│ │ │ │ ├── detail.component.scss
│ │ │ │ ├── detail.component.ts
│ │ │ │ ├── detail.module.ts
│ │ │ │ ├── detail.resolvers.ts
│ │ │ │ ├── detail.routing.ts
│ │ │ │ ├── detail.service.spec.ts
│ │ │ │ └── detail.service.ts
│ │ │ └── landing/
│ │ │ └── home/
│ │ │ ├── home.component.html
│ │ │ ├── home.component.scss
│ │ │ ├── home.component.ts
│ │ │ ├── home.module.ts
│ │ │ └── home.routing.ts
│ │ └── shared/
│ │ ├── device-hours.pipe.spec.ts
│ │ ├── device-hours.pipe.ts
│ │ ├── device-sort.pipe.spec.ts
│ │ ├── device-sort.pipe.ts
│ │ ├── device-status.pipe.spec.ts
│ │ ├── device-status.pipe.ts
│ │ ├── device-title.pipe.spec.ts
│ │ ├── device-title.pipe.ts
│ │ ├── file-size.pipe.spec.ts
│ │ ├── file-size.pipe.ts
│ │ ├── shared.module.ts
│ │ ├── temperature.pipe.spec.ts
│ │ └── temperature.pipe.ts
│ ├── assets/
│ │ ├── .gitkeep
│ │ ├── fonts/
│ │ │ ├── ibm-plex-mono/
│ │ │ │ └── ibm-plex-mono.css
│ │ │ ├── inter/
│ │ │ │ └── inter.css
│ │ │ ├── material-icons/
│ │ │ │ ├── MaterialIcons-Regular.codepoints
│ │ │ │ ├── MaterialIconsOutlined-Regular.codepoints
│ │ │ │ ├── MaterialIconsOutlined-Regular.otf
│ │ │ │ ├── MaterialIconsRound-Regular.codepoints
│ │ │ │ ├── MaterialIconsRound-Regular.otf
│ │ │ │ ├── MaterialIconsSharp-Regular.codepoints
│ │ │ │ ├── MaterialIconsSharp-Regular.otf
│ │ │ │ ├── MaterialIconsTwoTone-Regular.codepoints
│ │ │ │ ├── MaterialIconsTwoTone-Regular.otf
│ │ │ │ ├── README.md
│ │ │ │ └── material-icons.css
│ │ │ └── roboto/
│ │ │ └── roboto.css
│ │ └── images/
│ │ └── logo/
│ │ ├── scrutiny-logo-dark-social.psd
│ │ ├── scrutiny-logo-dark-text.psd
│ │ ├── scrutiny-logo-white-text.psd
│ │ └── scrutiny-logo-white.psd
│ ├── browserconfig.xml
│ ├── environments/
│ │ ├── environment.prod.ts
│ │ ├── environment.ts
│ │ └── versions.ts
│ ├── index.html
│ ├── main.ts
│ ├── manifest.json
│ ├── polyfills.ts
│ ├── styles/
│ │ ├── styles.scss
│ │ ├── tailwind.scss
│ │ ├── themes.scss
│ │ └── vendors.scss
│ ├── tailwind/
│ │ ├── config.js
│ │ └── main.css
│ └── test.ts
├── tsconfig.app.json
├── tsconfig.json
├── tsconfig.spec.json
└── tslint.json
================================================
FILE CONTENTS
================================================
================================================
FILE: .devcontainer/docker/devcontainer.json
================================================
{
"name": "Scrutiny Dev (docker)",
"dockerComposeFile": "../docker-compose.yml",
"service": "app",
"workspaceFolder": "/workspaces/scrutiny",
"features": {
"ghcr.io/devcontainers/features/go:1": "1.25",
"ghcr.io/devcontainers/features/node:1": "lts"
},
"onCreateCommand": "sudo apt-get update && sudo apt-get install -y smartmontools iputils-ping chromium-browser",
"customizations": {
"vscode": {
"extensions": [
"golang.go",
"dbaeumer.vscode-eslint",
"esbenp.prettier-vscode"
]
}
},
"forwardPorts": [8080, 8086],
"postCreateCommand": "bash .devcontainer/setup.sh",
"remoteUser": "vscode"
}
================================================
FILE: .devcontainer/docker-compose.yml
================================================
services:
app:
image: mcr.microsoft.com/devcontainers/base:ubuntu-22.04
volumes:
- ..:/workspaces/scrutiny:cached
command: sleep infinity
network_mode: service:influxdb
influxdb:
image: influxdb:2.8
restart: unless-stopped
ports:
- "8086:8086"
environment:
- DOCKER_INFLUXDB_INIT_MODE=setup
- DOCKER_INFLUXDB_INIT_USERNAME=admin
- DOCKER_INFLUXDB_INIT_PASSWORD=password12345
- DOCKER_INFLUXDB_INIT_ORG=scrutiny
- DOCKER_INFLUXDB_INIT_BUCKET=metrics
- DOCKER_INFLUXDB_INIT_ADMIN_TOKEN=my-super-secret-auth-token
volumes:
- scrutiny-influxdb-data:/var/lib/influxdb2
volumes:
scrutiny-influxdb-data:
================================================
FILE: .devcontainer/docker-rootless/devcontainer.json
================================================
{
"name": "Scrutiny Dev (rootless docker)",
"dockerComposeFile": "../docker-compose.yml",
"service": "app",
"workspaceFolder": "/workspaces/scrutiny",
"features": {
"ghcr.io/devcontainers/features/go:1": "1.25",
"ghcr.io/devcontainers/features/node:1": "lts"
},
"onCreateCommand": "sudo apt-get update && sudo apt-get install -y smartmontools iputils-ping chromium-browser",
"customizations": {
"vscode": {
"extensions": [
"golang.go",
"dbaeumer.vscode-eslint",
"esbenp.prettier-vscode"
]
}
},
"forwardPorts": [8080, 8086],
"postCreateCommand": "bash .devcontainer/setup.sh",
"remoteUser": "root",
"containerUser": "root",
"updateRemoteUserUID": false
}
================================================
FILE: .devcontainer/podman/devcontainer.json
================================================
{
"name": "Scrutiny Dev (podman)",
"dockerComposeFile": "../docker-compose.yml",
"service": "app",
"workspaceFolder": "/workspaces/scrutiny",
"features": {
"ghcr.io/devcontainers/features/go:1": "1.25",
"ghcr.io/devcontainers/features/node:1": "lts"
},
"onCreateCommand": "sudo apt-get update && sudo apt-get install -y smartmontools iputils-ping chromium-browser",
"customizations": {
"vscode": {
"extensions": [
"golang.go",
"dbaeumer.vscode-eslint",
"esbenp.prettier-vscode"
]
}
},
"forwardPorts": [8080, 8086],
"postCreateCommand": "bash .devcontainer/setup.sh",
"remoteEnv": {
"PODMAN_USERNS": "keep-id"
},
"containerUser": "vscode",
"updateRemoteUserUID": true
}
================================================
FILE: .devcontainer/setup.sh
================================================
#!/bin/bash
echo "Starting Scrutiny Setup..."
if [ ! -f "scrutiny.yaml" ]; then
echo "Creating scrutiny.yaml from template..."
cat <<EOF > scrutiny.yaml
version: 1
web:
listen:
port: 8080
host: 0.0.0.0
database:
location: ./scrutiny.db
src:
frontend:
path: ./dist
influxdb:
retention_policy: false
token: "my-super-secret-auth-token"
org: "scrutiny"
bucket: "metrics"
host: "localhost"
port: 8086
log:
file: 'web.log'
level: DEBUG
EOF
else
echo "scrutiny.yaml already exists."
fi
echo "Vendoring Go modules..."
go mod vendor
echo "Installing Node modules..."
cd webapp/frontend
npm install
echo "Setup Complete! Ready to code."
================================================
FILE: .dockerignore
================================================
/vendor
/.idea
/.github
/.git
/webapp/frontend/node_modules
================================================
FILE: .gitattributes
================================================
*.css linguist-detectable=false
*.scss linguist-detectable=false
*.js linguist-detectable=false
*.ts linguist-detectable=false
================================================
FILE: .github/DISCUSSION_TEMPLATE/issue-triage.yml
================================================
labels: ["needs-confirmation"]
body:
- type: markdown
attributes:
value: |
> [!IMPORTANT]
> Please read through [the Discussion rules](https://github.com/AnalogJ/scrutiny/discussions/876), review [the docs](https://github.com/AnalogJ/scrutiny/tree/master/docs), and check for both existing [Discussions](https://github.com/AnalogJ/scrutiny/discussions?discussions_q=) and [Issues](https://github.com/AnalogJ/scrutiny/issues?q=sort%3Areactions-desc) prior to opening a new Discussion.
- type: markdown
attributes:
value: "# Issue Details"
- type: textarea
attributes:
label: Issue Description
description: |
Provide a detailed description of the issue. Include relevant information, such as:
- The feature or configuration option you encounter the issue with.
- Screenshots, screen recordings, or other supporting media (as needed).
- If this is a regression of an existing issue that was closed or resolved, please include the previous item reference (Discussion, Issue, PR, commit) in your description.
placeholder: |
Temperature data is missing from the plots.
validations:
required: true
- type: textarea
attributes:
label: Expected Behavior
description: |
Describe how you expect scrutiny to behave in this situation. Include any relevant documentation links.
placeholder: |
All temperature data uploaded by collectors should make it into the plots.
validations:
required: true
- type: textarea
attributes:
label: Actual Behavior
description: |
Describe how scrutiny actually behaves in this situation. If it is not immediately obvious how the actual behavior differs from the expected behavior described above, please be sure to mention the deviation specifically.
placeholder: |
Only half the points appear.
validations:
required: true
- type: textarea
attributes:
label: Reproduction Steps
description: |
Provide a detailed set of step-by-step instructions for reproducing this issue. If you can't, describe what you were doing when the issue occurred.
placeholder: |
1. Set up the omnibus docker image
2. Launch the web dashboard
validations:
required: true
- type: textarea
attributes:
label: scrutiny debug logs
description: |
Provide any captured scrutiny logs or panic dumps during your issue reproduction in this field.
Make sure to turn on debug logging with the environment variable DEBUG=true
render: text
- type: input
attributes:
label: Scrutiny Version
description: The version of scrutiny you are using
placeholder: v0.8.2
validations:
required: true
- type: input
attributes:
label: Smartmontools Version
description: The version of smartmontools you are using (or "docker", if you're using the docker image)
placeholder: "7.2"
validations:
required: true
- type: input
attributes:
label: OS Version Information
description: |
Please tell us what operating system (name and version) you are using.
placeholder: Ubuntu 24.04.1 (Noble Numbat)
validations:
required: true
- type: dropdown
attributes:
label: Component
description: Which component of scrutiny has a problem?
options:
- web
- collector
- omnibus (docker only)
validations:
required: true
- type: checkboxes
attributes:
label: Deployment Method
description: How are you running scrutiny?
options:
- label: docker
- label: binaries
- label: systemd
validations:
required: true
- type: input
attributes:
label: Hard Drive Information
description: |
If the problem is related to a specific hard drive, what are the make and model?
placeholder: Seagate ST8000DM004-2CX188
validations:
required: false
- type: textarea
attributes:
label: smartctl output
description: |
What is the output of smartctl --xall --json <drive>?
render: json
validations:
required: false
- type: textarea
attributes:
label: docker-compose.yml
description: |
If using docker, please provide your full docker-compose.yml file.
render: yaml
validations:
required: false
- type: textarea
attributes:
label: scrutiny.yaml
description: |
Please provide your full scrutiny.yaml file.
render: yaml
validations:
required: false
- type: textarea
attributes:
label: collector.yaml
description: |
Please provide your full collector.yaml file.
render: yaml
validations:
required: false
- type: textarea
attributes:
label: Additional relevant configuration
description: |
Please any additional relevant configuration (e.g. systemd service definitions, OS configuration)
render: text
validations:
required: false
- type: markdown
attributes:
value: |
# User Acknowledgements
> [!TIP]
> Use these links to review the existing scrutiny [Discussions](https://github.com/AnalogJ/scrutiny/discussions?discussions_q=) and [Issues](https://github.com/AnalogJ/scrutiny/issues?q=sort%3Areactions-desc).
- type: checkboxes
attributes:
label: "I acknowledge that:"
options:
- label: I have reviewed the FAQ and confirm that my issue is NOT among them.
required: true
- label: I have searched the scrutiny repository (both open and closed Discussions and Issues) and confirm this is not a duplicate of an existing issue or discussion.
required: true
- label: I have checked the "Preview" tab on all text fields to ensure that everything looks right, and have wrapped all configuration and code in code blocks with a group of three backticks (` ``` `) on separate lines.
required: true
================================================
FILE: .github/ISSUE_TEMPLATE/config.yml
================================================
blank_issues_enabled: false
contact_links:
- name: Features, Bug Reports, Questions
url: https://github.com/AnalogJ/scrutiny/discussions/new/choose
about: Our preferred starting point if you have any questions or suggestions about configuration, features or behavior.
================================================
FILE: .github/ISSUE_TEMPLATE/preapproved.md
================================================
---
name: Pre-Discussed and Approved Topics
about: |-
Only for topics already discussed and approved in the GitHub Discussions section.
---
**DO NOT OPEN A NEW ISSUE. PLEASE USE THE DISCUSSIONS SECTION.**
**I DIDN'T READ THE ABOVE LINE. PLEASE CLOSE THIS ISSUE.**
================================================
FILE: .github/workflows/ci.yaml
================================================
name: CI
# This workflow is triggered on pushes & pull requests
on:
push:
branches:
- master
pull_request:
permissions:
contents: read
jobs:
test-frontend:
name: Test Frontend
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Test Frontend
run: |
make binary-frontend-test-coverage
- name: Upload coverage
uses: actions/upload-artifact@v4
with:
name: coverage
path: ${{ github.workspace }}/webapp/frontend/coverage/lcov.info
retention-days: 1
test-backend:
name: Test Backend
runs-on: ubuntu-latest
# Service containers to run with `build` (Required for end-to-end testing)
services:
influxdb:
image: influxdb:2.8
env:
DOCKER_INFLUXDB_INIT_MODE: setup
DOCKER_INFLUXDB_INIT_USERNAME: admin
DOCKER_INFLUXDB_INIT_PASSWORD: password12345
DOCKER_INFLUXDB_INIT_ORG: scrutiny
DOCKER_INFLUXDB_INIT_BUCKET: metrics
DOCKER_INFLUXDB_INIT_ADMIN_TOKEN: my-super-secret-auth-token
ports:
- 8086:8086
env:
STATIC: true
steps:
- name: Add influxdb to hosts
run: echo "127.0.0.1 influxdb" | sudo tee -a /etc/hosts
- name: Checkout
uses: actions/checkout@v6
- name: Test Backend
run: |
make binary-clean binary-test-coverage
- name: Upload coverage
uses: actions/upload-artifact@v4
with:
name: coverage
path: ${{ github.workspace }}/coverage.txt
retention-days: 1
test-coverage:
name: Test Coverage Upload
needs:
- test-backend
- test-frontend
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Download coverage reports
uses: actions/download-artifact@v4
with:
name: coverage
- name: Upload coverage reports
uses: codecov/codecov-action@v5
with:
token: ${{ secrets.CODECOV_TOKEN }}
files: ${{ github.workspace }}/coverage.txt,${{ github.workspace }}/lcov.info
flags: unittests
fail_ci_if_error: true
verbose: true
golangci:
name: lint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/setup-go@v6
with:
go-version: 1.25
- name: golangci-lint
uses: golangci/golangci-lint-action@v9
with:
args: --issues-exit-code=0
build:
name: Build ${{ matrix.cfg.goos }}/${{ matrix.cfg.goarch }}
runs-on: ${{ matrix.cfg.on }}
env:
GOOS: ${{ matrix.cfg.goos }}
GOARCH: ${{ matrix.cfg.goarch }}
GOARM: ${{ matrix.cfg.goarm }}
STATIC: true
strategy:
matrix:
cfg:
- { on: ubuntu-latest, goos: linux, goarch: amd64 }
- { on: ubuntu-latest, goos: linux, goarch: arm, goarm: 5 }
- { on: ubuntu-latest, goos: linux, goarch: arm, goarm: 6 }
- { on: ubuntu-latest, goos: linux, goarch: arm, goarm: 7 }
- { on: ubuntu-latest, goos: linux, goarch: arm64 }
- { on: macos-latest, goos: darwin, goarch: amd64 }
- { on: macos-latest, goos: darwin, goarch: arm64 }
- { on: macos-latest, goos: freebsd, goarch: amd64 }
- { on: windows-latest, goos: windows, goarch: amd64 }
- { on: windows-latest, goos: windows, goarch: arm64 }
steps:
- name: Checkout
uses: actions/checkout@v2
- uses: actions/setup-go@v3
with:
go-version: '^1.25'
- name: Build Binaries
run: |
make binary-clean binary-all
- name: Archive
uses: actions/upload-artifact@v4
with:
name: binaries-${{ matrix.cfg.on }}-${{ matrix.cfg.goos }}-${{ matrix.cfg.goarch }}-${{ matrix.cfg.goarm || 'na' }}.zip
path: |
scrutiny-web-*
scrutiny-collector-metrics-*
makefile-docker-omnibus:
name: Build Docker Omnibus From Makefile
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Build
run: make docker-omnibus
makefile-docker-web:
name: Build Docker Web From Makefile
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Build
run: make docker-web
makefile-docker-collector:
name: Build Docker Collector From Makefile
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Build
run: make docker-collector
================================================
FILE: .github/workflows/docker-build.yaml
================================================
name: Docker
on:
push:
# Publish semver tags as releases.
tags: [ 'v*.*.*' ]
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
jobs:
collector:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- name: Checkout repository
uses: actions/checkout@v6
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
with:
platforms: 'arm64,arm'
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
# Login against a Docker registry except on PR
# https://github.com/docker/login-action
- name: Log into registry ${{ env.REGISTRY }}
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
# Extract metadata (tags, labels) for Docker
# https://github.com/docker/metadata-action
- name: Extract Docker metadata
id: meta
uses: docker/metadata-action@v5
with:
flavor: |
latest=true
suffix=-collector,onlatest=true
tags: |
type=semver,pattern=v{{major}}.{{minor}}.{{patch}}
type=semver,pattern=v{{major}}.{{minor}}
type=semver,pattern=v{{major}}
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
# Build and push Docker image with Buildx
# https://github.com/docker/build-push-action
- name: Build and push Docker image
uses: docker/build-push-action@v6
with:
platforms: linux/amd64,linux/arm64,linux/arm/v7
context: .
file: docker/Dockerfile.collector
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
web:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- name: Checkout repository
uses: actions/checkout@v6
- name: "Populate frontend version information"
run: "cd webapp/frontend && ./git.version.sh"
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
with:
platforms: 'arm64,arm'
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
# Login against a Docker registry except on PR
# https://github.com/docker/login-action
- name: Log into registry ${{ env.REGISTRY }}
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
# Extract metadata (tags, labels) for Docker
# https://github.com/docker/metadata-action
- name: Extract Docker metadata
id: meta
uses: docker/metadata-action@v5
with:
flavor: |
latest=true
suffix=-web,onlatest=true
tags: |
type=semver,pattern=v{{major}}.{{minor}}.{{patch}}
type=semver,pattern=v{{major}}.{{minor}}
type=semver,pattern=v{{major}}
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
# Build and push Docker image with Buildx
# https://github.com/docker/build-push-action
- name: Build and push Docker image
uses: docker/build-push-action@v6
with:
platforms: linux/amd64,linux/arm64,linux/arm/v7
context: .
file: docker/Dockerfile.web
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
omnibus:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- name: Checkout repository
uses: actions/checkout@v6
- name: "Populate frontend version information"
run: "cd webapp/frontend && ./git.version.sh"
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
with:
platforms: 'arm64,arm'
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
# Login against a Docker registry except on PR
# https://github.com/docker/login-action
- name: Log into registry ${{ env.REGISTRY }}
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
# Extract metadata (tags, labels) for Docker
# https://github.com/docker/metadata-action
- name: Extract Docker metadata
id: meta
uses: docker/metadata-action@v5
# tag latest and latest-omnibus
with:
flavor: |
latest=true
suffix=-omnibus,onlatest=false
tags: |
type=raw,value=latest
type=semver,pattern=v{{major}}.{{minor}}.{{patch}}
type=semver,pattern=v{{major}}.{{minor}}
type=semver,pattern=v{{major}}
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
# Build and push Docker image with Buildx
# https://github.com/docker/build-push-action
- name: Build and push Docker image
uses: docker/build-push-action@v6
with:
platforms: linux/amd64,linux/arm64
context: .
file: docker/Dockerfile
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
================================================
FILE: .github/workflows/docker-nightly.yaml
================================================
name: Docker - Nightly
on:
workflow_dispatch:
# Note: this only runs on the default branch
schedule:
- cron: '36 12 * * *'
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
jobs:
build_nightlies:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- name: Checkout repository
uses: actions/checkout@v6
- name: "Populate frontend version information"
run: "cd webapp/frontend && ./git.version.sh"
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
with:
platforms: 'arm64'
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
# Login against a Docker registry except on PR
# https://github.com/docker/login-action
- name: Log into registry ${{ env.REGISTRY }}
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
# Extract metadata (tags, labels) for Docker
# https://github.com/docker/metadata-action
- name: Extract Docker metadata for omnibus
id: meta_omnibus
uses: docker/metadata-action@v5
with:
tags: |
type=raw,enable=true,value=nightly,suffix=-omnibus
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
# Build and push Docker image with Buildx (don't push on PR)
# https://github.com/docker/build-push-action
- name: Build and push omnibus Docker image
uses: docker/build-push-action@v6
with:
platforms: linux/amd64,linux/arm64
context: .
file: docker/Dockerfile
push: true
tags: ${{ steps.meta_omnibus.outputs.tags }}
labels: ${{ steps.meta_omnibus.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
# Extract metadata (tags, labels) for Docker
# https://github.com/docker/metadata-action
- name: Extract Docker metadata for collector
id: meta_collector
uses: docker/metadata-action@v5
with:
tags: |
type=raw,enable=true,value=nightly,suffix=-collector
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
# Build and push Docker image with Buildx (don't push on PR)
# https://github.com/docker/build-push-action
- name: Build and push collector Docker image
uses: docker/build-push-action@v6
with:
platforms: linux/amd64,linux/arm64
context: .
file: docker/Dockerfile.collector
push: true
tags: ${{ steps.meta_collector.outputs.tags }}
labels: ${{ steps.meta_collector.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
# Extract metadata (tags, labels) for Docker
# https://github.com/docker/metadata-action
- name: Extract Docker metadata for web
id: meta_web
uses: docker/metadata-action@v5
with:
tags: |
type=raw,enable=true,value=nightly,suffix=-web
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
# Build and push Docker image with Buildx (don't push on PR)
# https://github.com/docker/build-push-action
- name: Build and push web Docker image
uses: docker/build-push-action@v6
with:
platforms: linux/amd64,linux/arm64
context: .
file: docker/Dockerfile.web
push: true
tags: ${{ steps.meta_web.outputs.tags }}
labels: ${{ steps.meta_web.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
================================================
FILE: .github/workflows/release.yaml
================================================
name: Release
# This workflow is triggered manually
on:
workflow_dispatch:
inputs:
version_bump_type:
description: 'Version Bump Type (major, minor, patch)'
required: true
default: 'patch'
version_metadata_path:
description: 'Path to file containing Version string'
required: true
default: 'webapp/backend/pkg/version/version.go'
jobs:
release:
name: Create Release Commit
runs-on: ubuntu-latest
container: ghcr.io/packagrio/packagr:latest-golang
# Service containers to run with `build` (Required for end-to-end testing)
services:
influxdb:
image: influxdb:2.8
env:
DOCKER_INFLUXDB_INIT_MODE: setup
DOCKER_INFLUXDB_INIT_USERNAME: admin
DOCKER_INFLUXDB_INIT_PASSWORD: password12345
DOCKER_INFLUXDB_INIT_ORG: scrutiny
DOCKER_INFLUXDB_INIT_BUCKET: metrics
DOCKER_INFLUXDB_INIT_ADMIN_TOKEN: my-super-secret-auth-token
ports:
- 8086:8086
env:
STATIC: true
steps:
- name: Git
run: |
apt-get update && apt-get install -y software-properties-common
add-apt-repository ppa:git-core/ppa && apt-get update && apt-get install -y git
git --version
- name: Checkout
uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Bump version
id: bump_version
uses: packagrio/action-bumpr-go@master
with:
version_bump_type: ${{ github.event.inputs.version_bump_type }}
version_metadata_path: ${{ github.event.inputs.version_metadata_path }}
env:
GITHUB_TOKEN: ${{ secrets.SCRUTINY_GITHUB_TOKEN }} # Leave this line unchanged
- name: Test
run: |
make binary-clean binary-test-coverage
- name: Commit Changes Locally
id: commit
uses: packagrio/action-releasr-go@master
env:
GITHUB_TOKEN: ${{ secrets.SCRUTINY_GITHUB_TOKEN }} # Leave this line unchanged
with:
version_metadata_path: ${{ github.event.inputs.version_metadata_path }}
- name: Upload workspace
uses: actions/upload-artifact@v4
with:
name: workspace
include-hidden-files: true
path: ${{ github.workspace }}/**/*
retention-days: 1
build:
name: Build ${{ matrix.cfg.goos }}/${{ matrix.cfg.goarch }}${{ matrix.cfg.goarm }}
needs: release
runs-on: ${{ matrix.cfg.on }}
env:
GOOS: ${{ matrix.cfg.goos }}
GOARCH: ${{ matrix.cfg.goarch }}
GOARM: ${{ matrix.cfg.goarm }}
STATIC: true
strategy:
matrix:
cfg:
- { on: ubuntu-latest, goos: linux, goarch: amd64 }
- { on: ubuntu-latest, goos: linux, goarch: arm, goarm: 5 }
- { on: ubuntu-latest, goos: linux, goarch: arm, goarm: 6 }
- { on: ubuntu-latest, goos: linux, goarch: arm, goarm: 7 }
- { on: ubuntu-latest, goos: linux, goarch: arm64 }
- { on: macos-latest, goos: darwin, goarch: amd64 }
- { on: macos-latest, goos: darwin, goarch: arm64 }
- { on: macos-latest, goos: freebsd, goarch: amd64 }
- { on: windows-latest, goos: windows, goarch: amd64 }
- { on: windows-latest, goos: windows, goarch: arm64 }
steps:
- name: Download workspace
uses: actions/download-artifact@v7
with:
name: workspace
- uses: actions/setup-go@v6
with:
go-version: '1.25' # The Go version to download (if necessary) and use.
- name: Build Binaries
run: |
make binary-clean binary-all
- name: Upload artifacts
uses: actions/upload-artifact@v6
with:
name: scrutiny-${{ matrix.cfg.goos }}-${{ matrix.cfg.goarch }}${{ case(matrix.cfg.goarm != '', format('-{0}', matrix.cfg.goarm), '') }}.zip
path: |
scrutiny-web-${{ matrix.cfg.goos }}-${{ matrix.cfg.goarch }}${{ case(matrix.cfg.goarm != '', format('-{0}', matrix.cfg.goarm), '') }}${{ case(matrix.cfg.goos == 'windows', '.exe', '') }}
scrutiny-collector-metrics-${{ matrix.cfg.goos }}-${{ matrix.cfg.goarch }}${{ case(matrix.cfg.goarm != '', format('-{0}', matrix.cfg.goarm), '') }}${{ case(matrix.cfg.goos == 'windows', '.exe', '') }}
build_frontend:
name: Build Frontend
needs: release
runs-on: ubuntu-latest
container: node:lts-slim
steps:
- name: Download workspace
uses: actions/download-artifact@v7
with:
name: workspace
- name: "Generate frontend version information"
run: "cd webapp/frontend && chmod +x git.version.sh && ./git.version.sh"
- name: Build Frontend
run: |
apt-get update && apt-get install -y make
make binary-frontend
tar -czf scrutiny-web-frontend.tar.gz dist
- name: Upload artifacts
uses: actions/upload-artifact@v6
with:
name: scrutiny-web-frontend.zip
path: scrutiny-web-frontend.tar.gz
release-publish:
name: Publish Release
needs:
- build
- build_frontend
runs-on: ubuntu-latest
steps:
- name: Download workspace
uses: actions/download-artifact@v7
with:
name: workspace
- name: Download binaries
uses: actions/download-artifact@v7
with:
merge-multiple: true
pattern: scrutiny-*.zip
- name: Download frontend
uses: actions/download-artifact@v7
with:
name: scrutiny-web-frontend.zip
- name: List
shell: bash
run: |
ls -alt
- name: Publish Release & Assets
id: publish
uses: packagrio/action-publishr-go@master
env:
# This is necessary in order to push a commit to the repo
GITHUB_TOKEN: ${{ secrets.SCRUTINY_GITHUB_TOKEN }} # Leave this line unchanged
with:
version_metadata_path: ${{ github.event.inputs.version_metadata_path }}
upload_assets:
scrutiny-collector-metrics-darwin-amd64
scrutiny-collector-metrics-darwin-arm64
scrutiny-collector-metrics-freebsd-amd64
scrutiny-collector-metrics-linux-amd64
scrutiny-collector-metrics-linux-arm-5
scrutiny-collector-metrics-linux-arm-6
scrutiny-collector-metrics-linux-arm-7
scrutiny-collector-metrics-linux-arm64
scrutiny-collector-metrics-windows-amd64.exe
scrutiny-collector-metrics-windows-arm64.exe
scrutiny-web-frontend.tar.gz
scrutiny-web-darwin-amd64
scrutiny-web-darwin-arm64
scrutiny-web-freebsd-amd64
scrutiny-web-linux-amd64
scrutiny-web-linux-arm-5
scrutiny-web-linux-arm-6
scrutiny-web-linux-arm-7
scrutiny-web-linux-arm64
scrutiny-web-windows-amd64.exe
scrutiny-web-windows-arm64.exe
================================================
FILE: .github/workflows/sponsors.yaml
================================================
name: Label sponsors
on:
pull_request:
types: [opened]
issues:
types: [opened]
jobs:
build:
name: is-sponsor-label
runs-on: ubuntu-latest
if: ${{ false }}
steps:
- uses: JasonEtco/is-sponsor-label-action@v1.2.0
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
================================================
FILE: .gitignore
================================================
# Created by .ignore support plugin (hsz.mobi)
### JetBrains template
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
# User-specific stuff
.idea/**/workspace.xml
.idea/**/tasks.xml
.idea/**/dictionaries
.idea/**/shelf
# Sensitive or high-churn files
.idea/**/dataSources/
.idea/**/dataSources.ids
.idea/**/dataSources.local.xml
.idea/**/sqlDataSources.xml
.idea/**/dynamic.xml
.idea/**/uiDesigner.xml
.idea/**/dbnavigator.xml
# Gradle
.idea/**/gradle.xml
.idea/**/libraries
# CMake
cmake-build-debug/
cmake-build-release/
# Mongo Explorer plugin
.idea/**/mongoSettings.xml
# File-based project format
*.iws
# IntelliJ
out/
# mpeltonen/sbt-idea plugin
.idea_modules/
# JIRA plugin
atlassian-ide-plugin.xml
# Cursive Clojure plugin
.idea/replstate.xml
# Crashlytics plugin (for Android Studio and IntelliJ)
com_crashlytics_export_strings.xml
crashlytics.properties
crashlytics-build.properties
fabric.properties
# Editor-based Rest Client
.idea/httpRequests
scrutiny.db
/dist/
vendor
/scrutiny
/scrutiny-collector-metrics-*
/scrutiny-web-*
scrutiny-*.db
scrutiny_test.db
scrutiny.yaml
coverage.txt
/config
/influxdb
.angular
web.log
================================================
FILE: .golangci.yml
================================================
version: "2"
formatters:
enable:
- gofmt
- goimports
linters:
enable:
- bodyclose
settings:
errcheck:
check-blank: true
================================================
FILE: .vscode/launch.json
================================================
{
"version": "0.2.0",
"configurations": [
{
"name": "Run Scrutiny",
"type": "go",
"request": "launch",
"mode": "auto",
"program": "${workspaceFolder}/webapp/backend/cmd/scrutiny/scrutiny.go",
"args": ["start", "--config", "./scrutiny.yaml"],
"cwd": "${workspaceFolder}",
"env": {
"DEBUG": "true"
},
"console": "integratedTerminal",
"preLaunchTask": "Build Frontend",
"serverReadyAction": {
"action": "openExternally",
"pattern": "Listening and serving HTTP on",
"uriFormat": "http://localhost:8080/web/"
}
},
{
"name": "Run Collector",
"type": "go",
"request": "launch",
"mode": "auto",
"program": "${workspaceFolder}/collector/cmd/collector-metrics/collector-metrics.go",
"args": ["run", "--debug"],
"cwd": "${workspaceFolder}",
"env": {
"COLLECTOR_DEBUG": "true"
},
"console": "integratedTerminal"
}
]
}
================================================
FILE: .vscode/tasks.json
================================================
{
"version": "2.0.0",
"tasks": [
{
"label": "Build Frontend",
"type": "shell",
"command": "cd webapp/frontend && npm run build:prod -- --output-path=../../dist"
}
]
}
================================================
FILE: AI_POLICY.md
================================================
# AI Usage Policy
scrutiny has strict rules for AI usage:
- **All AI usage in any form must be disclosed.** You must state
the tool you used (e.g. Claude Code, Cursor, Amp) along with
the extent that the work was AI-assisted.
- **Pull requests created in any way by AI can only be for accepted issues.**
Drive-by pull requests that do not reference an accepted issue will be
closed. If AI isn't disclosed but a maintainer suspects its use, the
PR will be closed. If you want to share code for a non-accepted issue,
open a discussion or attach it to an existing discussion.
- **Pull requests created by AI must have been fully verified with
human use.** AI must not create hypothetically correct code that
hasn't been tested. Importantly, you must not allow AI to write
code for platforms or environments you don't have access to manually
test on.
- **Issues and discussions can use AI assistance but must have a full
human-in-the-loop.** This means that any content generated with AI
must have been reviewed _and edited_ by a human before submission.
AI is very good at being overly verbose and including noise that
distracts from the main point. Humans must do their research and
trim this down.
- **No AI-generated media is allowed (art, images, videos, audio, etc.).**
Text and code are the only acceptable AI-generated content, per the
other rules in this policy.
- **Bad AI drivers will be banned and ridiculed in public.** You've
been warned. We love to help junior developers learn and grow, but
if you're interested in that then don't use AI, and we'll help you.
I'm sorry that bad AI drivers have ruined this for you.
These rules apply only to outside contributions to scrutiny. Maintainers
and repeat contributors (with explicit permission) are exempt from these
rules and may use AI tools at their discretion; they've proven themselves
trustworthy to apply good judgment.
## There are Humans Here
Please remember that scrutiny is maintained by humans.
Every discussion, issue, and pull request is read and reviewed by
humans (and sometimes machines, too). It is a boundary point at which
people interact with each other and the work done. It is rude and
disrespectful to approach this boundary with low-effort, unqualified
work, since it puts the burden of validation on the maintainer.
In a perfect world, AI would produce high-quality, accurate work
every time. But today, that reality depends on the driver of the AI.
And today, most drivers of AI are just not good enough. So, until either
the people get better, the AI gets better, or both, we have to have
strict rules to protect maintainers.
## AI is Welcome Here
Many maintainers embrace AI tools as a productive tool in their workflow.
As a project, scrutiny welcomes AI as a tool!
**Our reason for the strict AI policy is not due to an anti-AI stance**, but
instead due to the number of highly unqualified people using AI. It's the
people, not the tools, that are the problem.
This section is included to be transparent about the project's usage about
AI for people who may disagree with it, and to address the misconception
that this policy is anti-AI in nature.
# Credit
Adopted from [ghostty's AI policy](https://github.com/ghostty-org/ghostty/blob/1b7a15899ad40fba4ce020f537055d30eaf99ee8/AI_POLICY.md)
================================================
FILE: CONTRIBUTING.md
================================================
# Contributing to scrutiny
This document describes the process of contributing to scrutiny. It is intended
for anyone considering opening an **issue**, **discussion** or **pull request**.
> [!NOTE]
>
> The intention of these policies is not to be difficult, and
> contributions are greatly appreciated. The goal is to streamline
> and simplify the efforts of both contributers and maintainers.
## AI Usage
scrutiny has strict rules for AI usage. Please see
the [AI Usage Policy](AI_POLICY.md). **This is very important.**
## Quick Guide
### I'd like to contribute
[All issues are actionable](#issues-are-actionable). Pick one and start
working on it. Thank you. If you need help or guidance, comment on the issue.
Issues that are extra friendly to new contributors are tagged with
["contributor friendly"].
["contributor friendly"]: https://github.com/AnalogJ/scrutiny/issues?q=is%3Aissue%20is%3Aopen%20label%3A%22contributor%20friendly%22
### I have a bug! / Something isn't working
First, search the issue tracker and discussions for similar issues. Tip: also
search for [closed issues] and [discussions] — your issue might have already
been fixed!
> [!NOTE]
>
> If there is an _open_ issue or discussion that matches your problem,
> **please do not comment on it unless you have valuable insight to add**.
>
> GitHub has a very _noisy_ set of default notification settings which
> sends an email to _every participant_ in an issue/discussion every time
> someone adds a comment. Instead, use the handy upvote button for discussions,
> and/or emoji reactions on both discussions and issues, which are a visible
> yet non-disruptive way to show your support.
If your issue hasn't been reported already, open an ["Issue Triage"] discussion
and make sure to fill in the template **completely**. They are vital for
maintainers to figure out important details about your setup.
> [!WARNING]
>
> A _very_ common mistake is to file a bug report either as a Q&A or a Feature
> Request. **Please don't do this.** Otherwise, maintainers would have to ask
> for your system information again manually, and sometimes they will even ask
> you to create a new discussion because of how few detailed information is
> required for other discussion types compared to Issue Triage.
>
> Because of this, please make sure that you _only_ use the "Issue Triage"
> category for reporting bugs — thank you!
[closed issues]: https://github.com/AnalogJ/scrutiny/issues?q=is%3Aissue%20state%3Aclosed
[discussions]: https://github.com/AnalogJ/scrutiny/discussions?discussions_q=is%3Aclosed
["Issue Triage"]: https://github.com/AnalogJ/scrutiny/discussions/new?category=issue-triage
### I have an idea for a feature
Like bug reports, first search through both issues and discussions and try to
find if your feature has already been requested. Otherwise, open a discussion
in the ["Feature Requests, Ideas"] category.
["Feature Requests, Ideas"]: https://github.com/AnalogJ/scrutiny/discussions/new?category=feature-requests-ideas
### I've implemented a feature
1. If there is an issue for the feature, open a pull request straight away.
2. If there is no issue, open a discussion and link to your branch.
3. If you want to live dangerously, open a pull request and
[hope for the best](#pull-requests-implement-an-issue).
### I have a question which is neither a bug report nor a feature request
Open a [Q&A discussion].
> [!NOTE]
> If your question is about a missing feature, please open a discussion under
> the ["Feature Requests, Ideas"] category. If scrutiny is behaving
> unexpectedly, use the ["Issue Triage"] category.
>
> The "Q&A" category is strictly for other kinds of discussions and do not
> require detailed information unlike the two other categories, meaning that
> maintainers would have to spend the extra effort to ask for basic information
> if you submit a bug report under this category.
>
> Therefore, please **pay attention to the category** before opening
> discussions to save us all some time and energy. Thank you!
[Q&A discussion]: https://github.com/AnalogJ/scrutiny/discussions/new?category=q-a
## General Patterns
### Issues are Actionable
The scrutiny [issue tracker](https://github.com/AnalogJ/scrutiny/issues)
is for _actionable items_.
Unlike some other projects, scrutiny **does not use the issue tracker for
discussion or feature requests**. Instead, we use GitHub
[discussions](https://github.com/AnalogJ/scrutiny/discussions) for that.
Once a discussion reaches a point where a well-understood, actionable
item is identified, it is moved to the issue tracker. **This pattern
makes it easier for maintainers or contributors to find issues to work on
since _every issue_ is ready to be worked on.**
If you are experiencing a bug and have clear steps to reproduce it, please
open an issue. If you are experiencing a bug but you are not sure how to
reproduce it or aren't sure if it's a bug, please open a discussion.
If you have an idea for a feature, please open a discussion.
### Pull Requests Implement an Issue
Pull requests should be associated with a previously accepted issue.
**If you open a pull request for something that wasn't previously discussed,**
it may be closed or remain stale for an indefinite period of time. I'm not
saying it will never be accepted, but the odds are stacked against you.
Issues tagged with "feature" represent accepted, well-scoped feature requests.
If you implement an issue tagged with feature as described in the issue, your
pull request will be accepted with a high degree of certainty.
> [!NOTE]
>
> **Pull requests are NOT a place to discuss feature design.** Please do
> not open a WIP pull request to discuss a feature. Instead, use a discussion
> and link to your branch.
# Developer Guide
> [!NOTE]
>
> **The remainder of this file is dedicated to developers actively
> working on scrutiny.** If you're a user reporting an issue, you can
> ignore the rest of this document.
The Scrutiny repository is a [monorepo](https://en.wikipedia.org/wiki/Monorepo) containing source code for:
- Scrutiny Backend Server (API)
- Scrutiny Frontend Angular SPA
- S.M.A.R.T Collector
Depending on the functionality you are adding, you may need to setup a development environment for 1 or more projects.
# Devcontainer
Devcontainer configurations are available to build and run Scrutiny (WebUI and Collector) in a fully isolated environment.
When opening the project with vscode, choose "Reopen in Container". Three configurations are available depending on your
container runtime and setup: docker, docker-rootless, and podman.
# Modifying the Scrutiny Backend Server (API)
1. install the [Go runtime](https://go.dev/doc/install) (v1.25)
2. download the `scrutiny-web-frontend.tar.gz` for
the [latest release](https://github.com/AnalogJ/scrutiny/releases/latest). Extract to a folder named `dist`
3. create a `scrutiny.yaml` config file
```yaml
# config file for local development. store as scrutiny.yaml
version: 1
web:
listen:
port: 8080
host: 0.0.0.0
database:
# can also set absolute path here
location: ./scrutiny.db
src:
frontend:
path: ./dist
influxdb:
retention_policy: false
log:
file: 'web.log' #absolute or relative paths allowed, eg. web.log
level: DEBUG
```
4. start a InfluxDB docker container.
```bash
docker run -p 8086:8086 --rm influxdb:2.8
```
5. start the scrutiny web server
```bash
go mod vendor
go run webapp/backend/cmd/scrutiny/scrutiny.go start --config ./scrutiny.yaml
```
6. open your browser to [http://localhost:8080/web](http://localhost:8080/web)
# Modifying the Scrutiny Frontend Angular SPA
The frontend is written in Angular. If you're working on the frontend and can use mocked data rather than a real backend, you can follow the instructions below:
1. install [NodeJS](https://nodejs.org/en/download/)
2. start the Angular Frontend Application
```bash
cd webapp/frontend
npm install
npm run start -- --serve-path="/web/" --port 4200
```
3. open your browser and visit [http://localhost:4200/web](http://localhost:4200/web)
# Modifying both Scrutiny Backend and Frontend Applications
If you're developing a feature that requires changes to the backend and the frontend, or a frontend feature that requires real data,
you'll need to follow the steps below:
1. install the [Go runtime](https://go.dev/doc/install) (v1.20+)
2. install [NodeJS](https://nodejs.org/en/download/)
3. create a `scrutiny.yaml` config file
```yaml
# config file for local development. store as scrutiny.yaml
version: 1
web:
listen:
port: 8080
host: 0.0.0.0
database:
# can also set absolute path here
location: ./scrutiny.db
src:
frontend:
path: ./dist
influxdb:
retention_policy: false
log:
file: 'web.log' #absolute or relative paths allowed, eg. web.log
level: DEBUG
```
4. start a InfluxDB docker container.
```bash
docker run -p 8086:8086 --rm influxdb:2.8
```
5. build the Angular Frontend Application
```bash
cd webapp/frontend
npm install
npm run build:prod -- --watch --output-path=../../dist
# Note: if you do not add `--prod` flag, app will display mocked data for api calls.
```
6. start the scrutiny web server
```bash
go mod vendor
go run webapp/backend/cmd/scrutiny/scrutiny.go start --config ./scrutiny.yaml
```
7. open your browser to [http://localhost:8080/web](http://localhost:8080/web)
If you'd like to populate the database with some test data, you can run the following commands:
> NOTE: you may need to update the `local_time` key within the JSON file, any timestamps older than ~3 weeks will be automatically ignored
> (since the downsampling & retention policy takes effect at 2 weeks)
> This is done automatically by the `webapp/backend/pkg/models/testdata/helper.go` script
```
docker run -p 8086:8086 --rm influxdb:2.8
# curl -X POST -H "Content-Type: application/json" -d @webapp/backend/pkg/web/testdata/register-devices-req.json localhost:8080/api/devices/register
# curl -X POST -H "Content-Type: application/json" -d @webapp/backend/pkg/models/testdata/smart-ata.json localhost:8080/api/device/0x5000cca264eb01d7/smart
# curl -X POST -H "Content-Type: application/json" -d @webapp/backend/pkg/models/testdata/smart-ata-date.json localhost:8080/api/device/0x5000cca264eb01d7/smart
# curl -X POST -H "Content-Type: application/json" -d @webapp/backend/pkg/models/testdata/smart-ata-date2.json localhost:8080/api/device/0x5000cca264eb01d7/smart
# curl -X POST -H "Content-Type: application/json" -d @webapp/backend/pkg/models/testdata/smart-fail2.json localhost:8080/api/device/0x5000cca264ec3183/smart
# curl -X POST -H "Content-Type: application/json" -d @webapp/backend/pkg/models/testdata/smart-nvme.json localhost:8080/api/device/0x5002538e40a22954/smart
# curl -X POST -H "Content-Type: application/json" -d @webapp/backend/pkg/models/testdata/smart-scsi.json localhost:8080/api/device/0x5000cca252c859cc/smart
# curl -X POST -H "Content-Type: application/json" -d @webapp/backend/pkg/models/testdata/smart-scsi2.json localhost:8080/api/device/0x5000cca264ebc248/smart
go run webapp/backend/pkg/models/testdata/helper.go
curl localhost:8080/api/summary
```
# Modifying the Collector
```
brew install smartmontools
go run collector/cmd/collector-metrics/collector-metrics.go run --debug
```
# Debugging
If you need more verbose logs for debugging, you can use the following environmental variables:
- `DEBUG=true` - enables debug level logging on both the `collector` and `webapp`
- `COLLECTOR_DEBUG=true` - enables debug level logging on the `collector`
- `SCRUTINY_DEBUG=true` - enables debug level logging on the `webapp`
In addition, you can instruct scrutiny to write its logs to a file using the following environmental variables:
- `COLLECTOR_LOG_FILE=/tmp/collector.log` - write the `collector` logs to a file
- `SCRUTINY_LOG_FILE=/tmp/web.log` - write the `webapp` logs to a file
Finally, you can copy the files from the scrutiny container to your host using the following command(s)
```
docker cp scrutiny:/tmp/collector.log collector.log
docker cp scrutiny:/tmp/web.log web.log
```
# Docker Development
```
docker build -f docker/Dockerfile . -t ghcr.io/analogj/scrutiny:master-omnibus
docker run -it --rm -p 8080:8080 \
-v /run/udev:/run/udev:ro \
--cap-add SYS_RAWIO \
--device=/dev/sda \
--device=/dev/sdb \
ghcr.io/analogj/scrutiny:master-omnibus
/opt/scrutiny/bin/scrutiny-collector-metrics run
```
# Running Tests
```bash
docker run -p 8086:8086 -d --rm \
-e DOCKER_INFLUXDB_INIT_MODE=setup \
-e DOCKER_INFLUXDB_INIT_USERNAME=admin \
-e DOCKER_INFLUXDB_INIT_PASSWORD=password12345 \
-e DOCKER_INFLUXDB_INIT_ORG=scrutiny \
-e DOCKER_INFLUXDB_INIT_BUCKET=metrics \
-e DOCKER_INFLUXDB_INIT_ADMIN_TOKEN=my-super-secret-auth-token \
influxdb:2.8
go test ./...
```
================================================
FILE: LICENSE
================================================
MIT License
Copyright (c) 2020 Jason Kulatunga
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================
FILE: Makefile
================================================
.ONESHELL: # Applies to every targets in the file! .ONESHELL instructs make to invoke a single instance of the shell and provide it with the entire recipe, regardless of how many lines it contains.
.SHELLFLAGS = -ec
export GOTOOLCHAIN=go1.25.5
########################################################################################################################
# Global Env Settings
########################################################################################################################
GO_WORKSPACE ?= /go/src/github.com/analogj/scrutiny
COLLECTOR_BINARY_NAME = scrutiny-collector-metrics
WEB_BINARY_NAME = scrutiny-web
LD_FLAGS =
STATIC_TAGS =
# enable multiarch docker image builds
DOCKER_TARGETARCH_BUILD_ARG =
ifdef TARGETARCH
DOCKER_TARGETARCH_BUILD_ARG := $(DOCKER_TARGETARCH_BUILD_ARG) --build-arg TARGETARCH=$(TARGETARCH)
endif
# enable to build static binaries.
ifdef STATIC
export CGO_ENABLED = 0
LD_FLAGS := $(LD_FLAGS) -extldflags=-static
STATIC_TAGS := $(STATIC_TAGS) -tags "static netgo"
endif
ifdef GOOS
COLLECTOR_BINARY_NAME := $(COLLECTOR_BINARY_NAME)-$(GOOS)
WEB_BINARY_NAME := $(WEB_BINARY_NAME)-$(GOOS)
LD_FLAGS := $(LD_FLAGS) -X main.goos=$(GOOS)
endif
ifdef GOARCH
COLLECTOR_BINARY_NAME := $(COLLECTOR_BINARY_NAME)-$(GOARCH)
WEB_BINARY_NAME := $(WEB_BINARY_NAME)-$(GOARCH)
LD_FLAGS := $(LD_FLAGS) -X main.goarch=$(GOARCH)
endif
ifdef GOARM
COLLECTOR_BINARY_NAME := $(COLLECTOR_BINARY_NAME)-$(GOARM)
WEB_BINARY_NAME := $(WEB_BINARY_NAME)-$(GOARM)
endif
ifeq ($(OS),Windows_NT)
COLLECTOR_BINARY_NAME := $(COLLECTOR_BINARY_NAME).exe
WEB_BINARY_NAME := $(WEB_BINARY_NAME).exe
endif
########################################################################################################################
# Binary
########################################################################################################################
.PHONY: all
all: binary-all
.PHONY: binary-all
binary-all: binary-collector binary-web
@echo "built binary-collector and binary-web targets"
.PHONY: binary-clean
binary-clean:
go clean
.PHONY: binary-dep
binary-dep:
go mod vendor
.PHONY: binary-test
binary-test: binary-dep
go test -v $(STATIC_TAGS) ./...
.PHONY: lint
lint:
GOTOOLCHAIN=go1.25.5 go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.8.0
golangci-lint run ./...
.PHONY: binary-test-coverage
binary-test-coverage: binary-dep
go test -coverprofile=coverage.txt -covermode=atomic -v $(STATIC_TAGS) ./...
.PHONY: binary-collector
binary-collector: binary-dep
go build -ldflags "$(LD_FLAGS)" -o $(COLLECTOR_BINARY_NAME) $(STATIC_TAGS) ./collector/cmd/collector-metrics/
ifneq ($(OS),Windows_NT)
chmod +x $(COLLECTOR_BINARY_NAME)
file $(COLLECTOR_BINARY_NAME) || true
ldd $(COLLECTOR_BINARY_NAME) || true
./$(COLLECTOR_BINARY_NAME) || true
endif
.PHONY: binary-web
binary-web: binary-dep
go build -ldflags "$(LD_FLAGS)" -o $(WEB_BINARY_NAME) $(STATIC_TAGS) ./webapp/backend/cmd/scrutiny/
ifneq ($(OS),Windows_NT)
chmod +x $(WEB_BINARY_NAME)
file $(WEB_BINARY_NAME) || true
ldd $(WEB_BINARY_NAME) || true
./$(WEB_BINARY_NAME) || true
endif
########################################################################################################################
# Binary
########################################################################################################################
.PHONY: binary-frontend
# reduce logging, disable angular-cli analytics for ci environment
binary-frontend: export NPM_CONFIG_LOGLEVEL = warn
binary-frontend: export NG_CLI_ANALYTICS = false
binary-frontend:
cd webapp/frontend
npm install -g @angular/cli@v13-lts
mkdir -p $(CURDIR)/dist
npm ci
npm run build:prod -- --output-path=$(CURDIR)/dist
.PHONY: binary-frontend-test-coverage
# reduce logging, disable angular-cli analytics for ci environment
binary-frontend-test-coverage:
cd webapp/frontend
npm ci
npx ng test --watch=false --browsers=ChromeHeadless --code-coverage
########################################################################################################################
# Docker
# NOTE: these docker make targets are only used for local development (not used by Github Actions/CI)
########################################################################################################################
.PHONY: docker-collector
docker-collector:
@echo "building collector docker image"
docker build $(DOCKER_TARGETARCH_BUILD_ARG) -f docker/Dockerfile.collector -t ghcr.io/analogj/scrutiny-dev:collector .
.PHONY: docker-web
docker-web:
@echo "building web docker image"
docker build $(DOCKER_TARGETARCH_BUILD_ARG) -f docker/Dockerfile.web -t ghcr.io/analogj/scrutiny-dev:web .
.PHONY: docker-omnibus
docker-omnibus:
@echo "building omnibus docker image"
docker build $(DOCKER_TARGETARCH_BUILD_ARG) -f docker/Dockerfile -t ghcr.io/analogj/scrutiny-dev:omnibus .
================================================
FILE: README.md
================================================
<p align="center">
<a href="https://github.com/AnalogJ/scrutiny">
<img width="300" alt="scrutiny_view" src="webapp/frontend/src/assets/images/logo/scrutiny-logo-dark.png">
</a>
</p>
# scrutiny
[](https://github.com/AnalogJ/scrutiny/actions/workflows/ci.yaml)
[](https://codecov.io/gh/AnalogJ/scrutiny)
[](https://github.com/AnalogJ/scrutiny/blob/master/LICENSE)
[](https://godoc.org/github.com/analogj/scrutiny)
[](https://goreportcard.com/report/github.com/AnalogJ/scrutiny)
[](https://github.com/AnalogJ/scrutiny/releases)
WebUI for smartd S.M.A.R.T monitoring
> NOTE: Scrutiny is a Work-in-Progress and still has some rough edges.
[](https://imgur.com/a/5k8qMzS)
# Introduction
If you run a server with more than a couple of hard drives, you're probably already familiar with S.M.A.R.T and the `smartd` daemon. If not, it's an incredible open source project described as the following:
> smartd is a daemon that monitors the Self-Monitoring, Analysis and Reporting Technology (SMART) system built into many ATA, IDE and SCSI-3 hard drives. The purpose of SMART is to monitor the reliability of the hard drive and predict drive failures, and to carry out different types of drive self-tests.
These S.M.A.R.T hard drive self-tests can help you detect and replace failing hard drives before they cause permanent data loss. However, there's a couple issues with `smartd`:
- There are more than a hundred S.M.A.R.T attributes, however `smartd` does not differentiate between critical and informational metrics
- `smartd` does not record S.M.A.R.T attribute history, so it can be hard to determine if an attribute is degrading slowly over time.
- S.M.A.R.T attribute thresholds are set by the manufacturer. In some cases these thresholds are unset, or are so high that they can only be used to confirm a failed drive, rather than detecting a drive about to fail.
- `smartd` is a command line only tool. For head-less servers a web UI would be more valuable.
**Scrutiny is a Hard Drive Health Dashboard & Monitoring solution, merging manufacturer provided S.M.A.R.T metrics with real-world failure rates.**
# Features
Scrutiny is a simple but focused application, with a couple of core features:
- Web UI Dashboard - focused on Critical metrics
- `smartd` integration (no re-inventing the wheel)
- Auto-detection of all connected hard-drives
- S.M.A.R.T metric tracking for historical trends
- Customized thresholds using real world failure rates
- Temperature tracking
- Provided as an all-in-one Docker image (but can be installed manually)
- Configurable Alerting/Notifications via Webhooks
- (Future) Hard Drive performance testing & tracking
# Getting Started
## RAID/Virtual Drives
Scrutiny uses `smartctl --scan` to detect devices/drives.
- All RAID controllers supported by `smartctl` are automatically supported by Scrutiny.
- While some RAID controllers support passing through the underlying SMART data to `smartctl` others do not.
- In some cases `--scan` does not correctly detect the device type, returning [incomplete SMART data](https://github.com/AnalogJ/scrutiny/issues/45).
Scrutiny supports overriding detected device type via the config file: see [example.collector.yaml](https://github.com/AnalogJ/scrutiny/blob/master/example.collector.yaml)
- If you use docker, you **must** pass though the RAID virtual disk to the container using `--device` (see below)
- This device may be in `/dev/*` or `/dev/bus/*`.
- If you're unsure, run `smartctl --scan` on your host, and pass all listed devices to the container.
See [docs/TROUBLESHOOTING_DEVICE_COLLECTOR.md](./docs/TROUBLESHOOTING_DEVICE_COLLECTOR.md) for help
## Docker
> [!IMPORTANT]
> Using `latest-` tags is dangerous as it can update your image without warning. It is a best practice to pin a specific version. scrutiny pushes releases with semver tags,
> so you can use tags like `v0.8.2-omnibus`, `v0.8-web`, `v0-collector`, etc. For a list of all image tags see
> [scrutiny package versions](https://github.com/AnalogJ/scrutiny/pkgs/container/scrutiny/versions?filters%5Bversion_type%5D=tagged)
If you're using Docker, getting started is as simple as running the following command:
> See [docker/example.omnibus.docker-compose.yml](https://github.com/AnalogJ/scrutiny/blob/master/docker/example.omnibus.docker-compose.yml) for a docker-compose file.
```bash
docker run -p 8080:8080 -p 8086:8086 --restart unless-stopped \
-v `pwd`/scrutiny:/opt/scrutiny/config \
-v `pwd`/influxdb2:/opt/scrutiny/influxdb \
-v /run/udev:/run/udev:ro \
--cap-add SYS_RAWIO \
--device=/dev/sda \
--device=/dev/sdb \
--name scrutiny \
ghcr.io/analogj/scrutiny:latest-omnibus
```
- `/run/udev` is necessary to provide the Scrutiny collector with access to your device metadata
- `--cap-add SYS_RAWIO` is necessary to allow `smartctl` permission to query your device SMART data
- NOTE: If you have **NVMe** drives, you must add `--cap-add SYS_ADMIN` as well. See issue [#26](https://github.com/AnalogJ/scrutiny/issues/26#issuecomment-696817130)
- `--device` entries are required to ensure that your hard disk devices are accessible within the container.
- `ghcr.io/analogj/scrutiny:latest-omnibus` is a omnibus image, containing both the webapp server (frontend & api) as well as the S.M.A.R.T metric collector. (see below)
### Hub/Spoke Deployment
In addition to the Omnibus image (available under the `latest` tag) you can deploy in Hub/Spoke mode, which requires 3
other Docker images:
- `ghcr.io/analogj/scrutiny:latest-collector` - Contains the Scrutiny data collector, `smartctl` binary and cron-like
scheduler. You can run one collector on each server.
- `ghcr.io/analogj/scrutiny:latest-web` - Contains the Web UI and API. Only one container necessary
- `influxdb:2.8` - InfluxDB image, used by the Web container to persist SMART data. Only one container necessary
See [docs/TROUBLESHOOTING_INFLUXDB.md](./docs/TROUBLESHOOTING_INFLUXDB.md)
> See [docker/example.hubspoke.docker-compose.yml](https://github.com/AnalogJ/scrutiny/blob/master/docker/example.hubspoke.docker-compose.yml) for a docker-compose file.
```bash
docker run -p 8086:8086 --restart unless-stopped \
-v `pwd`/influxdb2:/var/lib/influxdb2 \
--name scrutiny-influxdb \
influxdb:2.8
docker run -p 8080:8080 --restart unless-stopped \
-v `pwd`/scrutiny:/opt/scrutiny/config \
--name scrutiny-web \
ghcr.io/analogj/scrutiny:latest-web
docker run --restart unless-stopped \
-v /run/udev:/run/udev:ro \
--cap-add SYS_RAWIO \
--device=/dev/sda \
--device=/dev/sdb \
-e COLLECTOR_API_ENDPOINT=http://SCRUTINY_WEB_IPADDRESS:8080 \
--name scrutiny-collector \
ghcr.io/analogj/scrutiny:latest-collector
```
### Hub rootless installation using Podman Quadlets
See [docs/INSTALL_ROOTLESS_PODMAN.md](docs/INSTALL_ROOTLESS_PODMAN.md) for instructions.
## Manual Installation (without-Docker)
While the easiest way to get started with [Scrutiny is using Docker](https://github.com/AnalogJ/scrutiny#docker),
it is possible to run it manually without much work. You can even mix and match, using Docker for one component and
a manual installation for the other.
See [docs/INSTALL_MANUAL.md](docs/INSTALL_MANUAL.md) for instructions.
## Usage
Once scrutiny is running, you can open your browser to `http://localhost:8080` and take a look at the dashboard.
If you're using the omnibus image, the collector should already have run, and your dashboard should be populate with every
drive that Scrutiny detected. The collector is configured to run once a day, but you can trigger it manually by running the command below.
For users of the docker Hub/Spoke deployment or manual install: initially the dashboard will be empty.
After the first collector run, you'll be greeted with a list of all your hard drives and their current smart status.
```bash
docker exec scrutiny /opt/scrutiny/bin/scrutiny-collector-metrics run
```
# Configuration
By default Scrutiny looks for its YAML configuration files in `/opt/scrutiny/config`
There are two configuration files available:
- Webapp/API config via `scrutiny.yaml` - [example.scrutiny.yaml](example.scrutiny.yaml).
- Collector config via `collector.yaml` - [example.collector.yaml](example.collector.yaml).
Neither file is required, however if provided, it allows you to configure how Scrutiny functions.
## Cron Schedule
Unfortunately the Cron schedule cannot be configured via the `collector.yaml` (as the collector binary needs to be trigged by a scheduler/cron).
However, if you are using the official `ghcr.io/analogj/scrutiny:latest-collector` or `ghcr.io/analogj/scrutiny:latest-omnibus` docker images,
you can use the `COLLECTOR_CRON_SCHEDULE` environmental variable to override the default cron schedule (daily @ midnight - `0 0 * * *`).
`docker run -e COLLECTOR_CRON_SCHEDULE="0 0 * * *" ...`
## Notifications
Scrutiny supports sending SMART device failure notifications via the following services:
- Custom Script (data provided via environmental variables)
- Email
- Webhooks
- Discord
- Gotify
- Hangouts
- IFTTT
- Join
- Mattermost
- ntfy
- Pushbullet
- Pushover
- Slack
- Teams
- Telegram
- Tulip
Check the `notify.urls` section of [example.scrutiny.yml](example.scrutiny.yaml) for examples.
For more information and troubleshooting, see the [TROUBLESHOOTING_NOTIFICATIONS.md](./docs/TROUBLESHOOTING_NOTIFICATIONS.md) file
### Testing Notifications
You can test that your notifications are configured correctly by posting an empty payload to the notifications health check API.
```bash
curl -X POST http://localhost:8080/api/health/notify
```
# Debug mode & Log Files
Scrutiny provides various methods to change the log level to debug and generate log files.
## Web Server/API
You can use environmental variables to enable debug logging and/or log files for the web server:
```bash
DEBUG=true
SCRUTINY_LOG_FILE=/tmp/web.log
```
You can configure the log level and log file in the config file:
```yml
log:
file: '/tmp/web.log'
level: DEBUG
```
Or if you're not using docker, you can pass CLI arguments to the web server during startup:
```bash
scrutiny start --debug --log-file /tmp/web.log
```
## Collector
You can use environmental variables to enable debug logging and/or log files for the collector:
```bash
DEBUG=true
COLLECTOR_LOG_FILE=/tmp/collector.log
```
Or if you're not using docker, you can pass CLI arguments to the collector during startup:
```bash
scrutiny-collector-metrics run --debug --log-file /tmp/collector.log
```
# Supported Architectures
| Architecture Name | Binaries | Docker |
| --- | --- | --- |
| linux-amd64 | :white_check_mark: | :white_check_mark: |
| linux-arm-5 | :white_check_mark: | |
| linux-arm-6 | :white_check_mark: | |
| linux-arm-7 | :white_check_mark: | web/collector only. see [#236](https://github.com/AnalogJ/scrutiny/issues/236) |
| linux-arm64 | :white_check_mark: | :white_check_mark: |
| freebsd-amd64 | :white_check_mark: | |
| macos-amd64 | :white_check_mark: | :white_check_mark: |
| macos-arm64 | :white_check_mark: | :white_check_mark: |
| windows-amd64 | :white_check_mark: | WIP, see [#15](https://github.com/AnalogJ/scrutiny/issues/15) |
| windows-arm64 | :white_check_mark: | |
# Contributing
Please see the [CONTRIBUTING.md](CONTRIBUTING.md) for instructions for how to develop and contribute to the scrutiny codebase.
Work your magic and then submit a pull request. We love pull requests!
If you find the documentation lacking, help us out and update this README.md. If you don't have the time to work on Scrutiny, but found something we should know about, please submit an issue.
# Versioning
We use SemVer for versioning. For the versions available, see the tags on this repository.
# Authors
* Jason Kulatunga - Initial Development - [@AnalogJ](https://github.com/AnalogJ/)
* Aram Akhavan - Maintenence - [@kaysond](https://github.com/kaysond/)
# Licenses
- MIT
- Logo: [Glasses by matias porta lezcano](https://thenounproject.com/term/glasses/775232)
# Sponsors
Scrutiny is only possible with the help of my [Github Sponsors](https://github.com/sponsors/AnalogJ/).
[](https://github.com/sponsors/AnalogJ/)
They read a simple [reddit announcement post](https://github.com/sponsors/AnalogJ/) and decided to trust & finance
a developer they've never met. It's an exciting and incredibly humbling experience.
If you found Scrutiny valuable, please consider [supporting my work](https://github.com/sponsors/AnalogJ/)
================================================
FILE: REFERENCES.md
================================================
# Gorm
- https://www.reddit.com/r/golang/comments/exmwos/golang_gorm_preload_with_last/
- https://blog.depado.eu/post/gorm-gotchas
-
# Smart Data
- https://kb.acronis.com/content/9123
================================================
FILE: collector/cmd/collector-metrics/collector-metrics.go
================================================
package main
import (
"encoding/json"
"fmt"
"io"
"log"
"os"
"strings"
"time"
"github.com/analogj/scrutiny/collector/pkg/collector"
"github.com/analogj/scrutiny/collector/pkg/config"
"github.com/analogj/scrutiny/collector/pkg/errors"
"github.com/analogj/scrutiny/webapp/backend/pkg/version"
"github.com/sirupsen/logrus"
utils "github.com/analogj/go-util/utils"
"github.com/fatih/color"
"github.com/urfave/cli/v2"
)
var goos string
var goarch string
func main() {
config, err := config.Create()
if err != nil {
fmt.Printf("FATAL: %+v\n", err)
os.Exit(1)
}
configFilePath := "/opt/scrutiny/config/collector.yaml"
configFilePathAlternative := "/opt/scrutiny/config/collector.yml"
if !utils.FileExists(configFilePath) && utils.FileExists(configFilePathAlternative) {
configFilePath = configFilePathAlternative
}
//we're going to load the config file manually, since we need to validate it.
err = config.ReadConfig(configFilePath) // Find and read the config file
if _, ok := err.(errors.ConfigFileMissingError); ok { // Handle errors reading the config file
//ignore "could not find config file"
} else if err != nil {
os.Exit(1)
}
cli.CommandHelpTemplate = `NAME:
{{.HelpName}} - {{.Usage}}
USAGE:
{{if .UsageText}}{{.UsageText}}{{else}}{{.HelpName}} {{if .ArgsUsage}}{{.ArgsUsage}}{{else}}[arguments...]{{end}}{{end}}{{if .Category}}
CATEGORY:
{{.Category}}{{end}}{{if .Description}}
DESCRIPTION:
{{.Description}}{{end}}{{if .VisibleFlags}}
OPTIONS:
{{range .VisibleFlags}}{{.}}
{{end}}{{end}}
`
app := &cli.App{
Name: "scrutiny-collector-metrics",
Usage: "smartctl data collector for scrutiny",
Version: version.VERSION,
Compiled: time.Now(),
Authors: []*cli.Author{
{
Name: "Jason Kulatunga",
Email: "jason@thesparktree.com",
},
},
Before: func(c *cli.Context) error {
collectorMetrics := "AnalogJ/scrutiny/metrics"
var versionInfo string
if len(goos) > 0 && len(goarch) > 0 {
versionInfo = fmt.Sprintf("%s.%s-%s", goos, goarch, version.VERSION)
} else {
versionInfo = fmt.Sprintf("dev-%s", version.VERSION)
}
subtitle := collectorMetrics + utils.LeftPad2Len(versionInfo, " ", 65-len(collectorMetrics))
color.New(color.FgGreen).Fprintf(c.App.Writer, utils.StripIndent(
`
___ ___ ____ __ __ ____ ____ _ _ _ _
/ __) / __)( _ \( )( )(_ _)(_ _)( \( )( \/ )
\__ \( (__ ) / )(__)( )( _)(_ ) ( \ /
(___/ \___)(_)\_)(______) (__) (____)(_)\_) (__)
%s
`), subtitle)
return nil
},
Commands: []*cli.Command{
{
Name: "run",
Usage: "Run the scrutiny smartctl metrics collector",
Action: func(c *cli.Context) error {
if c.IsSet("config") {
err = config.ReadConfig(c.String("config")) // Find and read the config file
if err != nil { // Handle errors reading the config file
//ignore "could not find config file"
fmt.Printf("Could not find config file at specified path: %s", c.String("config"))
return err
}
}
//override config with flags if set
if c.IsSet("host-id") {
config.Set("host.id", c.String("host-id")) // set/override the host-id using CLI.
}
if c.Bool("debug") {
config.Set("log.level", "DEBUG")
}
if c.IsSet("log-file") {
config.Set("log.file", c.String("log-file"))
}
if c.IsSet("api-endpoint") {
//if the user is providing an api-endpoint with a basepath (eg. http://localhost:8080/scrutiny),
//we need to ensure the basepath has a trailing slash, otherwise the url.Parse() path concatenation doesnt work.
apiEndpoint := strings.TrimSuffix(c.String("api-endpoint"), "/") + "/"
config.Set("api.endpoint", apiEndpoint)
}
collectorLogger, logFile, err := CreateLogger(config)
if logFile != nil {
defer logFile.Close()
}
if err != nil {
return err
}
settingsData, err := json.MarshalIndent(config.AllSettings(), "", "\t")
collectorLogger.Debug(string(settingsData), err)
metricCollector, err := collector.CreateMetricsCollector(
config,
collectorLogger,
config.GetString("api.endpoint"),
)
if err != nil {
return err
}
return metricCollector.Run()
},
Flags: []cli.Flag{
&cli.StringFlag{
Name: "config",
Usage: "Specify the path to the devices file",
},
&cli.StringFlag{
Name: "api-endpoint",
Usage: "The api server endpoint",
EnvVars: []string{"COLLECTOR_API_ENDPOINT", "SCRUTINY_API_ENDPOINT"},
//SCRUTINY_API_ENDPOINT is deprecated, but kept for backwards compatibility
},
&cli.StringFlag{
Name: "log-file",
Usage: "Path to file for logging. Leave empty to use STDOUT",
EnvVars: []string{"COLLECTOR_LOG_FILE"},
},
&cli.BoolFlag{
Name: "debug",
Usage: "Enable debug logging",
EnvVars: []string{"COLLECTOR_DEBUG", "DEBUG"},
},
&cli.StringFlag{
Name: "host-id",
Usage: "Host identifier/label, used for grouping devices",
Value: "",
EnvVars: []string{"COLLECTOR_HOST_ID"},
},
},
},
},
}
err = app.Run(os.Args)
if err != nil {
log.Fatal(color.HiRedString("ERROR: %v", err))
}
}
func CreateLogger(appConfig config.Interface) (*logrus.Entry, *os.File, error) {
logger := logrus.WithFields(logrus.Fields{
"type": "metrics",
})
if level, err := logrus.ParseLevel(appConfig.GetString("log.level")); err == nil {
logger.Logger.SetLevel(level)
} else {
logger.Logger.SetLevel(logrus.InfoLevel)
}
var logFile *os.File
var err error
if appConfig.IsSet("log.file") && len(appConfig.GetString("log.file")) > 0 {
logFile, err = os.OpenFile(appConfig.GetString("log.file"), os.O_CREATE|os.O_WRONLY, 0644)
if err != nil {
logger.Logger.Errorf("Failed to open log file %s for output: %s", appConfig.GetString("log.file"), err)
return nil, logFile, err
}
logger.Logger.SetOutput(io.MultiWriter(os.Stderr, logFile))
}
return logger, logFile, nil
}
================================================
FILE: collector/cmd/collector-selftest/collector-selftest.go
================================================
package main
import (
"fmt"
"io"
"log"
"os"
"time"
"github.com/analogj/scrutiny/collector/pkg/collector"
"github.com/analogj/scrutiny/webapp/backend/pkg/version"
"github.com/sirupsen/logrus"
utils "github.com/analogj/go-util/utils"
"github.com/fatih/color"
"github.com/urfave/cli/v2"
)
var goos string
var goarch string
func main() {
cli.CommandHelpTemplate = `NAME:
{{.HelpName}} - {{.Usage}}
USAGE:
{{if .UsageText}}{{.UsageText}}{{else}}{{.HelpName}} {{if .ArgsUsage}}{{.ArgsUsage}}{{else}}[arguments...]{{end}}{{end}}{{if .Category}}
CATEGORY:
{{.Category}}{{end}}{{if .Description}}
DESCRIPTION:
{{.Description}}{{end}}{{if .VisibleFlags}}
OPTIONS:
{{range .VisibleFlags}}{{.}}
{{end}}{{end}}
`
app := &cli.App{
Name: "scrutiny-collector-selftest",
Usage: "smartctl self-test data collector for scrutiny",
Version: version.VERSION,
Compiled: time.Now(),
Authors: []*cli.Author{
{
Name: "Jason Kulatunga",
Email: "jason@thesparktree.com",
},
},
Before: func(c *cli.Context) error {
collectorSelfTest := "AnalogJ/scrutiny/selftest"
var versionInfo string
if len(goos) > 0 && len(goarch) > 0 {
versionInfo = fmt.Sprintf("%s.%s-%s", goos, goarch, version.VERSION)
} else {
versionInfo = fmt.Sprintf("dev-%s", version.VERSION)
}
subtitle := collectorSelfTest + utils.LeftPad2Len(versionInfo, " ", 65-len(collectorSelfTest))
color.New(color.FgGreen).Fprintf(c.App.Writer, utils.StripIndent(
`
___ ___ ____ __ __ ____ ____ _ _ _ _
/ __) / __)( _ \( )( )(_ _)(_ _)( \( )( \/ )
\__ \( (__ ) / )(__)( )( _)(_ ) ( \ /
(___/ \___)(_)\_)(______) (__) (____)(_)\_) (__)
%s
`), subtitle)
return nil
},
Commands: []*cli.Command{
{
Name: "run",
Usage: "Run the scrutiny self-test data collector",
Action: func(c *cli.Context) error {
collectorLogger := logrus.WithFields(logrus.Fields{
"type": "selftest",
})
if c.Bool("debug") {
logrus.SetLevel(logrus.DebugLevel)
} else {
logrus.SetLevel(logrus.InfoLevel)
}
if c.IsSet("log-file") {
logFile, err := os.OpenFile(c.String("log-file"), os.O_CREATE|os.O_WRONLY, 0644)
if err != nil {
logrus.Errorf("Failed to open log file %s for output: %s", c.String("log-file"), err)
return err
}
defer logFile.Close()
logrus.SetOutput(io.MultiWriter(os.Stderr, logFile))
}
//TODO: pass in the collector, use configuration from collector-metrics
stCollector, err := collector.CreateSelfTestCollector(
collectorLogger,
c.String("api-endpoint"),
)
if err != nil {
return err
}
return stCollector.Run()
},
Flags: []cli.Flag{
&cli.StringFlag{
Name: "api-endpoint",
Usage: "The api server endpoint",
Value: "http://localhost:8080",
EnvVars: []string{"COLLECTOR_API_ENDPOINT"},
},
&cli.StringFlag{
Name: "log-file",
Usage: "Path to file for logging. Leave empty to use STDOUT",
Value: "",
EnvVars: []string{"COLLECTOR_LOG_FILE"},
},
&cli.BoolFlag{
Name: "debug",
Usage: "Enable debug logging",
EnvVars: []string{"COLLECTOR_DEBUG", "DEBUG"},
},
},
},
},
}
err := app.Run(os.Args)
if err != nil {
log.Fatal(color.HiRedString("ERROR: %v", err))
}
}
================================================
FILE: collector/pkg/collector/base.go
================================================
package collector
import (
"bytes"
"encoding/json"
"net/http"
"time"
"github.com/sirupsen/logrus"
)
var httpClient = &http.Client{Timeout: 60 * time.Second}
type BaseCollector struct {
logger *logrus.Entry
}
func (c *BaseCollector) postJson(url string, body interface{}, target interface{}) error {
requestBody, err := json.Marshal(body)
if err != nil {
return err
}
r, err := httpClient.Post(url, "application/json", bytes.NewBuffer(requestBody))
if err != nil {
return err
}
defer r.Body.Close()
return json.NewDecoder(r.Body).Decode(target)
}
// http://www.linuxguide.it/command_line/linux-manpage/do.php?file=smartctl#sect7
func (c *BaseCollector) LogSmartctlExitCode(exitCode int) {
if exitCode&0x01 != 0 {
c.logger.Errorln("smartctl could not parse commandline")
} else if exitCode&0x02 != 0 {
c.logger.Errorln("smartctl could not open device")
} else if exitCode&0x04 != 0 {
c.logger.Errorln("smartctl detected a checksum error")
} else if exitCode&0x08 != 0 {
c.logger.Errorln("smartctl detected a failing disk ")
} else if exitCode&0x10 != 0 {
c.logger.Errorln("smartctl detected a disk in pre-fail")
} else if exitCode&0x20 != 0 {
c.logger.Errorln("smartctl detected a disk close to failure")
} else if exitCode&0x40 != 0 {
c.logger.Errorln("smartctl detected a error log with errors")
} else if exitCode&0x80 != 0 {
c.logger.Errorln("smartctl detected a self test log with errors")
}
}
================================================
FILE: collector/pkg/collector/metrics.go
================================================
package collector
import (
"bytes"
"encoding/json"
"fmt"
"net/url"
"os"
"os/exec"
"strings"
"time"
"github.com/analogj/scrutiny/collector/pkg/common/shell"
"github.com/analogj/scrutiny/collector/pkg/config"
"github.com/analogj/scrutiny/collector/pkg/detect"
"github.com/analogj/scrutiny/collector/pkg/errors"
"github.com/analogj/scrutiny/collector/pkg/models"
"github.com/samber/lo"
"github.com/sirupsen/logrus"
)
type MetricsCollector struct {
config config.Interface
BaseCollector
apiEndpoint *url.URL
shell shell.Interface
}
func CreateMetricsCollector(appConfig config.Interface, logger *logrus.Entry, apiEndpoint string) (MetricsCollector, error) {
apiEndpointUrl, err := url.Parse(apiEndpoint)
if err != nil {
return MetricsCollector{}, err
}
sc := MetricsCollector{
config: appConfig,
apiEndpoint: apiEndpointUrl,
BaseCollector: BaseCollector{
logger: logger,
},
shell: shell.Create(),
}
return sc, nil
}
func (mc *MetricsCollector) Run() error {
err := mc.Validate()
if err != nil {
return err
}
apiEndpoint, _ := url.Parse(mc.apiEndpoint.String())
apiEndpoint, _ = apiEndpoint.Parse("api/devices/register") //this acts like filepath.Join()
deviceRespWrapper := new(models.DeviceWrapper)
deviceDetector := detect.Detect{
Logger: mc.logger,
Config: mc.config,
}
rawDetectedStorageDevices, err := deviceDetector.Start()
if err != nil {
return err
}
//filter any device with empty wwn (they are invalid)
detectedStorageDevices := lo.Filter[models.Device](rawDetectedStorageDevices, func(dev models.Device, _ int) bool {
return len(dev.WWN) > 0
})
mc.logger.Infoln("Sending detected devices to API, for filtering & validation")
jsonObj, _ := json.Marshal(detectedStorageDevices)
mc.logger.Debugf("Detected devices: %v", string(jsonObj))
err = mc.postJson(apiEndpoint.String(), models.DeviceWrapper{
Data: detectedStorageDevices,
}, &deviceRespWrapper)
if err != nil {
return err
}
if !deviceRespWrapper.Success {
mc.logger.Errorln("An error occurred while retrieving filtered devices")
mc.logger.Debugln(deviceRespWrapper)
return errors.ApiServerCommunicationError("An error occurred while retrieving filtered devices")
} else {
mc.logger.Debugln(deviceRespWrapper)
//var wg sync.WaitGroup
for _, device := range deviceRespWrapper.Data {
// execute collection in parallel go-routines
//wg.Add(1)
//go mc.Collect(&wg, device.WWN, device.DeviceName, device.DeviceType)
mc.Collect(device.WWN, device.DeviceName, device.DeviceType)
if mc.config.GetInt("commands.metrics_smartctl_wait") > 0 {
time.Sleep(time.Duration(mc.config.GetInt("commands.metrics_smartctl_wait")) * time.Second)
}
}
//mc.logger.Infoln("Main: Waiting for workers to finish")
//wg.Wait()
mc.logger.Infoln("Main: Completed")
}
return nil
}
func (mc *MetricsCollector) Validate() error {
mc.logger.Infoln("Verifying required tools")
_, lookErr := exec.LookPath(mc.config.GetString("commands.metrics_smartctl_bin"))
if lookErr != nil {
return errors.DependencyMissingError(fmt.Sprintf("%s binary is missing", mc.config.GetString("commands.metrics_smartctl_bin")))
}
return nil
}
// func (mc *MetricsCollector) Collect(wg *sync.WaitGroup, deviceWWN string, deviceName string, deviceType string) {
func (mc *MetricsCollector) Collect(deviceWWN string, deviceName string, deviceType string) {
//defer wg.Done()
if len(deviceWWN) == 0 {
mc.logger.Errorf("no device WWN detected for %s. Skipping collection for this device (no data association possible).\n", deviceName)
return
}
mc.logger.Infof("Collecting smartctl results for %s\n", deviceName)
fullDeviceName := fmt.Sprintf("%s%s", detect.DevicePrefix(), deviceName)
args := strings.Split(mc.config.GetCommandMetricsSmartArgs(fullDeviceName), " ")
//only include the device type if its a non-standard one. In some cases ata drives are detected as scsi in docker, and metadata is lost.
if len(deviceType) > 0 && deviceType != "scsi" && deviceType != "ata" {
args = append(args, "--device", deviceType)
}
args = append(args, fullDeviceName)
result, err := mc.shell.Command(mc.logger, mc.config.GetString("commands.metrics_smartctl_bin"), args, "", os.Environ())
resultBytes := []byte(result)
if err != nil {
if exitError, ok := err.(*exec.ExitError); ok {
// smartctl command exited with an error, we should still push the data to the API server
mc.logger.Errorf("smartctl returned an error code (%d) while processing %s\n", exitError.ExitCode(), deviceName)
mc.LogSmartctlExitCode(exitError.ExitCode())
mc.Publish(deviceWWN, resultBytes)
} else {
mc.logger.Errorf("error while attempting to execute smartctl: %s\n", deviceName)
mc.logger.Errorf("ERROR MESSAGE: %v", err)
mc.logger.Errorf("IGNORING RESULT: %v", result)
}
return
} else {
//successful run, pass the results directly to webapp backend for parsing and processing.
mc.Publish(deviceWWN, resultBytes)
}
}
func (mc *MetricsCollector) Publish(deviceWWN string, payload []byte) error {
mc.logger.Infof("Publishing smartctl results for %s\n", deviceWWN)
apiEndpoint, _ := url.Parse(mc.apiEndpoint.String())
apiEndpoint, _ = apiEndpoint.Parse(fmt.Sprintf("api/device/%s/smart", strings.ToLower(deviceWWN)))
resp, err := httpClient.Post(apiEndpoint.String(), "application/json", bytes.NewBuffer(payload))
if err != nil {
mc.logger.Errorf("An error occurred while publishing SMART data for device (%s): %v", deviceWWN, err)
return err
}
defer resp.Body.Close()
return nil
}
================================================
FILE: collector/pkg/collector/metrics_test.go
================================================
package collector
import (
"github.com/stretchr/testify/require"
"net/url"
"testing"
)
func TestApiEndpointParse(t *testing.T) {
baseURL, _ := url.Parse("http://localhost:8080/")
url1, _ := baseURL.Parse("d/e")
require.Equal(t, "http://localhost:8080/d/e", url1.String())
url2, _ := baseURL.Parse("/d/e")
require.Equal(t, "http://localhost:8080/d/e", url2.String())
}
func TestApiEndpointParse_WithBasepathWithoutTrailingSlash(t *testing.T) {
baseURL, _ := url.Parse("http://localhost:8080/scrutiny")
//This testcase is unexpected and can cause issues. We need to ensure the apiEndpoint always has a trailing slash.
url1, _ := baseURL.Parse("d/e")
require.Equal(t, "http://localhost:8080/d/e", url1.String())
url2, _ := baseURL.Parse("/d/e")
require.Equal(t, "http://localhost:8080/d/e", url2.String())
}
func TestApiEndpointParse_WithBasepathWithTrailingSlash(t *testing.T) {
baseURL, _ := url.Parse("http://localhost:8080/scrutiny/")
url1, _ := baseURL.Parse("d/e")
require.Equal(t, "http://localhost:8080/scrutiny/d/e", url1.String())
url2, _ := baseURL.Parse("/d/e")
require.Equal(t, "http://localhost:8080/d/e", url2.String())
}
================================================
FILE: collector/pkg/collector/selftest.go
================================================
package collector
import (
"github.com/sirupsen/logrus"
"net/url"
)
type SelfTestCollector struct {
BaseCollector
apiEndpoint *url.URL
logger *logrus.Entry
}
func CreateSelfTestCollector(logger *logrus.Entry, apiEndpoint string) (SelfTestCollector, error) {
apiEndpointUrl, err := url.Parse(apiEndpoint)
if err != nil {
return SelfTestCollector{}, err
}
stc := SelfTestCollector{
apiEndpoint: apiEndpointUrl,
logger: logger,
}
return stc, nil
}
func (sc *SelfTestCollector) Run() error {
return nil
}
================================================
FILE: collector/pkg/common/shell/factory.go
================================================
package shell
func Create() Interface {
return new(localShell)
}
================================================
FILE: collector/pkg/common/shell/interface.go
================================================
package shell
import (
"github.com/sirupsen/logrus"
)
// Create mock using:
// mockgen -source=collector/pkg/common/shell/interface.go -destination=collector/pkg/common/shell/mock/mock_shell.go
type Interface interface {
Command(logger *logrus.Entry, cmdName string, cmdArgs []string, workingDir string, environ []string) (string, error)
}
================================================
FILE: collector/pkg/common/shell/local_shell.go
================================================
package shell
import (
"bytes"
"errors"
"io"
"os/exec"
"path"
"strings"
"github.com/sirupsen/logrus"
)
type localShell struct{}
func (s *localShell) Command(logger *logrus.Entry, cmdName string, cmdArgs []string, workingDir string, environ []string) (string, error) {
logger.Infof("Executing command: %s %s", cmdName, strings.Join(cmdArgs, " "))
cmd := exec.Command(cmdName, cmdArgs...)
var stdBuffer bytes.Buffer
logWriters := []io.Writer{
&stdBuffer,
}
if logger.Logger.Level == logrus.DebugLevel {
logWriters = append(logWriters, logger.Logger.Out)
}
mw := io.MultiWriter(logWriters...)
cmd.Stdout = mw
cmd.Stderr = mw
if environ != nil {
cmd.Env = environ
}
if workingDir != "" && path.IsAbs(workingDir) {
cmd.Dir = workingDir
} else if workingDir != "" {
return "", errors.New("working directory must be an absolute path")
}
err := cmd.Run()
return stdBuffer.String(), err
}
================================================
FILE: collector/pkg/common/shell/local_shell_test.go
================================================
package shell
import (
"github.com/sirupsen/logrus"
"github.com/stretchr/testify/require"
"os/exec"
"testing"
)
func TestLocalShellCommand(t *testing.T) {
t.Parallel()
//setup
testShell := localShell{}
//test
result, err := testShell.Command(logrus.WithField("exec", "test"), "echo", []string{"hello world"}, "", nil)
//assert
require.NoError(t, err)
require.Equal(t, "hello world\n", result)
}
func TestLocalShellCommand_Date(t *testing.T) {
t.Parallel()
//setup
testShell := localShell{}
//test
_, err := testShell.Command(logrus.WithField("exec", "test"), "date", []string{}, "", nil)
//assert
require.NoError(t, err)
}
//
//func TestExecCmd_Error(t *testing.T) {
// t.Parallel()
//
// //setup
// bc := collector.BaseCollector {}
//
// //test
// _, err := bc.ExecCmd("smartctl", []string{"-a", "/dev/doesnotexist"}, "", nil)
//
// //assert
// exitError, castOk := err.(*exec.ExitError);
// require.True(t, castOk)
// require.Equal(t, 1, exitError.ExitCode())
//
//}
//
func TestLocalShellCommand_InvalidCommand(t *testing.T) {
t.Parallel()
//setup
testShell := localShell{}
//test
_, err := testShell.Command(logrus.WithField("exec", "test"), "invalid_binary", []string{}, "", nil)
//assert
_, castOk := err.(*exec.ExitError)
require.False(t, castOk)
}
================================================
FILE: collector/pkg/common/shell/mock/mock_shell.go
================================================
// Code generated by MockGen. DO NOT EDIT.
// Source: collector/pkg/common/shell/interface.go
// Package mock_shell is a generated GoMock package.
package mock_shell
import (
reflect "reflect"
logrus "github.com/sirupsen/logrus"
gomock "go.uber.org/mock/gomock"
)
// MockInterface is a mock of Interface interface.
type MockInterface struct {
ctrl *gomock.Controller
recorder *MockInterfaceMockRecorder
}
// MockInterfaceMockRecorder is the mock recorder for MockInterface.
type MockInterfaceMockRecorder struct {
mock *MockInterface
}
// NewMockInterface creates a new mock instance.
func NewMockInterface(ctrl *gomock.Controller) *MockInterface {
mock := &MockInterface{ctrl: ctrl}
mock.recorder = &MockInterfaceMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockInterface) EXPECT() *MockInterfaceMockRecorder {
return m.recorder
}
// Command mocks base method.
func (m *MockInterface) Command(logger *logrus.Entry, cmdName string, cmdArgs []string, workingDir string, environ []string) (string, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Command", logger, cmdName, cmdArgs, workingDir, environ)
ret0, _ := ret[0].(string)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Command indicates an expected call of Command.
func (mr *MockInterfaceMockRecorder) Command(logger, cmdName, cmdArgs, workingDir, environ interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Command", reflect.TypeOf((*MockInterface)(nil).Command), logger, cmdName, cmdArgs, workingDir, environ)
}
================================================
FILE: collector/pkg/config/config.go
================================================
package config
import (
"fmt"
"log"
"os"
"sort"
"strings"
"github.com/analogj/go-util/utils"
"github.com/analogj/scrutiny/collector/pkg/errors"
"github.com/analogj/scrutiny/collector/pkg/models"
"github.com/go-viper/mapstructure/v2"
"github.com/spf13/viper"
)
// When initializing this class the following methods must be called:
// Config.New
// Config.Init
// This is done automatically when created via the Factory.
type configuration struct {
*viper.Viper
deviceOverrides []models.ScanOverride
}
//Viper uses the following precedence order. Each item takes precedence over the item below it:
// explicit call to Set
// flag
// env
// config
// key/value store
// default
func (c *configuration) Init() error {
c.Viper = viper.New()
//set defaults
c.SetDefault("host.id", "")
c.SetDefault("devices", []string{})
c.SetDefault("log.level", "INFO")
c.SetDefault("log.file", "")
c.SetDefault("api.endpoint", "http://localhost:8080")
c.SetDefault("commands.metrics_smartctl_bin", "smartctl")
c.SetDefault("commands.metrics_scan_args", "--scan --json")
c.SetDefault("commands.metrics_info_args", "--info --json")
c.SetDefault("commands.metrics_smart_args", "--xall --json")
c.SetDefault("commands.metrics_smartctl_wait", 0)
//configure env variable parsing.
c.SetEnvPrefix("COLLECTOR")
c.SetEnvKeyReplacer(strings.NewReplacer("-", "_", ".", "_"))
c.AutomaticEnv()
//c.SetDefault("collect.short.command", "-a -o on -S on")
c.SetDefault("allow_listed_devices", []string{})
//if you want to load a non-standard location system config file (~/drawbridge.yml), use ReadConfig
c.SetConfigType("yaml")
//c.SetConfigName("drawbridge")
//c.AddConfigPath("$HOME/")
//CLI options will be added via the `Set()` function
return nil
}
func (c *configuration) ReadConfig(configFilePath string) error {
configFilePath, err := utils.ExpandPath(configFilePath)
if err != nil {
return err
}
if !utils.FileExists(configFilePath) {
log.Printf("No configuration file found at %v. Using Defaults.", configFilePath)
return errors.ConfigFileMissingError("The configuration file could not be found.")
}
//validate config file contents
//err = c.ValidateConfigFile(configFilePath)
//if err != nil {
// log.Printf("Config file at `%v` is invalid: %s", configFilePath, err)
// return err
//}
log.Printf("Loading configuration file: %s", configFilePath)
config_data, err := os.Open(configFilePath)
if err != nil {
log.Printf("Error reading configuration file: %s", err)
return err
}
err = c.MergeConfig(config_data)
if err != nil {
return err
}
return c.ValidateConfig()
}
// This function ensures that the merged config works correctly.
func (c *configuration) ValidateConfig() error {
//TODO:
// check that device prefix matches OS
// check that schema of config file is valid
// check that the collector commands are valid
commandArgStrings := map[string]string{
"commands.metrics_scan_args": c.GetString("commands.metrics_scan_args"),
"commands.metrics_info_args": c.GetString("commands.metrics_info_args"),
"commands.metrics_smart_args": c.GetString("commands.metrics_smart_args"),
}
errorStrings := []string{}
for configKey, commandArgString := range commandArgStrings {
args := strings.Split(commandArgString, " ")
//ensure that the args string contains `--json` or `-j` flag
containsJsonFlag := false
containsDeviceFlag := false
for _, flag := range args {
if strings.HasPrefix(flag, "--json") || strings.HasPrefix(flag, "-j") {
containsJsonFlag = true
}
if strings.HasPrefix(flag, "--device") || strings.HasPrefix(flag, "-d") {
containsDeviceFlag = true
}
}
if !containsJsonFlag {
errorStrings = append(errorStrings, fmt.Sprintf("configuration key '%s' is missing '--json' flag", configKey))
}
if containsDeviceFlag {
errorStrings = append(errorStrings, fmt.Sprintf("configuration key '%s' must not contain '--device' or '-d' flag", configKey))
}
}
//sort(errorStrings)
sort.Strings(errorStrings)
if len(errorStrings) == 0 {
return nil
} else {
return errors.ConfigValidationError(strings.Join(errorStrings, ", "))
}
}
func (c *configuration) GetDeviceOverrides() []models.ScanOverride {
// we have to support 2 types of device types.
// - simple device type (device_type: 'sat')
// and list of device types (type: \n- 3ware,0 \n- 3ware,1 \n- 3ware,2)
// GetString will return "" if this is a list of device types.
if c.deviceOverrides == nil {
overrides := []models.ScanOverride{}
c.UnmarshalKey("devices", &overrides, func(c *mapstructure.DecoderConfig) { c.WeaklyTypedInput = true })
c.deviceOverrides = overrides
}
return c.deviceOverrides
}
func (c *configuration) GetCommandMetricsInfoArgs(deviceName string) string {
overrides := c.GetDeviceOverrides()
for _, deviceOverrides := range overrides {
if strings.EqualFold(deviceName, deviceOverrides.Device) {
//found matching device
if len(deviceOverrides.Commands.MetricsInfoArgs) > 0 {
return deviceOverrides.Commands.MetricsInfoArgs
} else {
return c.GetString("commands.metrics_info_args")
}
}
}
return c.GetString("commands.metrics_info_args")
}
func (c *configuration) GetCommandMetricsSmartArgs(deviceName string) string {
overrides := c.GetDeviceOverrides()
for _, deviceOverrides := range overrides {
if strings.EqualFold(deviceName, deviceOverrides.Device) {
//found matching device
if len(deviceOverrides.Commands.MetricsSmartArgs) > 0 {
return deviceOverrides.Commands.MetricsSmartArgs
} else {
return c.GetString("commands.metrics_smart_args")
}
}
}
return c.GetString("commands.metrics_smart_args")
}
func (c *configuration) IsAllowlistedDevice(deviceName string) bool {
allowList := c.GetStringSlice("allow_listed_devices")
if len(allowList) == 0 {
return true
}
for _, item := range allowList {
if item == deviceName {
return true
}
}
return false
}
================================================
FILE: collector/pkg/config/config_test.go
================================================
package config_test
import (
"github.com/analogj/scrutiny/collector/pkg/config"
"github.com/analogj/scrutiny/collector/pkg/models"
"github.com/stretchr/testify/require"
"path"
"testing"
)
func TestConfiguration_InvalidConfigPath(t *testing.T) {
t.Parallel()
//setup
testConfig, _ := config.Create()
//test
err := testConfig.ReadConfig("does_not_exist.yaml")
//assert
require.Error(t, err, "should return an error")
}
func TestConfiguration_GetScanOverrides_Simple(t *testing.T) {
t.Parallel()
//setup
testConfig, _ := config.Create()
//test
err := testConfig.ReadConfig(path.Join("testdata", "simple_device.yaml"))
require.NoError(t, err, "should correctly load simple device config")
scanOverrides := testConfig.GetDeviceOverrides()
//assert
require.Equal(t, []models.ScanOverride{{Device: "/dev/sda", DeviceType: []string{"sat"}, Ignore: false}}, scanOverrides)
}
// fixes #418
func TestConfiguration_GetScanOverrides_DeviceTypeComma(t *testing.T) {
t.Parallel()
//setup
testConfig, _ := config.Create()
//test
err := testConfig.ReadConfig(path.Join("testdata", "device_type_comma.yaml"))
require.NoError(t, err, "should correctly load simple device config")
scanOverrides := testConfig.GetDeviceOverrides()
//assert
require.Equal(t, []models.ScanOverride{
{Device: "/dev/sda", DeviceType: []string{"sat", "auto"}, Ignore: false},
{Device: "/dev/sdb", DeviceType: []string{"sat,auto"}, Ignore: false},
}, scanOverrides)
}
func TestConfiguration_GetScanOverrides_Ignore(t *testing.T) {
t.Parallel()
//setup
testConfig, _ := config.Create()
//test
err := testConfig.ReadConfig(path.Join("testdata", "ignore_device.yaml"))
require.NoError(t, err, "should correctly load ignore device config")
scanOverrides := testConfig.GetDeviceOverrides()
//assert
require.Equal(t, []models.ScanOverride{{Device: "/dev/sda", DeviceType: nil, Ignore: true}}, scanOverrides)
}
func TestConfiguration_GetScanOverrides_Raid(t *testing.T) {
t.Parallel()
//setup
testConfig, _ := config.Create()
//test
err := testConfig.ReadConfig(path.Join("testdata", "raid_device.yaml"))
require.NoError(t, err, "should correctly load ignore device config")
scanOverrides := testConfig.GetDeviceOverrides()
//assert
require.Equal(t, []models.ScanOverride{
{
Device: "/dev/bus/0",
DeviceType: []string{"megaraid,14", "megaraid,15", "megaraid,18", "megaraid,19", "megaraid,20", "megaraid,21"},
Ignore: false,
},
{
Device: "/dev/twa0",
DeviceType: []string{"3ware,0", "3ware,1", "3ware,2", "3ware,3", "3ware,4", "3ware,5"},
Ignore: false,
}}, scanOverrides)
}
func TestConfiguration_InvalidCommands_MissingJson(t *testing.T) {
t.Parallel()
//setup
testConfig, _ := config.Create()
//test
err := testConfig.ReadConfig(path.Join("testdata", "invalid_commands_missing_json.yaml"))
require.EqualError(t, err, `ConfigValidationError: "configuration key 'commands.metrics_scan_args' is missing '--json' flag"`, "should throw an error because json flag is missing")
}
func TestConfiguration_InvalidCommands_IncludesDevice(t *testing.T) {
t.Parallel()
//setup
testConfig, _ := config.Create()
//test
err := testConfig.ReadConfig(path.Join("testdata", "invalid_commands_includes_device.yaml"))
require.EqualError(t, err, `ConfigValidationError: "configuration key 'commands.metrics_info_args' must not contain '--device' or '-d' flag, configuration key 'commands.metrics_smart_args' must not contain '--device' or '-d' flag"`, "should throw an error because device flags detected")
}
func TestConfiguration_OverrideCommands(t *testing.T) {
t.Parallel()
//setup
testConfig, _ := config.Create()
//test
err := testConfig.ReadConfig(path.Join("testdata", "override_commands.yaml"))
require.NoError(t, err, "should not throw an error")
require.Equal(t, "--xall --json -T permissive", testConfig.GetString("commands.metrics_smart_args"))
}
func TestConfiguration_OverrideDeviceCommands_MetricsInfoArgs(t *testing.T) {
t.Parallel()
//setup
testConfig, _ := config.Create()
//test
err := testConfig.ReadConfig(path.Join("testdata", "override_device_commands.yaml"))
require.NoError(t, err, "should correctly override device command")
//assert
require.Equal(t, "--info --json -T permissive", testConfig.GetCommandMetricsInfoArgs("/dev/sda"))
require.Equal(t, "--info --json", testConfig.GetCommandMetricsInfoArgs("/dev/sdb"))
//require.Equal(t, []models.ScanOverride{{Device: "/dev/sda", DeviceType: nil, Commands: {MetricsInfoArgs: "--info --json -T "}}}, scanOverrides)
}
func TestConfiguration_DeviceAllowList(t *testing.T) {
t.Parallel()
t.Run("present", func(t *testing.T) {
testConfig, err := config.Create()
require.NoError(t, err)
require.NoError(t, testConfig.ReadConfig(path.Join("testdata", "allow_listed_devices_present.yaml")))
require.True(t, testConfig.IsAllowlistedDevice("/dev/sda"), "/dev/sda should be allow listed")
require.False(t, testConfig.IsAllowlistedDevice("/dev/sdc"), "/dev/sda should not be allow listed")
})
t.Run("missing", func(t *testing.T) {
testConfig, err := config.Create()
require.NoError(t, err)
// Really just any other config where the key is full missing
require.NoError(t, testConfig.ReadConfig(path.Join("testdata", "override_device_commands.yaml")))
// Anything should be allow listed if the key isnt there
require.True(t, testConfig.IsAllowlistedDevice("/dev/sda"), "/dev/sda should be allow listed")
require.True(t, testConfig.IsAllowlistedDevice("/dev/sdc"), "/dev/sda should be allow listed")
})
}
================================================
FILE: collector/pkg/config/factory.go
================================================
package config
func Create() (Interface, error) {
config := new(configuration)
if err := config.Init(); err != nil {
return nil, err
}
return config, nil
}
================================================
FILE: collector/pkg/config/interface.go
================================================
package config
import (
"github.com/analogj/scrutiny/collector/pkg/models"
"github.com/spf13/viper"
)
// Create mock using:
// mockgen -source=collector/pkg/config/interface.go -destination=collector/pkg/config/mock/mock_config.go
type Interface interface {
Init() error
ReadConfig(configFilePath string) error
Set(key string, value interface{})
SetDefault(key string, value interface{})
AllSettings() map[string]interface{}
IsSet(key string) bool
Get(key string) interface{}
GetBool(key string) bool
GetInt(key string) int
GetString(key string) string
GetStringSlice(key string) []string
UnmarshalKey(key string, rawVal interface{}, decoderOpts ...viper.DecoderConfigOption) error
GetDeviceOverrides() []models.ScanOverride
GetCommandMetricsInfoArgs(deviceName string) string
GetCommandMetricsSmartArgs(deviceName string) string
IsAllowlistedDevice(deviceName string) bool
}
================================================
FILE: collector/pkg/config/mock/mock_config.go
================================================
// Code generated by MockGen. DO NOT EDIT.
// Source: collector/pkg/config/interface.go
// Package mock_config is a generated GoMock package.
package mock_config
import (
reflect "reflect"
models "github.com/analogj/scrutiny/collector/pkg/models"
viper "github.com/spf13/viper"
gomock "go.uber.org/mock/gomock"
)
// MockInterface is a mock of Interface interface.
type MockInterface struct {
ctrl *gomock.Controller
recorder *MockInterfaceMockRecorder
}
// MockInterfaceMockRecorder is the mock recorder for MockInterface.
type MockInterfaceMockRecorder struct {
mock *MockInterface
}
// NewMockInterface creates a new mock instance.
func NewMockInterface(ctrl *gomock.Controller) *MockInterface {
mock := &MockInterface{ctrl: ctrl}
mock.recorder = &MockInterfaceMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockInterface) EXPECT() *MockInterfaceMockRecorder {
return m.recorder
}
// AllSettings mocks base method.
func (m *MockInterface) AllSettings() map[string]interface{} {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "AllSettings")
ret0, _ := ret[0].(map[string]interface{})
return ret0
}
// AllSettings indicates an expected call of AllSettings.
func (mr *MockInterfaceMockRecorder) AllSettings() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AllSettings", reflect.TypeOf((*MockInterface)(nil).AllSettings))
}
// Get mocks base method.
func (m *MockInterface) Get(key string) interface{} {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Get", key)
ret0, _ := ret[0].(interface{})
return ret0
}
// Get indicates an expected call of Get.
func (mr *MockInterfaceMockRecorder) Get(key interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockInterface)(nil).Get), key)
}
// GetBool mocks base method.
func (m *MockInterface) GetBool(key string) bool {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetBool", key)
ret0, _ := ret[0].(bool)
return ret0
}
// GetBool indicates an expected call of GetBool.
func (mr *MockInterfaceMockRecorder) GetBool(key interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetBool", reflect.TypeOf((*MockInterface)(nil).GetBool), key)
}
// GetCommandMetricsInfoArgs mocks base method.
func (m *MockInterface) GetCommandMetricsInfoArgs(deviceName string) string {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetCommandMetricsInfoArgs", deviceName)
ret0, _ := ret[0].(string)
return ret0
}
// GetCommandMetricsInfoArgs indicates an expected call of GetCommandMetricsInfoArgs.
func (mr *MockInterfaceMockRecorder) GetCommandMetricsInfoArgs(deviceName interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetCommandMetricsInfoArgs", reflect.TypeOf((*MockInterface)(nil).GetCommandMetricsInfoArgs), deviceName)
}
// GetCommandMetricsSmartArgs mocks base method.
func (m *MockInterface) GetCommandMetricsSmartArgs(deviceName string) string {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetCommandMetricsSmartArgs", deviceName)
ret0, _ := ret[0].(string)
return ret0
}
// GetCommandMetricsSmartArgs indicates an expected call of GetCommandMetricsSmartArgs.
func (mr *MockInterfaceMockRecorder) GetCommandMetricsSmartArgs(deviceName interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetCommandMetricsSmartArgs", reflect.TypeOf((*MockInterface)(nil).GetCommandMetricsSmartArgs), deviceName)
}
// GetDeviceOverrides mocks base method.
func (m *MockInterface) GetDeviceOverrides() []models.ScanOverride {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetDeviceOverrides")
ret0, _ := ret[0].([]models.ScanOverride)
return ret0
}
// GetDeviceOverrides indicates an expected call of GetDeviceOverrides.
func (mr *MockInterfaceMockRecorder) GetDeviceOverrides() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetDeviceOverrides", reflect.TypeOf((*MockInterface)(nil).GetDeviceOverrides))
}
// GetInt mocks base method.
func (m *MockInterface) GetInt(key string) int {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetInt", key)
ret0, _ := ret[0].(int)
return ret0
}
// GetInt indicates an expected call of GetInt.
func (mr *MockInterfaceMockRecorder) GetInt(key interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetInt", reflect.TypeOf((*MockInterface)(nil).GetInt), key)
}
// GetString mocks base method.
func (m *MockInterface) GetString(key string) string {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetString", key)
ret0, _ := ret[0].(string)
return ret0
}
// GetString indicates an expected call of GetString.
func (mr *MockInterfaceMockRecorder) GetString(key interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetString", reflect.TypeOf((*MockInterface)(nil).GetString), key)
}
// GetStringSlice mocks base method.
func (m *MockInterface) GetStringSlice(key string) []string {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetStringSlice", key)
ret0, _ := ret[0].([]string)
return ret0
}
// GetStringSlice indicates an expected call of GetStringSlice.
func (mr *MockInterfaceMockRecorder) GetStringSlice(key interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetStringSlice", reflect.TypeOf((*MockInterface)(nil).GetStringSlice), key)
}
// Init mocks base method.
func (m *MockInterface) Init() error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Init")
ret0, _ := ret[0].(error)
return ret0
}
// Init indicates an expected call of Init.
func (mr *MockInterfaceMockRecorder) Init() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Init", reflect.TypeOf((*MockInterface)(nil).Init))
}
// IsAllowlistedDevice mocks base method.
func (m *MockInterface) IsAllowlistedDevice(deviceName string) bool {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "IsAllowlistedDevice", deviceName)
ret0, _ := ret[0].(bool)
return ret0
}
// IsAllowlistedDevice indicates an expected call of IsAllowlistedDevice.
func (mr *MockInterfaceMockRecorder) IsAllowlistedDevice(deviceName interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IsAllowlistedDevice", reflect.TypeOf((*MockInterface)(nil).IsAllowlistedDevice), deviceName)
}
// IsSet mocks base method.
func (m *MockInterface) IsSet(key string) bool {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "IsSet", key)
ret0, _ := ret[0].(bool)
return ret0
}
// IsSet indicates an expected call of IsSet.
func (mr *MockInterfaceMockRecorder) IsSet(key interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IsSet", reflect.TypeOf((*MockInterface)(nil).IsSet), key)
}
// ReadConfig mocks base method.
func (m *MockInterface) ReadConfig(configFilePath string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ReadConfig", configFilePath)
ret0, _ := ret[0].(error)
return ret0
}
// ReadConfig indicates an expected call of ReadConfig.
func (mr *MockInterfaceMockRecorder) ReadConfig(configFilePath interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ReadConfig", reflect.TypeOf((*MockInterface)(nil).ReadConfig), configFilePath)
}
// Set mocks base method.
func (m *MockInterface) Set(key string, value interface{}) {
m.ctrl.T.Helper()
m.ctrl.Call(m, "Set", key, value)
}
// Set indicates an expected call of Set.
func (mr *MockInterfaceMockRecorder) Set(key, value interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Set", reflect.TypeOf((*MockInterface)(nil).Set), key, value)
}
// SetDefault mocks base method.
func (m *MockInterface) SetDefault(key string, value interface{}) {
m.ctrl.T.Helper()
m.ctrl.Call(m, "SetDefault", key, value)
}
// SetDefault indicates an expected call of SetDefault.
func (mr *MockInterfaceMockRecorder) SetDefault(key, value interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetDefault", reflect.TypeOf((*MockInterface)(nil).SetDefault), key, value)
}
// UnmarshalKey mocks base method.
func (m *MockInterface) UnmarshalKey(key string, rawVal interface{}, decoderOpts ...viper.DecoderConfigOption) error {
m.ctrl.T.Helper()
varargs := []interface{}{key, rawVal}
for _, a := range decoderOpts {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "UnmarshalKey", varargs...)
ret0, _ := ret[0].(error)
return ret0
}
// UnmarshalKey indicates an expected call of UnmarshalKey.
func (mr *MockInterfaceMockRecorder) UnmarshalKey(key, rawVal interface{}, decoderOpts ...interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]interface{}{key, rawVal}, decoderOpts...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UnmarshalKey", reflect.TypeOf((*MockInterface)(nil).UnmarshalKey), varargs...)
}
================================================
FILE: collector/pkg/config/testdata/allow_listed_devices_present.yaml
================================================
allow_listed_devices:
- /dev/sda
- /dev/sdb
================================================
FILE: collector/pkg/config/testdata/device_type_comma.yaml
================================================
version: 1
devices:
# the scrutiny config parser will detect `sat,auto` as two separate items in a list. If you want to use `-d sat,auto` you must
# set 'sat,auto' in a list (see eg. /dev/sbd)
- device: /dev/sda
type: 'sat,auto'
- device: /dev/sdb
type:
- sat,auto
================================================
FILE: collector/pkg/config/testdata/ignore_device.yaml
================================================
version: 1
devices:
- device: /dev/sda
ignore: true
================================================
FILE: collector/pkg/config/testdata/invalid_commands_includes_device.yaml
================================================
commands:
metrics_scan_args: '--scan --json' # used to detect devices
metrics_info_args: '--info --json --device=sat' # used to determine device unique ID & register device with Scrutiny
metrics_smart_args: '--xall --json -d sat' # used to retrieve smart data for each device.
================================================
FILE: collector/pkg/config/testdata/invalid_commands_missing_json.yaml
================================================
commands:
metrics_scan_args: '--scan' # used to detect devices
metrics_info_args: '--info -j' # used to determine device unique ID & register device with Scrutiny
metrics_smart_args: '--xall --json' # used to retrieve smart data for each device.
================================================
FILE: collector/pkg/config/testdata/override_commands.yaml
================================================
commands:
metrics_scan_args: '--scan --json' # used to detect devices
metrics_info_args: '--info -j' # used to determine device unique ID & register device with Scrutiny
metrics_smart_args: '--xall --json -T permissive' # used to retrieve smart data for each device.
================================================
FILE: collector/pkg/config/testdata/override_device_commands.yaml
================================================
version: 1
devices:
- device: /dev/sda
commands:
metrics_info_args: "--info --json -T permissive"
================================================
FILE: collector/pkg/config/testdata/raid_device.yaml
================================================
version: 1
devices:
- device: /dev/bus/0
type:
- megaraid,14
- megaraid,15
- megaraid,18
- megaraid,19
- megaraid,20
- megaraid,21
- device: /dev/twa0
type:
- 3ware,0
- 3ware,1
- 3ware,2
- 3ware,3
- 3ware,4
- 3ware,5
================================================
FILE: collector/pkg/config/testdata/simple_device.yaml
================================================
version: 1
devices:
- device: /dev/sda
type: 'sat'
#
# # example to show how to ignore a specific disk/device.
# - device: /dev/sda
# ignore: true
#
# # examples showing how to force smartctl to detect disks inside a raid array/virtual disk
# - device: /dev/bus/0
# type:
# - megaraid,14
# - megaraid,15
# - megaraid,18
# - megaraid,19
# - megaraid,20
# - megaraid,21
#
# - device: /dev/twa0
# type:
# - 3ware,0
# - 3ware,1
# - 3ware,2
# - 3ware,3
# - 3ware,4
# - 3ware,5
================================================
FILE: collector/pkg/detect/detect.go
================================================
package detect
import (
"encoding/json"
"fmt"
"os"
"strings"
"github.com/analogj/scrutiny/collector/pkg/common/shell"
"github.com/analogj/scrutiny/collector/pkg/config"
"github.com/analogj/scrutiny/collector/pkg/models"
"github.com/analogj/scrutiny/webapp/backend/pkg/models/collector"
"github.com/sirupsen/logrus"
)
type Detect struct {
Logger *logrus.Entry
Config config.Interface
Shell shell.Interface
}
//private/common functions
// This function calls smartctl --scan which can be used to detect storage devices.
// It has a couple of issues however:
// - --scan does not return any results on mac
//
// To handle these issues, we have OS specific wrapper functions that update/modify these detected devices.
// models.Device returned from this function only contain the minimum data for smartctl to execute: device type and device name (device file).
func (d *Detect) SmartctlScan() ([]models.Device, error) {
//we use smartctl to detect all the drives available.
args := strings.Split(d.Config.GetString("commands.metrics_scan_args"), " ")
detectedDeviceConnJson, err := d.Shell.Command(d.Logger, d.Config.GetString("commands.metrics_smartctl_bin"), args, "", os.Environ())
if err != nil {
d.Logger.Errorf("Error scanning for devices: %v", err)
return nil, err
}
var detectedDeviceConns models.Scan
err = json.Unmarshal([]byte(detectedDeviceConnJson), &detectedDeviceConns)
if err != nil {
d.Logger.Errorf("Error decoding detected devices: %v", err)
return nil, err
}
detectedDevices := d.TransformDetectedDevices(detectedDeviceConns)
return detectedDevices, nil
}
// updates a device model with information from smartctl --scan
// It has a couple of issues however:
// - WWN is provided as component data, rather than a "string". We'll have to generate the WWN value ourselves
// - WWN from smartctl only provided for ATA protocol drives, NVMe and SCSI drives do not include WWN.
func (d *Detect) SmartCtlInfo(device *models.Device) error {
fullDeviceName := fmt.Sprintf("%s%s", DevicePrefix(), device.DeviceName)
args := strings.Split(d.Config.GetCommandMetricsInfoArgs(fullDeviceName), " ")
//only include the device type if its a non-standard one. In some cases ata drives are detected as scsi in docker, and metadata is lost.
if len(device.DeviceType) > 0 && device.DeviceType != "scsi" && device.DeviceType != "ata" {
args = append(args, "--device", device.DeviceType)
}
args = append(args, fullDeviceName)
availableDeviceInfoJson, err := d.Shell.Command(d.Logger, d.Config.GetString("commands.metrics_smartctl_bin"), args, "", os.Environ())
if err != nil {
d.Logger.Errorf("Could not retrieve device information for %s: %v", device.DeviceName, err)
return err
}
var availableDeviceInfo collector.SmartInfo
err = json.Unmarshal([]byte(availableDeviceInfoJson), &availableDeviceInfo)
if err != nil {
d.Logger.Errorf("Could not decode device information for %s: %v", device.DeviceName, err)
return err
}
//WWN: this is a serial number/world-wide number that will not change.
//DeviceType and DeviceName are already populated, however may change between collector runs (eg. config/host restart)
//InterfaceType:
device.ModelName = availableDeviceInfo.ModelName
device.InterfaceSpeed = availableDeviceInfo.InterfaceSpeed.Current.String
device.SerialNumber = availableDeviceInfo.SerialNumber
device.Firmware = availableDeviceInfo.FirmwareVersion
device.RotationSpeed = availableDeviceInfo.RotationRate
device.Capacity = availableDeviceInfo.Capacity()
device.FormFactor = availableDeviceInfo.FormFactor.Name
device.DeviceType = availableDeviceInfo.Device.Type
device.DeviceProtocol = availableDeviceInfo.Device.Protocol
if len(availableDeviceInfo.Vendor) > 0 {
device.Manufacturer = availableDeviceInfo.Vendor
}
//populate WWN is possible if present
if availableDeviceInfo.Wwn.Naa != 0 { //valid values are 1-6 (5 is what we handle correctly)
d.Logger.Info("Generating WWN")
wwn := Wwn{
Naa: availableDeviceInfo.Wwn.Naa,
Oui: availableDeviceInfo.Wwn.Oui,
Id: availableDeviceInfo.Wwn.ID,
}
device.WWN = strings.ToLower(wwn.ToString())
d.Logger.Debugf("NAA: %d OUI: %d Id: %d => WWN: %s", wwn.Naa, wwn.Oui, wwn.Id, device.WWN)
} else {
d.Logger.Info("Using WWN Fallback")
d.wwnFallback(device)
}
if len(device.WWN) == 0 {
// no WWN populated after WWN lookup and fallback. we need to throw an error
errMsg := fmt.Sprintf("no WWN (or fallback) populated for device: %s. Device will be registered, but no data will be published for this device. ", device.DeviceName)
d.Logger.Errorf("%v", errMsg)
return fmt.Errorf("%v", errMsg)
}
return nil
}
// function will remove devices that are marked for "ignore" in config file
// will also add devices that are specified in config file, but "missing" from smartctl --scan
// this function will also update the deviceType to the option specified in config.
func (d *Detect) TransformDetectedDevices(detectedDeviceConns models.Scan) []models.Device {
groupedDevices := map[string][]models.Device{}
for _, scannedDevice := range detectedDeviceConns.Devices {
deviceFile := strings.ToLower(scannedDevice.Name)
// If the user has defined a device allow list, and this device isnt there, then ignore it
if !d.Config.IsAllowlistedDevice(deviceFile) {
continue
}
detectedDevice := models.Device{
HostId: d.Config.GetString("host.id"),
DeviceType: scannedDevice.Type,
DeviceName: strings.TrimPrefix(deviceFile, DevicePrefix()),
}
//find (or create) a slice to contain the devices in this group
if groupedDevices[deviceFile] == nil {
groupedDevices[deviceFile] = []models.Device{}
}
// add this scanned device to the group
groupedDevices[deviceFile] = append(groupedDevices[deviceFile], detectedDevice)
}
//now tha we've "grouped" all the devices, lets override any groups specified in the config file.
for _, overrideDevice := range d.Config.GetDeviceOverrides() {
overrideDeviceFile := strings.ToLower(overrideDevice.Device)
if overrideDevice.Ignore {
// this device file should be deleted if it exists
delete(groupedDevices, overrideDeviceFile)
} else {
//create a new device group, and replace the one generated by smartctl --scan
overrideDeviceGroup := []models.Device{}
if overrideDevice.DeviceType != nil {
for _, overrideDeviceType := range overrideDevice.DeviceType {
overrideDeviceGroup = append(overrideDeviceGroup, models.Device{
HostId: d.Config.GetString("host.id"),
DeviceType: overrideDeviceType,
DeviceName: strings.TrimPrefix(overrideDeviceFile, DevicePrefix()),
})
}
} else {
//user may have specified device in config file without device type (default to scanned device type)
//check if the device file was detected by the scanner
var deviceType string
if scannedDevice, foundScannedDevice := groupedDevices[overrideDeviceFile]; foundScannedDevice {
if len(scannedDevice) > 0 {
//take the device type from the first grouped device
deviceType = scannedDevice[0].DeviceType
} else {
deviceType = "ata"
}
} else {
//fallback to ata if no scanned device detected
deviceType = "ata"
}
overrideDeviceGroup = append(overrideDeviceGroup, models.Device{
HostId: d.Config.GetString("host.id"),
DeviceType: deviceType,
DeviceName: strings.TrimPrefix(overrideDeviceFile, DevicePrefix()),
})
}
groupedDevices[overrideDeviceFile] = overrideDeviceGroup
}
}
//flatten map
detectedDevices := []models.Device{}
for _, group := range groupedDevices {
detectedDevices = append(detectedDevices, group...)
}
return detectedDevices
}
================================================
FILE: collector/pkg/detect/detect_test.go
================================================
package detect_test
import (
"os"
"strings"
"testing"
mock_shell "github.com/analogj/scrutiny/collector/pkg/common/shell/mock"
mock_config "github.com/analogj/scrutiny/collector/pkg/config/mock"
"github.com/analogj/scrutiny/collector/pkg/detect"
"github.com/analogj/scrutiny/collector/pkg/models"
"github.com/sirupsen/logrus"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.uber.org/mock/gomock"
)
func TestDetect_SmartctlScan(t *testing.T) {
// setup
mockCtrl := gomock.NewController(t)
fakeConfig := mock_config.NewMockInterface(mockCtrl)
fakeConfig.EXPECT().GetString("host.id").AnyTimes().Return("")
fakeConfig.EXPECT().GetDeviceOverrides().AnyTimes().Return([]models.ScanOverride{})
fakeConfig.EXPECT().GetString("commands.metrics_smartctl_bin").AnyTimes().Return("smartctl")
fakeConfig.EXPECT().GetString("commands.metrics_scan_args").AnyTimes().Return("--scan --json")
fakeConfig.EXPECT().IsAllowlistedDevice(gomock.Any()).AnyTimes().Return(true)
fakeShell := mock_shell.NewMockInterface(mockCtrl)
testScanResults, err := os.ReadFile("testdata/smartctl_scan_simple.json")
fakeShell.EXPECT().Command(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes().Return(string(testScanResults), err)
d := detect.Detect{
Logger: logrus.WithFields(logrus.Fields{}),
Shell: fakeShell,
Config: fakeConfig,
}
// test
scannedDevices, err := d.SmartctlScan()
// assert
require.NoError(t, err)
require.Equal(t, 7, len(scannedDevices))
require.Equal(t, "scsi", scannedDevices[0].DeviceType)
}
func TestDetect_SmartctlScan_Megaraid(t *testing.T) {
// setup
mockCtrl := gomock.NewController(t)
fakeConfig := mock_config.NewMockInterface(mockCtrl)
fakeConfig.EXPECT().GetString("host.id").AnyTimes().Return("")
fakeConfig.EXPECT().GetDeviceOverrides().AnyTimes().Return([]models.ScanOverride{})
fakeConfig.EXPECT().GetString("commands.metrics_smartctl_bin").AnyTimes().Return("smartctl")
fakeConfig.EXPECT().GetString("commands.metrics_scan_args").AnyTimes().Return("--scan --json")
fakeConfig.EXPECT().IsAllowlistedDevice(gomock.Any()).AnyTimes().Return(true)
fakeShell := mock_shell.NewMockInterface(mockCtrl)
testScanResults, err := os.ReadFile("testdata/smartctl_scan_megaraid.json")
fakeShell.EXPECT().Command(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes().Return(string(testScanResults), err)
d := detect.Detect{
Logger: logrus.WithFields(logrus.Fields{}),
Shell: fakeShell,
Config: fakeConfig,
}
// test
scannedDevices, err := d.SmartctlScan()
// assert
require.NoError(t, err)
require.Equal(t, 2, len(scannedDevices))
require.Equal(t, []models.Device{
{DeviceName: "bus/0", DeviceType: "megaraid,0"},
{DeviceName: "bus/0", DeviceType: "megaraid,1"},
}, scannedDevices)
}
func TestDetect_SmartctlScan_Nvme(t *testing.T) {
// setup
mockCtrl := gomock.NewController(t)
fakeConfig := mock_config.NewMockInterface(mockCtrl)
fakeConfig.EXPECT().GetString("host.id").AnyTimes().Return("")
fakeConfig.EXPECT().GetDeviceOverrides().AnyTimes().Return([]models.ScanOverride{})
fakeConfig.EXPECT().GetString("commands.metrics_smartctl_bin").AnyTimes().Return("smartctl")
fakeConfig.EXPECT().GetString("commands.metrics_scan_args").AnyTimes().Return("--scan --json")
fakeConfig.EXPECT().IsAllowlistedDevice(gomock.Any()).AnyTimes().Return(true)
fakeShell := mock_shell.NewMockInterface(mockCtrl)
testScanResults, err := os.ReadFile("testdata/smartctl_scan_nvme.json")
fakeShell.EXPECT().Command(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes().Return(string(testScanResults), err)
d := detect.Detect{
Logger: logrus.WithFields(logrus.Fields{}),
Shell: fakeShell,
Config: fakeConfig,
}
// test
scannedDevices, err := d.SmartctlScan()
// assert
require.NoError(t, err)
require.Equal(t, 1, len(scannedDevices))
require.Equal(t, []models.Device{
{DeviceName: "nvme0", DeviceType: "nvme"},
}, scannedDevices)
}
func TestDetect_TransformDetectedDevices_Empty(t *testing.T) {
// setup
mockCtrl := gomock.NewController(t)
fakeConfig := mock_config.NewMockInterface(mockCtrl)
fakeConfig.EXPECT().GetString("host.id").AnyTimes().Return("")
fakeConfig.EXPECT().GetDeviceOverrides().AnyTimes().Return([]models.ScanOverride{})
fakeConfig.EXPECT().GetString("commands.metrics_smartctl_bin").AnyTimes().Return("smartctl")
fakeConfig.EXPECT().GetString("commands.metrics_scan_args").AnyTimes().Return("--scan --json")
fakeConfig.EXPECT().IsAllowlistedDevice(gomock.Any()).AnyTimes().Return(true)
detectedDevices := models.Scan{
Devices: []models.ScanDevice{
{
Name: "/dev/sda",
InfoName: "/dev/sda",
Protocol: "scsi",
Type: "scsi",
},
},
}
d := detect.Detect{
Config: fakeConfig,
}
// test
transformedDevices := d.TransformDetectedDevices(detectedDevices)
// assert
require.Equal(t, "sda", transformedDevices[0].DeviceName)
require.Equal(t, "scsi", transformedDevices[0].DeviceType)
}
func TestDetect_TransformDetectedDevices_Ignore(t *testing.T) {
// setup
mockCtrl := gomock.NewController(t)
fakeConfig := mock_config.NewMockInterface(mockCtrl)
fakeConfig.EXPECT().GetString("host.id").AnyTimes().Return("")
fakeConfig.EXPECT().GetDeviceOverrides().AnyTimes().Return([]models.ScanOverride{{Device: "/dev/sda", DeviceType: nil, Ignore: true}})
fakeConfig.EXPECT().GetString("commands.metrics_smartctl_bin").AnyTimes().Return("smartctl")
fakeConfig.EXPECT().GetString("commands.metrics_scan_args").AnyTimes().Return("--scan --json")
fakeConfig.EXPECT().IsAllowlistedDevice(gomock.Any()).AnyTimes().Return(true)
detectedDevices := models.Scan{
Devices: []models.ScanDevice{
{
Name: "/dev/sda",
InfoName: "/dev/sda",
Protocol: "scsi",
Type: "scsi",
},
},
}
d := detect.Detect{
Config: fakeConfig,
}
// test
transformedDevices := d.TransformDetectedDevices(detectedDevices)
// assert
require.Equal(t, []models.Device{}, transformedDevices)
}
func TestDetect_TransformDetectedDevices_Raid(t *testing.T) {
// setup
mockCtrl := gomock.NewController(t)
fakeConfig := mock_config.NewMockInterface(mockCtrl)
fakeConfig.EXPECT().GetString("host.id").AnyTimes().Return("")
fakeConfig.EXPECT().GetString("commands.metrics_smartctl_bin").AnyTimes().Return("smartctl")
fakeConfig.EXPECT().GetString("commands.metrics_scan_args").AnyTimes().Return("--scan --json")
fakeConfig.EXPECT().IsAllowlistedDevice(gomock.Any()).AnyTimes().Return(true)
fakeConfig.EXPECT().GetDeviceOverrides().AnyTimes().Return([]models.ScanOverride{
{
Device: "/dev/bus/0",
DeviceType: []string{"megaraid,14", "megaraid,15", "megaraid,18", "megaraid,19", "megaraid,20", "megaraid,21"},
Ignore: false,
},
{
Device: "/dev/twa0",
DeviceType: []string{"3ware,0", "3ware,1", "3ware,2", "3ware,3", "3ware,4", "3ware,5"},
Ignore: false,
},
})
detectedDevices := models.Scan{
Devices: []models.ScanDevice{
{
Name: "/dev/bus/0",
InfoName: "/dev/bus/0",
Protocol: "scsi",
Type: "scsi",
},
},
}
d := detect.Detect{
Config: fakeConfig,
}
// test
transformedDevices := d.TransformDetectedDevices(detectedDevices)
// assert
require.Equal(t, 12, len(transformedDevices))
}
func TestDetect_TransformDetectedDevices_Simple(t *testing.T) {
// setup
mockCtrl := gomock.NewController(t)
fakeConfig := mock_config.NewMockInterface(mockCtrl)
fakeConfig.EXPECT().GetString("host.id").AnyTimes().Return("")
fakeConfig.EXPECT().GetString("commands.metrics_smartctl_bin").AnyTimes().Return("smartctl")
fakeConfig.EXPECT().GetString("commands.metrics_scan_args").AnyTimes().Return("--scan --json")
fakeConfig.EXPECT().GetDeviceOverrides().AnyTimes().Return([]models.ScanOverride{{Device: "/dev/sda", DeviceType: []string{"sat+megaraid"}}})
fakeConfig.EXPECT().IsAllowlistedDevice(gomock.Any()).AnyTimes().Return(true)
detectedDevices := models.Scan{
Devices: []models.ScanDevice{
{
Name: "/dev/sda",
InfoName: "/dev/sda",
Protocol: "ata",
Type: "ata",
},
},
}
d := detect.Detect{
Config: fakeConfig,
}
// test
transformedDevices := d.TransformDetectedDevices(detectedDevices)
// assert
require.Equal(t, 1, len(transformedDevices))
require.Equal(t, "sat+megaraid", transformedDevices[0].DeviceType)
}
// test https://github.com/AnalogJ/scrutiny/issues/255#issuecomment-1164024126
func TestDetect_TransformDetectedDevices_WithoutDeviceTypeOverride(t *testing.T) {
// setup
mockCtrl := gomock.NewController(t)
fakeConfig := mock_config.NewMockInterface(mockCtrl)
fakeConfig.EXPECT().GetString("host.id").AnyTimes().Return("")
fakeConfig.EXPECT().GetString("commands.metrics_smartctl_bin").AnyTimes().Return("smartctl")
fakeConfig.EXPECT().GetString("commands.metrics_scan_args").AnyTimes().Return("--scan --json")
fakeConfig.EXPECT().GetDeviceOverrides().AnyTimes().Return([]models.ScanOverride{{Device: "/dev/sda"}})
fakeConfig.EXPECT().IsAllowlistedDevice(gomock.Any()).AnyTimes().Return(true)
detectedDevices := models.Scan{
Devices: []models.ScanDevice{
{
Name: "/dev/sda",
InfoName: "/dev/sda",
Protocol: "ata",
Type: "scsi",
},
},
}
d := detect.Detect{
Config: fakeConfig,
}
// test
transformedDevices := d.TransformDetectedDevices(detectedDevices)
// assert
require.Equal(t, 1, len(transformedDevices))
require.Equal(t, "scsi", transformedDevices[0].DeviceType)
}
func TestDetect_TransformDetectedDevices_WhenDeviceNotDetected(t *testing.T) {
// setup
mockCtrl := gomock.NewController(t)
fakeConfig := mock_config.NewMockInterface(mockCtrl)
fakeConfig.EXPECT().GetString("host.id").AnyTimes().Return("")
fakeConfig.EXPECT().GetString("commands.metrics_smartctl_bin").AnyTimes().Return("smartctl")
fakeConfig.EXPECT().GetString("commands.metrics_scan_args").AnyTimes().Return("--scan --json")
fakeConfig.EXPECT().GetDeviceOverrides().AnyTimes().Return([]models.ScanOverride{{Device: "/dev/sda"}})
detectedDevices := models.Scan{}
d := detect.Detect{
Config: fakeConfig,
}
// test
transformedDevices := d.TransformDetectedDevices(detectedDevices)
// assert
require.Equal(t, 1, len(transformedDevices))
require.Equal(t, "ata", transformedDevices[0].DeviceType)
}
func TestDetect_TransformDetectedDevices_AllowListFilters(t *testing.T) {
mockCtrl := gomock.NewController(t)
fakeConfig := mock_config.NewMockInterface(mockCtrl)
fakeConfig.EXPECT().GetString("host.id").AnyTimes().Return("")
fakeConfig.EXPECT().GetString("commands.metrics_smartctl_bin").AnyTimes().Return("smartctl")
fakeConfig.EXPECT().GetString("commands.metrics_scan_args").AnyTimes().Return("--scan --json")
fakeConfig.EXPECT().GetDeviceOverrides().AnyTimes().Return([]models.ScanOverride{{Device: "/dev/sda", DeviceType: []string{"sat+megaraid"}}})
fakeConfig.EXPECT().IsAllowlistedDevice("/dev/sda").Return(true)
fakeConfig.EXPECT().IsAllowlistedDevice("/dev/sdb").Return(false)
detectedDevices := models.Scan{
Devices: []models.ScanDevice{
{
Name: "/dev/sda",
InfoName: "/dev/sda",
Protocol: "ata",
Type: "ata",
},
{
Name: "/dev/sdb",
InfoName: "/dev/sdb",
Protocol: "ata",
Type: "ata",
},
},
}
d := detect.Detect{
Config: fakeConfig,
}
// test
transformedDevices := d.TransformDetectedDevices(detectedDevices)
// assert
require.Equal(t, 1, len(transformedDevices))
require.Equal(t, "sda", transformedDevices[0].DeviceName)
}
func TestDetect_SmartCtlInfo(t *testing.T) {
t.Run("should report nvme info", func(t *testing.T) {
ctrl := gomock.NewController(t)
const (
someArgs = "--info --json"
// device info
someDeviceName = "some-device-name"
someModelName = "KCD61LUL3T84"
someSerialNumber = "61Q0A05UT7B8"
someFirmware = "8002"
someDeviceProtocol = "NVMe"
someDeviceType = "nvme"
someCapacity int64 = 3840755982336
)
fakeConfig := mock_config.NewMockInterface(ctrl)
fakeConfig.EXPECT().
GetCommandMetricsInfoArgs("/dev/" + someDeviceName).
Return(someArgs)
fakeConfig.EXPECT().
GetString("commands.metrics_smartctl_bin").
Return("smartctl")
someLogger := logrus.WithFields(logrus.Fields{})
smartctlInfoResults, err := os.ReadFile("testdata/smartctl_info_nvme.json")
require.NoError(t, err)
fakeShell := mock_shell.NewMockInterface(ctrl)
fakeShell.EXPECT().
Command(someLogger, "smartctl", append(strings.Split(someArgs, " "), "/dev/"+someDeviceName), "", gomock.Any()).
Return(string(smartctlInfoResults), err)
d := detect.Detect{
Logger: someLogger,
Shell: fakeShell,
Config: fakeConfig,
}
someDevice := &models.Device{
WWN: "some wwn",
DeviceName: someDeviceName,
}
require.NoError(t, d.SmartCtlInfo(someDevice))
assert.Equal(t, someDeviceName, someDevice.DeviceName)
assert.Equal(t, someModelName, someDevice.ModelName)
assert.Equal(t, someSerialNumber, someDevice.SerialNumber)
assert.Equal(t, someFirmware, someDevice.Firmware)
assert.Equal(t, someDeviceProtocol, someDevice.DeviceProtocol)
assert.Equal(t, someDeviceType, someDevice.DeviceType)
assert.Equal(t, someCapacity, someDevice.Capacity)
})
}
================================================
FILE: collector/pkg/detect/devices_darwin.go
================================================
package detect
import (
"github.com/analogj/scrutiny/collector/pkg/common/shell"
"github.com/analogj/scrutiny/collector/pkg/models"
"github.com/jaypipes/ghw"
"strings"
)
func DevicePrefix() string {
return "/dev/"
}
func (d *Detect) Start() ([]models.Device, error) {
d.Shell = shell.Create()
// call the base/common functionality to get a list of devices
detectedDevices, err := d.SmartctlScan()
if err != nil {
return nil, err
}
//smartctl --scan doesn't seem to detect mac nvme drives, lets see if we can detect them manually.
missingDevices, err := d.findMissingDevices(detectedDevices) //we dont care about the error here, just continue retrieving device info.
if err == nil {
detectedDevices = append(detectedDevices, missingDevices...)
}
//inflate device info for detected devices.
for ndx, _ := range detectedDevices {
d.SmartCtlInfo(&detectedDevices[ndx]) //ignore errors.
}
return detectedDevices, nil
}
func (d *Detect) findMissingDevices(detectedDevices []models.Device) ([]models.Device, error) {
missingDevices := []models.Device{}
block, err := ghw.Block()
if err != nil {
d.Logger.Errorf("Error getting block storage info: %v", err)
return nil, err
}
for _, disk := range block.Disks {
// ignore optical drives and floppy disks
if disk.DriveType == ghw.DRIVE_TYPE_FDD || disk.DriveType == ghw.DRIVE_TYPE_ODD {
d.Logger.Debugf(" => Ignore: Optical or floppy disk - (found %s)\n", disk.DriveType.String())
continue
}
// ignore removable disks
if disk.IsRemovable {
d.Logger.Debugf(" => Ignore: Removable disk (%v)\n", disk.IsRemovable)
continue
}
// ignore virtual disks & mobile phone storage devices
if disk.StorageController == ghw.STORAGE_CONTROLLER_VIRTIO || disk.StorageController == ghw.STORAGE_CONTROLLER_MMC {
d.Logger.Debugf(" => Ignore: Virtual/multi-media storage controller - (found %s)\n", disk.StorageController.String())
continue
}
// Skip unknown storage controllers, not usually S.M.A.R.T compatible.
if disk.StorageController == ghw.STORAGE_CONTROLLER_UNKNOWN {
d.Logger.Debugf(" => Ignore: Unknown storage controller - (found %s)\n", disk.StorageController.String())
continue
}
//check if device is already detected.
alreadyDetected := false
diskName := strings.TrimPrefix(disk.Name, DevicePrefix())
for _, detectedDevice := range detectedDevices {
if detectedDevice.DeviceName == diskName {
alreadyDetected = true
break
}
}
if !alreadyDetected {
missingDevices = append(missingDevices, models.Device{
DeviceName: diskName,
DeviceType: "",
})
}
}
return missingDevices, nil
}
//WWN values NVMe and SCSI
func (d *Detect) wwnFallback(detectedDevice *models.Device) {
block, err := ghw.Block()
if err == nil {
for _, disk := range block.Disks {
if disk.Name == detectedDevice.DeviceName && strings.ToLower(disk.WWN) != "unknown" {
d.Logger.Debugf("Found matching block device. WWN: %s", disk.WWN)
detectedDevice.WWN = disk.WWN
break
}
}
}
//no WWN found, or could not open Block devices. Either way, fallback to serial number
if len(detectedDevice.WWN) == 0 {
d.Logger.Debugf("WWN is empty, falling back to serial number: %s", detectedDevice.SerialNumber)
detectedDevice.WWN = detectedDevice.SerialNumber
}
//wwn must always be lowercase.
detectedDevice.WWN = strings.ToLower(detectedDevice.WWN)
}
================================================
FILE: collector/pkg/detect/devices_freebsd.go
================================================
package detect
import (
"github.com/analogj/scrutiny/collector/pkg/common/shell"
"github.com/analogj/scrutiny/collector/pkg/models"
"github.com/jaypipes/ghw"
"strings"
)
func DevicePrefix() string {
return "/dev/"
}
func (d *Detect) Start() ([]models.Device, error) {
d.Shell = shell.Create()
// call the base/common functionality to get a list of devices
detectedDevices, err := d.SmartctlScan()
if err != nil {
return nil, err
}
//inflate device info for detected devices.
for ndx, _ := range detectedDevices {
d.SmartCtlInfo(&detectedDevices[ndx]) //ignore errors.
}
return detectedDevices, nil
}
//WWN values NVMe and SCSI
func (d *Detect) wwnFallback(detectedDevice *models.Device) {
block, err := ghw.Block()
if err == nil {
for _, disk := range block.Disks {
if disk.Name == detectedDevice.DeviceName && strings.ToLower(disk.WWN) != "unknown" {
d.Logger.Debugf("Found matching block device. WWN: %s", disk.WWN)
detectedDevice.WWN = disk.WWN
break
}
}
}
//no WWN found, or could not open Block devices. Either way, fallback to serial number
if len(detectedDevice.WWN) == 0 {
d.Logger.Debugf("WWN is empty, falling back to serial number: %s", detectedDevice.SerialNumber)
detectedDevice.WWN = detectedDevice.SerialNumber
}
//wwn must always be lowercase.
detectedDevice.WWN = strings.ToLower(detectedDevice.WWN)
}
================================================
FILE: collector/pkg/detect/devices_linux.go
================================================
package detect
import (
"fmt"
"os"
"path/filepath"
"strings"
"github.com/analogj/scrutiny/collector/pkg/common/shell"
"github.com/analogj/scrutiny/collector/pkg/models"
"github.com/jaypipes/ghw"
)
func DevicePrefix() string {
return "/dev/"
}
func (d *Detect) Start() ([]models.Device, error) {
d.Shell = shell.Create()
// call the base/common functionality to get a list of devices
detectedDevices, err := d.SmartctlScan()
if err != nil {
return nil, err
}
//inflate device info for detected devices.
for ndx := range detectedDevices {
d.SmartCtlInfo(&detectedDevices[ndx]) //ignore errors.
populateUdevInfo(&detectedDevices[ndx]) //ignore errors.
}
return detectedDevices, nil
}
// WWN values NVMe and SCSI
func (d *Detect) wwnFallback(detectedDevice *models.Device) {
block, err := ghw.Block()
if err == nil {
for _, disk := range block.Disks {
if disk.Name == detectedDevice.DeviceName && strings.ToLower(disk.WWN) != "unknown" {
d.Logger.Debugf("Found matching block device. WWN: %s", disk.WWN)
detectedDevice.WWN = disk.WWN
break
}
}
}
//no WWN found, or could not open Block devices. Either way, fallback to serial number
if len(detectedDevice.WWN) == 0 {
d.Logger.Debugf("WWN is empty, falling back to serial number: %s", detectedDevice.SerialNumber)
detectedDevice.WWN = detectedDevice.SerialNumber
}
//wwn must always be lowercase.
detectedDevice.WWN = strings.ToLower(detectedDevice.WWN)
}
// as discussed in
// - https://github.com/AnalogJ/scrutiny/issues/225
// - https://github.com/jaypipes/ghw/issues/59#issue-361915216
// udev exposes its data in a standardized way under /run/udev/data/....
func populateUdevInfo(detectedDevice *models.Device) error {
// Get device major:minor numbers
// `cat /sys/class/block/sda/dev`
devNo, err := os.ReadFile(filepath.Join("/sys/class/block/", detectedDevice.DeviceName, "dev"))
if err != nil {
return err
}
// Look up block device in udev runtime database
// `cat /run/udev/data/b8:0`
udevID := "b" + strings.TrimSpace(string(devNo))
udevBytes, err := os.ReadFile(filepath.Join("/run/udev/data/", udevID))
if err != nil {
return err
}
deviceMountPaths := []string{}
udevInfo := make(map[string]string)
for _, udevLine := range strings.Split(string(udevBytes), "\n") {
if strings.HasPrefix(udevLine, "E:") {
if s := strings.SplitN(udevLine[2:], "=", 2); len(s) == 2 {
udevInfo[s[0]] = s[1]
}
} else if strings.HasPrefix(udevLine, "S:") {
deviceMountPaths = append(deviceMountPaths, udevLine[2:])
}
}
//Set additional device information.
if deviceLabel, exists := udevInfo["ID_FS_LABEL"]; exists {
detectedDevice.DeviceLabel = deviceLabel
}
if deviceUUID, exists := udevInfo["ID_FS_UUID"]; exists {
detectedDevice.DeviceUUID = deviceUUID
}
if deviceSerialID, exists := udevInfo["ID_SERIAL"]; exists {
detectedDevice.DeviceSerialID = fmt.Sprintf("%s-%s", udevInfo["ID_BUS"], deviceSerialID)
}
return nil
}
================================================
FILE: collector/pkg/detect/devices_linux_test.go
================================================
package detect_test
import (
"github.com/analogj/scrutiny/collector/pkg/detect"
"github.com/stretchr/testify/require"
"testing"
)
func TestDevicePrefix(t *testing.T) {
//setup
//test
//assert
require.Equal(t, "/dev/", detect.DevicePrefix())
}
================================================
FILE: collector/pkg/detect/devices_windows.go
================================================
package detect
import (
"github.com/analogj/scrutiny/collector/pkg/common/shell"
"github.com/analogj/scrutiny/collector/pkg/models"
"strings"
)
func DevicePrefix() string {
return ""
}
func (d *Detect) Start() ([]models.Device, error) {
d.Shell = shell.Create()
// call the base/common functionality to get a list of devices
detectedDevices, err := d.SmartctlScan()
if err != nil {
return nil, err
}
//inflate device info for detected devices.
for ndx, _ := range detectedDevices {
d.SmartCtlInfo(&detectedDevices[ndx]) //ignore errors.
}
return detectedDevices, nil
}
//WWN values NVMe and SCSI
func (d *Detect) wwnFallback(detectedDevice *models.Device) {
//fallback to serial number
if len(detectedDevice.WWN) == 0 {
detectedDevice.WWN = detectedDevice.SerialNumber
}
//wwn must always be lowercase.
detectedDevice.WWN = strings.ToLower(detectedDevice.WWN)
}
================================================
FILE: collector/pkg/detect/testdata/smartctl_info_nvme.json
================================================
{
"json_format_version": [
1,
0
],
"smartctl": {
"version": [
7,
2
],
"svn_revision": "5155",
"platform_info": "x86_64-linux-6.1.69-talos",
"build_info": "(local build)",
"argv": [
"smartctl",
"--info",
"--json",
"/dev/nvme4"
],
"exit_status": 0
},
"device": {
"name": "/dev/nvme4",
"info_name": "/dev/nvme4",
"type": "nvme",
"protocol": "NVMe"
},
"model_name": "KCD61LUL3T84",
"serial_number": "61Q0A05UT7B8",
"firmware_version": "8002",
"nvme_pci_vendor": {
"id": 7695,
"subsystem_id": 7695
},
"nvme_ieee_oui_identifier": 9233294,
"nvme_total_capacity": 3840755982336,
"nvme_unallocated_capacity": 0,
"nvme_controller_id": 1,
"nvme_version": {
"string": "1.4",
"value": 66560
},
"nvme_number_of_namespaces": 16,
"local_time": {
"time_t": 1706045146,
"asctime": "Tue Jan 23 21:25:46 2024 UTC"
}
}
================================================
FILE: collector/pkg/detect/testdata/smartctl_scan_megaraid.json
================================================
{
"json_format_version": [
1,
0
],
"smartctl": {
"version": [
7,
1
],
"svn_revision": "5022",
"platform_info": "x86_64-linux-5.4.0-45-generic",
"build_info": "(local build)",
"argv": [
"smartctl",
"-j",
"--scan"
],
"exit_status": 0
},
"devices": [
{
"name": "/dev/bus/0",
"info_name": "/dev/bus/0 [megaraid_disk_00]",
"type": "megaraid,0",
"protocol": "SCSI"
},
{
"name": "/dev/bus/0",
"info_name": "/dev/bus/0 [megaraid_disk_01]",
"type": "megaraid,1",
"protocol": "SCSI"
}
]
}
================================================
FILE: collector/pkg/detect/testdata/smartctl_scan_nvme.json
================================================
{
"json_format_version": [
1,
0
],
"smartctl": {
"version": [
7,
0
],
"svn_revision": "4883",
"platform_info": "x86_64-linux-4.19.107-Unraid",
"build_info": "(local build)",
"argv": [
"smartctl",
"-j",
"--scan"
],
"exit_status": 0
},
"devices": [
{
"name": "/dev/nvme0",
"info_name": "/dev/nvme0",
"type": "nvme",
"protocol": "NVMe"
}
]
}
================================================
FILE: collector/pkg/detect/testdata/smartctl_scan_simple.json
================================================
{
"json_format_version": [
1,
0
],
"smartctl": {
"version": [
7,
0
],
"svn_revision": "4883",
"platform_info": "x86_64-linux-5.15.32-flatcar",
"build_info": "(local build)",
"argv": [
"smartctl",
"--scan",
"-j"
],
"exit_status": 0
},
"devices": [
{
"name": "/dev/sda",
"info_name": "/dev/sda",
"type": "scsi",
"protocol": "SCSI"
},
{
"name": "/dev/sdb",
"info_name": "/dev/sdb",
"type": "scsi",
"protocol": "SCSI"
},
{
"name": "/dev/sdc",
"info_name": "/dev/sdc",
"type": "scsi",
"protocol": "SCSI"
},
{
"name": "/dev/sdd",
"info_name": "/dev/sdd",
"type": "scsi",
"protocol": "SCSI"
},
{
"name": "/dev/sde",
"info_name": "/dev/sde",
"type": "scsi",
"protocol": "SCSI"
},
{
"name": "/dev/sdf",
"info_name": "/dev/sdf",
"type": "scsi",
"protocol": "SCSI"
},
{
"name": "/dev/sdg",
"info_name": "/dev/sdg",
"type": "scsi",
"protocol": "SCSI"
}
]
}
================================================
FILE: collector/pkg/detect/wwn.go
================================================
package detect
import (
"fmt"
"strings"
)
type Wwn struct {
Naa uint64 `json:"naa"`
Oui uint64 `json:"oui"`
Id uint64 `json:"id"`
VendorCode string `json:"vendor_code"`
}
// this is an incredibly basic converter, that only works for "Registered" IEEE format - NAA5
// https://standards.ieee.org/content/dam/ieee-standards/standards/web/documents/tutorials/fibre.pdf
// references:
// - https://metacpan.org/pod/Device::WWN
// - https://en.wikipedia.org/wiki/World_Wide_Name
// - https://storagemeat.blogspot.com/2012/08/decoding-wwids-or-how-to-tell-whats-what.html
// - https://bryanchain.com/2016/01/20/breaking-down-an-naa-id-world-wide-name/
/*
+----------+---+---+---+---+---+---+---+---+
| Byte/Bit | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
+----------+---+---+---+---+---+---+---+---+
| 0 | NAA (5h) | (MSB) |
+----------+---------------+ +
| 1 | |
+----------+ IEEE OUI |
| 2 | |
+----------+ +---------------+
| 3 | (LSB) | (MSB) |
+----------+---------------+ +
| 4 | |
| | |
+----------+ |
| 5 | Vendor ID |
+----------+ |
| 6 | |
+----------+ |
| 7 | (LSB) |
+----------+-------------------------------+
*/
func (wwn *Wwn) ToString() string {
var wwnBuffer uint64
wwnBuffer = wwn.Id //start with vendor ID
wwnBuffer += (wwn.Oui << 36) //add left-shifted OUI
wwnBuffer += (wwn.Naa << 60) //NAA is a number from 1-6, so decimal == hex.
//TODO: may need to support additional versions in the future.
return strings.ToLower(fmt.Sprintf("%#x", wwnBuffer))
}
================================================
FILE: collector/pkg/detect/wwn_test.go
================================================
package detect_test
import (
"testing"
"github.com/analogj/scrutiny/collector/pkg/detect"
"github.com/stretchr/testify/require"
)
func TestWwn_FromStringTable(t *testing.T) {
//setup
var tests = []struct {
wwnStr string
wwn detect.Wwn
}{
{"0x5002538e40a22954", detect.Wwn{Naa: 5, Oui: 9528, Id: 61213911380}}, //sda
{"0x5000cca264eb01d7", detect.Wwn{Naa: 5, Oui: 3274, Id: 10283057623}}, //sdb
{"0x5000cca264ec3183", detect.Wwn{Naa: 5, Oui: 3274, Id: 10283135363}}, //sdc
{"0x5000cca252c859cc", detect.Wwn{Naa: 5, Oui: 3274, Id: 9978796492}}, //sdd
{"0x50014ee20b2a72a9", detect.Wwn{Naa: 5, Oui: 5358, Id: 8777265833}}, //sde
{"0x5000cca264ebc248", detect.Wwn{Naa: 5, Oui: 3274, Id: 10283106888}}, //sdf
{"0x5000c500673e6b5f", detect.Wwn{Naa: 5, Oui: 3152, Id: 1732143967}}, //sdg
}
//test
for _, tt := range tests {
t.Run(tt.wwnStr, func(t *testing.T) {
str := tt.wwn.ToString()
require.Equal(t, tt.wwnStr, str)
})
}
}
================================================
FILE: collector/pkg/errors/errors.go
================================================
package errors
import (
"fmt"
)
// Raised when config file is missing
type ConfigFileMissingError string
func (str ConfigFileMissingError) Error() string {
return fmt.Sprintf("ConfigFileMissingError: %q", string(str))
}
// Raised when the config file doesnt match schema
type ConfigValidationError string
func (str ConfigValidationError) Error() string {
return fmt.Sprintf("ConfigValidationError: %q", string(str))
}
// Raised when a dependency (like smartd or ssh-agent) is missing
type DependencyMissingError string
func (str DependencyMissingError) Error() string {
return fmt.Sprintf("DependencyMissingError: %q", string(str))
}
// Raised when there was an error communicating with API server
type ApiServerCommunicationError string
func (str ApiServerCommunicationError) Error() string {
return fmt.Sprintf("ApiServerCommunicationError: %q", string(str))
}
================================================
FILE: collector/pkg/errors/errors_test.go
================================================
package errors_test
import (
"github.com/analogj/scrutiny/collector/pkg/errors"
"github.com/stretchr/testify/require"
"testing"
)
//func TestCheckErr_WithoutError(t *testing.T) {
// t.Parallel()
//
// //assert
// require.NotPanics(t, func() {
// errors.CheckErr(nil)
// })
//}
//func TestCheckErr_Error(t *testing.T) {
// t.Parallel()
//
// //assert
// require.Panics(t, func() {
// errors.CheckErr(stderrors.New("This is an error"))
// })
//}
func TestErrors(t *testing.T) {
t.Parallel()
//assert
require.Implements(t, (*error)(nil), errors.ConfigFileMissingError("test"), "should implement the error interface")
require.Implements(t, (*error)(nil), errors.ConfigValidationError("test"), "should implement the error interface")
require.Implements(t, (*error)(nil), errors.DependencyMissingError("test"), "should implement the error interface")
}
================================================
FILE: collector/pkg/models/device.go
================================================
package models
type Device struct {
WWN string `json:"wwn"`
DeviceName string `json:"device_name"`
DeviceUUID string `json:"device_uuid"`
DeviceSerialID string `json:"device_serial_id"`
DeviceLabel string `json:"device_label"`
Manufacturer string `json:"manufacturer"`
ModelName string `json:"model_name"`
InterfaceType string `json:"interface_type"`
InterfaceSpeed string `json:"interface_speed"`
SerialNumber string `json:"serial_number"`
Firmware string `json:"firmware"`
RotationSpeed int `json:"rotational_speed"`
Capacity int64 `json:"capacity"`
FormFactor string `json:"form_factor"`
SmartSupport bool `json:"smart_support"`
DeviceProtocol string `json:"device_protocol"` //protocol determines which smart attribute types are available (ATA, NVMe, SCSI)
DeviceType string `json:"device_type"` //device type is used for querying with -d/t flag, should only be used by collector.
// User provided metadata
Label string `json:"label"`
HostId string `json:"host_id"`
}
type DeviceWrapper struct {
Success bool `json:"success,omitempty"`
Errors []error `json:"errors,omitempty"`
Data []Device `json:"data"`
}
================================================
FILE: collector/pkg/models/scan.go
================================================
package models
type Scan struct {
JSONFormatVersion []int `json:"json_format_version"`
Smartctl struct {
Version []int `json:"version"`
SvnRevision string `json:"svn_revision"`
PlatformInfo string `json:"platform_info"`
BuildInfo string `json:"build_info"`
Argv []string `json:"argv"`
ExitStatus int `json:"exit_status"`
} `json:"smartctl"`
Devices []ScanDevice `json:"devices"`
}
type ScanDevice struct {
Name string `json:"name"`
InfoName string `json:"info_name"`
Type string `json:"type"`
Protocol string `json:"protocol"`
}
================================================
FILE: collector/pkg/models/scan_override.go
================================================
package models
type ScanOverride struct {
Device string `mapstructure:"device"`
DeviceType []string `mapstructure:"type"`
Ignore bool `mapstructure:"ignore"`
Commands struct {
MetricsInfoArgs string `mapstructure:"metrics_info_args"`
MetricsSmartArgs string `mapstructure:"metrics_smart_args"`
} `mapstructure:"commands"`
}
================================================
FILE: docker/Dockerfile
================================================
# syntax=docker/dockerfile:1.4
########################################################################################################################
# Omnibus Image
########################################################################################################################
######## Build the frontend
FROM --platform=${BUILDPLATFORM} node AS frontendbuild
WORKDIR /go/src/github.com/analogj/scrutiny
COPY --link Makefile /go/src/github.com/analogj/scrutiny/
COPY --link webapp/frontend /go/src/github.com/analogj/scrutiny/webapp/frontend
RUN make binary-frontend
######## Build the backend
FROM golang:1.25-trixie as backendbuild
WORKDIR /go/src/github.com/analogj/scrutiny
COPY --link Makefile /go/src/github.com/analogj/scrutiny/
COPY --link go.mod go.sum /go/src/github.com/analogj/scrutiny/
COPY --link collector /go/src/github.com/analogj/scrutiny/collector
COPY --link webapp/backend /go/src/github.com/analogj/scrutiny/webapp/backend
RUN apt-get update && DEBIAN_FRONTEND=noninteractive \
apt-get install -y --no-install-recommends \
file \
&& rm -rf /var/lib/apt/lists/*
RUN make binary-clean binary-all WEB_BINARY_NAME=scrutiny
######## Build smartmontools from source
FROM debian:trixie-slim AS smartmontoolsbuild
ARG SMARTMONTOOLS_VER=7.5
RUN apt-get update && DEBIAN_FRONTEND=noninteractive \
apt-get install -y --no-install-recommends \
ca-certificates curl gcc g++ gnupg make \
&& rm -rf /var/lib/apt/lists/*
RUN curl -L "https://github.com/smartmontools/smartmontools/releases/download/RELEASE_$(echo ${SMARTMONTOOLS_VER} | tr '.' '_')/smartmontools-${SMARTMONTOOLS_VER}.tar.gz" -o /tmp/smartmontools.tar.gz \
&& tar -xzf /tmp/smartmontools.tar.gz -C /tmp \
&& cd /tmp/smartmontools-${SMARTMONTOOLS_VER} \
&& ./configure --prefix=/usr LDFLAGS='-static' --without-libcap-ng --without-libsystemd \
&& make -j"$(nproc)" \
&& make install \
&& /usr/sbin/update-smart-drivedb \
&& rm -rf /tmp/smartmontools*
######## Combine build artifacts in runtime image
FROM debian:trixie-slim AS runtime
ARG TARGETARCH
EXPOSE 8080
WORKDIR /opt/scrutiny
ENV PATH="/opt/scrutiny/bin:${PATH}"
ENV INFLUXD_CONFIG_PATH=/opt/scrutiny/influxdb
ENV S6VER="3.1.6.2"
ENV INFLUXVER="2.2.0"
ENV S6_SERVICES_READYTIME=1000
SHELL ["/usr/bin/sh", "-c"]
RUN apt-get update && DEBIAN_FRONTEND=noninteractive \
apt-get install -y --no-install-recommends \
ca-certificates \
cron \
curl \
tzdata \
procps \
xz-utils \
&& rm -rf /var/lib/apt/lists/* \
&& update-ca-certificates \
&& case ${TARGETARCH} in \
"amd64") S6_ARCH=x86_64 ;; \
"arm64") S6_ARCH=aarch64 ;; \
esac \
&& curl https://github.com/just-containers/s6-overlay/releases/download/v${S6VER}/s6-overlay-noarch.tar.xz -L -s --output /tmp/s6-overlay-noarch.tar.xz \
&& tar -Jxpf /tmp/s6-overlay-noarch.tar.xz -C / \
&& rm -rf /tmp/s6-overlay-noarch.tar.xz \
&& curl https://github.com/just-containers/s6-overlay/releases/download/v${S6VER}/s6-overlay-${S6_ARCH}.tar.xz -L -s --output /tmp/s6-overlay-${S6_ARCH}.tar.xz \
&& tar -Jxpf /tmp/s6-overlay-${S6_ARCH}.tar.xz -C / \
&& rm -rf /tmp/s6-overlay-${S6_ARCH}.tar.xz
RUN curl -L https://dl.influxdata.com/influxdb/releases/influxdb2-${INFLUXVER}-${TARGETARCH}.deb --output /tmp/influxdb2-${INFLUXVER}-${TARGETARCH}.deb \
&& dpkg -i --force-all /tmp/influxdb2-${INFLUXVER}-${TARGETARCH}.deb \
&& rm -rf /tmp/influxdb2-${INFLUXVER}-${TARGETARCH}.deb
COPY /rootfs /
COPY --from=smartmontoolsbuild /usr/sbin/smartctl /usr/sbin/smartctl
COPY --from=smartmontoolsbuild /usr/share/smartmontools/ /usr/share/smartmontools/
COPY --link --from=backendbuild --chmod=755 /go/src/github.com/analogj/scrutiny/scrutiny /opt/scrutiny/bin/
COPY --link --from=backendbuild --chmod=755 /go/src/github.com/analogj/scrutiny/scrutiny-collector-metrics /opt/scrutiny/bin/
COPY --link --from=frontendbuild --chmod=644 /go/src/github.com/analogj/scrutiny/dist /opt/scrutiny/web
RUN chmod 0644 /etc/cron.d/scrutiny && \
rm -f /etc/cron.daily/* && \
mkdir -p /opt/scrutiny/web && \
mkdir -p /opt/scrutiny/config && \
chmod -R ugo+rwx /opt/scrutiny/config && \
chmod +x /etc/cont-init.d/* && \
chmod +x /etc/services.d/*/run && \
chmod +x /etc/services.d/*/finish
CMD ["/init"]
================================================
FILE: docker/Dockerfile.collector
================================================
########################################################################################################################
# Collector Image
########################################################################################################################
########
FROM golang:1.25-trixie AS backendbuild
WORKDIR /go/src/github.com/analogj/scrutiny
COPY --link Makefile /go/src/github.com/analogj/scrutiny/
COPY --link go.mod go.sum /go/src/github.com/analogj/scrutiny/
COPY --link collector /go/src/github.com/analogj/scrutiny/collector
COPY --link webapp/backend /go/src/github.com/analogj/scrutiny/webapp/backend
RUN apt-get update && apt-get install -y file && rm -rf /var/lib/apt/lists/*
RUN make binary-clean binary-collector
######## Build smartmontools from source
FROM debian:trixie-slim AS smartmontoolsbuild
ARG SMARTMONTOOLS_VER=7.5
RUN apt-get update && DEBIAN_FRONTEND=noninteractive \
apt-get install -y --no-install-recommends \
ca-certificates curl gcc g++ gnupg make \
&& rm -rf /var/lib/apt/lists/*
RUN curl -L "https://github.com/smartmontools/smartmontools/releases/download/RELEASE_$(echo ${SMARTMONTOOLS_VER} | tr '.' '_')/smartmontools-${SMARTMONTOOLS_VER}.tar.gz" -o /tmp/smartmontools.tar.gz \
&& tar -xzf /tmp/smartmontools.tar.gz -C /tmp \
&& cd /tmp/smartmontools-${SMARTMONTOOLS_VER} \
&& ./configure --prefix=/usr LDFLAGS='-static' --without-libcap-ng --without-libsystemd \
&& make -j"$(nproc)" \
&& make install \
&& /usr/sbin/update-smart-drivedb \
&& rm -rf /tmp/smartmontools*
########
FROM debian:trixie-slim AS runtime
WORKDIR /opt/scrutiny
ENV PATH="/opt/scrutiny/bin:${PATH}"
RUN apt-get update && apt-get install -y cron ca-certificates tzdata && rm -rf /var/lib/apt/lists/* && update-ca-certificates
COPY --from=smartmontoolsbuild /usr/sbin/smartctl /usr/sbin/smartctl
COPY --from=smartmontoolsbuild /usr/share/smartmontools/ /usr/share/smartmontools/
COPY /docker/entrypoint-collector.sh /entrypoint-collector.sh
COPY /rootfs/etc/cron.d/scrutiny /etc/cron.d/scrutiny
COPY --from=backendbuild /go/src/github.com/analogj/scrutiny/scrutiny-collector-metrics /opt/scrutiny/bin/
RUN chmod +x /opt/scrutiny/bin/scrutiny-collector-metrics && \
chmod +x /entrypoint-collector.sh && \
chmod 0644 /etc/cron.d/scrutiny && \
rm -f /etc/cron.daily/apt /etc/cron.daily/dpkg /etc/cron.daily/passwd
CMD ["/entrypoint-collector.sh"]
================================================
FILE: docker/Dockerfile.smartmontools
================================================
########################################################################################################################
# Smartmontools Builder
# - Builds smartctl from source as a static binary.
# - Updates the drive database to include the latest drive models since it can change between releases.
# - Used as a shared build stage by Dockerfile and Dockerfile.collector.
########################################################################################################################
FROM debian:trixie-slim
ARG SMARTMONTOOLS_VER=7.5
RUN apt-get update && DEBIAN_FRONTEND=noninteractive \
apt-get install -y --no-install-recommends \
ca-certificates curl gcc g++ gnupg make \
&& rm -rf /var/lib/apt/lists/*
RUN curl -L "https://github.com/smartmontools/smartmontools/releases/download/RELEASE_$(echo ${SMARTMONTOOLS_VER} | tr '.' '_')/smartmontools-${SMARTMONTOOLS_VER}.tar.gz" -o /tmp/smartmontools.tar.gz \
&& tar -xzf /tmp/smartmontools.tar.gz -C /tmp \
&& cd /tmp/smartmontools-${SMARTMONTOOLS_VER} \
&& ./configure --prefix=/usr LDFLAGS='-static' --without-libcap-ng --without-libsystemd \
&& make -j"$(nproc)" \
&& make install \
&& /usr/sbin/update-smart-drivedb \
&& rm -rf /tmp/smartmontools*
================================================
FILE: docker/Dockerfile.web
================================================
# syntax=docker/dockerfile:1.4
########################################################################################################################
# Web Image
########################################################################################################################
######## Build the frontend
FROM --platform=${BUILDPLATFORM} node AS frontendbuild
WORKDIR /go/src/github.com/analogj/scrutiny
COPY --link Makefile /go/src/github.com/analogj/scrutiny/
COPY --link webapp/frontend /go/src/github.com/analogj/scrutiny/webapp/frontend
RUN make binary-frontend
######## Build the backend
FROM golang:1.25-trixie as backendbuild
WORKDIR /go/src/github.com/analogj/scrutiny
COPY --link Makefile /go/src/github.com/analogj/scrutiny/
COPY --link go.mod go.sum /go/src/github.com/analogj/scrutiny/
COPY --link collector /go/src/github.com/analogj/scrutiny/collector
COPY --link webapp/backend /go/src/github.com/analogj/scrutiny/webapp/backend
RUN apt-get update && apt-get install -y file && rm -rf /var/lib/apt/lists/*
RUN make binary-clean binary-all WEB_BINARY_NAME=scrutiny
######## Combine build artifacts in runtime image
FROM debian:trixie-slim as runtime
EXPOSE 8080
WORKDIR /opt/scrutiny
ENV PATH="/opt/scrutiny/bin:${PATH}"
RUN apt-get update && apt-get install -y ca-certificates curl tzdata && rm -rf /var/lib/apt/lists/* && update-ca-certificates
COPY --link --from=backendbuild --chmod=755 /go/src/github.com/analogj/scrutiny/scrutiny /opt/scrutiny/bin/
COPY --link --from=frontendbuild --chmod=644 /go/src/github.com/analogj/scrutiny/dist /opt/scrutiny/web
RUN mkdir -p /opt/scrutiny/web && \
mkdir -p /opt/scrutiny/config && \
chmod -R a+rX /opt/scrutiny && \
chmod -R a+w /opt/scrutiny/config
CMD ["/opt/scrutiny/bin/scrutiny", "start"]
================================================
FILE: docker/README.md
================================================
`rootfs` is only used by Dockerfile and Dockerfile.collector
================================================
FILE: docker/entrypoint-collector.sh
================================================
#!/bin/bash
# Cron runs in its own isolated environment (usually using only /etc/environment )
# So when the container starts up, we will do a dump of the runtime environment into a .env file that we
# will then source into the crontab file (/etc/cron.d/scrutiny)
(set -o posix; export -p) > /env.sh
# adding ability to customize the cron schedule.
COLLECTOR_CRON_SCHEDULE=${COLLECTOR_CRON_SCHEDULE:-"0 0 * * *"}
COLLECTOR_RUN_STARTUP=${COLLECTOR_RUN_STARTUP:-"false"}
COLLECTOR_RUN_STARTUP_SLEEP=${COLLECTOR_RUN_STARTUP_SLEEP:-"1"}
# if the cron schedule has been overridden via env variable (eg docker-compose) we should make sure to strip quotes
[[ "${COLLECTOR_CRON_SCHEDULE}" == \"*\" || "${COLLECTOR_CRON_SCHEDULE}" == \'*\' ]] && COLLECTOR_CRON_SCHEDULE="${COLLECTOR_CRON_SCHEDULE:1:-1}"
# replace placeholder with correct value
sed -i 's|{COLLECTOR_CRON_SCHEDULE}|'"${COLLECTOR_CRON_SCHEDULE}"'|g' /etc/cron.d/scrutiny
if [[ "${COLLECTOR_RUN_STARTUP}" == "true" ]]; then
sleep ${COLLECTOR_RUN_STARTUP_SLEEP}
echo "starting scrutiny collector (run-once mode. subsequent calls will be triggered via cron service)"
/opt/scrutiny/bin/scrutiny-collector-metrics run
fi
# now that we have the env start cron in the foreground
echo "starting cron"
exec su -c "cron -f -L 15" root
================================================
FILE: docker/example.hubspoke.docker-compose.yml
================================================
version: '2.4'
services:
influxdb:
restart: unless-stopped
image: influxdb:2.8
ports:
- '8086:8086'
volumes:
- './influxdb:/var/lib/influxdb2'
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8086/health"]
interval: 5s
timeout: 10s
retries: 20
web:
restart: unless-stopped
image: 'ghcr.io/analogj/scrutiny:master-web'
ports:
- '8080:8080'
volumes:
- './config:/opt/scrutiny/config'
environment:
SCRUTINY_WEB_INFLUXDB_HOST: 'influxdb'
depends_on:
influxdb:
condition: service_healthy
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8080/api/health"]
interval: 5s
timeout: 10s
retries: 20
start_period: 10s
collector:
restart: unless-stopped
image: 'ghcr.io/analogj/scrutiny:master-collector'
cap_add:
- SYS_RAWIO
volumes:
- '/run/udev:/run/udev:ro'
environment:
COLLECTOR_API_ENDPOINT: 'http://web:8080'
COLLECTOR_HOST_ID: 'scrutiny-collector-hostname'
# If true forces the collector to run on startup (cron will be started after the collector completes)
# see: https://github.com/AnalogJ/scrutiny/blob/master/docs/TROUBLESHOOTING_DEVICE_COLLECTOR.md#collector-trigger-on-startup
COLLECTOR_RUN_STARTUP: false
depends_on:
web:
condition: service_healthy
devices:
- "/dev/sda"
- "/dev/sdb"
================================================
FILE: docker/example.omnibus.docker-compose.yml
================================================
version: '3.5'
services:
scrutiny:
restart: unless-stopped
container_name: scrutiny
image: ghcr.io/analogj/scrutiny:master-omnibus
cap_add:
- SYS_RAWIO
ports:
- "8080:8080" # webapp
- "8086:8086" # influxDB admin
volumes:
- /run/udev:/run/udev:ro
- ./config:/opt/scrutiny/config
- ./influxdb:/opt/scrutiny/influxdb
devices:
- "/dev/sda"
- "/dev/sdb"
================================================
FILE: docs/DOWNSAMPLING.md
================================================
# Downsampling
Scrutiny collects alot of data, that can cause the database to grow unbounded.
- Smart data
- Smart test data
- Temperature data
- Disk metrics (capacity/usage)
- etc
This data must be accurate in the short term, and is useful for doing trend analysis in the long term.
However, for trend analysis we only need aggregate data, individual data points are not as useful.
Scrutiny will automatically downsample data on a schedule to ensure that the database size stays reasonable, while still
ensuring historical data is present for comparisons.
| Bucket Name | Retention Period | Downsampling Range | Downsampling Aggregation Window | Downsampling Cron | Comments |
| --- | --- | --- | --- | --- | --- |
| `metrics` | 15 days | `-2w -1w` | `1w` | main bucket, weekly on Sunday at 1:00am |
| `metrics_weekly` | 9 weeks | `-2mo -1mo` | `1mo` | monthly on first day of the month at 1:30am
| `metrics_monthly` | 25 months | `-2y -1y` | `1y` | yearly on the first day of the year at 2:00am
| `metrics_yearly` | forever | - | - | - | |
After 5 months, here's how may data points should exist in each bucket for one disk
| Bucket Name | Datapoints | Comments |
| --- | --- | --- |
| `metrics` | 15 | 7 daily datapoints , up to 7 pending data, 1 buffer data point |
| `metrics_weekly` | 9 | 4 aggregated weekly data points, 4 pending datapoints, 1 buffer data point |
| `metrics_monthly` | 3 | 3 aggregated monthly data points |
| `metrics_yearly` | 0 | |
After 5 years, here's how may data points should exist in each bucket for one disk
| Bucket Name | Datapoints | Comments |
| --- | --- | --- |
| `metrics` | - | - |
| `metrics_weekly` | - |
| `metrics_monthly` | - |
| `metrics_yearly` | - |
================================================
FILE: docs/INSTALL_ANSIBLE.md
================================================
# Ansible Install
[Zorlin](https://github.com/Zorlin) has developed and now maintains [an Ansible playbook](https://github.com/Zorlin/scrutiny-playbook) which automates the steps involved in manually setting up Scrutiny.
Using it is simple:
* Grab a copy of the playbook
* Follow the directions in the playbook repository
* Run `ansible-playbook site.yml`
* Visit http://your-machine:8080 to see your new Scrutiny installation.
It will automatically pull metrics from machines once a day, at 1am.
You can see it in action below.
[](https://asciinema.org/a/493531)
================================================
FILE: docs/INSTALL_HUB_SPOKE.md
================================================
>
See [docker/example.hubspoke.docker-compose.yml](https://github.com/AnalogJ/scrutiny/blob/master/docker/example.hubspoke.docker-compose.yml)
for a docker-compose file.
> The following guide was contributed by @TinJoy59 in #417
> It describes how to deploy the Scrutiny in Hub/Spoke mode, where the Hub is running in Docker, and the Spokes (
> collectors) are running as binaries.
> He's using Proxmox & Synology in his guide, however this should be applicable for almost anyone
# S.M.A.R.T. Monitoring with Scrutiny across machines

### 🤔 The problem:
Scrutiny offers a nice Docker package called "Omnibus" that can monitor HDDs attached to a Docker host with relative
ease. Scrutiny can also be installed in a Hub-Spoke layout where Web interface, Database and Collector come in 3
separate packages. The official documentation assumes that the spokes in the "Hub-Spokes layout" run Docker, which is
not always the case. The third approach is to install Scrutiny manually, entirely outside of Docker.
### 💡 The solution:
This tutorial provides a hybrid configuration where the Hub lives in a Docker instance while the spokes have only
Scrutiny Collector installed manually. The Collector periodically send data to the Hub. It's not mind-boggling hard to
understand but someone might struggle with the setup. This is for them.
### 🖥️ My setup:
I have a Proxmox cluster where one VM runs Docker and all monitoring services - Grafana, Prometheus, various exporters,
InfluxDB and so forth. Another VM runs the NAS - OpenMediaVault v6, where all hard drives reside. The Scrutiny Collector
is triggered every 30min to collect data on the drives. The data is sent to the Docker VM, running InfluxDB.
## Setting up the Hub

The Hub consists of Scrutiny Web - a web interface for viewing the SMART data. And InfluxDB, where the smartmon data is
stored.
[🔗This is the official Hub-Spoke layout in docker-compose.](https://github.com/AnalogJ/scrutiny/blob/master/docker/example.hubspoke.docker-compose.yml)
We are going to reuse parts of it. The ENV variables provide the necessary configuration for the initial setup, both for
InfluxDB and Scrutiny.
If you are working with and existing InfluxDB instance, you can forgo all the `INIT` variables as they already exist.
The official Scrutiny documentation has a
sample [scrutiny.yaml ](https://github.com/AnalogJ/scrutiny/blob/master/example.scrutiny.yaml)file that normally
contains the connection and notification details but I always find it easier to configure as much as possible in the
docker-compose.
```yaml
networks:
monitoring: # A common network for all monitoring services to communicate into
notifications: # To Gotify or another Notification service
services:
influxdb:
restart: unless-stopped
container_name: influxdb
image: influxdb:2.8
ports:
- 8086:8086
volumes:
- ${DIR_CONFIG}/influxdb2/db:/var/lib/influxdb2
- ${DIR_CONFIG}/influxdb2/config:/etc/influxdb2
environment:
- DOCKER_INFLUXDB_INIT_MODE=setup
- DOCKER_INFLUXDB_INIT_USERNAME=Admin
- DOCKER_INFLUXDB_INIT_PASSWORD=${PASSWORD}
- DOCKER_INFLUXDB_INIT_ORG=homelab
- DOCKER_INFLUXDB_INIT_BUCKET=scrutiny
- DOCKER_INFLUXDB_INIT_ADMIN_TOKEN=SUPER-SECRET-TOKEN
- TZ=Europe/Stockholm
networks:
- monitoring
scrutiny:
restart: unless-stopped
container_name: scrutiny
# best practice: pin to a specific release instead of latest
image: ghcr.io/analogj/scrutiny:latest-web
ports:
- 8080:8080
volumes:
- ${DIR_CONFIG}/config:/opt/scrutiny/config
environment:
- SCRUTINY_WEB_INFLUXDB_HOST=influxdb
- SCRUTINY_WEB_INFLUXDB_PORT=8086
- SCRUTINY_WEB_INFLUXDB_TOKEN=SUPER-SECRET-TOKEN
- SCRUTINY_WEB_INFLUXDB_ORG=homelab
- SCRUTINY_WEB_INFLUXDB_BUCKET=scrutiny
# Optional but highly recommended to notify you in case of a problem; space-separated list of shoutrrr uri's
# https://github.com/AnalogJ/scrutiny/blob/master/docs/TROUBLESHOOTING_NOTIFICATIONS.md
- SCRUTINY_NOTIFY_URLS=http://gotify:80/message?token=a-gotify-token ntfy://username:password@host:port/topic
- TZ=Europe/Stockholm
depends_on:
influxdb:
condition: service_healthy
networks:
- notifications
- monitoring
```
A freshly initialized Scrutiny instance can be accessed on port 8080, eg. `192.168.0.100:8080`. The interface will be
empty because no metrics have been collected yet.
## Setting up a Spoke ***without*** Docker

A spoke consists of the Scrutiny Collector binary that is run on a set interval via crontab and sends the data to the
Hub. The official
documentation [describes the manual setup of the Collector](https://github.com/AnalogJ/scrutiny/blob/master/docs/INSTALL_MANUAL.md#collector)
- dependencies and step by step commands. I have a shortened version that does the same thing but in one line of code.
```bash
# Installing dependencies
apt install smartmontools -y
# 1. Create directory for the binary
# 2. Download the binary into that directory
# 3. Make it exacutable
# 4. List the contents of the library for confirmation
mkdir -p /opt/scrutiny/bin && \
curl -L https://github.com/AnalogJ/scrutiny/releases/download/v0.8.1/scrutiny-collector-metrics-linux-amd64 > /opt/scrutiny/bin/scrutiny-collector-metrics-linux-amd64 && \
chmod +x /opt/scrutiny/bin/scrutiny-collector-metrics-linux-amd64 && \
ls -lha /opt/scrutiny/bin
```
<p class="callout warning">When downloading Github Release Assests, make sure that you have the correct version. The provided example is with Release v0.5.0. [The release list can be found here.](https://github.com/analogj/scrutiny/releases) </p>
Once the Collector is installed, you can run it with the following command. Make sure to add the correct address and
port of your Hub as `--api-endpoint`.
```bash
/opt/scrutiny/bin/scrutiny-collector-metrics-linux-amd64 run --api-endpoint "http://192.168.0.100:8080"
```
This will run the Collector once and populate the Web interface of your Scrutiny instance. In order to collect metrics
for a time series, you need to run the command repeatedly. Here is an example for crontab, running the Collector every
15min.
```bash
# open crontab
crontab -e
# add a line for Scrutiny
*/15 * * * * /opt/scrutiny/bin/scrutiny-collector-metrics-linux-amd64 run --api-endpoint "http://192.168.0.100:8080"
```
The Collector has its own independent config file that lives in `/opt/scrutiny/config/collector.yaml` but I did not find
a need to modify
it. [A default collector.yaml can be found in the official documentation.](https://github.com/AnalogJ/scrutiny/blob/master/example.collector.yaml)
## Setting up a Spoke ***with*** Docker

Setting up a remote Spoke in Docker requires you to split
the [official Hub-Spoke layout docker-compose.yml](https://github.com/AnalogJ/scrutiny/blob/master/docker/example.hubspoke.docker-compose.yml)
. In the following docker-compose you need to provide the `${API_ENDPOINT}`, in my case `http://192.168.0.100:8080`.
Also all drives that you wish to monitor need to be presented to the container under `devices`.
The image handles the periodic scanning of the drives.
```yaml
services:
collector:
restart: unless-stopped
# best practice: pin to a specific release instead of latest
image: 'ghcr.io/analogj/scrutiny:latest-collector'
cap_add:
- SYS_RAWIO
volumes:
- '/run/udev:/run/udev:ro'
environment:
COLLECTOR_API_ENDPOINT: ${API_ENDPOINT}
devices:
- "/dev/sda"
- "/dev/sdb"
```
================================================
FILE: docs/INSTALL_MANUAL.md
================================================
# Manual Install
While the easiest way to get started with [Scrutiny is using Docker](https://github.com/AnalogJ/scrutiny#docker),
it is possible to run it manually without much work. You can even mix and match, using Docker for one component and
a manual installation for the other. There's also [an installer](INSTALL_ANSIBLE.md) which automates this manual installation procedure.
Scrutiny is made up of three components: an influxdb Database, a collector and a webapp/api. Here's how each component can be deployed manually.
> Note: the `/opt/scrutiny` directory is not hardcoded, you can use any directory name/path.
## InfluxDB
Please follow the official InfluxDB installation guide. Note, you'll need to install v2.8.0+.
https://docs.influxdata.com/influxdb/v2/install/
## Webapp/API
### Dependencies
Since the webapp is packaged as a stand alone binary, there isn't really any software you need to install other than `glibc`
which is included by most linux OS's already.
### Directory Structure
Now let's create a directory structure to contain the Scrutiny files & binary.
```
mkdir -p /opt/scrutiny/config
mkdir -p /opt/scrutiny/web
mkdir -p /opt/scrutiny/bin
```
### Config file
While it is possible to run the webapp/api without a config file, the defaults are designed for use in a container environment,
and so will need to be overridden. So the first thing you'll need to do is create a config file that looks like the following:
```
# stored in /opt/scrutiny/config/scrutiny.yaml
version: 1
web:
database:
# The Scrutiny webapp will create a database for you, however the parent directory must exist.
location: /opt/scrutiny/config/scrutiny.db
src:
frontend:
# The path to the Scrutiny frontend files (js, css, images) must be specified.
# We'll populate it with files in the next section
path: /opt/scrutiny/web
# if you're runnning influxdb on a different host (or using a cloud-provider) you'll need to update the host & port below.
# token, org, bucket are unnecessary for a new InfluxDB installation, as Scrutiny will automatically run the InfluxDB setup,
# and store the information in the config file. If you 're re-using an existing influxdb installation, you'll need to provide
# the `token`
influxdb:
host: localhost
port: 8086
# token: 'my-token'
# org: 'my-org'
# bucket: 'bucket'
```
> Note: for a full list of available configuration options, please check the [example.scrutiny.yaml](https://github.com/AnalogJ/scrutiny/blob/master/example.scrutiny.yaml) file.
### Download Files
Next, we'll download the Scrutiny API binary and frontend files from the [latest Github release](https://github.com/analogj/scrutiny/releases).
The files you need to download are named:
- **scrutiny-web-linux-amd64** - save this file to `/opt/scrutiny/bin`
- **scrutiny-web-frontend.tar.gz** - save this file to `/opt/scrutiny/web`
### Prepare Scrutiny
Now that we have downloaded the required files, let's prepare the filesystem.
```
# Let's make sure the Scrutiny webapp is executable.
chmod +x /opt/scrutiny/bin/scrutiny-web-linux-amd64
# Next, lets extract the frontend files.
# NOTE: after extraction, there **should not** be a `dist` subdirectory in `/opt/scrutiny/web` directory.
cd /opt/scrutiny/web
tar xvzf scrutiny-web-frontend.tar.gz --strip-components 1 -C .
# Cleanup
rm -rf scrutiny-web-frontend.tar.gz
```
### Start Scrutiny Webapp
Finally, we start the Scrutiny webapp:
```
/opt/scrutiny/bin/scrutiny-web-linux-amd64 start --config /opt/scrutiny/config/scrutiny.yaml
```
The webapp listens for traffic on `http://0.0.0.0:8080` by default.
## Collector
### Dependencies
Unlike the webapp, the collector does have some dependencies:
- `smartctl`, v7+
- `cron` (or an alternative process scheduler)
Unfortunately the version of `smartmontools` (which contains `smartctl`) available in some of the base OS repositories is ancient.
So you'll need to install the v7+ version using one of the following commands:
- **Ubuntu (22.04/Jammy/LTS):** `apt-get install -y smartmontools`
- **Ubuntu (18.04/Bionic):** `apt-get install -y smartmontools=7.0-0ubuntu1~ubuntu18.04.1`
- **Centos8:**
- `dnf install https://extras.getpagespeed.com/release-el8-latest.rpm`
- `dnf install smartmontools`
- **FreeBSD:** `pkg install smartmontools`
The following additional dependencies are needed if you want to run the collector as an unprivileged user:
- systemd version > 235
- a restricted user account
### Directory Structure
Now let's create a directory structure to contain the Scrutiny collector binary.
```
mkdir -p /opt/scrutiny/bin
```
### Download Files
Next, we'll download the Scrutiny collector binary from the [latest Github release](https://github.com/analogj/scrutiny/releases). You are looking for the one titled **scrutiny-collector-metrics-linux-amd64** unless you know you are on arm.
```sh
wget -O /tmp/scrutiny-collector-metrics https://github.com/AnalogJ/scrutiny/releases/latest/download/scrutiny-collector-metrics-linux-amd64
```
Optional, but recommended: Before continuing it's recommended you compare the sha from the release page with the downloaded file to ensure it's the same file and not corrupted/tampered with. The command to do this is:
`echo "SHA_GOES_HERE /tmp/scrutiny-collector-metrics" | sha256sum -c`
example for the v0.8.6 release:
`echo "4c163645ce24e5487f4684a25ec73485d77a82a57f084808ff5aad0c11499ad2 /tmp/scrutiny-collector-metrics" | sha256sum -c`
followed by:
`sudo mv /tmp/scrutiny-collector-metrics /opt/scrutiny/bin/`
to move the binary to its final resting place
### Prepare Scrutiny
Now that we have downloaded the required files, let's prepare the filesystem.
```sh
# Let's make sure the Scrutiny collector is executable.
chmod +x /opt/scrutiny/bin/scrutiny-collector-metrics
```
if you are using SELinux, you may need to also do the following:
```sh
# tell SELinux to allow these binaries
sudo semanage fcontext -a -t bin_t "/opt/scrutiny/bin(/.*)?"
# update labels
sudo restorecon -Rv /opt/scrutiny/bin
```
### Start Scrutiny Collector, Populate Webapp
Next, we will manually trigger the collector, to populate the Scrutiny dashboard:
> NOTE: if you need to pass a config file to the scrutiny collector, you can provide it using the `--config` flag.
```sh
/opt/scrutiny/bin/scrutiny-collector-metrics run --api-endpoint "http://localhost:8080"
```
### Schedule Collector with (root) Cron
Finally you need to schedule the collector to run periodically.
This may be different depending on your OS/environment, but it may look something like this:
```sh
# open crontab
sudo crontab -e
# add a line for Scrutiny
*/15 * * * * . /etc/profile; /opt/scrutiny/bin/scrutiny-collector-metrics run --api-endpoint "http://localhost:8080"
```
### Schedule Collector with Systemd (rootless)
Alternatively you can run `scrutiny-collector-metrics` as non-root so long as the relevant capabilities and permissions are granted.
#### Creating a Restricted Service Account
This is the account that will run `scrutiny-collector-metrics`. Note this isn't strictly needed for all setups, but is useful from a logging/auditing perspective.
- Debian-based distros:
- `sudo adduser --system scrutiny-svc --group --home /opt/scrutiny-svc`
- RHEL-based distros:
- `sudo useradd --system --home-dir /opt/scrutiny-svc --shell /sbin/nologin scrutiny-svc`
Next, add the user to the `disk` group:
```sh
sudo usermod -aG disk scrutiny-svc
```
#### Creating a Restricted Systemd Service using AmbientCapabilities (easier)
This is the simpler setup, which allows you to run scrutiny rootless, but depending on what you want, may require granting more permissions to scrutiny than you would like to.
1. go to `/etc/systemd/system`
2. create scrutiny-collector.service with the following contents:
```ini
[Unit]
Description=Daily Restricted Scrutiny Collector
After=network.target
[Service]
[Unit]
Description=Daily Restricted Scrutiny Collector
After=network.target
[Service]
Type=oneshot
User=scrutiny-svc
Group=disk
ExecStart=/opt/scrutiny/bin/scrutiny-collector-metrics run --api-endpoint "http://localhost:8080"
# --- PRIVILEGE LOCKDOW
gitextract_a01nueng/
├── .devcontainer/
│ ├── docker/
│ │ └── devcontainer.json
│ ├── docker-compose.yml
│ ├── docker-rootless/
│ │ └── devcontainer.json
│ ├── podman/
│ │ └── devcontainer.json
│ └── setup.sh
├── .dockerignore
├── .gitattributes
├── .github/
│ ├── DISCUSSION_TEMPLATE/
│ │ └── issue-triage.yml
│ ├── ISSUE_TEMPLATE/
│ │ ├── config.yml
│ │ └── preapproved.md
│ └── workflows/
│ ├── ci.yaml
│ ├── docker-build.yaml
│ ├── docker-nightly.yaml
│ ├── release.yaml
│ └── sponsors.yaml
├── .gitignore
├── .golangci.yml
├── .vscode/
│ ├── launch.json
│ └── tasks.json
├── AI_POLICY.md
├── CONTRIBUTING.md
├── LICENSE
├── Makefile
├── README.md
├── REFERENCES.md
├── collector/
│ ├── cmd/
│ │ ├── collector-metrics/
│ │ │ └── collector-metrics.go
│ │ └── collector-selftest/
│ │ └── collector-selftest.go
│ └── pkg/
│ ├── collector/
│ │ ├── base.go
│ │ ├── metrics.go
│ │ ├── metrics_test.go
│ │ └── selftest.go
│ ├── common/
│ │ └── shell/
│ │ ├── factory.go
│ │ ├── interface.go
│ │ ├── local_shell.go
│ │ ├── local_shell_test.go
│ │ └── mock/
│ │ └── mock_shell.go
│ ├── config/
│ │ ├── config.go
│ │ ├── config_test.go
│ │ ├── factory.go
│ │ ├── interface.go
│ │ ├── mock/
│ │ │ └── mock_config.go
│ │ └── testdata/
│ │ ├── allow_listed_devices_present.yaml
│ │ ├── device_type_comma.yaml
│ │ ├── ignore_device.yaml
│ │ ├── invalid_commands_includes_device.yaml
│ │ ├── invalid_commands_missing_json.yaml
│ │ ├── override_commands.yaml
│ │ ├── override_device_commands.yaml
│ │ ├── raid_device.yaml
│ │ └── simple_device.yaml
│ ├── detect/
│ │ ├── detect.go
│ │ ├── detect_test.go
│ │ ├── devices_darwin.go
│ │ ├── devices_freebsd.go
│ │ ├── devices_linux.go
│ │ ├── devices_linux_test.go
│ │ ├── devices_windows.go
│ │ ├── testdata/
│ │ │ ├── smartctl_info_nvme.json
│ │ │ ├── smartctl_scan_megaraid.json
│ │ │ ├── smartctl_scan_nvme.json
│ │ │ └── smartctl_scan_simple.json
│ │ ├── wwn.go
│ │ └── wwn_test.go
│ ├── errors/
│ │ ├── errors.go
│ │ └── errors_test.go
│ └── models/
│ ├── device.go
│ ├── scan.go
│ └── scan_override.go
├── docker/
│ ├── Dockerfile
│ ├── Dockerfile.collector
│ ├── Dockerfile.smartmontools
│ ├── Dockerfile.web
│ ├── README.md
│ ├── entrypoint-collector.sh
│ ├── example.hubspoke.docker-compose.yml
│ └── example.omnibus.docker-compose.yml
├── docs/
│ ├── DOWNSAMPLING.md
│ ├── INSTALL_ANSIBLE.md
│ ├── INSTALL_HUB_SPOKE.md
│ ├── INSTALL_MANUAL.md
│ ├── INSTALL_MANUAL_WINDOWS.md
│ ├── INSTALL_NAS.md
│ ├── INSTALL_PFSENSE.md
│ ├── INSTALL_ROOTLESS_PODMAN.md
│ ├── INSTALL_SYNOLOGY_COLLECTOR.md
│ ├── INSTALL_UNRAID.md
│ ├── SUPPORTED_NAS_OS.md
│ ├── TESTERS.md
│ ├── TROUBLESHOOTING_DEVICE_COLLECTOR.md
│ ├── TROUBLESHOOTING_DOCKER.md
│ ├── TROUBLESHOOTING_INFLUXDB.md
│ ├── TROUBLESHOOTING_NOTIFICATIONS.md
│ ├── TROUBLESHOOTING_REVERSE_PROXY.md
│ ├── TROUBLESHOOTING_UDEV.md
│ └── dbdiagram.io.txt
├── example.collector.yaml
├── example.scrutiny.yaml
├── go.mod
├── go.sum
├── packagr.yml
├── rootfs/
│ └── etc/
│ ├── cont-init.d/
│ │ ├── 01-timezone
│ │ └── 50-cron-config
│ ├── cron.d/
│ │ └── scrutiny
│ └── services.d/
│ ├── collector-once/
│ │ └── run
│ ├── cron/
│ │ ├── finish
│ │ └── run
│ ├── influxdb/
│ │ └── run
│ └── scrutiny/
│ └── run
└── webapp/
├── backend/
│ ├── cmd/
│ │ └── scrutiny/
│ │ └── scrutiny.go
│ └── pkg/
│ ├── config/
│ │ ├── config.go
│ │ ├── config_test.go
│ │ ├── factory.go
│ │ ├── interface.go
│ │ └── mock/
│ │ └── mock_config.go
│ ├── constants.go
│ ├── database/
│ │ ├── interface.go
│ │ ├── migrations/
│ │ │ ├── m20201107210306/
│ │ │ │ ├── device.go
│ │ │ │ ├── smart.go
│ │ │ │ ├── smart_ata_attribute.go
│ │ │ │ ├── smart_nvme_attribute.go
│ │ │ │ └── smart_scsci_attribute.go
│ │ │ ├── m20220503120000/
│ │ │ │ └── device.go
│ │ │ ├── m20220509170100/
│ │ │ │ └── device.go
│ │ │ ├── m20220716214900/
│ │ │ │ └── setting.go
│ │ │ └── m20250221084400/
│ │ │ └── device.go
│ │ ├── mock/
│ │ │ └── mock_database.go
│ │ ├── scrutiny_repository.go
│ │ ├── scrutiny_repository_device.go
│ │ ├── scrutiny_repository_device_smart_attributes.go
│ │ ├── scrutiny_repository_migrations.go
│ │ ├── scrutiny_repository_settings.go
│ │ ├── scrutiny_repository_tasks.go
│ │ ├── scrutiny_repository_tasks_test.go
│ │ ├── scrutiny_repository_temperature.go
│ │ └── scrutiny_repository_temperature_test.go
│ ├── errors/
│ │ ├── errors.go
│ │ └── errors_test.go
│ ├── models/
│ │ ├── collector/
│ │ │ ├── smart.go
│ │ │ └── smart_test.go
│ │ ├── device.go
│ │ ├── device_summary.go
│ │ ├── measurements/
│ │ │ ├── smart.go
│ │ │ ├── smart_ata_attribute.go
│ │ │ ├── smart_attribute.go
│ │ │ ├── smart_nvme_attribute.go
│ │ │ ├── smart_scsci_attribute.go
│ │ │ ├── smart_temperature.go
│ │ │ └── smart_test.go
│ │ ├── setting_entry.go
│ │ ├── settings.go
│ │ └── testdata/
│ │ ├── helper.go
│ │ ├── smart-ata-date.json
│ │ ├── smart-ata-date2.json
│ │ ├── smart-ata-failed-scrutiny.json
│ │ ├── smart-ata-full.json
│ │ ├── smart-ata.json
│ │ ├── smart-ata2.json
│ │ ├── smart-fail.json
│ │ ├── smart-fail2.json
│ │ ├── smart-megaraid0.json
│ │ ├── smart-megaraid1.json
│ │ ├── smart-nvme-failed.json
│ │ ├── smart-nvme.json
│ │ ├── smart-nvme2.json
│ │ ├── smart-pass.json
│ │ ├── smart-raid.json
│ │ ├── smart-sat.json
│ │ ├── smart-scsi.json
│ │ └── smart-scsi2.json
│ ├── notify/
│ │ ├── notify.go
│ │ └── notify_test.go
│ ├── thresholds/
│ │ ├── ata_attribute_metadata.go
│ │ ├── nvme_attribute_metadata.go
│ │ └── scsi_attribute_metadata.go
│ ├── version/
│ │ └── version.go
│ └── web/
│ ├── handler/
│ │ ├── archive_device.go
│ │ ├── delete_device.go
│ │ ├── get_device_details.go
│ │ ├── get_devices_summary.go
│ │ ├── get_devices_summary_temp_history.go
│ │ ├── get_settings.go
│ │ ├── health_check.go
│ │ ├── register_devices.go
│ │ ├── save_settings.go
│ │ ├── send_test_notification.go
│ │ ├── unarchive_device.go
│ │ ├── upload_device_metrics.go
│ │ └── upload_device_self_tests.go
│ ├── middleware/
│ │ ├── config.go
│ │ ├── logger.go
│ │ └── repository.go
│ ├── server.go
│ ├── server_test.go
│ └── testdata/
│ ├── register-devices-req-2.json
│ ├── register-devices-req.json
│ ├── register-devices-single-req.json
│ └── upload-device-metrics-req.json
└── frontend/
├── .editorconfig
├── .gitignore
├── CREDITS
├── LICENSE.md
├── README.md
├── angular.json
├── browserslist
├── e2e/
│ ├── protractor.conf.js
│ ├── src/
│ │ ├── app.e2e-spec.ts
│ │ └── app.po.ts
│ └── tsconfig.json
├── git.version.sh
├── karma.conf.js
├── package.json
├── src/
│ ├── @treo/
│ │ ├── animations/
│ │ │ ├── defaults.ts
│ │ │ ├── expand-collapse.ts
│ │ │ ├── fade.ts
│ │ │ ├── index.ts
│ │ │ ├── public-api.ts
│ │ │ ├── shake.ts
│ │ │ ├── slide.ts
│ │ │ └── zoom.ts
│ │ ├── components/
│ │ │ ├── card/
│ │ │ │ ├── card.component.html
│ │ │ │ ├── card.component.scss
│ │ │ │ ├── card.component.ts
│ │ │ │ ├── card.module.ts
│ │ │ │ ├── index.ts
│ │ │ │ └── public-api.ts
│ │ │ ├── date-range/
│ │ │ │ ├── date-range.component.html
│ │ │ │ ├── date-range.component.scss
│ │ │ │ ├── date-range.component.ts
│ │ │ │ ├── date-range.module.ts
│ │ │ │ ├── index.ts
│ │ │ │ └── public-api.ts
│ │ │ ├── drawer/
│ │ │ │ ├── drawer.component.html
│ │ │ │ ├── drawer.component.scss
│ │ │ │ ├── drawer.component.ts
│ │ │ │ ├── drawer.module.ts
│ │ │ │ ├── drawer.service.ts
│ │ │ │ ├── drawer.types.ts
│ │ │ │ ├── index.ts
│ │ │ │ └── public-api.ts
│ │ │ ├── highlight/
│ │ │ │ ├── highlight.component.html
│ │ │ │ ├── highlight.component.scss
│ │ │ │ ├── highlight.component.ts
│ │ │ │ ├── highlight.module.ts
│ │ │ │ ├── highlight.service.ts
│ │ │ │ ├── index.ts
│ │ │ │ └── public-api.ts
│ │ │ ├── message/
│ │ │ │ ├── index.ts
│ │ │ │ ├── message.component.html
│ │ │ │ ├── message.component.scss
│ │ │ │ ├── message.component.ts
│ │ │ │ ├── message.module.ts
│ │ │ │ ├── message.service.ts
│ │ │ │ ├── message.types.ts
│ │ │ │ └── public-api.ts
│ │ │ └── navigation/
│ │ │ ├── horizontal/
│ │ │ │ ├── components/
│ │ │ │ │ ├── basic/
│ │ │ │ │ │ ├── basic.component.html
│ │ │ │ │ │ └── basic.component.ts
│ │ │ │ │ ├── branch/
│ │ │ │ │ │ ├── branch.component.html
│ │ │ │ │ │ └── branch.component.ts
│ │ │ │ │ ├── divider/
│ │ │ │ │ │ ├── divider.component.html
│ │ │ │ │ │ └── divider.component.ts
│ │ │ │ │ └── spacer/
│ │ │ │ │ ├── spacer.component.html
│ │ │ │ │ └── spacer.component.ts
│ │ │ │ ├── horizontal.component.html
│ │ │ │ ├── horizontal.component.scss
│ │ │ │ └── horizontal.component.ts
│ │ │ ├── index.ts
│ │ │ ├── navigation.module.ts
│ │ │ ├── navigation.service.ts
│ │ │ ├── navigation.types.ts
│ │ │ ├── public-api.ts
│ │ │ └── vertical/
│ │ │ ├── components/
│ │ │ │ ├── aside/
│ │ │ │ │ ├── aside.component.html
│ │ │ │ │ └── aside.component.ts
│ │ │ │ ├── basic/
│ │ │ │ │ ├── basic.component.html
│ │ │ │ │ └── basic.component.ts
│ │ │ │ ├── collapsable/
│ │ │ │ │ ├── collapsable.component.html
│ │ │ │ │ └── collapsable.component.ts
│ │ │ │ ├── divider/
│ │ │ │ │ ├── divider.component.html
│ │ │ │ │ └── divider.component.ts
│ │ │ │ ├── group/
│ │ │ │ │ ├── group.component.html
│ │ │ │ │ └── group.component.ts
│ │ │ │ └── spacer/
│ │ │ │ ├── spacer.component.html
│ │ │ │ └── spacer.component.ts
│ │ │ ├── vertical.component.html
│ │ │ ├── vertical.component.scss
│ │ │ └── vertical.component.ts
│ │ ├── directives/
│ │ │ ├── autogrow/
│ │ │ │ ├── autogrow.directive.ts
│ │ │ │ ├── autogrow.module.ts
│ │ │ │ ├── index.ts
│ │ │ │ └── public-api.ts
│ │ │ └── scrollbar/
│ │ │ ├── index.ts
│ │ │ ├── public-api.ts
│ │ │ ├── scrollbar.directive.ts
│ │ │ ├── scrollbar.interfaces.ts
│ │ │ └── scrollbar.module.ts
│ │ ├── index.ts
│ │ ├── lib/
│ │ │ └── mock-api/
│ │ │ ├── index.ts
│ │ │ ├── mock-api.interceptor.ts
│ │ │ ├── mock-api.interfaces.ts
│ │ │ ├── mock-api.module.ts
│ │ │ ├── mock-api.request-handler.ts
│ │ │ ├── mock-api.service.ts
│ │ │ └── mock-api.utils.ts
│ │ ├── pipes/
│ │ │ └── find-by-key/
│ │ │ ├── find-by-key.module.ts
│ │ │ ├── find-by-key.pipe.ts
│ │ │ ├── index.ts
│ │ │ └── public-api.ts
│ │ ├── services/
│ │ │ ├── config/
│ │ │ │ ├── config.constants.ts
│ │ │ │ ├── config.module.ts
│ │ │ │ ├── config.service.ts
│ │ │ │ ├── index.ts
│ │ │ │ └── public-api.ts
│ │ │ ├── media-watcher/
│ │ │ │ ├── index.ts
│ │ │ │ ├── media-watcher.module.ts
│ │ │ │ ├── media-watcher.service.ts
│ │ │ │ └── public-api.ts
│ │ │ └── splash-screen/
│ │ │ ├── index.ts
│ │ │ ├── public-api.ts
│ │ │ ├── splash-screen.module.ts
│ │ │ └── splash-screen.service.ts
│ │ ├── styles/
│ │ │ ├── base/
│ │ │ │ ├── _colors.scss
│ │ │ │ ├── _preflight.scss
│ │ │ │ ├── _theming.scss
│ │ │ │ └── _typography.scss
│ │ │ ├── components/
│ │ │ │ ├── _card.scss
│ │ │ │ ├── _input.scss
│ │ │ │ └── _table.scss
│ │ │ ├── layout/
│ │ │ │ └── _content.scss
│ │ │ ├── main.scss
│ │ │ ├── overrides/
│ │ │ │ ├── _angular-material.scss
│ │ │ │ ├── _highlightjs.scss
│ │ │ │ ├── _perfect-scrollbar.scss
│ │ │ │ └── _quill.scss
│ │ │ ├── treo.scss
│ │ │ ├── utilities/
│ │ │ │ ├── _breakpoints.scss
│ │ │ │ ├── _colors.scss
│ │ │ │ ├── _elevations.scss
│ │ │ │ ├── _icons.scss
│ │ │ │ ├── _keyframes.scss
│ │ │ │ └── _theming.scss
│ │ │ └── vendors/
│ │ │ ├── _angular-material.scss
│ │ │ └── _normalize.scss
│ │ ├── tailwind/
│ │ │ ├── export.css
│ │ │ ├── export.js
│ │ │ ├── exported/
│ │ │ │ ├── _variables.scss
│ │ │ │ └── variables.ts
│ │ │ └── plugins/
│ │ │ ├── index.js
│ │ │ ├── utilities/
│ │ │ │ ├── color-combinations.js
│ │ │ │ ├── color-contrasts.js
│ │ │ │ ├── icon-color.js
│ │ │ │ ├── icon-size.js
│ │ │ │ └── mirror.js
│ │ │ └── variants/
│ │ │ ├── dark-light.js
│ │ │ ├── export-box-shadow.js
│ │ │ ├── export-colors.js
│ │ │ ├── export-font-family.js
│ │ │ └── export-screens.js
│ │ ├── treo.module.ts
│ │ └── validators/
│ │ ├── index.ts
│ │ ├── public-api.ts
│ │ └── validators.ts
│ ├── app/
│ │ ├── app.component.html
│ │ ├── app.component.scss
│ │ ├── app.component.ts
│ │ ├── app.module.ts
│ │ ├── app.routing.ts
│ │ ├── core/
│ │ │ ├── config/
│ │ │ │ ├── app.config.ts
│ │ │ │ ├── scrutiny-config.module.ts
│ │ │ │ └── scrutiny-config.service.ts
│ │ │ ├── core.module.ts
│ │ │ └── models/
│ │ │ ├── device-details-response-wrapper.ts
│ │ │ ├── device-model.ts
│ │ │ ├── device-summary-model.ts
│ │ │ ├── device-summary-response-wrapper.ts
│ │ │ ├── device-summary-temp-response-wrapper.ts
│ │ │ ├── measurements/
│ │ │ │ ├── smart-attribute-model.ts
│ │ │ │ ├── smart-model.ts
│ │ │ │ └── smart-temperature-model.ts
│ │ │ └── thresholds/
│ │ │ └── attribute-metadata-model.ts
│ │ ├── data/
│ │ │ └── mock/
│ │ │ ├── device/
│ │ │ │ └── details/
│ │ │ │ ├── index.ts
│ │ │ │ ├── sda.ts
│ │ │ │ ├── sdb.ts
│ │ │ │ ├── sdc.ts
│ │ │ │ ├── sdd.ts
│ │ │ │ ├── sde.ts
│ │ │ │ └── sdf.ts
│ │ │ ├── index.ts
│ │ │ └── summary/
│ │ │ ├── data.ts
│ │ │ ├── index.ts
│ │ │ └── temp_history.ts
│ │ ├── layout/
│ │ │ ├── common/
│ │ │ │ ├── dashboard-device/
│ │ │ │ │ ├── dashboard-device.component.html
│ │ │ │ │ ├── dashboard-device.component.scss
│ │ │ │ │ ├── dashboard-device.component.spec.ts
│ │ │ │ │ ├── dashboard-device.component.ts
│ │ │ │ │ └── dashboard-device.module.ts
│ │ │ │ ├── dashboard-device-archive-dialog/
│ │ │ │ │ ├── dashboard-device-archive-dialog.component.html
│ │ │ │ │ ├── dashboard-device-archive-dialog.component.scss
│ │ │ │ │ ├── dashboard-device-archive-dialog.component.spec.ts
│ │ │ │ │ ├── dashboard-device-archive-dialog.component.ts
│ │ │ │ │ ├── dashboard-device-archive-dialog.module.ts
│ │ │ │ │ └── dashboard-device-archive-dialog.service.ts
│ │ │ │ ├── dashboard-device-delete-dialog/
│ │ │ │ │ ├── dashboard-device-delete-dialog.component.html
│ │ │ │ │ ├── dashboard-device-delete-dialog.component.scss
│ │ │ │ │ ├── dashboard-device-delete-dialog.component.spec.ts
│ │ │ │ │ ├── dashboard-device-delete-dialog.component.ts
│ │ │ │ │ ├── dashboard-device-delete-dialog.module.ts
│ │ │ │ │ └── dashboard-device-delete-dialog.service.ts
│ │ │ │ ├── dashboard-settings/
│ │ │ │ │ ├── dashboard-settings.component.html
│ │ │ │ │ ├── dashboard-settings.component.scss
│ │ │ │ │ ├── dashboard-settings.component.ts
│ │ │ │ │ └── dashboard-settings.module.ts
│ │ │ │ ├── detail-settings/
│ │ │ │ │ ├── detail-settings.component.html
│ │ │ │ │ ├── detail-settings.component.scss
│ │ │ │ │ ├── detail-settings.component.spec.ts
│ │ │ │ │ ├── detail-settings.component.ts
│ │ │ │ │ └── detail-settings.module.ts
│ │ │ │ └── search/
│ │ │ │ ├── search.component.html
│ │ │ │ ├── search.component.scss
│ │ │ │ ├── search.component.ts
│ │ │ │ └── search.module.ts
│ │ │ ├── layout.component.html
│ │ │ ├── layout.component.scss
│ │ │ ├── layout.component.ts
│ │ │ ├── layout.module.ts
│ │ │ ├── layout.types.ts
│ │ │ └── layouts/
│ │ │ ├── empty/
│ │ │ │ ├── empty.component.html
│ │ │ │ ├── empty.component.scss
│ │ │ │ ├── empty.component.ts
│ │ │ │ └── empty.module.ts
│ │ │ └── horizontal/
│ │ │ └── material/
│ │ │ ├── material.component.html
│ │ │ ├── material.component.scss
│ │ │ ├── material.component.ts
│ │ │ └── material.module.ts
│ │ ├── modules/
│ │ │ ├── dashboard/
│ │ │ │ ├── dashboard.component.html
│ │ │ │ ├── dashboard.component.scss
│ │ │ │ ├── dashboard.component.ts
│ │ │ │ ├── dashboard.module.ts
│ │ │ │ ├── dashboard.resolvers.ts
│ │ │ │ ├── dashboard.routing.ts
│ │ │ │ ├── dashboard.service.spec.ts
│ │ │ │ └── dashboard.service.ts
│ │ │ ├── detail/
│ │ │ │ ├── detail.component.html
│ │ │ │ ├── detail.component.scss
│ │ │ │ ├── detail.component.ts
│ │ │ │ ├── detail.module.ts
│ │ │ │ ├── detail.resolvers.ts
│ │ │ │ ├── detail.routing.ts
│ │ │ │ ├── detail.service.spec.ts
│ │ │ │ └── detail.service.ts
│ │ │ └── landing/
│ │ │ └── home/
│ │ │ ├── home.component.html
│ │ │ ├── home.component.scss
│ │ │ ├── home.component.ts
│ │ │ ├── home.module.ts
│ │ │ └── home.routing.ts
│ │ └── shared/
│ │ ├── device-hours.pipe.spec.ts
│ │ ├── device-hours.pipe.ts
│ │ ├── device-sort.pipe.spec.ts
│ │ ├── device-sort.pipe.ts
│ │ ├── device-status.pipe.spec.ts
│ │ ├── device-status.pipe.ts
│ │ ├── device-title.pipe.spec.ts
│ │ ├── device-title.pipe.ts
│ │ ├── file-size.pipe.spec.ts
│ │ ├── file-size.pipe.ts
│ │ ├── shared.module.ts
│ │ ├── temperature.pipe.spec.ts
│ │ └── temperature.pipe.ts
│ ├── assets/
│ │ ├── .gitkeep
│ │ ├── fonts/
│ │ │ ├── ibm-plex-mono/
│ │ │ │ └── ibm-plex-mono.css
│ │ │ ├── inter/
│ │ │ │ └── inter.css
│ │ │ ├── material-icons/
│ │ │ │ ├── MaterialIcons-Regular.codepoints
│ │ │ │ ├── MaterialIconsOutlined-Regular.codepoints
│ │ │ │ ├── MaterialIconsOutlined-Regular.otf
│ │ │ │ ├── MaterialIconsRound-Regular.codepoints
│ │ │ │ ├── MaterialIconsRound-Regular.otf
│ │ │ │ ├── MaterialIconsSharp-Regular.codepoints
│ │ │ │ ├── MaterialIconsSharp-Regular.otf
│ │ │ │ ├── MaterialIconsTwoTone-Regular.codepoints
│ │ │ │ ├── MaterialIconsTwoTone-Regular.otf
│ │ │ │ ├── README.md
│ │ │ │ └── material-icons.css
│ │ │ └── roboto/
│ │ │ └── roboto.css
│ │ └── images/
│ │ └── logo/
│ │ ├── scrutiny-logo-dark-social.psd
│ │ ├── scrutiny-logo-dark-text.psd
│ │ ├── scrutiny-logo-white-text.psd
│ │ └── scrutiny-logo-white.psd
│ ├── browserconfig.xml
│ ├── environments/
│ │ ├── environment.prod.ts
│ │ ├── environment.ts
│ │ └── versions.ts
│ ├── index.html
│ ├── main.ts
│ ├── manifest.json
│ ├── polyfills.ts
│ ├── styles/
│ │ ├── styles.scss
│ │ ├── tailwind.scss
│ │ ├── themes.scss
│ │ └── vendors.scss
│ ├── tailwind/
│ │ ├── config.js
│ │ └── main.css
│ └── test.ts
├── tsconfig.app.json
├── tsconfig.json
├── tsconfig.spec.json
└── tslint.json
SYMBOL INDEX (984 symbols across 208 files)
FILE: collector/cmd/collector-metrics/collector-metrics.go
function main (line 26) | func main() {
function CreateLogger (line 195) | func CreateLogger(appConfig config.Interface) (*logrus.Entry, *os.File, ...
FILE: collector/cmd/collector-selftest/collector-selftest.go
function main (line 22) | func main() {
FILE: collector/pkg/collector/base.go
type BaseCollector (line 14) | type BaseCollector struct
method postJson (line 18) | func (c *BaseCollector) postJson(url string, body interface{}, target ...
method LogSmartctlExitCode (line 34) | func (c *BaseCollector) LogSmartctlExitCode(exitCode int) {
FILE: collector/pkg/collector/metrics.go
type MetricsCollector (line 22) | type MetricsCollector struct
method Run (line 47) | func (mc *MetricsCollector) Run() error {
method Validate (line 108) | func (mc *MetricsCollector) Validate() error {
method Collect (line 120) | func (mc *MetricsCollector) Collect(deviceWWN string, deviceName strin...
method Publish (line 156) | func (mc *MetricsCollector) Publish(deviceWWN string, payload []byte) ...
function CreateMetricsCollector (line 29) | func CreateMetricsCollector(appConfig config.Interface, logger *logrus.E...
FILE: collector/pkg/collector/metrics_test.go
function TestApiEndpointParse (line 9) | func TestApiEndpointParse(t *testing.T) {
function TestApiEndpointParse_WithBasepathWithoutTrailingSlash (line 19) | func TestApiEndpointParse_WithBasepathWithoutTrailingSlash(t *testing.T) {
function TestApiEndpointParse_WithBasepathWithTrailingSlash (line 30) | func TestApiEndpointParse_WithBasepathWithTrailingSlash(t *testing.T) {
FILE: collector/pkg/collector/selftest.go
type SelfTestCollector (line 8) | type SelfTestCollector struct
method Run (line 29) | func (sc *SelfTestCollector) Run() error {
function CreateSelfTestCollector (line 15) | func CreateSelfTestCollector(logger *logrus.Entry, apiEndpoint string) (...
FILE: collector/pkg/common/shell/factory.go
function Create (line 3) | func Create() Interface {
FILE: collector/pkg/common/shell/interface.go
type Interface (line 9) | type Interface interface
FILE: collector/pkg/common/shell/local_shell.go
type localShell (line 14) | type localShell struct
method Command (line 16) | func (s *localShell) Command(logger *logrus.Entry, cmdName string, cmd...
FILE: collector/pkg/common/shell/local_shell_test.go
function TestLocalShellCommand (line 10) | func TestLocalShellCommand(t *testing.T) {
function TestLocalShellCommand_Date (line 23) | func TestLocalShellCommand_Date(t *testing.T) {
function TestLocalShellCommand_InvalidCommand (line 54) | func TestLocalShellCommand_InvalidCommand(t *testing.T) {
FILE: collector/pkg/common/shell/mock/mock_shell.go
type MockInterface (line 15) | type MockInterface struct
method EXPECT (line 33) | func (m *MockInterface) EXPECT() *MockInterfaceMockRecorder {
method Command (line 38) | func (m *MockInterface) Command(logger *logrus.Entry, cmdName string, ...
type MockInterfaceMockRecorder (line 21) | type MockInterfaceMockRecorder struct
method Command (line 47) | func (mr *MockInterfaceMockRecorder) Command(logger, cmdName, cmdArgs,...
function NewMockInterface (line 26) | func NewMockInterface(ctrl *gomock.Controller) *MockInterface {
FILE: collector/pkg/config/config.go
type configuration (line 21) | type configuration struct
method Init (line 35) | func (c *configuration) Init() error {
method ReadConfig (line 71) | func (c *configuration) ReadConfig(configFilePath string) error {
method ValidateConfig (line 106) | func (c *configuration) ValidateConfig() error {
method GetDeviceOverrides (line 152) | func (c *configuration) GetDeviceOverrides() []models.ScanOverride {
method GetCommandMetricsInfoArgs (line 167) | func (c *configuration) GetCommandMetricsInfoArgs(deviceName string) s...
method GetCommandMetricsSmartArgs (line 183) | func (c *configuration) GetCommandMetricsSmartArgs(deviceName string) ...
method IsAllowlistedDevice (line 199) | func (c *configuration) IsAllowlistedDevice(deviceName string) bool {
FILE: collector/pkg/config/config_test.go
function TestConfiguration_InvalidConfigPath (line 11) | func TestConfiguration_InvalidConfigPath(t *testing.T) {
function TestConfiguration_GetScanOverrides_Simple (line 24) | func TestConfiguration_GetScanOverrides_Simple(t *testing.T) {
function TestConfiguration_GetScanOverrides_DeviceTypeComma (line 40) | func TestConfiguration_GetScanOverrides_DeviceTypeComma(t *testing.T) {
function TestConfiguration_GetScanOverrides_Ignore (line 58) | func TestConfiguration_GetScanOverrides_Ignore(t *testing.T) {
function TestConfiguration_GetScanOverrides_Raid (line 73) | func TestConfiguration_GetScanOverrides_Raid(t *testing.T) {
function TestConfiguration_InvalidCommands_MissingJson (line 98) | func TestConfiguration_InvalidCommands_MissingJson(t *testing.T) {
function TestConfiguration_InvalidCommands_IncludesDevice (line 109) | func TestConfiguration_InvalidCommands_IncludesDevice(t *testing.T) {
function TestConfiguration_OverrideCommands (line 120) | func TestConfiguration_OverrideCommands(t *testing.T) {
function TestConfiguration_OverrideDeviceCommands_MetricsInfoArgs (line 132) | func TestConfiguration_OverrideDeviceCommands_MetricsInfoArgs(t *testing...
function TestConfiguration_DeviceAllowList (line 148) | func TestConfiguration_DeviceAllowList(t *testing.T) {
FILE: collector/pkg/config/factory.go
function Create (line 3) | func Create() (Interface, error) {
FILE: collector/pkg/config/interface.go
type Interface (line 10) | type Interface interface
FILE: collector/pkg/config/mock/mock_config.go
type MockInterface (line 16) | type MockInterface struct
method EXPECT (line 34) | func (m *MockInterface) EXPECT() *MockInterfaceMockRecorder {
method AllSettings (line 39) | func (m *MockInterface) AllSettings() map[string]interface{} {
method Get (line 53) | func (m *MockInterface) Get(key string) interface{} {
method GetBool (line 67) | func (m *MockInterface) GetBool(key string) bool {
method GetCommandMetricsInfoArgs (line 81) | func (m *MockInterface) GetCommandMetricsInfoArgs(deviceName string) s...
method GetCommandMetricsSmartArgs (line 95) | func (m *MockInterface) GetCommandMetricsSmartArgs(deviceName string) ...
method GetDeviceOverrides (line 109) | func (m *MockInterface) GetDeviceOverrides() []models.ScanOverride {
method GetInt (line 123) | func (m *MockInterface) GetInt(key string) int {
method GetString (line 137) | func (m *MockInterface) GetString(key string) string {
method GetStringSlice (line 151) | func (m *MockInterface) GetStringSlice(key string) []string {
method Init (line 165) | func (m *MockInterface) Init() error {
method IsAllowlistedDevice (line 179) | func (m *MockInterface) IsAllowlistedDevice(deviceName string) bool {
method IsSet (line 193) | func (m *MockInterface) IsSet(key string) bool {
method ReadConfig (line 207) | func (m *MockInterface) ReadConfig(configFilePath string) error {
method Set (line 221) | func (m *MockInterface) Set(key string, value interface{}) {
method SetDefault (line 233) | func (m *MockInterface) SetDefault(key string, value interface{}) {
method UnmarshalKey (line 245) | func (m *MockInterface) UnmarshalKey(key string, rawVal interface{}, d...
type MockInterfaceMockRecorder (line 22) | type MockInterfaceMockRecorder struct
method AllSettings (line 47) | func (mr *MockInterfaceMockRecorder) AllSettings() *gomock.Call {
method Get (line 61) | func (mr *MockInterfaceMockRecorder) Get(key interface{}) *gomock.Call {
method GetBool (line 75) | func (mr *MockInterfaceMockRecorder) GetBool(key interface{}) *gomock....
method GetCommandMetricsInfoArgs (line 89) | func (mr *MockInterfaceMockRecorder) GetCommandMetricsInfoArgs(deviceN...
method GetCommandMetricsSmartArgs (line 103) | func (mr *MockInterfaceMockRecorder) GetCommandMetricsSmartArgs(device...
method GetDeviceOverrides (line 117) | func (mr *MockInterfaceMockRecorder) GetDeviceOverrides() *gomock.Call {
method GetInt (line 131) | func (mr *MockInterfaceMockRecorder) GetInt(key interface{}) *gomock.C...
method GetString (line 145) | func (mr *MockInterfaceMockRecorder) GetString(key interface{}) *gomoc...
method GetStringSlice (line 159) | func (mr *MockInterfaceMockRecorder) GetStringSlice(key interface{}) *...
method Init (line 173) | func (mr *MockInterfaceMockRecorder) Init() *gomock.Call {
method IsAllowlistedDevice (line 187) | func (mr *MockInterfaceMockRecorder) IsAllowlistedDevice(deviceName in...
method IsSet (line 201) | func (mr *MockInterfaceMockRecorder) IsSet(key interface{}) *gomock.Ca...
method ReadConfig (line 215) | func (mr *MockInterfaceMockRecorder) ReadConfig(configFilePath interfa...
method Set (line 227) | func (mr *MockInterfaceMockRecorder) Set(key, value interface{}) *gomo...
method SetDefault (line 239) | func (mr *MockInterfaceMockRecorder) SetDefault(key, value interface{}...
method UnmarshalKey (line 257) | func (mr *MockInterfaceMockRecorder) UnmarshalKey(key, rawVal interfac...
function NewMockInterface (line 27) | func NewMockInterface(ctrl *gomock.Controller) *MockInterface {
FILE: collector/pkg/detect/detect.go
type Detect (line 16) | type Detect struct
method SmartctlScan (line 30) | func (d *Detect) SmartctlScan() ([]models.Device, error) {
method SmartCtlInfo (line 55) | func (d *Detect) SmartCtlInfo(device *models.Device) error {
method TransformDetectedDevices (line 120) | func (d *Detect) TransformDetectedDevices(detectedDeviceConns models.S...
FILE: collector/pkg/detect/detect_test.go
function TestDetect_SmartctlScan (line 18) | func TestDetect_SmartctlScan(t *testing.T) {
function TestDetect_SmartctlScan_Megaraid (line 47) | func TestDetect_SmartctlScan_Megaraid(t *testing.T) {
function TestDetect_SmartctlScan_Nvme (line 79) | func TestDetect_SmartctlScan_Nvme(t *testing.T) {
function TestDetect_TransformDetectedDevices_Empty (line 110) | func TestDetect_TransformDetectedDevices_Empty(t *testing.T) {
function TestDetect_TransformDetectedDevices_Ignore (line 143) | func TestDetect_TransformDetectedDevices_Ignore(t *testing.T) {
function TestDetect_TransformDetectedDevices_Raid (line 175) | func TestDetect_TransformDetectedDevices_Raid(t *testing.T) {
function TestDetect_TransformDetectedDevices_Simple (line 217) | func TestDetect_TransformDetectedDevices_Simple(t *testing.T) {
function TestDetect_TransformDetectedDevices_WithoutDeviceTypeOverride (line 250) | func TestDetect_TransformDetectedDevices_WithoutDeviceTypeOverride(t *te...
function TestDetect_TransformDetectedDevices_WhenDeviceNotDetected (line 282) | func TestDetect_TransformDetectedDevices_WhenDeviceNotDetected(t *testin...
function TestDetect_TransformDetectedDevices_AllowListFilters (line 304) | func TestDetect_TransformDetectedDevices_AllowListFilters(t *testing.T) {
function TestDetect_SmartCtlInfo (line 343) | func TestDetect_SmartCtlInfo(t *testing.T) {
FILE: collector/pkg/detect/devices_darwin.go
function DevicePrefix (line 10) | func DevicePrefix() string {
method Start (line 14) | func (d *Detect) Start() ([]models.Device, error) {
method findMissingDevices (line 36) | func (d *Detect) findMissingDevices(detectedDevices []models.Device) ([]...
method wwnFallback (line 93) | func (d *Detect) wwnFallback(detectedDevice *models.Device) {
FILE: collector/pkg/detect/devices_freebsd.go
function DevicePrefix (line 10) | func DevicePrefix() string {
method Start (line 14) | func (d *Detect) Start() ([]models.Device, error) {
method wwnFallback (line 31) | func (d *Detect) wwnFallback(detectedDevice *models.Device) {
FILE: collector/pkg/detect/devices_linux.go
function DevicePrefix (line 14) | func DevicePrefix() string {
method Start (line 18) | func (d *Detect) Start() ([]models.Device, error) {
method wwnFallback (line 36) | func (d *Detect) wwnFallback(detectedDevice *models.Device) {
function populateUdevInfo (line 62) | func populateUdevInfo(detectedDevice *models.Device) error {
FILE: collector/pkg/detect/devices_linux_test.go
function TestDevicePrefix (line 9) | func TestDevicePrefix(t *testing.T) {
FILE: collector/pkg/detect/devices_windows.go
function DevicePrefix (line 9) | func DevicePrefix() string {
method Start (line 13) | func (d *Detect) Start() ([]models.Device, error) {
method wwnFallback (line 30) | func (d *Detect) wwnFallback(detectedDevice *models.Device) {
FILE: collector/pkg/detect/wwn.go
type Wwn (line 8) | type Wwn struct
method ToString (line 48) | func (wwn *Wwn) ToString() string {
FILE: collector/pkg/detect/wwn_test.go
function TestWwn_FromStringTable (line 10) | func TestWwn_FromStringTable(t *testing.T) {
FILE: collector/pkg/errors/errors.go
type ConfigFileMissingError (line 8) | type ConfigFileMissingError
method Error (line 10) | func (str ConfigFileMissingError) Error() string {
type ConfigValidationError (line 15) | type ConfigValidationError
method Error (line 17) | func (str ConfigValidationError) Error() string {
type DependencyMissingError (line 22) | type DependencyMissingError
method Error (line 24) | func (str DependencyMissingError) Error() string {
type ApiServerCommunicationError (line 29) | type ApiServerCommunicationError
method Error (line 31) | func (str ApiServerCommunicationError) Error() string {
FILE: collector/pkg/errors/errors_test.go
function TestErrors (line 27) | func TestErrors(t *testing.T) {
FILE: collector/pkg/models/device.go
type Device (line 3) | type Device struct
type DeviceWrapper (line 29) | type DeviceWrapper struct
FILE: collector/pkg/models/scan.go
type Scan (line 3) | type Scan struct
type ScanDevice (line 15) | type ScanDevice struct
FILE: collector/pkg/models/scan_override.go
type ScanOverride (line 3) | type ScanOverride struct
FILE: webapp/backend/cmd/scrutiny/scrutiny.go
function main (line 25) | func main() {
function CreateLogger (line 166) | func CreateLogger(appConfig config.Interface) (*logrus.Entry, *os.File, ...
FILE: webapp/backend/pkg/config/config.go
constant DB_USER_SETTINGS_SUBKEY (line 12) | DB_USER_SETTINGS_SUBKEY = "user"
type configuration (line 18) | type configuration struct
method Init (line 30) | func (c *configuration) Init() error {
method SubKeys (line 72) | func (c *configuration) SubKeys(key string) []string {
method Sub (line 76) | func (c *configuration) Sub(key string) Interface {
method ReadConfig (line 83) | func (c *configuration) ReadConfig(configFilePath string) error {
method ValidateConfig (line 121) | func (c *configuration) ValidateConfig() error {
FILE: webapp/backend/pkg/config/config_test.go
function Test_MergeConfigMap (line 9) | func Test_MergeConfigMap(t *testing.T) {
FILE: webapp/backend/pkg/config/factory.go
function Create (line 3) | func Create() (Interface, error) {
FILE: webapp/backend/pkg/config/interface.go
type Interface (line 9) | type Interface interface
FILE: webapp/backend/pkg/config/mock/mock_config.go
type MockInterface (line 16) | type MockInterface struct
method EXPECT (line 34) | func (m *MockInterface) EXPECT() *MockInterfaceMockRecorder {
method AllKeys (line 39) | func (m *MockInterface) AllKeys() []string {
method AllSettings (line 53) | func (m *MockInterface) AllSettings() map[string]interface{} {
method Get (line 67) | func (m *MockInterface) Get(key string) interface{} {
method GetBool (line 81) | func (m *MockInterface) GetBool(key string) bool {
method GetInt (line 95) | func (m *MockInterface) GetInt(key string) int {
method GetInt64 (line 109) | func (m *MockInterface) GetInt64(key string) int64 {
method GetString (line 123) | func (m *MockInterface) GetString(key string) string {
method GetStringSlice (line 137) | func (m *MockInterface) GetStringSlice(key string) []string {
method Init (line 151) | func (m *MockInterface) Init() error {
method IsSet (line 165) | func (m *MockInterface) IsSet(key string) bool {
method MergeConfigMap (line 179) | func (m *MockInterface) MergeConfigMap(cfg map[string]interface{}) err...
method ReadConfig (line 193) | func (m *MockInterface) ReadConfig(configFilePath string) error {
method Set (line 207) | func (m *MockInterface) Set(key string, value interface{}) {
method SetDefault (line 219) | func (m *MockInterface) SetDefault(key string, value interface{}) {
method Sub (line 231) | func (m *MockInterface) Sub(key string) config.Interface {
method SubKeys (line 245) | func (m *MockInterface) SubKeys(key string) []string {
method UnmarshalKey (line 259) | func (m *MockInterface) UnmarshalKey(key string, rawVal interface{}, d...
method WriteConfig (line 278) | func (m *MockInterface) WriteConfig() error {
type MockInterfaceMockRecorder (line 22) | type MockInterfaceMockRecorder struct
method AllKeys (line 47) | func (mr *MockInterfaceMockRecorder) AllKeys() *gomock.Call {
method AllSettings (line 61) | func (mr *MockInterfaceMockRecorder) AllSettings() *gomock.Call {
method Get (line 75) | func (mr *MockInterfaceMockRecorder) Get(key interface{}) *gomock.Call {
method GetBool (line 89) | func (mr *MockInterfaceMockRecorder) GetBool(key interface{}) *gomock....
method GetInt (line 103) | func (mr *MockInterfaceMockRecorder) GetInt(key interface{}) *gomock.C...
method GetInt64 (line 117) | func (mr *MockInterfaceMockRecorder) GetInt64(key interface{}) *gomock...
method GetString (line 131) | func (mr *MockInterfaceMockRecorder) GetString(key interface{}) *gomoc...
method GetStringSlice (line 145) | func (mr *MockInterfaceMockRecorder) GetStringSlice(key interface{}) *...
method Init (line 159) | func (mr *MockInterfaceMockRecorder) Init() *gomock.Call {
method IsSet (line 173) | func (mr *MockInterfaceMockRecorder) IsSet(key interface{}) *gomock.Ca...
method MergeConfigMap (line 187) | func (mr *MockInterfaceMockRecorder) MergeConfigMap(cfg interface{}) *...
method ReadConfig (line 201) | func (mr *MockInterfaceMockRecorder) ReadConfig(configFilePath interfa...
method Set (line 213) | func (mr *MockInterfaceMockRecorder) Set(key, value interface{}) *gomo...
method SetDefault (line 225) | func (mr *MockInterfaceMockRecorder) SetDefault(key, value interface{}...
method Sub (line 239) | func (mr *MockInterfaceMockRecorder) Sub(key interface{}) *gomock.Call {
method SubKeys (line 253) | func (mr *MockInterfaceMockRecorder) SubKeys(key interface{}) *gomock....
method UnmarshalKey (line 271) | func (mr *MockInterfaceMockRecorder) UnmarshalKey(key, rawVal interfac...
method WriteConfig (line 286) | func (mr *MockInterfaceMockRecorder) WriteConfig() *gomock.Call {
function NewMockInterface (line 27) | func NewMockInterface(ctrl *gomock.Controller) *MockInterface {
FILE: webapp/backend/pkg/constants.go
constant DeviceProtocolAta (line 3) | DeviceProtocolAta = "ATA"
constant DeviceProtocolScsi (line 4) | DeviceProtocolScsi = "SCSI"
constant DeviceProtocolNvme (line 5) | DeviceProtocolNvme = "NVMe"
type AttributeStatus (line 9) | type AttributeStatus
constant AttributeStatusPassed (line 12) | AttributeStatusPassed AttributeStatus = 0
constant AttributeStatusFailedSmart (line 13) | AttributeStatusFailedSmart AttributeStatus = 1
constant AttributeStatusWarningScrutiny (line 14) | AttributeStatusWarningScrutiny AttributeStatus = 2
constant AttributeStatusFailedScrutiny (line 15) | AttributeStatusFailedScrutiny AttributeStatus = 4
constant AttributeWhenFailedFailingNow (line 18) | AttributeWhenFailedFailingNow = "FAILING_NOW"
constant AttributeWhenFailedInThePast (line 19) | AttributeWhenFailedInThePast = "IN_THE_PAST"
function AttributeStatusSet (line 21) | func AttributeStatusSet(b, flag AttributeStatus) AttributeStatus { re...
function AttributeStatusClear (line 22) | func AttributeStatusClear(b, flag AttributeStatus) AttributeStatus { re...
function AttributeStatusToggle (line 23) | func AttributeStatusToggle(b, flag AttributeStatus) AttributeStatus { re...
function AttributeStatusHas (line 24) | func AttributeStatusHas(b, flag AttributeStatus) bool { re...
type DeviceStatus (line 28) | type DeviceStatus
constant DeviceStatusPassed (line 31) | DeviceStatusPassed DeviceStatus = 0
constant DeviceStatusFailedSmart (line 32) | DeviceStatusFailedSmart DeviceStatus = 1
constant DeviceStatusFailedScrutiny (line 33) | DeviceStatusFailedScrutiny DeviceStatus = 2
function DeviceStatusSet (line 36) | func DeviceStatusSet(b, flag DeviceStatus) DeviceStatus { return b | ...
function DeviceStatusClear (line 37) | func DeviceStatusClear(b, flag DeviceStatus) DeviceStatus { return b &^...
function DeviceStatusToggle (line 38) | func DeviceStatusToggle(b, flag DeviceStatus) DeviceStatus { return b ^ ...
function DeviceStatusHas (line 39) | func DeviceStatusHas(b, flag DeviceStatus) bool { return b&fl...
type MetricsNotifyLevel (line 42) | type MetricsNotifyLevel
constant MetricsNotifyLevelWarn (line 45) | MetricsNotifyLevelWarn MetricsNotifyLevel = 1
constant MetricsNotifyLevelFail (line 46) | MetricsNotifyLevelFail MetricsNotifyLevel = 2
type MetricsStatusFilterAttributes (line 49) | type MetricsStatusFilterAttributes
constant MetricsStatusFilterAttributesAll (line 52) | MetricsStatusFilterAttributesAll MetricsStatusFilterAttributes = 0
constant MetricsStatusFilterAttributesCritical (line 53) | MetricsStatusFilterAttributesCritical MetricsStatusFilterAttributes = 1
type MetricsStatusThreshold (line 57) | type MetricsStatusThreshold
constant MetricsStatusThresholdSmart (line 60) | MetricsStatusThresholdSmart MetricsStatusThreshold = 1
constant MetricsStatusThresholdScrutiny (line 61) | MetricsStatusThresholdScrutiny MetricsStatusThreshold = 2
constant MetricsStatusThresholdBoth (line 64) | MetricsStatusThresholdBoth MetricsStatusThreshold = 3
FILE: webapp/backend/pkg/database/interface.go
type DeviceRepo (line 14) | type DeviceRepo interface
FILE: webapp/backend/pkg/database/migrations/m20201107210306/device.go
type Device (line 8) | type Device struct
method IsAta (line 37) | func (dv *Device) IsAta() bool {
method IsScsi (line 41) | func (dv *Device) IsScsi() bool {
method IsNvme (line 45) | func (dv *Device) IsNvme() bool {
constant DeviceProtocolAta (line 33) | DeviceProtocolAta = "ATA"
constant DeviceProtocolScsi (line 34) | DeviceProtocolScsi = "SCSI"
constant DeviceProtocolNvme (line 35) | DeviceProtocolNvme = "NVMe"
FILE: webapp/backend/pkg/database/migrations/m20201107210306/smart.go
type Smart (line 9) | type Smart struct
FILE: webapp/backend/pkg/database/migrations/m20201107210306/smart_ata_attribute.go
type SmartAtaAttribute (line 6) | type SmartAtaAttribute struct
FILE: webapp/backend/pkg/database/migrations/m20201107210306/smart_nvme_attribute.go
type SmartNvmeAttribute (line 6) | type SmartNvmeAttribute struct
FILE: webapp/backend/pkg/database/migrations/m20201107210306/smart_scsci_attribute.go
type SmartScsiAttribute (line 6) | type SmartScsiAttribute struct
FILE: webapp/backend/pkg/database/migrations/m20220503120000/device.go
type Device (line 9) | type Device struct
FILE: webapp/backend/pkg/database/migrations/m20220509170100/device.go
type Device (line 9) | type Device struct
FILE: webapp/backend/pkg/database/migrations/m20220716214900/setting.go
type Setting (line 7) | type Setting struct
FILE: webapp/backend/pkg/database/migrations/m20250221084400/device.go
type Device (line 8) | type Device struct
FILE: webapp/backend/pkg/database/mock/mock_database.go
type MockDeviceRepo (line 19) | type MockDeviceRepo struct
method EXPECT (line 37) | func (m *MockDeviceRepo) EXPECT() *MockDeviceRepoMockRecorder {
method Close (line 42) | func (m *MockDeviceRepo) Close() error {
method UpdateDeviceArchived (line 56) | func (m *MockDeviceRepo) UpdateDeviceArchived(ctx context.Context, wwn...
method DeleteDevice (line 70) | func (m *MockDeviceRepo) DeleteDevice(ctx context.Context, wwn string)...
method GetDeviceDetails (line 84) | func (m *MockDeviceRepo) GetDeviceDetails(ctx context.Context, wwn str...
method GetDevices (line 99) | func (m *MockDeviceRepo) GetDevices(ctx context.Context) ([]models.Dev...
method GetSmartAttributeHistory (line 114) | func (m *MockDeviceRepo) GetSmartAttributeHistory(ctx context.Context,...
method GetSmartTemperatureHistory (line 129) | func (m *MockDeviceRepo) GetSmartTemperatureHistory(ctx context.Contex...
method GetSummary (line 144) | func (m *MockDeviceRepo) GetSummary(ctx context.Context) (map[string]*...
method HealthCheck (line 159) | func (m *MockDeviceRepo) HealthCheck(ctx context.Context) error {
method LoadSettings (line 173) | func (m *MockDeviceRepo) LoadSettings(ctx context.Context) (*models.Se...
method RegisterDevice (line 188) | func (m *MockDeviceRepo) RegisterDevice(ctx context.Context, dev model...
method SaveSettings (line 202) | func (m *MockDeviceRepo) SaveSettings(ctx context.Context, settings mo...
method SaveSmartAttributes (line 216) | func (m *MockDeviceRepo) SaveSmartAttributes(ctx context.Context, wwn ...
method SaveSmartTemperature (line 231) | func (m *MockDeviceRepo) SaveSmartTemperature(ctx context.Context, wwn...
method UpdateDevice (line 245) | func (m *MockDeviceRepo) UpdateDevice(ctx context.Context, wwn string,...
method UpdateDeviceStatus (line 260) | func (m *MockDeviceRepo) UpdateDeviceStatus(ctx context.Context, wwn s...
type MockDeviceRepoMockRecorder (line 25) | type MockDeviceRepoMockRecorder struct
method Close (line 50) | func (mr *MockDeviceRepoMockRecorder) Close() *gomock.Call {
method UpdateDeviceArchived (line 64) | func (mr *MockDeviceRepoMockRecorder) UpdateDeviceArchived(ctx, wwn, a...
method DeleteDevice (line 78) | func (mr *MockDeviceRepoMockRecorder) DeleteDevice(ctx, wwn interface{...
method GetDeviceDetails (line 93) | func (mr *MockDeviceRepoMockRecorder) GetDeviceDetails(ctx, wwn interf...
method GetDevices (line 108) | func (mr *MockDeviceRepoMockRecorder) GetDevices(ctx interface{}) *gom...
method GetSmartAttributeHistory (line 123) | func (mr *MockDeviceRepoMockRecorder) GetSmartAttributeHistory(ctx, ww...
method GetSmartTemperatureHistory (line 138) | func (mr *MockDeviceRepoMockRecorder) GetSmartTemperatureHistory(ctx, ...
method GetSummary (line 153) | func (mr *MockDeviceRepoMockRecorder) GetSummary(ctx interface{}) *gom...
method HealthCheck (line 167) | func (mr *MockDeviceRepoMockRecorder) HealthCheck(ctx interface{}) *go...
method LoadSettings (line 182) | func (mr *MockDeviceRepoMockRecorder) LoadSettings(ctx interface{}) *g...
method RegisterDevice (line 196) | func (mr *MockDeviceRepoMockRecorder) RegisterDevice(ctx, dev interfac...
method SaveSettings (line 210) | func (mr *MockDeviceRepoMockRecorder) SaveSettings(ctx, settings inter...
method SaveSmartAttributes (line 225) | func (mr *MockDeviceRepoMockRecorder) SaveSmartAttributes(ctx, wwn, co...
method SaveSmartTemperature (line 239) | func (mr *MockDeviceRepoMockRecorder) SaveSmartTemperature(ctx, wwn, d...
method UpdateDevice (line 254) | func (mr *MockDeviceRepoMockRecorder) UpdateDevice(ctx, wwn, collector...
method UpdateDeviceStatus (line 269) | func (mr *MockDeviceRepoMockRecorder) UpdateDeviceStatus(ctx, wwn, sta...
function NewMockDeviceRepo (line 30) | func NewMockDeviceRepo(ctrl *gomock.Controller) *MockDeviceRepo {
FILE: webapp/backend/pkg/database/scrutiny_repository.go
constant RETENTION_PERIOD_15_DAYS_IN_SECONDS (line 25) | RETENTION_PERIOD_15_DAYS_IN_SECONDS = 1_296_000
constant RETENTION_PERIOD_9_WEEKS_IN_SECONDS (line 28) | RETENTION_PERIOD_9_WEEKS_IN_SECONDS = 5_443_200
constant RETENTION_PERIOD_25_MONTHS_IN_SECONDS (line 31) | RETENTION_PERIOD_25_MONTHS_IN_SECONDS = 65_318_400
constant DURATION_KEY_DAY (line 33) | DURATION_KEY_DAY = "day"
constant DURATION_KEY_WEEK (line 34) | DURATION_KEY_WEEK = "week"
constant DURATION_KEY_MONTH (line 35) | DURATION_KEY_MONTH = "month"
constant DURATION_KEY_YEAR (line 36) | DURATION_KEY_YEAR = "year"
constant DURATION_KEY_FOREVER (line 37) | DURATION_KEY_FOREVER = "forever"
function NewScrutinyRepository (line 61) | func NewScrutinyRepository(appConfig config.Interface, globalLogger logr...
type scrutinyRepository (line 193) | type scrutinyRepository struct
method Close (line 205) | func (sr *scrutinyRepository) Close() error {
method HealthCheck (line 210) | func (sr *scrutinyRepository) HealthCheck(ctx context.Context) error {
method EnsureBuckets (line 265) | func (sr *scrutinyRepository) EnsureBuckets(ctx context.Context, org *...
method GetSummary (line 336) | func (sr *scrutinyRepository) GetSummary(ctx context.Context) (map[str...
method lookupBucketName (line 448) | func (sr *scrutinyRepository) lookupBucketName(durationKey string) str...
method lookupDuration (line 467) | func (sr *scrutinyRepository) lookupDuration(durationKey string) []str...
method lookupResolution (line 488) | func (sr *scrutinyRepository) lookupResolution(durationKey string) str...
method lookupNestedDurationKeys (line 499) | func (sr *scrutinyRepository) lookupNestedDurationKeys(durationKey str...
function InfluxSetupComplete (line 233) | func InfluxSetupComplete(influxEndpoint string, tlsConfig *tls.Config) (...
function sqlitePragmaString (line 520) | func sqlitePragmaString(pragmas map[string]string) string {
FILE: webapp/backend/pkg/database/scrutiny_repository_device.go
method RegisterDevice (line 20) | func (sr *scrutinyRepository) RegisterDevice(ctx context.Context, dev mo...
method GetDevices (line 31) | func (sr *scrutinyRepository) GetDevices(ctx context.Context) ([]models....
method UpdateDevice (line 41) | func (sr *scrutinyRepository) UpdateDevice(ctx context.Context, wwn stri...
method UpdateDeviceStatus (line 56) | func (sr *scrutinyRepository) UpdateDeviceStatus(ctx context.Context, ww...
method GetDeviceDetails (line 66) | func (sr *scrutinyRepository) GetDeviceDetails(ctx context.Context, wwn ...
method UpdateDeviceArchived (line 79) | func (sr *scrutinyRepository) UpdateDeviceArchived(ctx context.Context, ...
method DeleteDevice (line 88) | func (sr *scrutinyRepository) DeleteDevice(ctx context.Context, wwn stri...
FILE: webapp/backend/pkg/database/scrutiny_repository_device_smart_attributes.go
method SaveSmartAttributes (line 19) | func (sr *scrutinyRepository) SaveSmartAttributes(ctx context.Context, w...
method GetSmartAttributeHistory (line 37) | func (sr *scrutinyRepository) GetSmartAttributeHistory(ctx context.Conte...
method saveDatapoint (line 92) | func (sr *scrutinyRepository) saveDatapoint(influxWriteApi api.WriteAPIB...
method aggregateSmartAttributesQuery (line 103) | func (sr *scrutinyRepository) aggregateSmartAttributesQuery(wwn string, ...
method generateSmartAttributesSubquery (line 187) | func (sr *scrutinyRepository) generateSmartAttributesSubquery(wwn string...
FILE: webapp/backend/pkg/database/scrutiny_repository_migrations.go
method Migrate (line 31) | func (sr *scrutinyRepository) Migrate(ctx context.Context) error {
function ignorePastRetentionPolicyError (line 465) | func ignorePastRetentionPolicyError(err error) error {
function m20201107210306_FromPreInfluxDBTempCreatePostInfluxDBTemp (line 477) | func m20201107210306_FromPreInfluxDBTempCreatePostInfluxDBTemp(preDevice...
function m20201107210306_FromPreInfluxDBSmartResultsCreatePostInfluxDBSmartResults (line 488) | func m20201107210306_FromPreInfluxDBSmartResultsCreatePostInfluxDBSmartR...
FILE: webapp/backend/pkg/database/scrutiny_repository_settings.go
method LoadSettings (line 14) | func (sr *scrutinyRepository) LoadSettings(ctx context.Context) (*models...
method SaveSettings (line 45) | func (sr *scrutinyRepository) SaveSettings(ctx context.Context, settings...
FILE: webapp/backend/pkg/database/scrutiny_repository_tasks.go
method EnsureTasks (line 12) | func (sr *scrutinyRepository) EnsureTasks(ctx context.Context, orgID str...
method DownsampleScript (line 78) | func (sr *scrutinyRepository) DownsampleScript(aggregationType string, n...
FILE: webapp/backend/pkg/database/scrutiny_repository_tasks_test.go
function Test_DownsampleScript_Weekly (line 11) | func Test_DownsampleScript_Weekly(t *testing.T) {
function Test_DownsampleScript_Monthly (line 62) | func Test_DownsampleScript_Monthly(t *testing.T) {
function Test_DownsampleScript_Yearly (line 113) | func Test_DownsampleScript_Yearly(t *testing.T) {
FILE: webapp/backend/pkg/database/scrutiny_repository_temperature.go
method SaveSmartTemperature (line 17) | func (sr *scrutinyRepository) SaveSmartTemperature(ctx context.Context, ...
method GetSmartTemperatureHistory (line 63) | func (sr *scrutinyRepository) GetSmartTemperatureHistory(ctx context.Con...
method aggregateTempQuery (line 108) | func (sr *scrutinyRepository) aggregateTempQuery(durationKey string) str...
FILE: webapp/backend/pkg/database/scrutiny_repository_temperature_test.go
function Test_aggregateTempQuery_Week (line 11) | func Test_aggregateTempQuery_Week(t *testing.T) {
function Test_aggregateTempQuery_Month (line 43) | func Test_aggregateTempQuery_Month(t *testing.T) {
function Test_aggregateTempQuery_Year (line 83) | func Test_aggregateTempQuery_Year(t *testing.T) {
function Test_aggregateTempQuery_Forever (line 130) | func Test_aggregateTempQuery_Forever(t *testing.T) {
FILE: webapp/backend/pkg/errors/errors.go
type ConfigFileMissingError (line 8) | type ConfigFileMissingError
method Error (line 10) | func (str ConfigFileMissingError) Error() string {
type ConfigValidationError (line 15) | type ConfigValidationError
method Error (line 17) | func (str ConfigValidationError) Error() string {
type DependencyMissingError (line 22) | type DependencyMissingError
method Error (line 24) | func (str DependencyMissingError) Error() string {
type NotificationValidationError (line 29) | type NotificationValidationError
method Error (line 31) | func (str NotificationValidationError) Error() string {
FILE: webapp/backend/pkg/errors/errors_test.go
function TestErrors (line 27) | func TestErrors(t *testing.T) {
FILE: webapp/backend/pkg/models/collector/smart.go
type SmartInfo (line 3) | type SmartInfo struct
method Capacity (line 241) | func (s *SmartInfo) Capacity() int64 {
type UserCapacity (line 251) | type UserCapacity struct
type AtaSmartAttributesTableItem (line 257) | type AtaSmartAttributesTableItem struct
type NvmeSmartHealthInformationLog (line 280) | type NvmeSmartHealthInformationLog struct
type ScsiErrorCounterLog (line 300) | type ScsiErrorCounterLog struct
FILE: webapp/backend/pkg/models/collector/smart_test.go
function TestSmartInfo_Capacity (line 9) | func TestSmartInfo_Capacity(t *testing.T) {
FILE: webapp/backend/pkg/models/device.go
type DeviceWrapper (line 9) | type DeviceWrapper struct
type Device (line 15) | type Device struct
method IsAta (line 50) | func (dv *Device) IsAta() bool {
method IsScsi (line 54) | func (dv *Device) IsScsi() bool {
method IsNvme (line 58) | func (dv *Device) IsNvme() bool {
method UpdateFromCollectorSmartInfo (line 165) | func (dv *Device) UpdateFromCollectorSmartInfo(info collector.SmartInf...
FILE: webapp/backend/pkg/models/device_summary.go
type DeviceSummaryWrapper (line 8) | type DeviceSummaryWrapper struct
type DeviceSummary (line 16) | type DeviceSummary struct
type SmartSummary (line 22) | type SmartSummary struct
FILE: webapp/backend/pkg/models/measurements/smart.go
type Smart (line 15) | type Smart struct
method Flatten (line 32) | func (sm *Smart) Flatten() (tags map[string]string, fields map[string]...
method FromCollectorSmartInfo (line 121) | func (sm *Smart) FromCollectorSmartInfo(wwn string, info collector.Sma...
method ProcessAtaSmartInfo (line 148) | func (sm *Smart) ProcessAtaSmartInfo(tableItems []collector.AtaSmartAt...
method ProcessNvmeSmartInfo (line 176) | func (sm *Smart) ProcessNvmeSmartInfo(nvmeSmartHealthInformationLog co...
method ProcessScsiSmartInfo (line 206) | func (sm *Smart) ProcessScsiSmartInfo(defectGrownList int64, scsiError...
function NewSmartFromInfluxDB (line 53) | func NewSmartFromInfluxDB(attrs map[string]interface{}) (*Smart, error) {
FILE: webapp/backend/pkg/models/measurements/smart_ata_attribute.go
type SmartAtaAttribute (line 12) | type SmartAtaAttribute struct
method GetTransformedValue (line 28) | func (sa *SmartAtaAttribute) GetTransformedValue() int64 {
method GetStatus (line 32) | func (sa *SmartAtaAttribute) GetStatus() pkg.AttributeStatus {
method Flatten (line 36) | func (sa *SmartAtaAttribute) Flatten() map[string]interface{} {
method Inflate (line 56) | func (sa *SmartAtaAttribute) Inflate(key string, val interface{}) {
method PopulateAttributeStatus (line 96) | func (sa *SmartAtaAttribute) PopulateAttributeStatus() *SmartAtaAttrib...
method ValidateThreshold (line 117) | func (sa *SmartAtaAttribute) ValidateThreshold(smartMetadata threshold...
FILE: webapp/backend/pkg/models/measurements/smart_attribute.go
type SmartAttribute (line 5) | type SmartAttribute interface
FILE: webapp/backend/pkg/models/measurements/smart_nvme_attribute.go
type SmartNvmeAttribute (line 11) | type SmartNvmeAttribute struct
method GetTransformedValue (line 22) | func (sa *SmartNvmeAttribute) GetTransformedValue() int64 {
method GetStatus (line 26) | func (sa *SmartNvmeAttribute) GetStatus() pkg.AttributeStatus {
method Flatten (line 30) | func (sa *SmartNvmeAttribute) Flatten() map[string]interface{} {
method Inflate (line 43) | func (sa *SmartNvmeAttribute) Inflate(key string, val interface{}) {
method PopulateAttributeStatus (line 72) | func (sa *SmartNvmeAttribute) PopulateAttributeStatus() *SmartNvmeAttr...
FILE: webapp/backend/pkg/models/measurements/smart_scsci_attribute.go
type SmartScsiAttribute (line 11) | type SmartScsiAttribute struct
method GetTransformedValue (line 22) | func (sa *SmartScsiAttribute) GetTransformedValue() int64 {
method GetStatus (line 26) | func (sa *SmartScsiAttribute) GetStatus() pkg.AttributeStatus {
method Flatten (line 30) | func (sa *SmartScsiAttribute) Flatten() map[string]interface{} {
method Inflate (line 43) | func (sa *SmartScsiAttribute) Inflate(key string, val interface{}) {
method PopulateAttributeStatus (line 73) | func (sa *SmartScsiAttribute) PopulateAttributeStatus() *SmartScsiAttr...
FILE: webapp/backend/pkg/models/measurements/smart_temperature.go
type SmartTemperature (line 7) | type SmartTemperature struct
method Flatten (line 12) | func (st *SmartTemperature) Flatten() (tags map[string]string, fields ...
method Inflate (line 21) | func (st *SmartTemperature) Inflate(key string, val interface{}) {
FILE: webapp/backend/pkg/models/measurements/smart_test.go
function TestSmart_Flatten (line 16) | func TestSmart_Flatten(t *testing.T) {
function TestSmart_Flatten_ATA (line 38) | func TestSmart_Flatten_ATA(t *testing.T) {
function TestSmart_Flatten_SCSI (line 107) | func TestSmart_Flatten_SCSI(t *testing.T) {
function TestSmart_Flatten_NVMe (line 145) | func TestSmart_Flatten_NVMe(t *testing.T) {
function TestNewSmartFromInfluxDB_ATA (line 182) | func TestNewSmartFromInfluxDB_ATA(t *testing.T) {
function TestNewSmartFromInfluxDB_NVMe (line 230) | func TestNewSmartFromInfluxDB_NVMe(t *testing.T) {
function TestNewSmartFromInfluxDB_SCSI (line 269) | func TestNewSmartFromInfluxDB_SCSI(t *testing.T) {
function TestFromCollectorSmartInfo (line 308) | func TestFromCollectorSmartInfo(t *testing.T) {
function TestFromCollectorSmartInfo_Fail_Smart (line 340) | func TestFromCollectorSmartInfo_Fail_Smart(t *testing.T) {
function TestFromCollectorSmartInfo_Fail_ScrutinySmart (line 364) | func TestFromCollectorSmartInfo_Fail_ScrutinySmart(t *testing.T) {
function TestFromCollectorSmartInfo_Fail_ScrutinyNonCriticalFailed (line 388) | func TestFromCollectorSmartInfo_Fail_ScrutinyNonCriticalFailed(t *testin...
function TestFromCollectorSmartInfo_NVMe_Fail_Scrutiny (line 421) | func TestFromCollectorSmartInfo_NVMe_Fail_Scrutiny(t *testing.T) {
function TestFromCollectorSmartInfo_Nvme (line 452) | func TestFromCollectorSmartInfo_Nvme(t *testing.T) {
function TestFromCollectorSmartInfo_Scsi (line 479) | func TestFromCollectorSmartInfo_Scsi(t *testing.T) {
FILE: webapp/backend/pkg/models/setting_entry.go
type SettingEntry (line 8) | type SettingEntry struct
method TableName (line 21) | func (s SettingEntry) TableName() string {
FILE: webapp/backend/pkg/models/settings.go
type Settings (line 10) | type Settings struct
FILE: webapp/backend/pkg/models/testdata/helper.go
function main (line 16) | func main() {
function SendPostRequest (line 61) | func SendPostRequest(url string, file io.Reader) ([]byte, error) {
function readSmartDataFileFixTimestamp (line 75) | func readSmartDataFileFixTimestamp(daysToSubtract int, smartDataFilepath...
FILE: webapp/backend/pkg/notify/notify.go
constant NotifyFailureTypeEmailTest (line 29) | NotifyFailureTypeEmailTest = "EmailTest"
constant NotifyFailureTypeBothFailure (line 30) | NotifyFailureTypeBothFailure = "SmartFailure"
constant NotifyFailureTypeSmartFailure (line 31) | NotifyFailureTypeSmartFailure = "SmartFailure"
constant NotifyFailureTypeScrutinyFailure (line 32) | NotifyFailureTypeScrutinyFailure = "ScrutinyFailure"
function ShouldNotify (line 35) | func ShouldNotify(logger logrus.FieldLogger, device models.Device, smart...
type Payload (line 125) | type Payload struct
method GenerateFailureType (line 163) | func (p *Payload) GenerateFailureType(deviceStatus pkg.DeviceStatus) s...
method GenerateSubject (line 177) | func (p *Payload) GenerateSubject() string {
method GenerateMessage (line 188) | func (p *Payload) GenerateMessage() string {
function NewPayload (line 139) | func NewPayload(device models.Device, test bool, currentTime ...time.Tim...
function New (line 214) | func New(logger logrus.FieldLogger, appconfig config.Interface, device m...
type Notify (line 222) | type Notify struct
method Send (line 228) | func (n *Notify) Send() error {
method SendWebhookNotification (line 296) | func (n *Notify) SendWebhookNotification(webhookUrl string) error {
method SendScriptNotification (line 314) | func (n *Notify) SendScriptNotification(scriptUrl string) error {
method SendShoutrrrNotification (line 343) | func (n *Notify) SendShoutrrrNotification(shoutrrrUrl string) error {
method GenShoutrrrNotificationParams (line 385) | func (n *Notify) GenShoutrrrNotificationParams(shoutrrrUrl string) (st...
FILE: webapp/backend/pkg/notify/notify_test.go
function TestShouldNotify_MustSkipPassingDevices (line 20) | func TestShouldNotify_MustSkipPassingDevices(t *testing.T) {
function TestShouldNotify_MetricsStatusThresholdBoth_FailingSmartDevice (line 36) | func TestShouldNotify_MetricsStatusThresholdBoth_FailingSmartDevice(t *t...
function TestShouldNotify_MetricsStatusThresholdSmart_FailingSmartDevice (line 51) | func TestShouldNotify_MetricsStatusThresholdSmart_FailingSmartDevice(t *...
function TestShouldNotify_MetricsStatusThresholdScrutiny_FailingSmartDevice (line 66) | func TestShouldNotify_MetricsStatusThresholdScrutiny_FailingSmartDevice(...
function TestShouldNotify_MetricsStatusFilterAttributesCritical_WithCriticalAttrs (line 81) | func TestShouldNotify_MetricsStatusFilterAttributesCritical_WithCritical...
function TestShouldNotify_MetricsStatusFilterAttributesCritical_WithMultipleCriticalAttrs (line 101) | func TestShouldNotify_MetricsStatusFilterAttributesCritical_WithMultiple...
function TestShouldNotify_MetricsStatusFilterAttributesCritical_WithNoCriticalAttrs (line 124) | func TestShouldNotify_MetricsStatusFilterAttributesCritical_WithNoCritic...
function TestShouldNotify_MetricsStatusFilterAttributesCritical_WithNoFailingCriticalAttrs (line 144) | func TestShouldNotify_MetricsStatusFilterAttributesCritical_WithNoFailin...
function TestShouldNotify_MetricsStatusFilterAttributesCritical_MetricsStatusThresholdSmart_WithCriticalAttrsFailingScrutiny (line 164) | func TestShouldNotify_MetricsStatusFilterAttributesCritical_MetricsStatu...
function TestShouldNotify_NoRepeat_DatabaseFailure (line 186) | func TestShouldNotify_NoRepeat_DatabaseFailure(t *testing.T) {
function TestShouldNotify_NoRepeat_NoDatabaseData (line 207) | func TestShouldNotify_NoRepeat_NoDatabaseData(t *testing.T) {
function TestShouldNotify_NoRepeat (line 227) | func TestShouldNotify_NoRepeat(t *testing.T) {
function TestNewPayload (line 249) | func TestNewPayload(t *testing.T) {
function TestNewPayload_TestMode (line 275) | func TestNewPayload_TestMode(t *testing.T) {
function TestNewPayload_WithHostId (line 302) | func TestNewPayload_WithHostId(t *testing.T) {
FILE: webapp/backend/pkg/thresholds/ata_attribute_metadata.go
constant AtaSmartAttributeDisplayTypeRaw (line 8) | AtaSmartAttributeDisplayTypeRaw = "raw"
constant AtaSmartAttributeDisplayTypeNormalized (line 9) | AtaSmartAttributeDisplayTypeNormalized = "normalized"
constant AtaSmartAttributeDisplayTypeTransformed (line 10) | AtaSmartAttributeDisplayTypeTransformed = "transformed"
type AtaAttributeMetadata (line 12) | type AtaAttributeMetadata struct
constant ObservedThresholdIdealLow (line 25) | ObservedThresholdIdealLow = "low"
constant ObservedThresholdIdealHigh (line 26) | ObservedThresholdIdealHigh = "high"
type ObservedThreshold (line 28) | type ObservedThreshold struct
FILE: webapp/backend/pkg/thresholds/nvme_attribute_metadata.go
type NvmeAttributeMetadata (line 7) | type NvmeAttributeMetadata struct
FILE: webapp/backend/pkg/thresholds/scsi_attribute_metadata.go
type ScsiAttributeMetadata (line 3) | type ScsiAttributeMetadata struct
FILE: webapp/backend/pkg/version/version.go
constant VERSION (line 5) | VERSION = "0.8.6"
FILE: webapp/backend/pkg/web/handler/archive_device.go
function ArchiveDevice (line 10) | func ArchiveDevice(c *gin.Context) {
FILE: webapp/backend/pkg/web/handler/delete_device.go
function DeleteDevice (line 10) | func DeleteDevice(c *gin.Context) {
FILE: webapp/backend/pkg/web/handler/get_device_details.go
function GetDeviceDetails (line 12) | func GetDeviceDetails(c *gin.Context) {
FILE: webapp/backend/pkg/web/handler/get_devices_summary.go
function GetDevicesSummary (line 10) | func GetDevicesSummary(c *gin.Context) {
FILE: webapp/backend/pkg/web/handler/get_devices_summary_temp_history.go
function GetDevicesSummaryTempHistory (line 10) | func GetDevicesSummaryTempHistory(c *gin.Context) {
FILE: webapp/backend/pkg/web/handler/get_settings.go
function GetSettings (line 10) | func GetSettings(c *gin.Context) {
FILE: webapp/backend/pkg/web/handler/health_check.go
function HealthCheck (line 10) | func HealthCheck(c *gin.Context) {
FILE: webapp/backend/pkg/web/handler/register_devices.go
function RegisterDevices (line 14) | func RegisterDevices(c *gin.Context) {
FILE: webapp/backend/pkg/web/handler/save_settings.go
function SaveSettings (line 11) | func SaveSettings(c *gin.Context) {
FILE: webapp/backend/pkg/web/handler/send_test_notification.go
function SendTestNotification (line 14) | func SendTestNotification(c *gin.Context) {
FILE: webapp/backend/pkg/web/handler/unarchive_device.go
function UnarchiveDevice (line 10) | func UnarchiveDevice(c *gin.Context) {
FILE: webapp/backend/pkg/web/handler/upload_device_metrics.go
function UploadDeviceMetrics (line 16) | func UploadDeviceMetrics(c *gin.Context) {
FILE: webapp/backend/pkg/web/handler/upload_device_self_tests.go
function UploadDeviceSelfTests (line 5) | func UploadDeviceSelfTests(c *gin.Context) {
FILE: webapp/backend/pkg/web/middleware/config.go
function ConfigMiddleware (line 8) | func ConfigMiddleware(appConfig config.Interface) gin.HandlerFunc {
FILE: webapp/backend/pkg/web/middleware/logger.go
function LoggerMiddleware (line 31) | func LoggerMiddleware(logger *logrus.Entry) gin.HandlerFunc {
type responseBodyLogWriter (line 104) | type responseBodyLogWriter struct
method Write (line 109) | func (w responseBodyLogWriter) Write(b []byte) (int, error) {
function readBody (line 116) | func readBody(reader io.Reader) string {
FILE: webapp/backend/pkg/web/middleware/repository.go
function RepositoryMiddleware (line 11) | func RepositoryMiddleware(appConfig config.Interface, globalLogger logru...
FILE: webapp/backend/pkg/web/server.go
type AppEngine (line 17) | type AppEngine struct
method Setup (line 22) | func (ae *AppEngine) Setup(logger *logrus.Entry) *gin.Engine {
method Start (line 70) | func (ae *AppEngine) Start() error {
FILE: webapp/backend/pkg/web/server_test.go
function helperReadSmartDataFileFixTimestamp (line 50) | func helperReadSmartDataFileFixTimestamp(t *testing.T, smartDataFilepath...
type ServerTestSuite (line 70) | type ServerTestSuite struct
method TestHealthRoute (line 87) | func (suite *ServerTestSuite) TestHealthRoute() {
method TestRegisterDevicesRoute (line 130) | func (suite *ServerTestSuite) TestRegisterDevicesRoute() {
method TestUploadDeviceMetricsRoute (line 172) | func (suite *ServerTestSuite) TestUploadDeviceMetricsRoute() {
method TestPopulateMultiple (line 226) | func (suite *ServerTestSuite) TestPopulateMultiple() {
method TestSendTestNotificationRoute_WebhookFailure (line 330) | func (suite *ServerTestSuite) TestSendTestNotificationRoute_WebhookFai...
method TestSendTestNotificationRoute_ScriptFailure (line 375) | func (suite *ServerTestSuite) TestSendTestNotificationRoute_ScriptFail...
method TestSendTestNotificationRoute_ScriptSuccess (line 420) | func (suite *ServerTestSuite) TestSendTestNotificationRoute_ScriptSucc...
method TestSendTestNotificationRoute_ShoutrrrFailure (line 465) | func (suite *ServerTestSuite) TestSendTestNotificationRoute_ShoutrrrFa...
method TestGetDevicesSummaryRoute_Nvme (line 509) | func (suite *ServerTestSuite) TestGetDevicesSummaryRoute_Nvme() {
function TestServerTestSuite_WithEmptyBasePath (line 75) | func TestServerTestSuite_WithEmptyBasePath(t *testing.T) {
function TestServerTestSuite_WithCustomBasePath (line 81) | func TestServerTestSuite_WithCustomBasePath(t *testing.T) {
FILE: webapp/frontend/e2e/protractor.conf.js
method onPrepare (line 28) | onPrepare()
FILE: webapp/frontend/e2e/src/app.po.ts
class AppPage (line 3) | class AppPage
method navigateTo (line 5) | navigateTo(): Promise<unknown>
method getTitleText (line 10) | getTitleText(): Promise<string>
FILE: webapp/frontend/src/@treo/animations/defaults.ts
class TreoAnimationCurves (line 1) | class TreoAnimationCurves
class TreoAnimationDurations (line 9) | class TreoAnimationDurations
FILE: webapp/frontend/src/@treo/components/card/card.component.ts
class TreoCardComponent (line 12) | class TreoCardComponent
method constructor (line 26) | constructor(
method flippable (line 47) | set flippable(value: boolean)
method flippable (line 69) | get flippable(): boolean
method expand (line 81) | expand(): void
method collapse (line 89) | collapse(): void
method toggleExpanded (line 97) | toggleExpanded(): void
method flip (line 105) | flip(): void
FILE: webapp/frontend/src/@treo/components/card/card.module.ts
class TreoCardModule (line 16) | class TreoCardModule
FILE: webapp/frontend/src/@treo/components/date-range/date-range.component.ts
class TreoDateRangeComponent (line 24) | class TreoDateRangeComponent implements ControlValueAccessor, OnInit, On...
method constructor (line 70) | constructor(
method dateFormat (line 115) | set dateFormat(value: string)
method dateFormat (line 127) | get dateFormat(): string
method timeFormat (line 138) | set timeFormat(value: string)
method timeFormat (line 150) | get timeFormat(): string
method timeRange (line 161) | set timeRange(value: boolean)
method timeRange (line 182) | get timeRange(): boolean
method range (line 193) | set range(value)
method range (line 319) | get range(): any
method registerOnChange (line 343) | registerOnChange(fn: any): void
method registerOnTouched (line 353) | registerOnTouched(fn: any): void
method writeValue (line 363) | writeValue(range: { start: string, end: string }): void
method ngOnInit (line 379) | ngOnInit(): void
method ngOnDestroy (line 387) | ngOnDestroy(): void
method _init (line 407) | private _init(): void
method _parseTime (line 431) | private _parseTime(value: string): Moment
method openPickerPanel (line 457) | openPickerPanel(): void
method getMonthLabel (line 516) | getMonthLabel(month: number): string
method dateClass (line 529) | dateClass(): any
method dateFilter (line 564) | dateFilter(): any
method onSelectedDateChange (line 578) | onSelectedDateChange(date: Moment): void
method prev (line 611) | prev(): void
method next (line 620) | next(): void
method updateStartTime (line 631) | updateStartTime(event): void
method updateEndTime (line 676) | updateEndTime(event): void
FILE: webapp/frontend/src/@treo/components/date-range/date-range.module.ts
class TreoDateRangeModule (line 30) | class TreoDateRangeModule
FILE: webapp/frontend/src/@treo/components/drawer/drawer.component.ts
class TreoDrawerComponent (line 13) | class TreoDrawerComponent implements OnInit, OnDestroy
method constructor (line 55) | constructor(
method fixed (line 89) | set fixed(value: boolean)
method fixed (line 114) | get fixed(): boolean
method mode (line 125) | set mode(value: TreoDrawerMode)
method mode (line 178) | get mode(): TreoDrawerMode
method opened (line 189) | set opened(value: boolean | '')
method opened (line 237) | get opened(): boolean | ''
method position (line 248) | set position(value: TreoDrawerPosition)
method position (line 273) | get position(): TreoDrawerPosition
method transparentOverlay (line 284) | set transparentOverlay(value: boolean | '')
method transparentOverlay (line 306) | get transparentOverlay(): boolean | ''
method ngOnInit (line 318) | ngOnInit(): void
method ngOnDestroy (line 327) | ngOnDestroy(): void
method _enableAnimations (line 342) | private _enableAnimations(): void
method _disableAnimations (line 359) | private _disableAnimations(): void
method _showOverlay (line 376) | private _showOverlay(): void
method _hideOverlay (line 420) | private _hideOverlay(): void
method _onMouseenter (line 456) | private _onMouseenter(): void
method _onMouseleave (line 471) | private _onMouseleave(): void
method open (line 487) | open(): void
method close (line 499) | close(): void
method toggle (line 511) | toggle(): void
FILE: webapp/frontend/src/@treo/components/drawer/drawer.module.ts
class TreoDrawerModule (line 16) | class TreoDrawerModule
FILE: webapp/frontend/src/@treo/components/drawer/drawer.service.ts
class TreoDrawerService (line 7) | class TreoDrawerService
method constructor (line 15) | constructor()
method registerComponent (line 31) | registerComponent(name: string, component: TreoDrawerComponent): void
method deregisterComponent (line 41) | deregisterComponent(name: string): void
method getComponent (line 51) | getComponent(name: string): TreoDrawerComponent
FILE: webapp/frontend/src/@treo/components/drawer/drawer.types.ts
type TreoDrawerMode (line 1) | type TreoDrawerMode = 'over' | 'side';
type TreoDrawerPosition (line 2) | type TreoDrawerPosition = 'left' | 'right';
FILE: webapp/frontend/src/@treo/components/highlight/highlight.component.ts
class TreoHighlightComponent (line 13) | class TreoHighlightComponent implements AfterViewInit
method constructor (line 35) | constructor(
method code (line 57) | set code(value: string)
method code (line 78) | get code(): string
method lang (line 87) | set lang(value: string)
method lang (line 108) | get lang(): string
method ngAfterViewInit (line 120) | ngAfterViewInit(): void
method _highlightAndInsert (line 149) | private _highlightAndInsert(): void
FILE: webapp/frontend/src/@treo/components/highlight/highlight.module.ts
class TreoHighlightModule (line 19) | class TreoHighlightModule
FILE: webapp/frontend/src/@treo/components/highlight/highlight.service.ts
class TreoHighlightService (line 7) | class TreoHighlightService
method constructor (line 12) | constructor()
method _format (line 28) | private _format(code: string): string
method highlight (line 79) | highlight(code: string, language: string): string
FILE: webapp/frontend/src/@treo/components/message/message.component.ts
class TreoMessageComponent (line 17) | class TreoMessageComponent implements OnInit, OnDestroy
method constructor (line 44) | constructor(
method appearance (line 73) | set appearance(value: TreoMessageAppearance)
method appearance (line 89) | get appearance(): TreoMessageAppearance
method dismissed (line 100) | set dismissed(value: null | boolean)
method dismissed (line 128) | get dismissed(): null | boolean
method showIcon (line 139) | set showIcon(value: boolean)
method showIcon (line 161) | get showIcon(): boolean
method type (line 172) | set type(value: TreoMessageType)
method type (line 188) | get type(): TreoMessageType
method ngOnInit (line 200) | ngOnInit(): void
method ngOnDestroy (line 235) | ngOnDestroy(): void
method dismiss (line 249) | dismiss(): void
method show (line 270) | show(): void
FILE: webapp/frontend/src/@treo/components/message/message.module.ts
class TreoMessageModule (line 20) | class TreoMessageModule
FILE: webapp/frontend/src/@treo/components/message/message.service.ts
class TreoMessageService (line 7) | class TreoMessageService
method constructor (line 16) | constructor()
method onDismiss (line 30) | get onDismiss(): Observable<any>
method onShow (line 38) | get onShow(): Observable<any>
method dismiss (line 52) | dismiss(name: string): void
method show (line 69) | show(name: string): void
FILE: webapp/frontend/src/@treo/components/message/message.types.ts
type TreoMessageAppearance (line 1) | type TreoMessageAppearance = 'border' | 'fill' | 'outline';
type TreoMessageType (line 2) | type TreoMessageType = 'primary' | 'accent' | 'warn' | 'basic' | 'info' ...
FILE: webapp/frontend/src/@treo/components/navigation/horizontal/components/basic/basic.component.ts
class TreoHorizontalNavigationBasicItemComponent (line 14) | class TreoHorizontalNavigationBasicItemComponent implements OnInit, OnDe...
method constructor (line 34) | constructor(
method ngOnInit (line 50) | ngOnInit(): void
method ngOnDestroy (line 68) | ngOnDestroy(): void
FILE: webapp/frontend/src/@treo/components/navigation/horizontal/components/branch/branch.component.ts
class TreoHorizontalNavigationBranchItemComponent (line 15) | class TreoHorizontalNavigationBranchItemComponent implements OnInit, OnD...
method constructor (line 43) | constructor(
method ngOnInit (line 62) | ngOnInit(): void
method ngOnDestroy (line 80) | ngOnDestroy(): void
method triggerChangeDetection (line 94) | triggerChangeDetection(): void
FILE: webapp/frontend/src/@treo/components/navigation/horizontal/components/divider/divider.component.ts
class TreoHorizontalNavigationDividerItemComponent (line 14) | class TreoHorizontalNavigationDividerItemComponent implements OnInit, On...
method constructor (line 34) | constructor(
method ngOnInit (line 50) | ngOnInit(): void
method ngOnDestroy (line 68) | ngOnDestroy(): void
FILE: webapp/frontend/src/@treo/components/navigation/horizontal/components/spacer/spacer.component.ts
class TreoHorizontalNavigationSpacerItemComponent (line 14) | class TreoHorizontalNavigationSpacerItemComponent implements OnInit, OnD...
method constructor (line 34) | constructor(
method ngOnInit (line 50) | ngOnInit(): void
method ngOnDestroy (line 68) | ngOnDestroy(): void
FILE: webapp/frontend/src/@treo/components/navigation/horizontal/horizontal.component.ts
class TreoHorizontalNavigationComponent (line 16) | class TreoHorizontalNavigationComponent implements OnInit, OnDestroy
method constructor (line 34) | constructor(
method navigation (line 54) | set navigation(value: TreoNavigationItem[])
method navigation (line 63) | get navigation(): TreoNavigationItem[]
method ngOnInit (line 75) | ngOnInit(): void
method ngOnDestroy (line 84) | ngOnDestroy(): void
method refresh (line 101) | refresh(): void
method trackByFn (line 116) | trackByFn(index: number, item: any): any
FILE: webapp/frontend/src/@treo/components/navigation/navigation.module.ts
class TreoNavigationModule (line 53) | class TreoNavigationModule
FILE: webapp/frontend/src/@treo/components/navigation/navigation.service.ts
class TreoNavigationService (line 7) | class TreoNavigationService
method constructor (line 16) | constructor()
method registerComponent (line 33) | registerComponent(name: string, component: any): void
method deregisterComponent (line 43) | deregisterComponent(name: string): void
method getComponent (line 53) | getComponent(name: string): any
method storeNavigation (line 64) | storeNavigation(key: string, navigation: TreoNavigationItem[]): void
method getNavigation (line 76) | getNavigation(key: string): TreoNavigationItem[]
method deleteNavigation (line 86) | deleteNavigation(key: string): void
method getFlatNavigation (line 106) | getFlatNavigation(navigation: TreoNavigationItem[], flatNavigation: Tr...
method getItem (line 135) | getItem(id: string, navigation: TreoNavigationItem[]): TreoNavigationI...
method getItemParent (line 166) | getItemParent(
FILE: webapp/frontend/src/@treo/components/navigation/navigation.types.ts
type TreoNavigationItem (line 1) | interface TreoNavigationItem
type TreoVerticalNavigationAppearance (line 26) | type TreoVerticalNavigationAppearance = string;
type TreoVerticalNavigationMode (line 27) | type TreoVerticalNavigationMode = 'over' | 'side';
type TreoVerticalNavigationPosition (line 28) | type TreoVerticalNavigationPosition = 'left' | 'right';
FILE: webapp/frontend/src/@treo/components/navigation/vertical/components/aside/aside.component.ts
class TreoVerticalNavigationAsideItemComponent (line 14) | class TreoVerticalNavigationAsideItemComponent implements OnInit, OnDestroy
method constructor (line 46) | constructor(
method ngOnInit (line 65) | ngOnInit(): void
method ngOnDestroy (line 83) | ngOnDestroy(): void
method trackByFn (line 100) | trackByFn(index: number, item: any): any
FILE: webapp/frontend/src/@treo/components/navigation/vertical/components/basic/basic.component.ts
class TreoVerticalNavigationBasicItemComponent (line 14) | class TreoVerticalNavigationBasicItemComponent implements OnInit, OnDestroy
method constructor (line 34) | constructor(
method ngOnInit (line 50) | ngOnInit(): void
method ngOnDestroy (line 68) | ngOnDestroy(): void
FILE: webapp/frontend/src/@treo/components/navigation/vertical/components/collapsable/collapsable.component.ts
class TreoVerticalNavigationCollapsableItemComponent (line 17) | class TreoVerticalNavigationCollapsableItemComponent implements OnInit, ...
method constructor (line 50) | constructor(
method ngOnInit (line 71) | ngOnInit(): void
method ngOnDestroy (line 182) | ngOnDestroy(): void
method _hasCurrentUrlInChildren (line 201) | private _hasCurrentUrlInChildren(item, url): boolean
method _isChildrenOf (line 249) | private _isChildrenOf(parent, item): boolean
method collapse (line 284) | collapse(): void
method expand (line 312) | expand(): void
method toggleCollapsable (line 340) | toggleCollapsable(): void
method trackByFn (line 359) | trackByFn(index: number, item: any): any
FILE: webapp/frontend/src/@treo/components/navigation/vertical/components/divider/divider.component.ts
class TreoVerticalNavigationDividerItemComponent (line 14) | class TreoVerticalNavigationDividerItemComponent implements OnInit, OnDe...
method constructor (line 34) | constructor(
method ngOnInit (line 50) | ngOnInit(): void
method ngOnDestroy (line 68) | ngOnDestroy(): void
FILE: webapp/frontend/src/@treo/components/navigation/vertical/components/group/group.component.ts
class TreoVerticalNavigationGroupItemComponent (line 14) | class TreoVerticalNavigationGroupItemComponent implements OnInit, OnDestroy
method constructor (line 38) | constructor(
method ngOnInit (line 54) | ngOnInit(): void
method ngOnDestroy (line 72) | ngOnDestroy(): void
method trackByFn (line 89) | trackByFn(index: number, item: any): any
FILE: webapp/frontend/src/@treo/components/navigation/vertical/components/spacer/spacer.component.ts
class TreoVerticalNavigationSpacerItemComponent (line 14) | class TreoVerticalNavigationSpacerItemComponent implements OnInit, OnDes...
method constructor (line 34) | constructor(
method ngOnInit (line 50) | ngOnInit(): void
method ngOnDestroy (line 68) | ngOnDestroy(): void
FILE: webapp/frontend/src/@treo/components/navigation/vertical/vertical.component.ts
class TreoVerticalNavigationComponent (line 21) | class TreoVerticalNavigationComponent implements OnInit, AfterViewInit, ...
method constructor (line 87) | constructor(
method appearance (line 140) | set appearance(value: TreoVerticalNavigationAppearance)
method appearance (line 165) | get appearance(): TreoVerticalNavigationAppearance
method treoScrollbarDirectives (line 174) | set treoScrollbarDirectives(treoScrollbarDirectives: QueryList<TreoScr...
method navigation (line 214) | set navigation(value: TreoNavigationItem[])
method navigation (line 223) | get navigation(): TreoNavigationItem[]
method inner (line 234) | set inner(value: boolean)
method inner (line 256) | get inner(): boolean
method mode (line 267) | set mode(value: TreoVerticalNavigationMode)
method mode (line 323) | get mode(): TreoVerticalNavigationMode
method opened (line 334) | set opened(value: boolean | '')
method opened (line 383) | get opened(): boolean | ''
method position (line 394) | set position(value: TreoVerticalNavigationPosition)
method position (line 419) | get position(): TreoVerticalNavigationPosition
method transparentOverlay (line 430) | set transparentOverlay(value: boolean | '')
method transparentOverlay (line 452) | get transparentOverlay(): boolean | ''
method ngOnInit (line 464) | ngOnInit(): void
method ngAfterViewInit (line 488) | ngAfterViewInit(): void
method ngOnDestroy (line 527) | ngOnDestroy(): void
method _enableAnimations (line 546) | private _enableAnimations(): void
method _disableAnimations (line 563) | private _disableAnimations(): void
method _showOverlay (line 580) | private _showOverlay(): void
method _hideOverlay (line 625) | private _hideOverlay(): void
method _showAsideOverlay (line 666) | private _showAsideOverlay(): void
method _hideAsideOverlay (line 702) | private _hideAsideOverlay(): void
method _onMouseenter (line 741) | private _onMouseenter(): void
method _onMouseleave (line 756) | private _onMouseleave(): void
method refresh (line 772) | refresh(): void
method open (line 784) | open(): void
method close (line 796) | close(): void
method toggle (line 811) | toggle(): void
method openAside (line 829) | openAside(item: TreoNavigationItem): void
method closeAside (line 850) | closeAside(): void
method toggleAside (line 867) | toggleAside(item: TreoNavigationItem): void
method trackByFn (line 886) | trackByFn(index: number, item: any): any
FILE: webapp/frontend/src/@treo/directives/autogrow/autogrow.directive.ts
class TreoAutogrowDirective (line 8) | class TreoAutogrowDirective implements OnInit, OnDestroy
method constructor (line 23) | constructor(
method padding (line 46) | set padding(value)
method padding (line 52) | get padding(): number
method ngOnInit (line 64) | ngOnInit(): void
method ngOnDestroy (line 79) | ngOnDestroy(): void
method _resize (line 97) | private _resize(): void
FILE: webapp/frontend/src/@treo/directives/autogrow/autogrow.module.ts
class TreoAutogrowModule (line 12) | class TreoAutogrowModule
FILE: webapp/frontend/src/@treo/directives/scrollbar/scrollbar.directive.ts
class TreoScrollbarDirective (line 18) | class TreoScrollbarDirective implements OnInit, OnDestroy
method constructor (line 36) | constructor(
method treoScrollbarOptions (line 62) | set treoScrollbarOptions(value: any)
method treoScrollbarOptions (line 77) | get treoScrollbarOptions(): any
method enabled (line 89) | set enabled(value: boolean | '')
method enabled (line 119) | get enabled(): boolean | ''
method elementRef (line 128) | get elementRef(): ElementRef
method ngOnInit (line 140) | ngOnInit(): void
method ngOnDestroy (line 158) | ngOnDestroy(): void
method _init (line 176) | private _init(): void
method _destroy (line 208) | private _destroy(): void
method update (line 229) | update(): void
method destroy (line 243) | destroy(): void
method geometry (line 253) | geometry(prefix: string = 'scroll'): ScrollbarGeometry
method position (line 269) | position(absolute: boolean = false): ScrollbarPosition
method scrollTo (line 298) | scrollTo(x: number, y?: number, speed?: number): void
method scrollToX (line 324) | scrollToX(x: number, speed?: number): void
method scrollToY (line 335) | scrollToY(y: number, speed?: number): void
method scrollToTop (line 346) | scrollToTop(offset: number = 0, speed?: number): void
method scrollToBottom (line 357) | scrollToBottom(offset: number = 0, speed?: number): void
method scrollToLeft (line 369) | scrollToLeft(offset: number = 0, speed?: number): void
method scrollToRight (line 380) | scrollToRight(offset: number = 0, speed?: number): void
method scrollToElement (line 394) | scrollToElement(qs: string, offset: number = 0, ignoreVisible: boolean...
method animateScrolling (line 440) | animateScrolling(target: string, value: number, speed?: number): void
FILE: webapp/frontend/src/@treo/directives/scrollbar/scrollbar.interfaces.ts
class ScrollbarGeometry (line 1) | class ScrollbarGeometry
method constructor (line 9) | constructor(x: number, y: number, w: number, h: number)
class ScrollbarPosition (line 18) | class ScrollbarPosition
method constructor (line 23) | constructor(x: number | 'start' | 'end', y: number | 'start' | 'end')
FILE: webapp/frontend/src/@treo/directives/scrollbar/scrollbar.module.ts
class TreoScrollbarModule (line 12) | class TreoScrollbarModule
FILE: webapp/frontend/src/@treo/lib/mock-api/mock-api.interceptor.ts
class TreoMockApiInterceptor (line 11) | class TreoMockApiInterceptor implements HttpInterceptor
method constructor (line 18) | constructor(
method intercept (line 30) | intercept(request: HttpRequest<any>, next: HttpHandler): Observable<Ht...
FILE: webapp/frontend/src/@treo/lib/mock-api/mock-api.interfaces.ts
type TreoMockApi (line 1) | interface TreoMockApi
FILE: webapp/frontend/src/@treo/lib/mock-api/mock-api.module.ts
class TreoMockApiModule (line 16) | class TreoMockApiModule
method forRoot (line 23) | static forRoot(mockDataServices: any[]): ModuleWithProviders<TreoMockA...
FILE: webapp/frontend/src/@treo/lib/mock-api/mock-api.request-handler.ts
class TreoMockApiRequestHandler (line 7) | class TreoMockApiRequestHandler
method constructor (line 20) | constructor()
method delay (line 36) | set delay(value: number)
method delay (line 48) | get delay(): number
method url (line 58) | set url(value: string)
method url (line 70) | get url(): string
method interceptedRequest (line 80) | set interceptedRequest(value: HttpRequest<any>)
method interceptedRequest (line 92) | get interceptedRequest(): HttpRequest<any>
method replyCallback (line 100) | get replyCallback(): Observable<any>
method reply (line 140) | reply(callback: (req: HttpRequest<any>) => ([number, any | string] | O...
method replyOnce (line 151) | replyOnce(callback: (req: HttpRequest<any>) => ([number, any | string]...
FILE: webapp/frontend/src/@treo/lib/mock-api/mock-api.service.ts
class TreoMockApiService (line 7) | class TreoMockApiService
method constructor (line 14) | constructor()
method onDelete (line 36) | onDelete(url: string, delay: number = 0): TreoMockApiRequestHandler
method onGet (line 47) | onGet(url: string, delay: number = 0): TreoMockApiRequestHandler
method onPatch (line 58) | onPatch(url: string, delay: number = 0): TreoMockApiRequestHandler
method onPost (line 69) | onPost(url: string, delay: number = 0): TreoMockApiRequestHandler
method onPut (line 80) | onPut(url: string, delay: number = 0): TreoMockApiRequestHandler
method _registerRequestHandler (line 97) | private _registerRequestHandler(requestType, url, delay): TreoMockApiR...
FILE: webapp/frontend/src/@treo/lib/mock-api/mock-api.utils.ts
class TreoMockApiUtils (line 1) | class TreoMockApiUtils
method constructor (line 6) | constructor()
method guid (line 18) | static guid(): string
FILE: webapp/frontend/src/@treo/pipes/find-by-key/find-by-key.module.ts
class TreoFindByKeyPipeModule (line 12) | class TreoFindByKeyPipeModule
FILE: webapp/frontend/src/@treo/pipes/find-by-key/find-by-key.pipe.ts
class TreoFindByKeyPipe (line 10) | class TreoFindByKeyPipe implements PipeTransform
method constructor (line 15) | constructor()
method transform (line 26) | transform(value: string | string[], key: string, source: any[]): any
FILE: webapp/frontend/src/@treo/services/config/config.constants.ts
constant TREO_APP_CONFIG (line 3) | const TREO_APP_CONFIG = new InjectionToken<any>('Default configuration f...
FILE: webapp/frontend/src/@treo/services/config/config.module.ts
class TreoConfigModule (line 6) | class TreoConfigModule
method constructor (line 13) | constructor(
method forRoot (line 24) | static forRoot(config: any): ModuleWithProviders
FILE: webapp/frontend/src/@treo/services/config/config.service.ts
constant SCRUTINY_CONFIG_LOCAL_STORAGE_KEY (line 7) | const SCRUTINY_CONFIG_LOCAL_STORAGE_KEY = 'scrutiny';
class TreoConfigService (line 12) | class TreoConfigService
method constructor (line 20) | constructor(@Inject(TREO_APP_CONFIG) defaultConfig: any)
method config (line 42) | set config(value: any)
method config$ (line 55) | get config$(): Observable<any>
method reset (line 71) | reset(): void
FILE: webapp/frontend/src/@treo/services/media-watcher/media-watcher.module.ts
class TreoMediaWatcherModule (line 9) | class TreoMediaWatcherModule
method constructor (line 16) | constructor(
FILE: webapp/frontend/src/@treo/services/media-watcher/media-watcher.service.ts
class TreoMediaWatcherService (line 7) | class TreoMediaWatcherService
method constructor (line 16) | constructor(
method onMediaChange$ (line 34) | get onMediaChange$(): Observable<{ matchingAliases: string[], matching...
method _init (line 48) | private _init(): void
method onMediaQueryChange$ (line 101) | onMediaQueryChange$(query: string): Observable<BreakpointState>
FILE: webapp/frontend/src/@treo/services/splash-screen/splash-screen.module.ts
class TreoSplashScreenModule (line 9) | class TreoSplashScreenModule
method constructor (line 16) | constructor(
FILE: webapp/frontend/src/@treo/services/splash-screen/splash-screen.service.ts
class TreoSplashScreenService (line 7) | class TreoSplashScreenService
method constructor (line 15) | constructor(
method _init (line 33) | private _init(): void
method show (line 55) | show(): void
method hide (line 63) | hide(): void
FILE: webapp/frontend/src/@treo/treo.module.ts
class TreoModule (line 21) | class TreoModule
method constructor (line 28) | constructor(
FILE: webapp/frontend/src/@treo/validators/validators.ts
class TreoValidators (line 3) | class TreoValidators
method isEmptyInputValue (line 10) | static isEmptyInputValue(value: any): boolean
method mustMatch (line 21) | static mustMatch(controlPath: string, matchingControlPath: string): Va...
FILE: webapp/frontend/src/app/app.component.ts
class AppComponent (line 8) | class AppComponent
method constructor (line 13) | constructor()
FILE: webapp/frontend/src/app/app.module.ts
class AppModule (line 66) | class AppModule
FILE: webapp/frontend/src/app/app.routing.ts
function getAppBaseHref (line 6) | function getAppBaseHref(): string {
function getBasePath (line 12) | function getBasePath(): string {
FILE: webapp/frontend/src/app/core/config/app.config.ts
type Theme (line 4) | type Theme = 'light' | 'dark' | 'system';
type DashboardDisplay (line 7) | type DashboardDisplay = 'name' | 'serial_id' | 'uuid' | 'label'
type DashboardSort (line 9) | type DashboardSort = 'status' | 'title' | 'age'
type TemperatureUnit (line 11) | type TemperatureUnit = 'celsius' | 'fahrenheit'
type LineStroke (line 13) | type LineStroke = 'smooth' | 'straight' | 'stepline'
type DevicePoweredOnUnit (line 15) | type DevicePoweredOnUnit = 'humanize' | 'device_hours'
type MetricsNotifyLevel (line 18) | enum MetricsNotifyLevel {
type MetricsStatusFilterAttributes (line 23) | enum MetricsStatusFilterAttributes {
type MetricsStatusThreshold (line 28) | enum MetricsStatusThreshold {
type AppConfig (line 40) | interface AppConfig {
FILE: webapp/frontend/src/app/core/config/scrutiny-config.module.ts
class ScrutinyConfigModule (line 6) | class ScrutinyConfigModule {
method constructor (line 12) | constructor(
method forRoot (line 22) | static forRoot(config: any): ModuleWithProviders<ScrutinyConfigModule> {
FILE: webapp/frontend/src/app/core/config/scrutiny-config.service.ts
class ScrutinyConfigService (line 13) | class ScrutinyConfigService {
method constructor (line 18) | constructor(
method config (line 35) | set config(value: AppConfig) {
method config$ (line 54) | get config$(): Observable<AppConfig> {
method reset (line 80) | reset(): void {
FILE: webapp/frontend/src/app/core/core.module.ts
class CoreModule (line 12) | class CoreModule
method constructor (line 21) | constructor(
FILE: webapp/frontend/src/app/core/models/device-details-response-wrapper.ts
type DeviceDetailsResponseWrapper (line 6) | interface DeviceDetailsResponseWrapper {
FILE: webapp/frontend/src/app/core/models/device-model.ts
type DeviceModel (line 2) | interface DeviceModel {
FILE: webapp/frontend/src/app/core/models/device-summary-model.ts
type DeviceSummaryModel (line 5) | interface DeviceSummaryModel {
type SmartSummary (line 11) | interface SmartSummary {
FILE: webapp/frontend/src/app/core/models/device-summary-response-wrapper.ts
type DeviceSummaryResponseWrapper (line 4) | interface DeviceSummaryResponseWrapper {
FILE: webapp/frontend/src/app/core/models/device-summary-temp-response-wrapper.ts
type DeviceSummaryTempResponseWrapper (line 3) | interface DeviceSummaryTempResponseWrapper {
FILE: webapp/frontend/src/app/core/models/measurements/smart-attribute-model.ts
type SmartAttributeModel (line 4) | interface SmartAttributeModel {
FILE: webapp/frontend/src/app/core/models/measurements/smart-model.ts
type SmartModel (line 4) | interface SmartModel {
FILE: webapp/frontend/src/app/core/models/measurements/smart-temperature-model.ts
type SmartTemperatureModel (line 2) | interface SmartTemperatureModel {
FILE: webapp/frontend/src/app/core/models/thresholds/attribute-metadata-model.ts
type AttributeMetadataModel (line 4) | interface AttributeMetadataModel {
FILE: webapp/frontend/src/app/data/mock/device/details/index.ts
class DetailsMockApi (line 15) | class DetailsMockApi implements TreoMockApi
method constructor (line 25) | constructor(
method register (line 40) | register(): void
FILE: webapp/frontend/src/app/data/mock/summary/index.ts
class SummaryMockApi (line 10) | class SummaryMockApi implements TreoMockApi
method constructor (line 20) | constructor(
method register (line 38) | register(): void
FILE: webapp/frontend/src/app/layout/common/dashboard-device-archive-dialog/dashboard-device-archive-dialog.component.ts
class DashboardDeviceArchiveDialogComponent (line 10) | class DashboardDeviceArchiveDialogComponent implements OnInit {
method constructor (line 12) | constructor(
method ngOnInit (line 19) | ngOnInit(): void {
method onArchiveClick (line 22) | onArchiveClick(): void {
FILE: webapp/frontend/src/app/layout/common/dashboard-device-archive-dialog/dashboard-device-archive-dialog.module.ts
class DashboardDeviceArchiveDialogModule (line 27) | class DashboardDeviceArchiveDialogModule
FILE: webapp/frontend/src/app/layout/common/dashboard-device-archive-dialog/dashboard-device-archive-dialog.service.ts
class DashboardDeviceArchiveDialogService (line 9) | class DashboardDeviceArchiveDialogService
method constructor (line 18) | constructor(
method archiveDevice (line 29) | archiveDevice(wwn: string): Observable<any>
method unarchiveDevice (line 34) | unarchiveDevice(wwn: string): Observable<any>
FILE: webapp/frontend/src/app/layout/common/dashboard-device-delete-dialog/dashboard-device-delete-dialog.component.ts
class DashboardDeviceDeleteDialogComponent (line 10) | class DashboardDeviceDeleteDialogComponent implements OnInit {
method constructor (line 12) | constructor(
method ngOnInit (line 19) | ngOnInit(): void {
method onDeleteClick (line 22) | onDeleteClick(): void {
FILE: webapp/frontend/src/app/layout/common/dashboard-device-delete-dialog/dashboard-device-delete-dialog.module.ts
class DashboardDeviceDeleteDialogModule (line 27) | class DashboardDeviceDeleteDialogModule
FILE: webapp/frontend/src/app/layout/common/dashboard-device-delete-dialog/dashboard-device-delete-dialog.service.ts
class DashboardDeviceDeleteDialogService (line 10) | class DashboardDeviceDeleteDialogService
method constructor (line 19) | constructor(
method deleteDevice (line 30) | deleteDevice(wwn: string): Observable<any>
FILE: webapp/frontend/src/app/layout/common/dashboard-device/dashboard-device.component.ts
class DashboardDeviceComponent (line 20) | class DashboardDeviceComponent implements OnInit {
method constructor (line 22) | constructor(
method ngOnInit (line 42) | ngOnInit(): void {
method classDeviceLastUpdatedOn (line 56) | classDeviceLastUpdatedOn(deviceSummary: DeviceSummaryModel): string {
method openArchiveDialog (line 76) | openArchiveDialog(): void {
method openDeleteDialog (line 98) | openDeleteDialog(): void {
FILE: webapp/frontend/src/app/layout/common/dashboard-device/dashboard-device.module.ts
class DashboardDeviceModule (line 31) | class DashboardDeviceModule {
FILE: webapp/frontend/src/app/layout/common/dashboard-settings/dashboard-settings.component.ts
class DashboardSettingsComponent (line 22) | class DashboardSettingsComponent implements OnInit {
method constructor (line 39) | constructor(
method ngOnInit (line 46) | ngOnInit(): void {
method saveSettings (line 71) | saveSettings(): void {
method formatLabel (line 93) | formatLabel(value: number): number {
FILE: webapp/frontend/src/app/layout/common/dashboard-settings/dashboard-settings.module.ts
class DashboardSettingsModule (line 44) | class DashboardSettingsModule
FILE: webapp/frontend/src/app/layout/common/detail-settings/detail-settings.component.ts
class DetailSettingsComponent (line 8) | class DetailSettingsComponent implements OnInit {
method constructor (line 10) | constructor() { }
method ngOnInit (line 12) | ngOnInit(): void {
FILE: webapp/frontend/src/app/layout/common/detail-settings/detail-settings.module.ts
class DetailSettingsModule (line 43) | class DetailSettingsModule {
FILE: webapp/frontend/src/app/layout/common/search/search.component.ts
class SearchComponent (line 18) | class SearchComponent implements OnInit, OnDestroy
method constructor (line 47) | constructor(
method appearance (line 75) | set appearance(value: 'basic' | 'bar')
method appearance (line 101) | get appearance(): 'basic' | 'bar'
method opened (line 111) | set opened(value: boolean)
method opened (line 135) | get opened(): boolean
method searchInput (line 146) | set searchInput(value: MatFormField)
method ngOnInit (line 175) | ngOnInit(): void
method ngOnDestroy (line 213) | ngOnDestroy(): void
method onKeydown (line 229) | onKeydown(event): void
method open (line 248) | open(): void
method close (line 264) | close(): void
FILE: webapp/frontend/src/app/layout/common/search/search.module.ts
class SearchModule (line 38) | class SearchModule
FILE: webapp/frontend/src/app/layout/layout.component.ts
class LayoutComponent (line 18) | class LayoutComponent implements OnInit, OnDestroy {
method constructor (line 36) | constructor(
method ngOnInit (line 58) | ngOnInit(): void
method ngOnDestroy (line 100) | ngOnDestroy(): void
method determineTheme (line 114) | private determineTheme(config:AppConfig): string {
method _updateLayout (line 125) | private _updateLayout(): void
method setLayout (line 182) | setLayout(layout: Layout): void {
method setTheme (line 201) | setTheme(change: MatSlideToggleChange): void
FILE: webapp/frontend/src/app/layout/layout.module.ts
class LayoutModule (line 30) | class LayoutModule
FILE: webapp/frontend/src/app/layout/layout.types.ts
type Layout (line 1) | type Layout = 'empty' |
FILE: webapp/frontend/src/app/layout/layouts/empty/empty.component.ts
class EmptyLayoutComponent (line 10) | class EmptyLayoutComponent implements OnInit, OnDestroy
method constructor (line 18) | constructor()
method ngOnInit (line 31) | ngOnInit(): void
method ngOnDestroy (line 39) | ngOnDestroy(): void
FILE: webapp/frontend/src/app/layout/layouts/empty/empty.module.ts
class EmptyLayoutModule (line 18) | class EmptyLayoutModule
FILE: webapp/frontend/src/app/layout/layouts/horizontal/material/material.component.ts
class MaterialLayoutComponent (line 15) | class MaterialLayoutComponent implements OnInit, OnDestroy
method constructor (line 38) | constructor(
method currentYear (line 62) | get currentYear(): number
method ngOnInit (line 74) | ngOnInit(): void
method ngOnDestroy (line 94) | ngOnDestroy(): void
method toggleNavigation (line 110) | toggleNavigation(key): void
FILE: webapp/frontend/src/app/layout/layouts/horizontal/material/material.module.ts
class MaterialLayoutModule (line 32) | class MaterialLayoutModule
FILE: webapp/frontend/src/app/modules/dashboard/dashboard.component.ts
class DashboardComponent (line 30) | class DashboardComponent implements OnInit, AfterViewInit, OnDestroy
method constructor (line 51) | constructor(
method ngOnInit (line 70) | ngOnInit(): void
method ngAfterViewInit (line 119) | ngAfterViewInit(): void
method ngOnDestroy (line 125) | ngOnDestroy(): void
method refreshComponent (line 135) | private refreshComponent(): void {
method _deviceDataTemperatureSeries (line 143) | private _deviceDataTemperatureSeries(): any[] {
method _prepareChartData (line 186) | private _prepareChartData(): void
method deviceSummariesForHostGroup (line 244) | deviceSummariesForHostGroup(hostGroupWWNs: string[]): DeviceSummaryMod...
method openDialog (line 254) | openDialog(): void {
method onDeviceDeleted (line 262) | onDeviceDeleted(wwn: string): void {
method onDeviceArchived (line 266) | onDeviceArchived(wwn: string): void {
method onDeviceUnarchived (line 270) | onDeviceUnarchived(wwn: string): void {
method changeSummaryTempDuration (line 282) | changeSummaryTempDuration(durationKey: string): void {
method trackByFn (line 305) | trackByFn(index: number, item: any): any
FILE: webapp/frontend/src/app/modules/dashboard/dashboard.module.ts
class DashboardModule (line 38) | class DashboardModule
FILE: webapp/frontend/src/app/modules/dashboard/dashboard.resolvers.ts
class DashboardResolver (line 10) | class DashboardResolver implements Resolve<any> {
method constructor (line 16) | constructor(
method resolve (line 32) | resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Ob...
FILE: webapp/frontend/src/app/modules/dashboard/dashboard.service.ts
class DashboardService (line 14) | class DashboardService {
method constructor (line 23) | constructor(
method data$ (line 38) | get data$(): Observable<{ [p: string]: DeviceSummaryModel }> {
method getSummaryData (line 49) | getSummaryData(): Observable<{ [key: string]: DeviceSummaryModel }> {
method getSummaryTempData (line 61) | getSummaryTempData(durationKey: string): Observable<{ [key: string]: S...
FILE: webapp/frontend/src/app/modules/detail/detail.component.ts
class DetailComponent (line 41) | class DetailComponent implements OnInit, AfterViewInit, OnDestroy {
method constructor (line 51) | constructor(
method ngOnInit (line 101) | ngOnInit(): void {
method ngAfterViewInit (line 133) | ngAfterViewInit(): void {
method ngOnDestroy (line 141) | ngOnDestroy(): void {
method getAttributeStatusName (line 151) | getAttributeStatusName(attributeStatus: number): string {
method getAttributeScrutinyStatusName (line 166) | getAttributeScrutinyStatusName(attributeStatus: number): string {
method getAttributeSmartStatusName (line 178) | getAttributeSmartStatusName(attributeStatus: number): string {
method getAttributeName (line 189) | getAttributeName(attributeData: SmartAttributeModel): string {
method getAttributeDescription (line 198) | getAttributeDescription(attributeData: SmartAttributeModel): string {
method getAttributeValue (line 207) | getAttributeValue(attributeData: SmartAttributeModel): number {
method getAttributeValueType (line 224) | getAttributeValueType(attributeData: SmartAttributeModel): string {
method getAttributeIdeal (line 237) | getAttributeIdeal(attributeData: SmartAttributeModel): string {
method getAttributeWorst (line 245) | getAttributeWorst(attributeData: SmartAttributeModel): number | string {
method getAttributeThreshold (line 254) | getAttributeThreshold(attributeData: SmartAttributeModel): number | st...
method getAttributeCritical (line 272) | getAttributeCritical(attributeData: SmartAttributeModel): boolean {
method getHiddenAttributes (line 276) | getHiddenAttributes(): number {
method isAta (line 290) | isAta(): boolean {
method isScsi (line 294) | isScsi(): boolean {
method isNvme (line 298) | isNvme(): boolean {
method _generateSmartAttributeTableDataSource (line 302) | private _generateSmartAttributeTableDataSource(smartResults: SmartMode...
method _prepareChartData (line 374) | private _prepareChartData(): void {
method determineTheme (line 421) | private determineTheme(config: AppConfig): string {
method toHex (line 433) | toHex(decimalNumb: number | string): string {
method toggleOnlyCritical (line 437) | toggleOnlyCritical(): void {
method openDialog (line 442) | openDialog(): void {
method trackByFn (line 456) | trackByFn(index: number, item: any): any {
FILE: webapp/frontend/src/app/modules/detail/detail.module.ts
class DetailModule (line 38) | class DetailModule
FILE: webapp/frontend/src/app/modules/detail/detail.resolvers.ts
class DetailResolver (line 10) | class DetailResolver implements Resolve<any> {
method constructor (line 16) | constructor(
method resolve (line 32) | resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Ob...
FILE: webapp/frontend/src/app/modules/detail/detail.service.ts
class DetailService (line 11) | class DetailService {
method constructor (line 20) | constructor(
method data$ (line 34) | get data$(): Observable<DeviceDetailsResponseWrapper> {
method getData (line 45) | getData(wwn): Observable<DeviceDetailsResponseWrapper> {
FILE: webapp/frontend/src/app/modules/landing/home/home.component.ts
class LandingHomeComponent (line 9) | class LandingHomeComponent
method constructor (line 14) | constructor()
FILE: webapp/frontend/src/app/modules/landing/home/home.module.ts
class LandingHomeModule (line 18) | class LandingHomeModule
FILE: webapp/frontend/src/app/shared/device-hours.pipe.ts
class DeviceHoursPipe (line 5) | class DeviceHoursPipe implements PipeTransform {
method format (line 6) | static format(hoursOfRunTime: number, unit: string, humanizeConfig: ob...
method transform (line 16) | transform(hoursOfRunTime: number, unit = 'humanize', humanizeConfig: a...
FILE: webapp/frontend/src/app/shared/device-sort.pipe.ts
class DeviceSortPipe (line 7) | class DeviceSortPipe implements PipeTransform {
method statusCompareFn (line 9) | statusCompareFn(a: any, b: any): number {
method titleCompareFn (line 26) | titleCompareFn(dashboardDisplay: string) {
method ageCompareFn (line 42) | ageCompareFn(a: any, b: any): number {
method transform (line 50) | transform(deviceSummaries: Array<unknown>, sortBy = 'status', dashboar...
FILE: webapp/frontend/src/app/shared/device-status.pipe.ts
constant DEVICE_STATUS_NAMES (line 5) | const DEVICE_STATUS_NAMES: { [key: number]: string } = {
constant DEVICE_STATUS_NAMES_WITH_REASON (line 12) | const DEVICE_STATUS_NAMES_WITH_REASON: { [key: number]: string } = {
class DeviceStatusPipe (line 23) | class DeviceStatusPipe implements PipeTransform {
method deviceStatusForModelWithThreshold (line 26) | static deviceStatusForModelWithThreshold(
method transform (line 62) | transform(
FILE: webapp/frontend/src/app/shared/device-title.pipe.ts
class DeviceTitlePipe (line 7) | class DeviceTitlePipe implements PipeTransform {
method deviceTitleForType (line 9) | static deviceTitleForType(device: DeviceModel, titleType: string): str...
method deviceTitleWithFallback (line 39) | static deviceTitleWithFallback(device: DeviceModel, titleType: string)...
method transform (line 51) | transform(device: DeviceModel, titleType: string = 'name'): string {
FILE: webapp/frontend/src/app/shared/file-size.pipe.ts
class FileSizePipe (line 4) | class FileSizePipe implements PipeTransform {
method transform (line 6) | transform(bytes: number = 0, si = false, dp = 1): string {
FILE: webapp/frontend/src/app/shared/shared.module.ts
class SharedModule (line 37) | class SharedModule
FILE: webapp/frontend/src/app/shared/temperature.pipe.ts
class TemperaturePipe (line 7) | class TemperaturePipe implements PipeTransform {
method celsiusToFahrenheit (line 8) | static celsiusToFahrenheit(celsiusTemp: number): number {
method formatTemperature (line 11) | static formatTemperature(temp: number, unit: string, includeUnits: boo...
method transform (line 28) | transform(celsiusTemp: number, unit = 'celsius', includeUnits = false)...
Condensed preview — 504 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (5,075K chars).
[
{
"path": ".devcontainer/docker/devcontainer.json",
"chars": 673,
"preview": "{\n \"name\": \"Scrutiny Dev (docker)\",\n \"dockerComposeFile\": \"../docker-compose.yml\",\n \"service\": \"app\",\n \"workspaceFol"
},
{
"path": ".devcontainer/docker-compose.yml",
"chars": 695,
"preview": "services:\n app:\n image: mcr.microsoft.com/devcontainers/base:ubuntu-22.04\n volumes:\n - ..:/workspaces/scruti"
},
{
"path": ".devcontainer/docker-rootless/devcontainer.json",
"chars": 739,
"preview": "{\n \"name\": \"Scrutiny Dev (rootless docker)\",\n \"dockerComposeFile\": \"../docker-compose.yml\",\n \"service\": \"app\",\n \"wor"
},
{
"path": ".devcontainer/podman/devcontainer.json",
"chars": 760,
"preview": "{\n \"name\": \"Scrutiny Dev (podman)\",\n \"dockerComposeFile\": \"../docker-compose.yml\",\n \"service\": \"app\",\n \"workspaceFol"
},
{
"path": ".devcontainer/setup.sh",
"chars": 703,
"preview": "#!/bin/bash\n\necho \"Starting Scrutiny Setup...\"\n\nif [ ! -f \"scrutiny.yaml\" ]; then\n echo \"Creating scrutiny.yaml from "
},
{
"path": ".dockerignore",
"chars": 60,
"preview": "/vendor\n/.idea\n/.github\n/.git\n/webapp/frontend/node_modules\n"
},
{
"path": ".gitattributes",
"chars": 127,
"preview": "*.css linguist-detectable=false\n*.scss linguist-detectable=false\n*.js linguist-detectable=false\n*.ts linguist-detectable"
},
{
"path": ".github/DISCUSSION_TEMPLATE/issue-triage.yml",
"chars": 6121,
"preview": "labels: [\"needs-confirmation\"]\nbody:\n - type: markdown\n attributes:\n value: |\n > [!IMPORTANT]\n > "
},
{
"path": ".github/ISSUE_TEMPLATE/config.yml",
"chars": 278,
"preview": "blank_issues_enabled: false\ncontact_links:\n - name: Features, Bug Reports, Questions\n url: https://github.com/Analog"
},
{
"path": ".github/ISSUE_TEMPLATE/preapproved.md",
"chars": 268,
"preview": "---\nname: Pre-Discussed and Approved Topics\nabout: |-\n Only for topics already discussed and approved in the GitHub Dis"
},
{
"path": ".github/workflows/ci.yaml",
"chars": 4747,
"preview": "name: CI\n# This workflow is triggered on pushes & pull requests\non:\n push:\n branches:\n - master\n pull_request"
},
{
"path": ".github/workflows/docker-build.yaml",
"chars": 5657,
"preview": "name: Docker\non:\n push:\n # Publish semver tags as releases.\n tags: [ 'v*.*.*' ]\n\nenv:\n REGISTRY: ghcr.io\n IMAGE"
},
{
"path": ".github/workflows/docker-nightly.yaml",
"chars": 3722,
"preview": "name: Docker - Nightly\non:\n workflow_dispatch:\n # Note: this only runs on the default branch\n schedule:\n - cron: '"
},
{
"path": ".github/workflows/release.yaml",
"chars": 7102,
"preview": "name: Release\n# This workflow is triggered manually\non:\n workflow_dispatch:\n inputs:\n version_bump_type:\n "
},
{
"path": ".github/workflows/sponsors.yaml",
"chars": 312,
"preview": "name: Label sponsors\non:\n pull_request:\n types: [opened]\n issues:\n types: [opened]\njobs:\n build:\n name: is-s"
},
{
"path": ".gitignore",
"chars": 1285,
"preview": "# Created by .ignore support plugin (hsz.mobi)\n### JetBrains template\n# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpSt"
},
{
"path": ".golangci.yml",
"chars": 148,
"preview": "version: \"2\"\nformatters:\n enable:\n - gofmt\n - goimports\nlinters:\n enable:\n - bodyclose\n settings:\n errche"
},
{
"path": ".vscode/launch.json",
"chars": 1206,
"preview": "{\n \"version\": \"0.2.0\",\n \"configurations\": [\n {\n \"name\": \"Run Scrutiny\",\n \"type\": \"go\""
},
{
"path": ".vscode/tasks.json",
"chars": 199,
"preview": "{\n \"version\": \"2.0.0\",\n \"tasks\": [\n {\n \"label\": \"Build Frontend\",\n \"type\": \"shell\",\n \"command\": \"cd "
},
{
"path": "AI_POLICY.md",
"chars": 3336,
"preview": "# AI Usage Policy\n\nscrutiny has strict rules for AI usage:\n\n- **All AI usage in any form must be disclosed.** You must s"
},
{
"path": "CONTRIBUTING.md",
"chars": 13085,
"preview": "# Contributing to scrutiny\n\nThis document describes the process of contributing to scrutiny. It is intended\nfor anyone c"
},
{
"path": "LICENSE",
"chars": 1072,
"preview": "MIT License\n\nCopyright (c) 2020 Jason Kulatunga\n\nPermission is hereby granted, free of charge, to any person obtaining a"
},
{
"path": "Makefile",
"chars": 4876,
"preview": ".ONESHELL: # Applies to every targets in the file! .ONESHELL instructs make to invoke a single instance of the shell and"
},
{
"path": "README.md",
"chars": 13083,
"preview": "<p align=\"center\">\n <a href=\"https://github.com/AnalogJ/scrutiny\">\n <img width=\"300\" alt=\"scrutiny_view\" src=\"webapp/f"
},
{
"path": "REFERENCES.md",
"chars": 187,
"preview": "\n# Gorm\n- https://www.reddit.com/r/golang/comments/exmwos/golang_gorm_preload_with_last/\n- https://blog.depado.eu/post/g"
},
{
"path": "collector/cmd/collector-metrics/collector-metrics.go",
"chars": 6148,
"preview": "package main\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"log\"\n\t\"os\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/analogj/scrutiny/coll"
},
{
"path": "collector/cmd/collector-selftest/collector-selftest.go",
"chars": 3440,
"preview": "package main\n\nimport (\n\t\"fmt\"\n\t\"io\"\n\t\"log\"\n\t\"os\"\n\t\"time\"\n\n\t\"github.com/analogj/scrutiny/collector/pkg/collector\"\n\t\"githu"
},
{
"path": "collector/pkg/collector/base.go",
"chars": 1448,
"preview": "package collector\n\nimport (\n\t\"bytes\"\n\t\"encoding/json\"\n\t\"net/http\"\n\t\"time\"\n\n\t\"github.com/sirupsen/logrus\"\n)\n\nvar httpClie"
},
{
"path": "collector/pkg/collector/metrics.go",
"chars": 5574,
"preview": "package collector\n\nimport (\n\t\"bytes\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"net/url\"\n\t\"os\"\n\t\"os/exec\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.co"
},
{
"path": "collector/pkg/collector/metrics_test.go",
"chars": 1163,
"preview": "package collector\n\nimport (\n\t\"github.com/stretchr/testify/require\"\n\t\"net/url\"\n\t\"testing\"\n)\n\nfunc TestApiEndpointParse(t "
},
{
"path": "collector/pkg/collector/selftest.go",
"chars": 536,
"preview": "package collector\n\nimport (\n\t\"github.com/sirupsen/logrus\"\n\t\"net/url\"\n)\n\ntype SelfTestCollector struct {\n\tBaseCollector\n\n"
},
{
"path": "collector/pkg/common/shell/factory.go",
"chars": 67,
"preview": "package shell\n\nfunc Create() Interface {\n\treturn new(localShell)\n}\n"
},
{
"path": "collector/pkg/common/shell/interface.go",
"chars": 344,
"preview": "package shell\n\nimport (\n\t\"github.com/sirupsen/logrus\"\n)\n\n// Create mock using:\n// mockgen -source=collector/pkg/common/s"
},
{
"path": "collector/pkg/common/shell/local_shell.go",
"chars": 925,
"preview": "package shell\n\nimport (\n\t\"bytes\"\n\t\"errors\"\n\t\"io\"\n\t\"os/exec\"\n\t\"path\"\n\t\"strings\"\n\n\t\"github.com/sirupsen/logrus\"\n)\n\ntype lo"
},
{
"path": "collector/pkg/common/shell/local_shell_test.go",
"chars": 1296,
"preview": "package shell\n\nimport (\n\t\"github.com/sirupsen/logrus\"\n\t\"github.com/stretchr/testify/require\"\n\t\"os/exec\"\n\t\"testing\"\n)\n\nfu"
},
{
"path": "collector/pkg/common/shell/mock/mock_shell.go",
"chars": 1634,
"preview": "// Code generated by MockGen. DO NOT EDIT.\n// Source: collector/pkg/common/shell/interface.go\n\n// Package mock_shell is "
},
{
"path": "collector/pkg/config/config.go",
"chars": 5958,
"preview": "package config\n\nimport (\n\t\"fmt\"\n\t\"log\"\n\t\"os\"\n\t\"sort\"\n\t\"strings\"\n\n\t\"github.com/analogj/go-util/utils\"\n\t\"github.com/analog"
},
{
"path": "collector/pkg/config/config_test.go",
"chars": 5588,
"preview": "package config_test\n\nimport (\n\t\"github.com/analogj/scrutiny/collector/pkg/config\"\n\t\"github.com/analogj/scrutiny/collecto"
},
{
"path": "collector/pkg/config/factory.go",
"chars": 163,
"preview": "package config\n\nfunc Create() (Interface, error) {\n\tconfig := new(configuration)\n\tif err := config.Init(); err != nil {\n"
},
{
"path": "collector/pkg/config/interface.go",
"chars": 899,
"preview": "package config\n\nimport (\n\t\"github.com/analogj/scrutiny/collector/pkg/models\"\n\t\"github.com/spf13/viper\"\n)\n\n// Create mock"
},
{
"path": "collector/pkg/config/mock/mock_config.go",
"chars": 9191,
"preview": "// Code generated by MockGen. DO NOT EDIT.\n// Source: collector/pkg/config/interface.go\n\n// Package mock_config is a gen"
},
{
"path": "collector/pkg/config/testdata/allow_listed_devices_present.yaml",
"chars": 44,
"preview": "allow_listed_devices:\n- /dev/sda\n- /dev/sdb\n"
},
{
"path": "collector/pkg/config/testdata/device_type_comma.yaml",
"chars": 287,
"preview": "version: 1\ndevices:\n # the scrutiny config parser will detect `sat,auto` as two separate items in a list. If you want t"
},
{
"path": "collector/pkg/config/testdata/ignore_device.yaml",
"chars": 58,
"preview": "version: 1\ndevices:\n - device: /dev/sda\n ignore: true\n"
},
{
"path": "collector/pkg/config/testdata/invalid_commands_includes_device.yaml",
"chars": 283,
"preview": "commands:\n metrics_scan_args: '--scan --json' # used to detect devices\n metrics_info_args: '--info --json --device=sat"
},
{
"path": "collector/pkg/config/testdata/invalid_commands_missing_json.yaml",
"chars": 252,
"preview": "commands:\n metrics_scan_args: '--scan' # used to detect devices\n metrics_info_args: '--info -j' # used to determine de"
},
{
"path": "collector/pkg/config/testdata/override_commands.yaml",
"chars": 273,
"preview": "commands:\n metrics_scan_args: '--scan --json' # used to detect devices\n metrics_info_args: '--info -j' # used to deter"
},
{
"path": "collector/pkg/config/testdata/override_device_commands.yaml",
"chars": 109,
"preview": "version: 1\ndevices:\n - device: /dev/sda\n commands:\n metrics_info_args: \"--info --json -T permissive\""
},
{
"path": "collector/pkg/config/testdata/raid_device.yaml",
"chars": 302,
"preview": "version: 1\ndevices:\n - device: /dev/bus/0\n type:\n - megaraid,14\n - megaraid,15\n - megaraid,18\n -"
},
{
"path": "collector/pkg/config/testdata/simple_device.yaml",
"chars": 552,
"preview": "version: 1\ndevices:\n - device: /dev/sda\n type: 'sat'\n#\n# # example to show how to ignore a specific disk/device.\n# "
},
{
"path": "collector/pkg/detect/detect.go",
"chars": 7728,
"preview": "package detect\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"os\"\n\t\"strings\"\n\n\t\"github.com/analogj/scrutiny/collector/pkg/common/sh"
},
{
"path": "collector/pkg/detect/detect_test.go",
"chars": 13344,
"preview": "package detect_test\n\nimport (\n\t\"os\"\n\t\"strings\"\n\t\"testing\"\n\n\tmock_shell \"github.com/analogj/scrutiny/collector/pkg/common"
},
{
"path": "collector/pkg/detect/devices_darwin.go",
"chars": 3407,
"preview": "package detect\n\nimport (\n\t\"github.com/analogj/scrutiny/collector/pkg/common/shell\"\n\t\"github.com/analogj/scrutiny/collect"
},
{
"path": "collector/pkg/detect/devices_freebsd.go",
"chars": 1379,
"preview": "package detect\n\nimport (\n\t\"github.com/analogj/scrutiny/collector/pkg/common/shell\"\n\t\"github.com/analogj/scrutiny/collect"
},
{
"path": "collector/pkg/detect/devices_linux.go",
"chars": 2983,
"preview": "package detect\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n\n\t\"github.com/analogj/scrutiny/collector/pkg/common/sh"
},
{
"path": "collector/pkg/detect/devices_linux_test.go",
"chars": 254,
"preview": "package detect_test\n\nimport (\n\t\"github.com/analogj/scrutiny/collector/pkg/detect\"\n\t\"github.com/stretchr/testify/require\""
},
{
"path": "collector/pkg/detect/devices_windows.go",
"chars": 894,
"preview": "package detect\n\nimport (\n\t\"github.com/analogj/scrutiny/collector/pkg/common/shell\"\n\t\"github.com/analogj/scrutiny/collect"
},
{
"path": "collector/pkg/detect/testdata/smartctl_info_nvme.json",
"chars": 954,
"preview": "{\n \"json_format_version\": [\n 1,\n 0\n ],\n \"smartctl\": {\n \"version\": [\n 7,\n 2\n ],\n \"svn_revisio"
},
{
"path": "collector/pkg/detect/testdata/smartctl_scan_megaraid.json",
"chars": 624,
"preview": "{\n \"json_format_version\": [\n 1,\n 0\n ],\n \"smartctl\": {\n \"version\": [\n 7,\n 1\n ],\n \"svn_revisio"
},
{
"path": "collector/pkg/detect/testdata/smartctl_scan_nvme.json",
"chars": 452,
"preview": "{\n \"json_format_version\": [\n 1,\n 0\n ],\n \"smartctl\": {\n \"version\": [\n 7,\n 0\n ],\n \"svn_revisio"
},
{
"path": "collector/pkg/detect/testdata/smartctl_scan_simple.json",
"chars": 1150,
"preview": "{\n \"json_format_version\": [\n 1,\n 0\n ],\n \"smartctl\": {\n \"version\": [\n 7,\n 0\n ],\n \"svn_revisio"
},
{
"path": "collector/pkg/detect/wwn.go",
"chars": 1953,
"preview": "package detect\n\nimport (\n\t\"fmt\"\n\t\"strings\"\n)\n\ntype Wwn struct {\n\tNaa uint64 `json:\"naa\"`\n\tOui uint64 `json"
},
{
"path": "collector/pkg/detect/wwn_test.go",
"chars": 970,
"preview": "package detect_test\n\nimport (\n\t\"testing\"\n\n\t\"github.com/analogj/scrutiny/collector/pkg/detect\"\n\t\"github.com/stretchr/test"
},
{
"path": "collector/pkg/errors/errors.go",
"chars": 877,
"preview": "package errors\n\nimport (\n\t\"fmt\"\n)\n\n// Raised when config file is missing\ntype ConfigFileMissingError string\n\nfunc (str C"
},
{
"path": "collector/pkg/errors/errors_test.go",
"chars": 862,
"preview": "package errors_test\n\nimport (\n\t\"github.com/analogj/scrutiny/collector/pkg/errors\"\n\t\"github.com/stretchr/testify/require\""
},
{
"path": "collector/pkg/models/device.go",
"chars": 1205,
"preview": "package models\n\ntype Device struct {\n\tWWN string `json:\"wwn\"`\n\n\tDeviceName string `json:\"device_name\"`\n\tDeviceUUID\t "
},
{
"path": "collector/pkg/models/scan.go",
"chars": 600,
"preview": "package models\n\ntype Scan struct {\n\tJSONFormatVersion []int `json:\"json_format_version\"`\n\tSmartctl struct {\n\t\tV"
},
{
"path": "collector/pkg/models/scan_override.go",
"chars": 351,
"preview": "package models\n\ntype ScanOverride struct {\n\tDevice string `mapstructure:\"device\"`\n\tDeviceType []string `mapstructu"
},
{
"path": "docker/Dockerfile",
"chars": 4356,
"preview": "# syntax=docker/dockerfile:1.4\n#########################################################################################"
},
{
"path": "docker/Dockerfile.collector",
"chars": 2426,
"preview": "########################################################################################################################"
},
{
"path": "docker/Dockerfile.smartmontools",
"chars": 1253,
"preview": "########################################################################################################################"
},
{
"path": "docker/Dockerfile.web",
"chars": 1785,
"preview": "# syntax=docker/dockerfile:1.4\n#########################################################################################"
},
{
"path": "docker/README.md",
"chars": 62,
"preview": "`rootfs` is only used by Dockerfile and Dockerfile.collector\n"
},
{
"path": "docker/entrypoint-collector.sh",
"chars": 1300,
"preview": "#!/bin/bash\n\n# Cron runs in its own isolated environment (usually using only /etc/environment )\n# So when the container "
},
{
"path": "docker/example.hubspoke.docker-compose.yml",
"chars": 1460,
"preview": "version: '2.4'\n\nservices:\n influxdb:\n restart: unless-stopped\n image: influxdb:2.8\n ports:\n - '8086:8086'"
},
{
"path": "docker/example.omnibus.docker-compose.yml",
"chars": 429,
"preview": "version: '3.5'\n\nservices:\n scrutiny:\n restart: unless-stopped\n container_name: scrutiny\n image: ghcr.io/analog"
},
{
"path": "docs/DOWNSAMPLING.md",
"chars": 1717,
"preview": "# Downsampling\n\nScrutiny collects alot of data, that can cause the database to grow unbounded. \n\n- Smart data\n- Smart te"
},
{
"path": "docs/INSTALL_ANSIBLE.md",
"chars": 619,
"preview": "# Ansible Install\n\n[Zorlin](https://github.com/Zorlin) has developed and now maintains [an Ansible playbook](https://git"
},
{
"path": "docs/INSTALL_HUB_SPOKE.md",
"chars": 8108,
"preview": ">\nSee [docker/example.hubspoke.docker-compose.yml](https://github.com/AnalogJ/scrutiny/blob/master/docker/example.hubspo"
},
{
"path": "docs/INSTALL_MANUAL.md",
"chars": 15430,
"preview": "# Manual Install\n\nWhile the easiest way to get started with [Scrutiny is using Docker](https://github.com/AnalogJ/scruti"
},
{
"path": "docs/INSTALL_MANUAL_WINDOWS.md",
"chars": 4289,
"preview": "# Manual Windows Install\n\nThis guide is specifically for people who are on a Windows machine using [WSL](https://learn.m"
},
{
"path": "docs/INSTALL_NAS.md",
"chars": 13,
"preview": "package docs\n"
},
{
"path": "docs/INSTALL_PFSENSE.md",
"chars": 2052,
"preview": "# pfsense Install\n\nThis bascially follows the [Manual collector instructions](https://github.com/AnalogJ/scrutiny/blob/m"
},
{
"path": "docs/INSTALL_ROOTLESS_PODMAN.md",
"chars": 5289,
"preview": "# Rootless Podman Quadlet Install\n\nNote: These instructions are written with Podman 4.9 in mind, as that's what's availa"
},
{
"path": "docs/INSTALL_SYNOLOGY_COLLECTOR.md",
"chars": 3387,
"preview": "# Install collector on Synology\n\n## Install Entware\n\nThis will allow you to install a newer version of smartmontools on "
},
{
"path": "docs/INSTALL_UNRAID.md",
"chars": 1889,
"preview": "# UnRAID Install\n\nInstallation of Scrutiny in UnRAID follows the same process as installing any other docker container, "
},
{
"path": "docs/SUPPORTED_NAS_OS.md",
"chars": 975,
"preview": "# Officially Supported NAS/OS's\n\nThese are the officially supported NAS OS's (with documentation and setup guides). Once"
},
{
"path": "docs/TESTERS.md",
"chars": 897,
"preview": "# Testers\n\nScrutiny supports many operating systems, CPU architectures and runtime environments. Unfortunately that make"
},
{
"path": "docs/TROUBLESHOOTING_DEVICE_COLLECTOR.md",
"chars": 17752,
"preview": "# Scrutiny <-> SmartMonTools \n\nScrutiny uses `smartctl --scan` to detect devices/drives. If your devices are not being d"
},
{
"path": "docs/TROUBLESHOOTING_DOCKER.md",
"chars": 1574,
"preview": "# Docker Images `latest` vs `nightly`\n\n> TL;DR; The `latest-omnibus`, `latest-collector`, and `latest-web` tags point to"
},
{
"path": "docs/TROUBLESHOOTING_INFLUXDB.md",
"chars": 16148,
"preview": "# InfluxDB Troubleshooting\n\n## Why??\n\nScrutiny has many features, but the relevant one to this conversation is the \"S.M."
},
{
"path": "docs/TROUBLESHOOTING_NOTIFICATIONS.md",
"chars": 1844,
"preview": "# Notifications\n\nAs documented in [example.scrutiny.yaml](https://github.com/AnalogJ/scrutiny/blob/master/example.scruti"
},
{
"path": "docs/TROUBLESHOOTING_REVERSE_PROXY.md",
"chars": 4607,
"preview": "# Reverse Proxy Support\n\nScrutiny is designed so that it can be used with a reverse proxy, leveraging `domain`, `port` o"
},
{
"path": "docs/TROUBLESHOOTING_UDEV.md",
"chars": 730,
"preview": "# Operating systems without udev\n\nSome operating systems do not come with `udev` out of the box, for example Alpine Linu"
},
{
"path": "docs/dbdiagram.io.txt",
"chars": 1805,
"preview": "\n// SQLite Table(s)\n\nTable Device {\n Archived bool\n\t//GORM attributes, see: http://gorm.io/docs/conventions.html\n\tCr"
},
{
"path": "example.collector.yaml",
"chars": 3615,
"preview": "# Commented Scrutiny Configuration File\n#\n# The default location for this file is /opt/scrutiny/config/collector.yaml.\n#"
},
{
"path": "example.scrutiny.yaml",
"chars": 4216,
"preview": "# Commented Scrutiny Configuration File\n#\n# The default location for this file is /opt/scrutiny/config/scrutiny.yaml.\n# "
},
{
"path": "go.mod",
"chars": 4012,
"preview": "module github.com/analogj/scrutiny\n\ngo 1.25\n\nrequire (\n\tgithub.com/analogj/go-util v0.0.0-20210417161720-39b497cca03b\n\tg"
},
{
"path": "go.sum",
"chars": 22299,
"preview": "github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0=\ngithub.com/Masterminds/semver/v3"
},
{
"path": "packagr.yml",
"chars": 192,
"preview": "---\nengine_enable_code_mutation: true\nmgr_keep_lock_file: true\nengine_version_metadata_path: 'webapp/backend/pkg/version"
},
{
"path": "rootfs/etc/cont-init.d/01-timezone",
"chars": 144,
"preview": "#!/command/with-contenv bash\n\nif [ -n \"${TZ}\" ]\nthen\n ln -snf \"/usr/share/zoneinfo/${TZ}\" /etc/localtime\n echo \"${"
},
{
"path": "rootfs/etc/cont-init.d/50-cron-config",
"chars": 828,
"preview": "#!/command/with-contenv bash\n\n# Cron runs in its own isolated environment (usually using only /etc/environment )\n# So wh"
},
{
"path": "rootfs/etc/cron.d/scrutiny",
"chars": 852,
"preview": "MAILTO=\"\"\n# Example of job definition:\n# .---------------- minute (0 - 59)\n# | .------------- hour (0 - 23)\n# | | .--"
},
{
"path": "rootfs/etc/services.d/collector-once/run",
"chars": 826,
"preview": "#!/command/with-contenv bash\n\n# ensure not run (successfully) before\nif [ -f /tmp/custom-init-performed ]; then\n echo '"
},
{
"path": "rootfs/etc/services.d/cron/finish",
"chars": 83,
"preview": "#!/command/execlineb -S0\n\necho \"cron exiting\"\ns6-svscanctl -t /var/run/s6/services\n"
},
{
"path": "rootfs/etc/services.d/cron/run",
"chars": 65,
"preview": "#!/command/with-contenv bash\n\necho \"starting cron\"\ncron -f -L 15\n"
},
{
"path": "rootfs/etc/services.d/influxdb/run",
"chars": 417,
"preview": "#!/command/with-contenv bash\n\nmkdir -p /opt/scrutiny/influxdb/\n\nif [ -f \"/opt/scrutiny/influxdb/config.yaml\" ]; then\n "
},
{
"path": "rootfs/etc/services.d/scrutiny/run",
"chars": 231,
"preview": "#!/command/with-contenv bash\n\necho \"waiting for influxdb\"\nuntil $(curl --output /dev/null --silent --head --fail http://"
},
{
"path": "webapp/backend/cmd/scrutiny/scrutiny.go",
"chars": 5072,
"preview": "package main\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"log\"\n\t\"os\"\n\t\"time\"\n\n\t\"github.com/analogj/scrutiny/webapp/backend/"
},
{
"path": "webapp/backend/pkg/config/config.go",
"chars": 4057,
"preview": "package config\n\nimport (\n\t\"github.com/analogj/go-util/utils\"\n\t\"github.com/analogj/scrutiny/webapp/backend/pkg/errors\"\n\t\""
},
{
"path": "webapp/backend/pkg/config/config_test.go",
"chars": 845,
"preview": "package config\n\nimport (\n\t\"github.com/spf13/viper\"\n\t\"github.com/stretchr/testify/require\"\n\t\"testing\"\n)\n\nfunc Test_MergeC"
},
{
"path": "webapp/backend/pkg/config/factory.go",
"chars": 163,
"preview": "package config\n\nfunc Create() (Interface, error) {\n\tconfig := new(configuration)\n\tif err := config.Init(); err != nil {\n"
},
{
"path": "webapp/backend/pkg/config/interface.go",
"chars": 835,
"preview": "package config\n\nimport (\n\t\"github.com/spf13/viper\"\n)\n\n// Create mock using:\n// mockgen -source=webapp/backend/pkg/config"
},
{
"path": "webapp/backend/pkg/config/mock/mock_config.go",
"chars": 9594,
"preview": "// Code generated by MockGen. DO NOT EDIT.\n// Source: webapp/backend/pkg/config/interface.go\n\n// Package mock_config is "
},
{
"path": "webapp/backend/pkg/constants.go",
"chars": 2176,
"preview": "package pkg\n\nconst DeviceProtocolAta = \"ATA\"\nconst DeviceProtocolScsi = \"SCSI\"\nconst DeviceProtocolNvme = \"NVMe\"\n\n//go:g"
},
{
"path": "webapp/backend/pkg/database/interface.go",
"chars": 1827,
"preview": "package database\n\nimport (\n\t\"context\"\n\n\t\"github.com/analogj/scrutiny/webapp/backend/pkg\"\n\t\"github.com/analogj/scrutiny/w"
},
{
"path": "webapp/backend/pkg/database/migrations/m20201107210306/device.go",
"chars": 1554,
"preview": "package m20201107210306\n\nimport (\n\t\"time\"\n)\n\n// Deprecated: m20201107210306.Device is deprecated, only used by db migrat"
},
{
"path": "webapp/backend/pkg/database/migrations/m20201107210306/smart.go",
"chars": 837,
"preview": "package m20201107210306\n\nimport (\n\t\"gorm.io/gorm\"\n\t\"time\"\n)\n\n// Deprecated: m20201107210306.Smart is deprecated, only us"
},
{
"path": "webapp/backend/pkg/database/migrations/m20201107210306/smart_ata_attribute.go",
"chars": 978,
"preview": "package m20201107210306\n\nimport \"gorm.io/gorm\"\n\n// Deprecated: m20201107210306.SmartAtaAttribute is deprecated, only use"
},
{
"path": "webapp/backend/pkg/database/migrations/m20201107210306/smart_nvme_attribute.go",
"chars": 858,
"preview": "package m20201107210306\n\nimport \"gorm.io/gorm\"\n\n// Deprecated: m20201107210306.SmartNvmeAttribute is deprecated, only us"
},
{
"path": "webapp/backend/pkg/database/migrations/m20201107210306/smart_scsci_attribute.go",
"chars": 858,
"preview": "package m20201107210306\n\nimport \"gorm.io/gorm\"\n\n// Deprecated: m20201107210306.SmartScsiAttribute is deprecated, only us"
},
{
"path": "webapp/backend/pkg/database/migrations/m20220503120000/device.go",
"chars": 1297,
"preview": "package m20220503120000\n\nimport (\n\t\"github.com/analogj/scrutiny/webapp/backend/pkg\"\n\t\"time\"\n)\n\n// Deprecated: m202205031"
},
{
"path": "webapp/backend/pkg/database/migrations/m20220509170100/device.go",
"chars": 1436,
"preview": "package m20220509170100\n\nimport (\n\t\"github.com/analogj/scrutiny/webapp/backend/pkg\"\n\t\"time\"\n)\n\n// Deprecated: m202205091"
},
{
"path": "webapp/backend/pkg/database/migrations/m20220716214900/setting.go",
"chars": 502,
"preview": "package m20220716214900\n\nimport (\n\t\"gorm.io/gorm\"\n)\n\ntype Setting struct {\n\t//GORM attributes, see: http://gorm.io/docs/"
},
{
"path": "webapp/backend/pkg/database/migrations/m20250221084400/device.go",
"chars": 1389,
"preview": "package m20250221084400\n\nimport (\n\t\"github.com/analogj/scrutiny/webapp/backend/pkg\"\n\t\"time\"\n)\n\ntype Device struct {\n\tArc"
},
{
"path": "webapp/backend/pkg/database/mock/mock_database.go",
"chars": 11504,
"preview": "// Code generated by MockGen. DO NOT EDIT.\n// Source: webapp/backend/pkg/database/interface.go\n\n// Package mock_database"
},
{
"path": "webapp/backend/pkg/database/scrutiny_repository.go",
"chars": 19274,
"preview": "package database\n\nimport (\n\t\"context\"\n\t\"crypto/tls\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"time\"\n\n\t\"gith"
},
{
"path": "webapp/backend/pkg/database/scrutiny_repository_device.go",
"chars": 4224,
"preview": "package database\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"time\"\n\n\t\"github.com/analogj/scrutiny/webapp/backend/pkg\"\n\t\"github.com/ana"
},
{
"path": "webapp/backend/pkg/database/scrutiny_repository_device_smart_attributes.go",
"chars": 8295,
"preview": "package database\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/analogj/scrutiny/webapp/backend/pkg/models"
},
{
"path": "webapp/backend/pkg/database/scrutiny_repository_migrations.go",
"chars": 26324,
"preview": "package database\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"strconv\"\n\t\"time\"\n\n\t\"github.com/analogj/scrutiny/webapp/backend/"
},
{
"path": "webapp/backend/pkg/database/scrutiny_repository_settings.go",
"chars": 3454,
"preview": "package database\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"strings\"\n\n\t\"github.com/analogj/scrutiny/webapp/backend/pkg/config\"\n\t\"gith"
},
{
"path": "webapp/backend/pkg/database/scrutiny_repository_tasks.go",
"chars": 5432,
"preview": "package database\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"github.com/influxdata/influxdb-client-go/v2/api\"\n)\n\n/////////////////////"
},
{
"path": "webapp/backend/pkg/database/scrutiny_repository_tasks_test.go",
"chars": 4415,
"preview": "package database\n\nimport (\n\t\"testing\"\n\n\tmock_config \"github.com/analogj/scrutiny/webapp/backend/pkg/config/mock\"\n\t\"githu"
},
{
"path": "webapp/backend/pkg/database/scrutiny_repository_temperature.go",
"chars": 5916,
"preview": "package database\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/analogj/scrutiny/webapp/backend/pkg/models"
},
{
"path": "webapp/backend/pkg/database/scrutiny_repository_temperature_test.go",
"chars": 5358,
"preview": "package database\n\nimport (\n\t\"testing\"\n\n\tmock_config \"github.com/analogj/scrutiny/webapp/backend/pkg/config/mock\"\n\t\"githu"
},
{
"path": "webapp/backend/pkg/errors/errors.go",
"chars": 878,
"preview": "package errors\n\nimport (\n\t\"fmt\"\n)\n\n// Raised when config file is missing\ntype ConfigFileMissingError string\n\nfunc (str C"
},
{
"path": "webapp/backend/pkg/errors/errors_test.go",
"chars": 867,
"preview": "package errors_test\n\nimport (\n\t\"github.com/analogj/scrutiny/webapp/backend/pkg/errors\"\n\t\"github.com/stretchr/testify/req"
},
{
"path": "webapp/backend/pkg/models/collector/smart.go",
"chars": 12061,
"preview": "package collector\n\ntype SmartInfo struct {\n\tJSONFormatVersion []int `json:\"json_format_version\"`\n\tSmartctl stru"
},
{
"path": "webapp/backend/pkg/models/collector/smart_test.go",
"chars": 696,
"preview": "package collector\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestSmartInfo_Capacity(t *testing."
},
{
"path": "webapp/backend/pkg/models/device.go",
"chars": 6217,
"preview": "package models\n\nimport (\n\t\"github.com/analogj/scrutiny/webapp/backend/pkg\"\n\t\"github.com/analogj/scrutiny/webapp/backend/"
},
{
"path": "webapp/backend/pkg/models/device_summary.go",
"chars": 733,
"preview": "package models\n\nimport (\n\t\"github.com/analogj/scrutiny/webapp/backend/pkg/models/measurements\"\n\t\"time\"\n)\n\ntype DeviceSum"
},
{
"path": "webapp/backend/pkg/models/measurements/smart.go",
"chars": 12088,
"preview": "package measurements\n\nimport (\n\t\"fmt\"\n\t\"log\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/analogj/scrutiny/webapp/backend"
},
{
"path": "webapp/backend/pkg/models/measurements/smart_ata_attribute.go",
"chars": 5850,
"preview": "package measurements\n\nimport (\n\t\"fmt\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"github.com/analogj/scrutiny/webapp/backend/pkg\"\n\t\"github."
},
{
"path": "webapp/backend/pkg/models/measurements/smart_attribute.go",
"chars": 256,
"preview": "package measurements\n\nimport \"github.com/analogj/scrutiny/webapp/backend/pkg\"\n\ntype SmartAttribute interface {\n\tFlatten("
},
{
"path": "webapp/backend/pkg/models/measurements/smart_nvme_attribute.go",
"chars": 2810,
"preview": "package measurements\n\nimport (\n\t\"fmt\"\n\t\"strings\"\n\n\t\"github.com/analogj/scrutiny/webapp/backend/pkg\"\n\t\"github.com/analogj"
},
{
"path": "webapp/backend/pkg/models/measurements/smart_scsci_attribute.go",
"chars": 2711,
"preview": "package measurements\n\nimport (\n\t\"fmt\"\n\t\"strings\"\n\n\t\"github.com/analogj/scrutiny/webapp/backend/pkg\"\n\t\"github.com/analogj"
},
{
"path": "webapp/backend/pkg/models/measurements/smart_temperature.go",
"chars": 561,
"preview": "package measurements\n\nimport (\n\t\"time\"\n)\n\ntype SmartTemperature struct {\n\tDate time.Time `json:\"date\"`\n\tTemp int64 `"
},
{
"path": "webapp/backend/pkg/models/measurements/smart_test.go",
"chars": 16109,
"preview": "package measurements_test\n\nimport (\n\t\"encoding/json\"\n\t\"io\"\n\t\"os\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/analogj/scrutiny/webap"
},
{
"path": "webapp/backend/pkg/models/setting_entry.go",
"chars": 640,
"preview": "package models\n\nimport (\n\t\"gorm.io/gorm\"\n)\n\n// SettingEntry matches a setting row in the database\ntype SettingEntry stru"
},
{
"path": "webapp/backend/pkg/models/settings.go",
"chars": 1825,
"preview": "package models\n\n// Settings is made up of parsed SettingEntry objects retrieved from the database\n//type Settings struct"
},
{
"path": "webapp/backend/pkg/models/testdata/helper.go",
"chars": 2883,
"preview": "package main\n\nimport (\n\t\"bytes\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"log\"\n\t\"net/http\"\n\t\"os\"\n\t\"time\"\n\n\t\"github.com/analogj/scr"
},
{
"path": "webapp/backend/pkg/models/testdata/smart-ata-date.json",
"chars": 19251,
"preview": "{\n \"json_format_version\": [\n 1,\n 0\n ],\n \"smartctl\": {\n \"version\": [\n 7,\n 0\n ],\n \"svn_revisio"
},
{
"path": "webapp/backend/pkg/models/testdata/smart-ata-date2.json",
"chars": 19247,
"preview": "{\n \"json_format_version\": [\n 1,\n 0\n ],\n \"smartctl\": {\n \"version\": [\n 7,\n 0\n ],\n \"svn_revisio"
},
{
"path": "webapp/backend/pkg/models/testdata/smart-ata-failed-scrutiny.json",
"chars": 24367,
"preview": "{\n \"json_format_version\": [\n 1,\n 0\n ],\n \"smartctl\": {\n \"version\": [\n 7,\n 0\n ],\n \"svn_revisio"
},
{
"path": "webapp/backend/pkg/models/testdata/smart-ata-full.json",
"chars": 38080,
"preview": "{\n \"json_format_version\": [\n 1,\n 0\n ],\n \"smartctl\": {\n \"version\": [\n 7,\n 0\n ],\n \"svn_revisio"
},
{
"path": "webapp/backend/pkg/models/testdata/smart-ata.json",
"chars": 19251,
"preview": "{\n \"json_format_version\": [\n 1,\n 0\n ],\n \"smartctl\": {\n \"version\": [\n 7,\n 0\n ],\n \"svn_revisio"
},
{
"path": "webapp/backend/pkg/models/testdata/smart-ata2.json",
"chars": 10985,
"preview": "{\n \"json_format_version\": [\n 1,\n 0\n ],\n \"smartctl\": {\n \"version\": [\n 7,\n 0\n ],\n \"svn_revisio"
},
{
"path": "webapp/backend/pkg/models/testdata/smart-fail.json",
"chars": 499,
"preview": "{\n \"json_format_version\": [\n 1,\n 0\n ],\n \"smartctl\": {\n \"version\": [\n 7,\n 0\n ],\n \"svn_revisio"
},
{
"path": "webapp/backend/pkg/models/testdata/smart-fail2.json",
"chars": 30059,
"preview": "{\n \"json_format_version\": [\n 1,\n 0\n ],\n \"smartctl\": {\n \"version\": [\n 7,\n 1\n ],\n \"svn_revisio"
},
{
"path": "webapp/backend/pkg/models/testdata/smart-megaraid0.json",
"chars": 15682,
"preview": "{\n \"json_format_version\": [\n 1,\n 0\n ],\n \"smartctl\": {\n \"version\": [\n 7,\n 1\n ],\n \"svn_revisio"
},
{
"path": "webapp/backend/pkg/models/testdata/smart-megaraid1.json",
"chars": 15973,
"preview": "{\n \"json_format_version\": [\n 1,\n 0\n ],\n \"smartctl\": {\n \"version\": [\n 7,\n 1\n ],\n \"svn_revisio"
},
{
"path": "webapp/backend/pkg/models/testdata/smart-nvme-failed.json",
"chars": 2224,
"preview": "{\n \"json_format_version\": [\n 1,\n 0\n ],\n \"smartctl\": {\n \"version\": [\n 7,\n 0\n ],\n \"svn_revisio"
},
{
"path": "webapp/backend/pkg/models/testdata/smart-nvme.json",
"chars": 2054,
"preview": "{\n \"json_format_version\": [\n 1,\n 0\n ],\n \"smartctl\": {\n \"version\": [\n 7,\n 1\n ],\n \"svn_revisio"
},
{
"path": "webapp/backend/pkg/models/testdata/smart-nvme2.json",
"chars": 2170,
"preview": "{\n \"json_format_version\": [\n 1,\n 0\n ],\n \"smartctl\": {\n \"version\": [\n 7,\n 0\n ],\n \"svn_revisio"
},
{
"path": "webapp/backend/pkg/models/testdata/smart-pass.json",
"chars": 477,
"preview": "{\n \"json_format_version\": [\n 1,\n 0\n ],\n \"smartctl\": {\n \"version\": [\n 7,\n 0\n ],\n \"svn_revisio"
},
{
"path": "webapp/backend/pkg/models/testdata/smart-raid.json",
"chars": 995,
"preview": "{\n \"json_format_version\": [\n 1,\n 0\n ],\n \"smartctl\": {\n \"version\": [\n 7,\n 0\n ],\n \"svn_revisio"
},
{
"path": "webapp/backend/pkg/models/testdata/smart-sat.json",
"chars": 11926,
"preview": "{\n \"json_format_version\": [\n 1,\n 0\n ],\n \"smartctl\": {\n \"version\": [\n 7,\n 0\n ],\n \"svn_revisio"
},
{
"path": "webapp/backend/pkg/models/testdata/smart-scsi.json",
"chars": 1542,
"preview": "{\n \"device\": {\n \"name\": \"/dev/sdg\",\n \"info_name\": \"/dev/sdg\",\n \"type\": \"scsi\",\n \"protocol\": \"SCSI\"\n },\n \""
},
{
"path": "webapp/backend/pkg/models/testdata/smart-scsi2.json",
"chars": 2063,
"preview": "{\n \"json_format_version\": [\n 0,\n 1\n ],\n \"smartctl\": {\n \"version\": [\n 6,\n 7\n ],\n \"platform_in"
},
{
"path": "webapp/backend/pkg/notify/notify.go",
"chars": 15316,
"preview": "package notify\n\nimport (\n\t\"bytes\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"os\"\n\t\"strconv\"\n\t\"strings\"\n\t"
},
{
"path": "webapp/backend/pkg/notify/notify_test.go",
"chars": 11739,
"preview": "package notify\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/analogj/scrutiny/webapp/backend/pkg\"\n\t\"github"
},
{
"path": "webapp/backend/pkg/thresholds/ata_attribute_metadata.go",
"chars": 54145,
"preview": "package thresholds\n\nimport (\n\t\"strconv\"\n\t\"strings\"\n)\n\nconst AtaSmartAttributeDisplayTypeRaw = \"raw\"\nconst AtaSmartAttrib"
},
{
"path": "webapp/backend/pkg/thresholds/nvme_attribute_metadata.go",
"chars": 7174,
"preview": "package thresholds\n\n// https://media.kingston.com/support/downloads/MKP_521.6_SMART-DCP1000_attribute.pdf\n// https://www"
},
{
"path": "webapp/backend/pkg/thresholds/scsi_attribute_metadata.go",
"chars": 8686,
"preview": "package thresholds\n\ntype ScsiAttributeMetadata struct {\n\tID string `json:\"-\"`\n\tDisplayName string `json:\"displa"
},
{
"path": "webapp/backend/pkg/version/version.go",
"chars": 146,
"preview": "package version\n\n// VERSION is the app-global version string, which will be replaced with a\n// new value during packagin"
},
{
"path": "webapp/backend/pkg/web/handler/archive_device.go",
"chars": 587,
"preview": "package handler\n\nimport (\n\t\"github.com/analogj/scrutiny/webapp/backend/pkg/database\"\n\t\"github.com/gin-gonic/gin\"\n\t\"githu"
},
{
"path": "webapp/backend/pkg/web/handler/delete_device.go",
"chars": 571,
"preview": "package handler\n\nimport (\n\t\"github.com/analogj/scrutiny/webapp/backend/pkg/database\"\n\t\"github.com/gin-gonic/gin\"\n\t\"githu"
},
{
"path": "webapp/backend/pkg/web/handler/get_device_details.go",
"chars": 1384,
"preview": "package handler\n\nimport (\n\t\"net/http\"\n\n\t\"github.com/analogj/scrutiny/webapp/backend/pkg/database\"\n\t\"github.com/analogj/s"
},
{
"path": "webapp/backend/pkg/web/handler/get_devices_summary.go",
"chars": 755,
"preview": "package handler\n\nimport (\n\t\"github.com/analogj/scrutiny/webapp/backend/pkg/database\"\n\t\"github.com/gin-gonic/gin\"\n\t\"githu"
},
{
"path": "webapp/backend/pkg/web/handler/get_devices_summary_temp_history.go",
"chars": 796,
"preview": "package handler\n\nimport (\n\t\"github.com/analogj/scrutiny/webapp/backend/pkg/database\"\n\t\"github.com/gin-gonic/gin\"\n\t\"githu"
},
{
"path": "webapp/backend/pkg/web/handler/get_settings.go",
"chars": 599,
"preview": "package handler\n\nimport (\n\t\"github.com/analogj/scrutiny/webapp/backend/pkg/database\"\n\t\"github.com/gin-gonic/gin\"\n\t\"githu"
},
{
"path": "webapp/backend/pkg/web/handler/health_check.go",
"chars": 718,
"preview": "package handler\n\nimport (\n\t\"github.com/analogj/scrutiny/webapp/backend/pkg/database\"\n\t\"github.com/gin-gonic/gin\"\n\t\"githu"
},
{
"path": "webapp/backend/pkg/web/handler/register_devices.go",
"chars": 1634,
"preview": "package handler\n\nimport (\n\t\"github.com/analogj/scrutiny/webapp/backend/pkg/database\"\n\t\"github.com/analogj/scrutiny/webap"
},
{
"path": "webapp/backend/pkg/web/handler/save_settings.go",
"chars": 863,
"preview": "package handler\n\nimport (\n\t\"github.com/analogj/scrutiny/webapp/backend/pkg/database\"\n\t\"github.com/analogj/scrutiny/webap"
},
{
"path": "webapp/backend/pkg/web/handler/send_test_notification.go",
"chars": 991,
"preview": "package handler\n\nimport (\n\t\"github.com/analogj/scrutiny/webapp/backend/pkg\"\n\t\"github.com/analogj/scrutiny/webapp/backend"
},
{
"path": "webapp/backend/pkg/web/handler/unarchive_device.go",
"chars": 592,
"preview": "package handler\n\nimport (\n\t\"github.com/analogj/scrutiny/webapp/backend/pkg/database\"\n\t\"github.com/gin-gonic/gin\"\n\t\"githu"
},
{
"path": "webapp/backend/pkg/web/handler/upload_device_metrics.go",
"chars": 3239,
"preview": "package handler\n\nimport (\n\t\"fmt\"\n\t\"net/http\"\n\n\t\"github.com/analogj/scrutiny/webapp/backend/pkg\"\n\t\"github.com/analogj/scr"
},
{
"path": "webapp/backend/pkg/web/handler/upload_device_self_tests.go",
"chars": 100,
"preview": "package handler\n\nimport \"github.com/gin-gonic/gin\"\n\nfunc UploadDeviceSelfTests(c *gin.Context) {\n\n}\n"
},
{
"path": "webapp/backend/pkg/web/middleware/config.go",
"chars": 261,
"preview": "package middleware\n\nimport (\n\t\"github.com/analogj/scrutiny/webapp/backend/pkg/config\"\n\t\"github.com/gin-gonic/gin\"\n)\n\nfun"
},
{
"path": "webapp/backend/pkg/web/middleware/logger.go",
"chars": 3259,
"preview": "package middleware\n\nimport (\n\t\"bytes\"\n\t\"fmt\"\n\t\"io\"\n\t\"math\"\n\t\"net/http\"\n\t\"os\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/gin-gonic/"
},
{
"path": "webapp/backend/pkg/web/middleware/repository.go",
"chars": 786,
"preview": "package middleware\n\nimport (\n\t\"context\"\n\t\"github.com/analogj/scrutiny/webapp/backend/pkg/config\"\n\t\"github.com/analogj/sc"
},
{
"path": "webapp/backend/pkg/web/server.go",
"chars": 3158,
"preview": "package web\n\nimport (\n\t\"fmt\"\n\t\"github.com/analogj/go-util/utils\"\n\t\"github.com/analogj/scrutiny/webapp/backend/pkg/config"
},
{
"path": "webapp/backend/pkg/web/server_test.go",
"chars": 29586,
"preview": "package web_test\n\nimport (\n\t\"bytes\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"os\"\n\t\"path\"\n\t\"strin"
},
{
"path": "webapp/backend/pkg/web/testdata/register-devices-req-2.json",
"chars": 503,
"preview": "{\n \"data\": [\n {\n \"wwn\": \"a4c8e8ed-11a0-4c97-9bba-306440f1b944\",\n \"device_name\": \"nvme0\",\n \"manufactur"
},
{
"path": "webapp/backend/pkg/web/testdata/register-devices-req.json",
"chars": 2724,
"preview": "{\n \"data\": [\n {\n \"wwn\": \"0x5002538e40a22954\",\n \"device_name\": \"sda\",\n \"manufacturer\": \"ATA\",\n \"m"
},
{
"path": "webapp/backend/pkg/web/testdata/register-devices-single-req.json",
"chars": 405,
"preview": "{\n \"data\": [\n {\n \"wwn\": \"0x5000cca264eb01d7\",\n \"device_name\": \"sdb\",\n \"manufacturer\": \"ATA\",\n \"m"
},
{
"path": "webapp/backend/pkg/web/testdata/upload-device-metrics-req.json",
"chars": 19251,
"preview": "{\n \"json_format_version\": [\n 1,\n 0\n ],\n \"smartctl\": {\n \"version\": [\n 7,\n 0\n ],\n \"svn_revisio"
},
{
"path": "webapp/frontend/.editorconfig",
"chars": 246,
"preview": "# Editor configuration, see https://editorconfig.org\nroot = true\n\n[*]\ncharset = utf-8\nindent_style = space\nindent_size ="
},
{
"path": "webapp/frontend/.gitignore",
"chars": 649,
"preview": "# See http://help.github.com/ignore-files/ for more about ignoring files.\n\n# compiled output\n/dist\n/tmp\n/out-tsc\n# Only "
},
{
"path": "webapp/frontend/CREDITS",
"chars": 4295,
"preview": "// -----------------------------------------------------------------------------------------------------\n// @ 3rd party "
}
]
// ... and 304 more files (download for full content)
About this extraction
This page contains the full source code of the AnalogJ/scrutiny GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 504 files (4.5 MB), approximately 1.2M tokens, and a symbol index with 984 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.