Compare commits

...

20 Commits

Author SHA1 Message Date
dependabot[bot] 35c6a7c2f8 Bump @angular/common, @angular/cdk, @angular/forms, @angular/material, @angular/material-moment-adapter, @angular/platform-browser, @angular/platform-browser-dynamic, @angular/router and ngx-markdown
Bumps [@angular/common](https://github.com/angular/angular/tree/HEAD/packages/common), [@angular/cdk](https://github.com/angular/components), [@angular/forms](https://github.com/angular/angular/tree/HEAD/packages/forms), [@angular/material](https://github.com/angular/components), [@angular/material-moment-adapter](https://github.com/angular/components), [@angular/platform-browser](https://github.com/angular/angular/tree/HEAD/packages/platform-browser), [@angular/platform-browser-dynamic](https://github.com/angular/angular/tree/HEAD/packages/platform-browser-dynamic), [@angular/router](https://github.com/angular/angular/tree/HEAD/packages/router) and [ngx-markdown](https://github.com/jfcere/ngx-markdown). These dependencies needed to be updated together.

Updates `@angular/common` from 13.3.12 to 21.1.3
- [Release notes](https://github.com/angular/angular/releases)
- [Changelog](https://github.com/angular/angular/blob/main/CHANGELOG.md)
- [Commits](https://github.com/angular/angular/commits/v21.1.3/packages/common)

Updates `@angular/cdk` from 13.3.9 to 21.1.3
- [Release notes](https://github.com/angular/components/releases)
- [Changelog](https://github.com/angular/components/blob/main/CHANGELOG.md)
- [Commits](https://github.com/angular/components/compare/13.3.9...v21.1.3)

Updates `@angular/forms` from 13.3.12 to 21.1.3
- [Release notes](https://github.com/angular/angular/releases)
- [Changelog](https://github.com/angular/angular/blob/main/CHANGELOG.md)
- [Commits](https://github.com/angular/angular/commits/v21.1.3/packages/forms)

Updates `@angular/material` from 13.3.9 to 21.1.3
- [Release notes](https://github.com/angular/components/releases)
- [Changelog](https://github.com/angular/components/blob/main/CHANGELOG.md)
- [Commits](https://github.com/angular/components/compare/13.3.9...v21.1.3)

Updates `@angular/material-moment-adapter` from 13.3.9 to 21.1.3
- [Release notes](https://github.com/angular/components/releases)
- [Changelog](https://github.com/angular/components/blob/main/CHANGELOG.md)
- [Commits](https://github.com/angular/components/compare/13.3.9...v21.1.3)

Updates `@angular/platform-browser` from 13.3.12 to 21.1.3
- [Release notes](https://github.com/angular/angular/releases)
- [Changelog](https://github.com/angular/angular/blob/main/CHANGELOG.md)
- [Commits](https://github.com/angular/angular/commits/v21.1.3/packages/platform-browser)

Updates `@angular/platform-browser-dynamic` from 13.3.12 to 21.1.3
- [Release notes](https://github.com/angular/angular/releases)
- [Changelog](https://github.com/angular/angular/blob/main/CHANGELOG.md)
- [Commits](https://github.com/angular/angular/commits/v21.1.3/packages/platform-browser-dynamic)

Updates `@angular/router` from 13.3.12 to 21.1.3
- [Release notes](https://github.com/angular/angular/releases)
- [Changelog](https://github.com/angular/angular/blob/main/CHANGELOG.md)
- [Commits](https://github.com/angular/angular/commits/v21.1.3/packages/router)

Updates `ngx-markdown` from 13.1.0 to 21.1.0
- [Release notes](https://github.com/jfcere/ngx-markdown/releases)
- [Commits](https://github.com/jfcere/ngx-markdown/compare/v13.1.0...v21.1.0)

---
updated-dependencies:
- dependency-name: "@angular/common"
  dependency-version: 21.1.3
  dependency-type: direct:production
- dependency-name: "@angular/cdk"
  dependency-version: 21.1.3
  dependency-type: direct:production
- dependency-name: "@angular/forms"
  dependency-version: 21.1.3
  dependency-type: direct:production
- dependency-name: "@angular/material"
  dependency-version: 21.1.3
  dependency-type: direct:production
- dependency-name: "@angular/material-moment-adapter"
  dependency-version: 21.1.3
  dependency-type: direct:production
- dependency-name: "@angular/platform-browser"
  dependency-version: 21.1.3
  dependency-type: direct:production
- dependency-name: "@angular/platform-browser-dynamic"
  dependency-version: 21.1.3
  dependency-type: direct:production
- dependency-name: "@angular/router"
  dependency-version: 21.1.3
  dependency-type: direct:production
- dependency-name: ngx-markdown
  dependency-version: 21.1.0
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-09 05:54:26 +00:00
dependabot[bot] 971249ba3f Bump js-yaml from 3.14.1 to 3.14.2 in /webapp/frontend (#905)
Bumps [js-yaml](https://github.com/nodeca/js-yaml) from 3.14.1 to 3.14.2.
- [Changelog](https://github.com/nodeca/js-yaml/blob/master/CHANGELOG.md)
- [Commits](https://github.com/nodeca/js-yaml/compare/3.14.1...3.14.2)

---
updated-dependencies:
- dependency-name: js-yaml
  dependency-version: 3.14.2
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-08 21:52:36 -08:00
dependabot[bot] 73417ca653 Bump follow-redirects from 1.15.2 to 1.15.6 in /webapp/frontend (#604)
Bumps [follow-redirects](https://github.com/follow-redirects/follow-redirects) from 1.15.2 to 1.15.6.
- [Release notes](https://github.com/follow-redirects/follow-redirects/releases)
- [Commits](https://github.com/follow-redirects/follow-redirects/compare/v1.15.2...v1.15.6)

---
updated-dependencies:
- dependency-name: follow-redirects
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Aram Akhavan <github@aram.nubmail.ca>
2026-02-08 21:52:14 -08:00
dependabot[bot] a6d092983d Bump express from 4.18.2 to 4.19.2 in /webapp/frontend (#613)
Bumps [express](https://github.com/expressjs/express) from 4.18.2 to 4.19.2.
- [Release notes](https://github.com/expressjs/express/releases)
- [Changelog](https://github.com/expressjs/express/blob/master/History.md)
- [Commits](https://github.com/expressjs/express/compare/4.18.2...4.19.2)

---
updated-dependencies:
- dependency-name: express
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Aram Akhavan <github@aram.nubmail.ca>
2026-02-08 21:52:07 -08:00
dependabot[bot] 1988b101e1 Bump lodash from 4.17.21 to 4.17.23 in /webapp/frontend (#856)
Bumps [lodash](https://github.com/lodash/lodash) from 4.17.21 to 4.17.23.
- [Release notes](https://github.com/lodash/lodash/releases)
- [Commits](https://github.com/lodash/lodash/compare/4.17.21...4.17.23)

---
updated-dependencies:
- dependency-name: lodash
  dependency-version: 4.17.23
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Aram Akhavan <github@aram.nubmail.ca>
2026-02-08 21:37:51 -08:00
dependabot[bot] 746ae76cfc Bump node-forge from 1.3.1 to 1.3.3 in /webapp/frontend (#857)
Bumps [node-forge](https://github.com/digitalbazaar/forge) from 1.3.1 to 1.3.3.
- [Changelog](https://github.com/digitalbazaar/forge/blob/main/CHANGELOG.md)
- [Commits](https://github.com/digitalbazaar/forge/compare/v1.3.1...v1.3.3)

---
updated-dependencies:
- dependency-name: node-forge
  dependency-version: 1.3.3
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Aram Akhavan <github@aram.nubmail.ca>
2026-02-08 21:37:44 -08:00
Aram Akhavan 3380023ad0 Run CI on master (#904)
* Run CI on master

* Consolidate go linting into main CI
2026-02-08 21:21:48 -08:00
mcarbonne 6362512406 Update go to 1.25 (#875)
Closes #872

* update go to 1.25

* update deprecated gomock

* remove deprecated ioutil

* update (and fix) ci

* add golang lint (as warning)

* enable formatters + freeze golang version
2026-02-08 20:46:36 -08:00
packagrio-bot c6323fb7ce (v0.8.3) Automated packaging of release by Packagr 2026-02-09 04:41:13 +00:00
Aram Akhavan 349c7d4def Fix temperature plot to use local time (#903)
Closes #893 #889
2026-02-08 20:35:28 -08:00
Aram Akhavan 19ac712b78 Update docker builds (#901)
Closes #895 #896

* dont build every master commit
* push latest tags with releases
* update docs accordingly
* bump action versions
2026-02-08 20:11:04 -08:00
Aram Akhavan c95b272485 Fix nightlies tagging (#899) 2026-02-08 15:34:33 -08:00
Aram Akhavan 43231d7ec3 Build nightlies for all images (#897)
Closes #894
2026-02-08 13:57:45 -08:00
Hybras 3f6537e94c Fix temperature conversion in temperature.pipe.ts (#815) 2026-02-07 23:23:48 -08:00
Aram Akhavan b021797919 Fix issue triage template (#879) 2026-02-07 20:18:57 -08:00
Aram Akhavan 7ad997cc0e Update issue triage, feature request, and contributions policies (#877)
Adopt ghostty's policy for using issues to track actionable bug reports or feature requests, and discussions for triaging issues or discussing potential feature requests.
2026-02-07 20:09:11 -08:00
packagrio-bot a3000fd6b0 (v0.8.2) Automated packaging of release by Packagr 2026-02-07 05:05:07 +00:00
Jason Kulatunga af59f2639c Enable inclusion of hidden files in artifact upload 2026-02-06 23:00:44 -05:00
Jason Kulatunga b0ff0b3a48 Rename download artifact name to 'workspace' 2026-02-06 22:58:35 -05:00
Jason Kulatunga 56056b2d6a Change workspace download path in release workflow 2026-02-06 22:33:18 -05:00
53 changed files with 2878 additions and 1480 deletions
@@ -0,0 +1,154 @@
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 logs
description: |
Provide any captured scrutiny logs or panic dumps during your issue reproduction in this field.
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: 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
-42
View File
@@ -1,42 +0,0 @@
---
name: Bug report
about: Create a report to help us improve
title: "[BUG]"
labels: bug
assignees: ''
---
**Describe the bug**
A clear and concise description of what the bug is.
**Expected behavior**
A clear and concise description of what you expected to happen.
**Screenshots**
If applicable, add screenshots to help explain your problem.
**Log Files**
If related to missing devices or SMART data, please run the `collector` in DEBUG mode, and attach the log file.
See [/docs/TROUBLESHOOTING_DEVICE_COLLECTOR.md](docs/TROUBLESHOOTING_DEVICE_COLLECTOR.md) for other troubleshooting tips.
```
docker run -it --rm -p 8080:8080 \
-v `pwd`/config:/opt/scrutiny/config \
-v /run/udev:/run/udev:ro \
--cap-add SYS_RAWIO \
--device=/dev/sda \
--device=/dev/sdb \
-e DEBUG=true \
-e COLLECTOR_LOG_FILE=/opt/scrutiny/config/collector.log \
-e SCRUTINY_LOG_FILE=/opt/scrutiny/config/web.log \
--name scrutiny \
ghcr.io/analogj/scrutiny:master-omnibus
# in another terminal trigger the collector
docker exec scrutiny scrutiny-collector-metrics run
```
The log files will be available on your host in the `config` directory. Please attach them to this issue.
Please also provide the output of `docker info`
+5
View File
@@ -0,0 +1,5 @@
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.
-17
View File
@@ -1,17 +0,0 @@
---
name: Feature request
about: Suggest an idea for this project
title: "[FEAT]"
labels: ''
assignees: ''
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Additional context**
Add any other context or screenshots about the feature request here.
+9
View File
@@ -0,0 +1,9 @@
---
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.**
+25 -9
View File
@@ -1,6 +1,13 @@
name: CI
# This workflow is triggered on pushes & pull requests
on: [pull_request]
on:
push:
branches:
- master
pull_request:
permissions:
contents: read
jobs:
test-frontend:
@@ -21,7 +28,6 @@ jobs:
test-backend:
name: Test Backend
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:
@@ -38,13 +44,10 @@ jobs:
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: Add influxdb to hosts
run: echo "127.0.0.1 influxdb" | sudo tee -a /etc/hosts
- name: Checkout
uses: actions/checkout@v2
uses: actions/checkout@v6
- name: Test Backend
run: |
make binary-clean binary-test-coverage
@@ -76,6 +79,19 @@ jobs:
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 }}
@@ -102,7 +118,7 @@ jobs:
uses: actions/checkout@v2
- uses: actions/setup-go@v3
with:
go-version: '^1.20.1'
go-version: '^1.25'
- name: Build Binaries
run: |
make binary-clean binary-all
+54 -53
View File
@@ -1,7 +1,6 @@
name: Docker
on:
push:
branches: [ master, beta ]
# Publish semver tags as releases.
tags: [ 'v*.*.*' ]
@@ -18,20 +17,17 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v3
with:
fetch-depth: 0
uses: actions/checkout@v6
- name: Set up QEMU
uses: docker/setup-qemu-action@v2
uses: docker/setup-qemu-action@v3
with:
platforms: 'arm64,arm'
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
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 }}
if: github.event_name != 'pull_request'
uses: docker/login-action@v2
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
@@ -40,30 +36,30 @@ jobs:
# https://github.com/docker/metadata-action
- name: Extract Docker metadata
id: meta
uses: docker/metadata-action@v4
uses: docker/metadata-action@v5
with:
flavor: |
latest=false
latest=true
suffix=-collector,onlatest=true
tags: |
type=ref,enable=true,event=branch,suffix=-collector
type=semver,pattern=v{{major}}.{{minor}}.{{patch}},suffix=-collector
type=semver,pattern=v{{major}}.{{minor}},suffix=-collector
type=semver,pattern=v{{major}},suffix=-collector
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 (don't push on PR)
# 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@v3
uses: docker/build-push-action@v6
with:
platforms: linux/amd64,linux/arm64,linux/arm/v7
context: .
file: docker/Dockerfile.collector
push: ${{ github.event_name != 'pull_request' }}
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
# cache-from: type=gha
# cache-to: type=gha,mode=max
cache-from: type=gha
cache-to: type=gha,mode=max
web:
runs-on: ubuntu-latest
@@ -73,20 +69,19 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v2
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@v2
uses: docker/setup-qemu-action@v3
with:
platforms: 'arm64,arm'
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
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 }}
if: github.event_name != 'pull_request'
uses: docker/login-action@v2
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
@@ -95,29 +90,31 @@ jobs:
# https://github.com/docker/metadata-action
- name: Extract Docker metadata
id: meta
uses: docker/metadata-action@v4
uses: docker/metadata-action@v5
with:
flavor: |
latest=false
latest=true
suffix=-web,onlatest=true
tags: |
type=ref,enable=true,event=branch,suffix=-web
type=semver,pattern=v{{major}}.{{minor}}.{{patch}},suffix=-web
type=semver,pattern=v{{major}}.{{minor}},suffix=-web
type=semver,pattern=v{{major}},suffix=-web
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 (don't push on PR)
# 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@v3
uses: docker/build-push-action@v6
with:
platforms: linux/amd64,linux/arm64,linux/arm/v7
context: .
file: docker/Dockerfile.web
push: ${{ github.event_name != 'pull_request' }}
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
cache-from: type=gha
cache-to: type=gha,mode=max
omnibus:
runs-on: ubuntu-latest
permissions:
@@ -126,20 +123,19 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v2
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@v2
uses: docker/setup-qemu-action@v3
with:
platforms: 'arm64,arm'
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
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 }}
if: github.event_name != 'pull_request'
uses: docker/login-action@v2
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
@@ -148,24 +144,29 @@ jobs:
# https://github.com/docker/metadata-action
- name: Extract Docker metadata
id: meta
uses: docker/metadata-action@v4
uses: docker/metadata-action@v5
with:
flavor: |
latest=true
# tag latest and latest-omnibus
suffix=-omnibus,onlatest=false
tags: |
type=ref,enable=true,event=branch,suffix=-omnibus
type=semver,pattern=v{{major}}.{{minor}}.{{patch}},suffix=-omnibus
type=semver,pattern=v{{major}}.{{minor}},suffix=-omnibus
type=semver,pattern=v{{major}},suffix=-omnibus
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 (don't push on PR)
# 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@v3
uses: docker/build-push-action@v6
with:
platforms: linux/amd64,linux/arm64
platforms: linux/amd64,linux/arm64,linux/arm/v7
context: .
file: docker/Dockerfile
push: ${{ github.event_name != 'pull_request' }}
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
cache-from: type=gha
cache-to: type=gha,mode=max
+63 -19
View File
@@ -1,5 +1,7 @@
name: Docker - Nightly
on:
workflow_dispatch:
# Note: this only runs on the default branch
schedule:
- cron: '36 12 * * *'
@@ -8,7 +10,7 @@ env:
IMAGE_NAME: ${{ github.repository }}
jobs:
omnibus:
build_nightlies:
runs-on: ubuntu-latest
permissions:
contents: read
@@ -16,44 +18,86 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v2
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@v2
uses: docker/setup-qemu-action@v3
with:
platforms: 'arm64,arm'
platforms: 'arm64'
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
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 }}
if: github.event_name != 'pull_request'
uses: docker/login-action@v2
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@v4
- name: Extract Docker metadata for omnibus
id: meta_omnibus
uses: docker/metadata-action@v5
with:
tags: |
type=ref,enable=true,event=branch,suffix=-omnibus-nightly
type=ref,enable=true,event=tag,suffix=-omnibus-nightly
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 Docker image
uses: docker/build-push-action@v3
- name: Build and push omnibus Docker image
uses: docker/build-push-action@v6
with:
platforms: linux/amd64,linux/arm64
context: .
file: docker/Dockerfile
push: false
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
# cache-from: type=gha
# cache-to: type=gha,mode=max
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
+2 -1
View File
@@ -64,6 +64,7 @@ jobs:
uses: actions/upload-artifact@v4
with:
name: workspace
include-hidden-files: true
path: ${{ github.workspace }}/**/*
retention-days: 1
@@ -96,7 +97,7 @@ jobs:
name: workspace
- uses: actions/setup-go@v6
with:
go-version: '1.20.1' # The Go version to download (if necessary) and use.
go-version: '1.25' # The Go version to download (if necessary) and use.
- name: Build Binaries
run: |
make binary-clean binary-all
+11
View File
@@ -0,0 +1,11 @@
version: "2"
formatters:
enable:
- gofmt
- goimports
linters:
enable:
- bodyclose
settings:
errcheck:
check-blank: true
+141 -3
View File
@@ -1,6 +1,144 @@
# Contributing
# Contributing to scrutiny
**Please see our [AI policy](./AI_POLICY.md).**
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)
@@ -11,7 +149,7 @@ Depending on the functionality you are adding, you may need to setup a developme
# Modifying the Scrutiny Backend Server (API)
1. install the [Go runtime](https://go.dev/doc/install) (v1.20+)
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
+6
View File
@@ -1,5 +1,6 @@
.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
@@ -66,6 +67,11 @@ binary-dep:
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) ./...
+12 -7
View File
@@ -67,6 +67,11 @@ See [docs/TROUBLESHOOTING_DEVICE_COLLECTOR.md](./docs/TROUBLESHOOTING_DEVICE_COL
## 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.
@@ -80,23 +85,23 @@ docker run -p 8080:8080 -p 8086:8086 --restart unless-stopped \
--device=/dev/sda \
--device=/dev/sdb \
--name scrutiny \
ghcr.io/analogj/scrutiny:master-omnibus
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:master-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)
- `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:master-collector` - Contains the Scrutiny data collector, `smartctl` binary and cron-like
- `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:master-web` - Contains the Web UI and API. Only one container necessary
- `ghcr.io/analogj/scrutiny:latest-web` - Contains the Web UI and API. Only one container necessary
- `influxdb:2.2` - InfluxDB image, used by the Web container to persist SMART data. Only one container necessary
See [docs/TROUBLESHOOTING_INFLUXDB.md](./docs/TROUBLESHOOTING_INFLUXDB.md)
@@ -111,7 +116,7 @@ docker run -p 8086:8086 --restart unless-stopped \
docker run -p 8080:8080 --restart unless-stopped \
-v `pwd`/scrutiny:/opt/scrutiny/config \
--name scrutiny-web \
ghcr.io/analogj/scrutiny:master-web
ghcr.io/analogj/scrutiny:latest-web
docker run --restart unless-stopped \
-v /run/udev:/run/udev:ro \
@@ -120,7 +125,7 @@ docker run --restart unless-stopped \
--device=/dev/sdb \
-e COLLECTOR_API_ENDPOINT=http://SCRUTINY_WEB_IPADDRESS:8080 \
--name scrutiny-collector \
ghcr.io/analogj/scrutiny:master-collector
ghcr.io/analogj/scrutiny:latest-collector
```
## Manual Installation (without-Docker)
@@ -157,7 +162,7 @@ Neither file is required, however if provided, it allows you to configure how Sc
## 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:master-collector` or `ghcr.io/analogj/scrutiny:master-omnibus` docker images,
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 * * *" ...`
@@ -3,17 +3,18 @@ package main
import (
"encoding/json"
"fmt"
"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"
"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"
@@ -37,8 +38,8 @@ func main() {
}
//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
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)
@@ -81,7 +82,7 @@ OPTIONS:
subtitle := collectorMetrics + utils.LeftPad2Len(versionInfo, " ", 65-len(collectorMetrics))
color.New(color.FgGreen).Fprintf(c.App.Writer, fmt.Sprintf(utils.StripIndent(
color.New(color.FgGreen).Fprintf(c.App.Writer, utils.StripIndent(
`
___ ___ ____ __ __ ____ ____ _ _ _ _
/ __) / __)( _ \( )( )(_ _)(_ _)( \( )( \/ )
@@ -89,7 +90,7 @@ OPTIONS:
(___/ \___)(_)\_)(______) (__) (____)(_)\_) (__)
%s
`), subtitle))
`), subtitle)
return nil
},
@@ -2,14 +2,15 @@ package main
import (
"fmt"
"github.com/analogj/scrutiny/collector/pkg/collector"
"github.com/analogj/scrutiny/webapp/backend/pkg/version"
"github.com/sirupsen/logrus"
"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"
@@ -57,7 +58,7 @@ OPTIONS:
subtitle := collectorSelfTest + utils.LeftPad2Len(versionInfo, " ", 65-len(collectorSelfTest))
color.New(color.FgGreen).Fprintf(c.App.Writer, fmt.Sprintf(utils.StripIndent(
color.New(color.FgGreen).Fprintf(c.App.Writer, utils.StripIndent(
`
___ ___ ____ __ __ ____ ____ _ _ _ _
/ __) / __)( _ \( )( )(_ _)(_ _)( \( )( \/ )
@@ -65,7 +66,7 @@ OPTIONS:
(___/ \___)(_)\_)(______) (__) (____)(_)\_) (__)
%s
`), subtitle))
`), subtitle)
return nil
},
+2 -12
View File
@@ -3,9 +3,10 @@ package collector
import (
"bytes"
"encoding/json"
"github.com/sirupsen/logrus"
"net/http"
"time"
"github.com/sirupsen/logrus"
)
var httpClient = &http.Client{Timeout: 60 * time.Second}
@@ -14,17 +15,6 @@ type BaseCollector struct {
logger *logrus.Entry
}
func (c *BaseCollector) getJson(url string, target interface{}) error {
r, err := httpClient.Get(url)
if err != nil {
return err
}
defer r.Body.Close()
return json.NewDecoder(r.Body).Decode(target)
}
func (c *BaseCollector) postJson(url string, body interface{}, target interface{}) error {
requestBody, err := json.Marshal(body)
if err != nil {
+3 -2
View File
@@ -3,11 +3,12 @@ package shell
import (
"bytes"
"errors"
"github.com/sirupsen/logrus"
"io"
"os/exec"
"path"
"strings"
"github.com/sirupsen/logrus"
)
type localShell struct{}
@@ -36,7 +37,7 @@ func (s *localShell) Command(logger *logrus.Entry, cmdName string, cmdArgs []str
if workingDir != "" && path.IsAbs(workingDir) {
cmd.Dir = workingDir
} else if workingDir != "" {
return "", errors.New("Working Directory must be an absolute path")
return "", errors.New("working directory must be an absolute path")
}
err := cmd.Run()
@@ -7,8 +7,8 @@ package mock_shell
import (
reflect "reflect"
gomock "github.com/golang/mock/gomock"
logrus "github.com/sirupsen/logrus"
gomock "go.uber.org/mock/gomock"
)
// MockInterface is a mock of Interface interface.
+10 -9
View File
@@ -2,15 +2,16 @@ package config
import (
"fmt"
"github.com/analogj/go-util/utils"
"github.com/analogj/scrutiny/collector/pkg/errors"
"github.com/analogj/scrutiny/collector/pkg/models"
"github.com/mitchellh/mapstructure"
"github.com/spf13/viper"
"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:
@@ -20,7 +21,7 @@ import (
type configuration struct {
*viper.Viper
deviceOverrides []models.ScanOverride
deviceOverrides []models.ScanOverride
}
//Viper uses the following precedence order. Each item takes precedence over the item below it:
@@ -53,7 +54,7 @@ func (c *configuration) Init() error {
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{})
@@ -167,7 +168,7 @@ func (c *configuration) GetCommandMetricsInfoArgs(deviceName string) string {
overrides := c.GetDeviceOverrides()
for _, deviceOverrides := range overrides {
if strings.ToLower(deviceName) == strings.ToLower(deviceOverrides.Device) {
if strings.EqualFold(deviceName, deviceOverrides.Device) {
//found matching device
if len(deviceOverrides.Commands.MetricsInfoArgs) > 0 {
return deviceOverrides.Commands.MetricsInfoArgs
@@ -183,7 +184,7 @@ func (c *configuration) GetCommandMetricsSmartArgs(deviceName string) string {
overrides := c.GetDeviceOverrides()
for _, deviceOverrides := range overrides {
if strings.ToLower(deviceName) == strings.ToLower(deviceOverrides.Device) {
if strings.EqualFold(deviceName, deviceOverrides.Device) {
//found matching device
if len(deviceOverrides.Commands.MetricsSmartArgs) > 0 {
return deviceOverrides.Commands.MetricsSmartArgs
+1 -1
View File
@@ -8,8 +8,8 @@ import (
reflect "reflect"
models "github.com/analogj/scrutiny/collector/pkg/models"
gomock "github.com/golang/mock/gomock"
viper "github.com/spf13/viper"
gomock "go.uber.org/mock/gomock"
)
// MockInterface is a mock of Interface interface.
+2 -2
View File
@@ -107,8 +107,8 @@ func (d *Detect) SmartCtlInfo(device *models.Device) error {
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(errMsg)
return fmt.Errorf(errMsg)
d.Logger.Errorf("%v", errMsg)
return fmt.Errorf("%v", errMsg)
}
return nil
+1 -12
View File
@@ -9,16 +9,15 @@ import (
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/golang/mock/gomock"
"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)
defer mockCtrl.Finish()
fakeConfig := mock_config.NewMockInterface(mockCtrl)
fakeConfig.EXPECT().GetString("host.id").AnyTimes().Return("")
fakeConfig.EXPECT().GetDeviceOverrides().AnyTimes().Return([]models.ScanOverride{})
@@ -48,7 +47,6 @@ func TestDetect_SmartctlScan(t *testing.T) {
func TestDetect_SmartctlScan_Megaraid(t *testing.T) {
// setup
mockCtrl := gomock.NewController(t)
defer mockCtrl.Finish()
fakeConfig := mock_config.NewMockInterface(mockCtrl)
fakeConfig.EXPECT().GetString("host.id").AnyTimes().Return("")
fakeConfig.EXPECT().GetDeviceOverrides().AnyTimes().Return([]models.ScanOverride{})
@@ -81,7 +79,6 @@ func TestDetect_SmartctlScan_Megaraid(t *testing.T) {
func TestDetect_SmartctlScan_Nvme(t *testing.T) {
// setup
mockCtrl := gomock.NewController(t)
defer mockCtrl.Finish()
fakeConfig := mock_config.NewMockInterface(mockCtrl)
fakeConfig.EXPECT().GetString("host.id").AnyTimes().Return("")
fakeConfig.EXPECT().GetDeviceOverrides().AnyTimes().Return([]models.ScanOverride{})
@@ -113,7 +110,6 @@ func TestDetect_SmartctlScan_Nvme(t *testing.T) {
func TestDetect_TransformDetectedDevices_Empty(t *testing.T) {
// setup
mockCtrl := gomock.NewController(t)
defer mockCtrl.Finish()
fakeConfig := mock_config.NewMockInterface(mockCtrl)
fakeConfig.EXPECT().GetString("host.id").AnyTimes().Return("")
fakeConfig.EXPECT().GetDeviceOverrides().AnyTimes().Return([]models.ScanOverride{})
@@ -147,7 +143,6 @@ func TestDetect_TransformDetectedDevices_Empty(t *testing.T) {
func TestDetect_TransformDetectedDevices_Ignore(t *testing.T) {
// setup
mockCtrl := gomock.NewController(t)
defer mockCtrl.Finish()
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}})
@@ -180,7 +175,6 @@ func TestDetect_TransformDetectedDevices_Ignore(t *testing.T) {
func TestDetect_TransformDetectedDevices_Raid(t *testing.T) {
// setup
mockCtrl := gomock.NewController(t)
defer mockCtrl.Finish()
fakeConfig := mock_config.NewMockInterface(mockCtrl)
fakeConfig.EXPECT().GetString("host.id").AnyTimes().Return("")
fakeConfig.EXPECT().GetString("commands.metrics_smartctl_bin").AnyTimes().Return("smartctl")
@@ -223,7 +217,6 @@ func TestDetect_TransformDetectedDevices_Raid(t *testing.T) {
func TestDetect_TransformDetectedDevices_Simple(t *testing.T) {
// setup
mockCtrl := gomock.NewController(t)
defer mockCtrl.Finish()
fakeConfig := mock_config.NewMockInterface(mockCtrl)
fakeConfig.EXPECT().GetString("host.id").AnyTimes().Return("")
fakeConfig.EXPECT().GetString("commands.metrics_smartctl_bin").AnyTimes().Return("smartctl")
@@ -257,7 +250,6 @@ func TestDetect_TransformDetectedDevices_Simple(t *testing.T) {
func TestDetect_TransformDetectedDevices_WithoutDeviceTypeOverride(t *testing.T) {
// setup
mockCtrl := gomock.NewController(t)
defer mockCtrl.Finish()
fakeConfig := mock_config.NewMockInterface(mockCtrl)
fakeConfig.EXPECT().GetString("host.id").AnyTimes().Return("")
fakeConfig.EXPECT().GetString("commands.metrics_smartctl_bin").AnyTimes().Return("smartctl")
@@ -290,7 +282,6 @@ func TestDetect_TransformDetectedDevices_WithoutDeviceTypeOverride(t *testing.T)
func TestDetect_TransformDetectedDevices_WhenDeviceNotDetected(t *testing.T) {
// setup
mockCtrl := gomock.NewController(t)
defer mockCtrl.Finish()
fakeConfig := mock_config.NewMockInterface(mockCtrl)
fakeConfig.EXPECT().GetString("host.id").AnyTimes().Return("")
fakeConfig.EXPECT().GetString("commands.metrics_smartctl_bin").AnyTimes().Return("smartctl")
@@ -312,7 +303,6 @@ func TestDetect_TransformDetectedDevices_WhenDeviceNotDetected(t *testing.T) {
func TestDetect_TransformDetectedDevices_AllowListFilters(t *testing.T) {
mockCtrl := gomock.NewController(t)
defer mockCtrl.Finish()
fakeConfig := mock_config.NewMockInterface(mockCtrl)
fakeConfig.EXPECT().GetString("host.id").AnyTimes().Return("")
@@ -353,7 +343,6 @@ func TestDetect_TransformDetectedDevices_AllowListFilters(t *testing.T) {
func TestDetect_SmartCtlInfo(t *testing.T) {
t.Run("should report nvme info", func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
const (
someArgs = "--info --json"
+9 -10
View File
@@ -2,12 +2,13 @@ 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"
"io/ioutil"
"path/filepath"
"strings"
)
func DevicePrefix() string {
@@ -23,15 +24,15 @@ func (d *Detect) Start() ([]models.Device, error) {
}
//inflate device info for detected devices.
for ndx, _ := range detectedDevices {
d.SmartCtlInfo(&detectedDevices[ndx]) //ignore errors.
for ndx := range detectedDevices {
d.SmartCtlInfo(&detectedDevices[ndx]) //ignore errors.
populateUdevInfo(&detectedDevices[ndx]) //ignore errors.
}
return detectedDevices, nil
}
//WWN values NVMe and SCSI
// WWN values NVMe and SCSI
func (d *Detect) wwnFallback(detectedDevice *models.Device) {
block, err := ghw.Block()
if err == nil {
@@ -61,7 +62,7 @@ func (d *Detect) wwnFallback(detectedDevice *models.Device) {
func populateUdevInfo(detectedDevice *models.Device) error {
// Get device major:minor numbers
// `cat /sys/class/block/sda/dev`
devNo, err := ioutil.ReadFile(filepath.Join("/sys/class/block/", detectedDevice.DeviceName, "dev"))
devNo, err := os.ReadFile(filepath.Join("/sys/class/block/", detectedDevice.DeviceName, "dev"))
if err != nil {
return err
}
@@ -69,7 +70,7 @@ func populateUdevInfo(detectedDevice *models.Device) error {
// Look up block device in udev runtime database
// `cat /run/udev/data/b8:0`
udevID := "b" + strings.TrimSpace(string(devNo))
udevBytes, err := ioutil.ReadFile(filepath.Join("/run/udev/data/", udevID))
udevBytes, err := os.ReadFile(filepath.Join("/run/udev/data/", udevID))
if err != nil {
return err
}
@@ -97,7 +98,5 @@ func populateUdevInfo(detectedDevice *models.Device) error {
detectedDevice.DeviceSerialID = fmt.Sprintf("%s-%s", udevInfo["ID_BUS"], deviceSerialID)
}
return nil
}
+3 -4
View File
@@ -1,10 +1,10 @@
package detect_test
import (
"fmt"
"testing"
"github.com/analogj/scrutiny/collector/pkg/detect"
"github.com/stretchr/testify/require"
"testing"
)
func TestWwn_FromStringTable(t *testing.T) {
@@ -25,8 +25,7 @@ func TestWwn_FromStringTable(t *testing.T) {
}
//test
for _, tt := range tests {
testname := fmt.Sprintf("%s", tt.wwnStr)
t.Run(testname, func(t *testing.T) {
t.Run(tt.wwnStr, func(t *testing.T) {
str := tt.wwn.ToString()
require.Equal(t, tt.wwnStr, str)
})
+1 -1
View File
@@ -12,7 +12,7 @@ RUN make binary-frontend
######## Build the backend
FROM golang:1.20-bookworm as backendbuild
FROM golang:1.25-bookworm as backendbuild
WORKDIR /go/src/github.com/analogj/scrutiny
COPY --link . /go/src/github.com/analogj/scrutiny
+1 -1
View File
@@ -4,7 +4,7 @@
########
FROM golang:1.20-bookworm as backendbuild
FROM golang:1.25-bookworm as backendbuild
WORKDIR /go/src/github.com/analogj/scrutiny
+1 -1
View File
@@ -11,7 +11,7 @@ COPY --link . /go/src/github.com/analogj/scrutiny
RUN make binary-frontend
######## Build the backend
FROM golang:1.20-bookworm as backendbuild
FROM golang:1.25-bookworm as backendbuild
WORKDIR /go/src/github.com/analogj/scrutiny
COPY --link . /go/src/github.com/analogj/scrutiny
+4 -2
View File
@@ -80,7 +80,8 @@ services:
scrutiny:
restart: unless-stopped
container_name: scrutiny
image: ghcr.io/analogj/scrutiny:master-web
# best practice: pin to a specific release instead of latest
image: ghcr.io/analogj/scrutiny:latest-web
ports:
- 8080:8080
volumes:
@@ -169,7 +170,8 @@ services:
collector:
restart: unless-stopped
image: 'ghcr.io/analogj/scrutiny:master-collector'
# best practice: pin to a specific release instead of latest
image: 'ghcr.io/analogj/scrutiny:latest-collector'
cap_add:
- SYS_RAWIO
volumes:
+11 -15
View File
@@ -1,22 +1,18 @@
# Docker Images `master-omnibus` vs `latest`
# Docker Images `latest` vs `nightly`
> TL;DR; The `master-omnibus` and `latest` tags are almost semantically identical, as I follow a `golden master`
development process. However if you want to ensure you're only using the latest release, you can change to `latest`
> TL;DR; The `latest-omnibus`, `latest-collector`, and `latest-web` tags point to the most recent release. (`latest` points to `latest-omnibus`)
> The `nightly-omnibus`, `nightly-collector`, and `nightly-web` tags point to builds that are generated every night from the latest commit on the `master` branch.
The CI script used to orchestrate the docker image builds can be found here: https://github.com/AnalogJ/scrutiny/blob/master/.github/workflows/docker-build.yaml#L166-L184
The CD scripts used to orchestrate the docker image builds can be found here:
* https://github.com/AnalogJ/scrutiny/blob/master/.github/workflows/docker-build.yaml
* https://github.com/AnalogJ/scrutiny/blob/master/.github/workflows/docker-nightly.yaml
In general Scrutiny follows a `golden master` development process, which means that the `master` branch is not directly updated (unless its for documentation changes),
instead development is done in a feature branch, or committed to the `beta` branch.
In general scrutiny follows a feature branch development process, which means that the `master` branch should ideally always be free of bugs
This is driven by the requirement that every PR be reviewed and pass all tests. Unfortunately, bugs do make it through, especially because of the
enormous number of hard drives that scrutiny must support..
As development progresses, and we're satisfied that a feature is complete, and the quality is acceptable,
I merge the changes to `master` and trigger the creation of a new release -- ie, when master is updated, a new release
is almost immediately created (and tagged with `latest`)
So changing from `master-omnibus -> latest` will be the same thing for all intents and purposes.
> NOTE: Previously, there was a `automated cron build` that ran on the `master` and `beta` branches.
They used to trigger a `nightly` build, even if nothing has changed on the branch. This has a couple of benefits, but one is to
ensure that there's no broken external dependencies in our (unchanged) code. This `nightly` build no longer updates the `master-omnibus` tag.
This means that while the nightly builds should have the latest features and bug fixes, there may be things that sneak through. Unless you need a particular
feature or bug fix, we recommend sticking to releases. Also note that using `latest` tags is generally considered a bad practice; pin a specific version instead.
# Running Docker `rootless`
+73 -61
View File
@@ -1,81 +1,93 @@
module github.com/analogj/scrutiny
go 1.20
go 1.25
require (
github.com/analogj/go-util v0.0.0-20190301173314-5295e364eb14
github.com/analogj/go-util v0.0.0-20210417161720-39b497cca03b
github.com/containrrr/shoutrrr v0.8.0
github.com/fatih/color v1.15.0
github.com/gin-gonic/gin v1.6.3
github.com/glebarez/sqlite v1.4.5
github.com/go-gormigrate/gormigrate/v2 v2.0.0
github.com/golang/mock v1.6.0
github.com/influxdata/influxdb-client-go/v2 v2.9.0
github.com/jaypipes/ghw v0.6.1
github.com/mitchellh/mapstructure v1.5.0
github.com/samber/lo v1.25.0
github.com/sirupsen/logrus v1.6.0
github.com/spf13/viper v1.15.0
github.com/stretchr/testify v1.8.1
github.com/urfave/cli/v2 v2.2.0
golang.org/x/sync v0.1.0
gorm.io/gorm v1.23.5
github.com/fatih/color v1.18.0
github.com/gin-gonic/gin v1.11.0
github.com/glebarez/sqlite v1.11.0
github.com/go-gormigrate/gormigrate/v2 v2.1.5
github.com/go-viper/mapstructure/v2 v2.5.0
github.com/influxdata/influxdb-client-go/v2 v2.14.0
github.com/jaypipes/ghw v0.21.2
github.com/samber/lo v1.52.0
github.com/sirupsen/logrus v1.9.4
github.com/spf13/viper v1.21.0
github.com/stretchr/testify v1.11.1
github.com/urfave/cli/v2 v2.27.7
go.uber.org/mock v0.6.0
golang.org/x/sync v0.19.0
gorm.io/gorm v1.31.1
)
require (
github.com/StackExchange/wmi v0.0.0-20190523213315-cbe66965904d // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect
github.com/apapsch/go-jsonmerge/v2 v2.0.0 // indirect
github.com/bytedance/gopkg v0.1.3 // indirect
github.com/bytedance/sonic v1.15.0 // indirect
github.com/bytedance/sonic/loader v0.5.0 // indirect
github.com/cloudwego/base64x v0.1.6 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/deepmap/oapi-codegen v1.8.2 // indirect
github.com/fsnotify/fsnotify v1.6.0 // indirect
github.com/ghodss/yaml v1.0.0 // indirect
github.com/gin-contrib/sse v0.1.0 // indirect
github.com/glebarez/go-sqlite v1.17.2 // indirect
github.com/go-ole/go-ole v1.2.4 // indirect
github.com/go-playground/locales v0.13.0 // indirect
github.com/go-playground/universal-translator v0.17.0 // indirect
github.com/go-playground/validator/v10 v10.2.0 // indirect
github.com/golang/protobuf v1.5.3 // indirect
github.com/google/uuid v1.3.0 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect
github.com/influxdata/line-protocol v0.0.0-20200327222509-2487e7298839 // indirect
github.com/jaypipes/pcidb v0.5.0 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/fsnotify/fsnotify v1.9.0 // indirect
github.com/gabriel-vasile/mimetype v1.4.13 // indirect
github.com/gin-contrib/sse v1.1.0 // indirect
github.com/glebarez/go-sqlite v1.22.0 // indirect
github.com/go-ole/go-ole v1.3.0 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.30.1 // indirect
github.com/goccy/go-json v0.10.5 // indirect
github.com/goccy/go-yaml v1.19.2 // indirect
github.com/golang/protobuf v1.5.4 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/influxdata/line-protocol v0.0.0-20210922203350-b1ad95c89adf // indirect
github.com/jaypipes/pcidb v1.1.1 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.4 // indirect
github.com/jinzhu/now v1.1.5 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/konsorten/go-windows-terminal-sequences v1.0.3 // indirect
github.com/kvz/logstreamer v0.0.0-20201023134116-02d20f4338f5 // indirect
github.com/leodido/go-urn v1.2.0 // indirect
github.com/magiconair/properties v1.8.7 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.18 // indirect
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
github.com/kvz/logstreamer v0.0.0-20221024075423-bf5cfbd32e39 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mitchellh/go-homedir v1.1.0 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/pelletier/go-toml/v2 v2.0.6 // indirect
github.com/ncruces/go-strftime v1.0.0 // indirect
github.com/oapi-codegen/runtime v1.1.2 // indirect
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0 // indirect
github.com/quic-go/qpack v0.6.0 // indirect
github.com/quic-go/quic-go v0.59.0 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/spf13/afero v1.9.3 // indirect
github.com/spf13/cast v1.5.0 // indirect
github.com/spf13/jwalterweatherman v1.1.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/subosito/gotenv v1.4.2 // indirect
github.com/ugorji/go/codec v1.1.7 // indirect
golang.org/x/crypto v0.1.0 // indirect
golang.org/x/exp v0.0.0-20220303212507-bbda1eaf7a17 // indirect
golang.org/x/net v0.8.0 // indirect
golang.org/x/sys v0.7.0 // indirect
golang.org/x/term v0.6.0 // indirect
golang.org/x/text v0.8.0 // indirect
google.golang.org/protobuf v1.28.1 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
github.com/sagikazarmark/locafero v0.12.0 // indirect
github.com/spf13/afero v1.15.0 // indirect
github.com/spf13/cast v1.10.0 // indirect
github.com/spf13/pflag v1.0.10 // indirect
github.com/subosito/gotenv v1.6.0 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.3.1 // indirect
github.com/xrash/smetrics v0.0.0-20250705151800-55b8f293f342 // indirect
github.com/yusufpapurcu/wmi v1.2.4 // indirect
go.yaml.in/yaml/v3 v3.0.4 // indirect
golang.org/x/arch v0.23.0 // indirect
golang.org/x/crypto v0.47.0 // indirect
golang.org/x/exp v0.0.0-20260112195511-716be5621a96 // indirect
golang.org/x/net v0.49.0 // indirect
golang.org/x/sys v0.40.0 // indirect
golang.org/x/term v0.39.0 // indirect
golang.org/x/text v0.33.0 // indirect
google.golang.org/protobuf v1.36.11 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
howett.net/plist v0.0.0-20181124034731-591f970eefbb // indirect
modernc.org/libc v1.16.8 // indirect
modernc.org/mathutil v1.4.1 // indirect
modernc.org/memory v1.1.1 // indirect
modernc.org/sqlite v1.17.2 // indirect
howett.net/plist v1.0.2-0.20250314012144-ee69052608d9 // indirect
modernc.org/libc v1.67.7 // indirect
modernc.org/mathutil v1.7.1 // indirect
modernc.org/memory v1.11.0 // indirect
modernc.org/sqlite v1.44.3 // indirect
)
+206 -772
View File
File diff suppressed because it is too large Load Diff
+9 -8
View File
@@ -3,15 +3,16 @@ package main
import (
"encoding/json"
"fmt"
"io"
"log"
"os"
"time"
"github.com/analogj/scrutiny/webapp/backend/pkg/config"
"github.com/analogj/scrutiny/webapp/backend/pkg/errors"
"github.com/analogj/scrutiny/webapp/backend/pkg/version"
"github.com/analogj/scrutiny/webapp/backend/pkg/web"
"github.com/sirupsen/logrus"
"io"
"log"
"os"
"time"
utils "github.com/analogj/go-util/utils"
"github.com/fatih/color"
@@ -36,8 +37,8 @@ func main() {
}
//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
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 {
log.Print(color.HiRedString("CONFIG ERROR: %v", err))
@@ -81,7 +82,7 @@ OPTIONS:
subtitle := scrutiny + utils.LeftPad2Len(versionInfo, " ", 65-len(scrutiny))
color.New(color.FgGreen).Fprintf(c.App.Writer, fmt.Sprintf(utils.StripIndent(
color.New(color.FgGreen).Fprintf(c.App.Writer, utils.StripIndent(
`
___ ___ ____ __ __ ____ ____ _ _ _ _
/ __) / __)( _ \( )( )(_ _)(_ _)( \( )( \/ )
@@ -89,7 +90,7 @@ OPTIONS:
(___/ \___)(_)\_)(______) (__) (____)(_)\_) (__)
%s
`), subtitle))
`), subtitle)
return nil
},
@@ -8,8 +8,8 @@ import (
reflect "reflect"
config "github.com/analogj/scrutiny/webapp/backend/pkg/config"
gomock "github.com/golang/mock/gomock"
viper "github.com/spf13/viper"
gomock "go.uber.org/mock/gomock"
)
// MockInterface is a mock of Interface interface.
@@ -12,7 +12,7 @@ import (
models "github.com/analogj/scrutiny/webapp/backend/pkg/models"
collector "github.com/analogj/scrutiny/webapp/backend/pkg/models/collector"
measurements "github.com/analogj/scrutiny/webapp/backend/pkg/models/measurements"
gomock "github.com/golang/mock/gomock"
gomock "go.uber.org/mock/gomock"
)
// MockDeviceRepo is a mock of DeviceRepo interface.
@@ -5,6 +5,11 @@ import (
"crypto/tls"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"time"
"github.com/analogj/scrutiny/webapp/backend/pkg/config"
"github.com/analogj/scrutiny/webapp/backend/pkg/models"
"github.com/glebarez/sqlite"
@@ -13,10 +18,6 @@ import (
"github.com/influxdata/influxdb-client-go/v2/domain"
"github.com/sirupsen/logrus"
"gorm.io/gorm"
"io/ioutil"
"net/http"
"net/url"
"time"
)
const (
@@ -82,7 +83,7 @@ func NewScrutinyRepository(appConfig config.Interface, globalLogger logrus.Field
DisableForeignKeyConstraintWhenMigrating: true,
})
if err != nil {
return nil, fmt.Errorf("Failed to connect to database! - %v", err)
return nil, fmt.Errorf("failed to connect to database! - %v", err)
}
globalLogger.Infof("Successfully connected to scrutiny sqlite db: %s\n", appConfig.GetString("web.database.location"))
@@ -146,7 +147,7 @@ func NewScrutinyRepository(appConfig config.Interface, globalLogger logrus.Field
taskAPI := client.TasksAPI()
if writeAPI == nil || queryAPI == nil || taskAPI == nil {
return nil, fmt.Errorf("Failed to connect to influxdb!")
return nil, fmt.Errorf("failed to connect to influxdb")
}
deviceRepo := scrutinyRepository{
@@ -238,13 +239,13 @@ func InfluxSetupComplete(influxEndpoint string, tlsConfig *tls.Config) (bool, er
return false, err
}
client := &http.Client{Transport: &http.Transport{TLSClientConfig: tlsConfig}}
client := &http.Client{Transport: &http.Transport{TLSClientConfig: tlsConfig}}
res, err := client.Get(influxUri.String())
if err != nil {
return false, err
}
body, err := ioutil.ReadAll(res.Body)
body, err := io.ReadAll(res.Body)
if err != nil {
return false, err
}
@@ -3,11 +3,12 @@ package database
import (
"context"
"fmt"
"time"
"github.com/analogj/scrutiny/webapp/backend/pkg"
"github.com/analogj/scrutiny/webapp/backend/pkg/models"
"github.com/analogj/scrutiny/webapp/backend/pkg/models/collector"
"gorm.io/gorm/clause"
"time"
)
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
@@ -31,7 +32,7 @@ func (sr *scrutinyRepository) GetDevices(ctx context.Context) ([]models.Device,
//Get a list of all the active devices.
devices := []models.Device{}
if err := sr.gormClient.WithContext(ctx).Find(&devices).Error; err != nil {
return nil, fmt.Errorf("Could not get device summary from DB: %v", err)
return nil, fmt.Errorf("could not get device summary from DB: %v", err)
}
return devices, nil
}
@@ -40,7 +41,7 @@ func (sr *scrutinyRepository) GetDevices(ctx context.Context) ([]models.Device,
func (sr *scrutinyRepository) UpdateDevice(ctx context.Context, wwn string, collectorSmartData collector.SmartInfo) (models.Device, error) {
var device models.Device
if err := sr.gormClient.WithContext(ctx).Where("wwn = ?", wwn).First(&device).Error; err != nil {
return device, fmt.Errorf("Could not get device from DB: %v", err)
return device, fmt.Errorf("could not get device from DB: %v", err)
}
//TODO catch GormClient err
@@ -55,7 +56,7 @@ func (sr *scrutinyRepository) UpdateDevice(ctx context.Context, wwn string, coll
func (sr *scrutinyRepository) UpdateDeviceStatus(ctx context.Context, wwn string, status pkg.DeviceStatus) (models.Device, error) {
var device models.Device
if err := sr.gormClient.WithContext(ctx).Where("wwn = ?", wwn).First(&device).Error; err != nil {
return device, fmt.Errorf("Could not get device from DB: %v", err)
return device, fmt.Errorf("could not get device from DB: %v", err)
}
device.DeviceStatus = pkg.DeviceStatusSet(device.DeviceStatus, status)
@@ -78,7 +79,7 @@ func (sr *scrutinyRepository) GetDeviceDetails(ctx context.Context, wwn string)
func (sr *scrutinyRepository) UpdateDeviceArchived(ctx context.Context, wwn string, archived bool) error {
var device models.Device
if err := sr.gormClient.WithContext(ctx).Where("wwn = ?", wwn).First(&device).Error; err != nil {
return fmt.Errorf("Could not get device from DB: %v", err)
return fmt.Errorf("could not get device from DB: %v", err)
}
return sr.gormClient.Model(&device).Where("wwn = ?", wwn).Update("archived", archived).Error
@@ -647,7 +647,7 @@ func m20201107210306_FromPreInfluxDBSmartResultsCreatePostInfluxDBSmartResults(d
}
postDeviceSmartData.ProcessScsiSmartInfo(postScsiGrownDefectList, postScsiErrorCounterLog)
} else {
return fmt.Errorf("Unknown device protocol: %s", preDevice.DeviceProtocol), postDeviceSmartData
return fmt.Errorf("unknown device protocol: %s", preDevice.DeviceProtocol), postDeviceSmartData
}
return nil, postDeviceSmartData
@@ -3,17 +3,18 @@ package database
import (
"context"
"fmt"
"strings"
"github.com/analogj/scrutiny/webapp/backend/pkg/config"
"github.com/analogj/scrutiny/webapp/backend/pkg/models"
"github.com/mitchellh/mapstructure"
"strings"
"github.com/go-viper/mapstructure/v2"
)
// LoadSettings will retrieve settings from the database, store them in the AppConfig object, and return a Settings struct
func (sr *scrutinyRepository) LoadSettings(ctx context.Context) (*models.Settings, error) {
settingsEntries := []models.SettingEntry{}
if err := sr.gormClient.WithContext(ctx).Find(&settingsEntries).Error; err != nil {
return nil, fmt.Errorf("Could not get settings from DB: %v", err)
return nil, fmt.Errorf("could not get settings from DB: %v", err)
}
// store retrieved settings in the AppConfig obj
@@ -58,7 +59,7 @@ func (sr *scrutinyRepository) SaveSettings(ctx context.Context, settings models.
//retrieve current settings from the database
settingsEntries := []models.SettingEntry{}
if err := sr.gormClient.WithContext(ctx).Find(&settingsEntries).Error; err != nil {
return fmt.Errorf("Could not get settings from DB: %v", err)
return fmt.Errorf("could not get settings from DB: %v", err)
}
//update settingsEntries
@@ -1,10 +1,11 @@
package database
import (
mock_config "github.com/analogj/scrutiny/webapp/backend/pkg/config/mock"
"github.com/golang/mock/gomock"
"github.com/stretchr/testify/require"
"testing"
mock_config "github.com/analogj/scrutiny/webapp/backend/pkg/config/mock"
"github.com/stretchr/testify/require"
"go.uber.org/mock/gomock"
)
func Test_DownsampleScript_Weekly(t *testing.T) {
@@ -12,7 +13,6 @@ func Test_DownsampleScript_Weekly(t *testing.T) {
//setup
mockCtrl := gomock.NewController(t)
defer mockCtrl.Finish()
fakeConfig := mock_config.NewMockInterface(mockCtrl)
fakeConfig.EXPECT().GetString("web.influxdb.bucket").Return("metrics").AnyTimes()
fakeConfig.EXPECT().GetString("web.influxdb.org").Return("scrutiny").AnyTimes()
@@ -64,7 +64,6 @@ func Test_DownsampleScript_Monthly(t *testing.T) {
//setup
mockCtrl := gomock.NewController(t)
defer mockCtrl.Finish()
fakeConfig := mock_config.NewMockInterface(mockCtrl)
fakeConfig.EXPECT().GetString("web.influxdb.bucket").Return("metrics").AnyTimes()
fakeConfig.EXPECT().GetString("web.influxdb.org").Return("scrutiny").AnyTimes()
@@ -116,7 +115,6 @@ func Test_DownsampleScript_Yearly(t *testing.T) {
//setup
mockCtrl := gomock.NewController(t)
defer mockCtrl.Finish()
fakeConfig := mock_config.NewMockInterface(mockCtrl)
fakeConfig.EXPECT().GetString("web.influxdb.bucket").Return("metrics").AnyTimes()
fakeConfig.EXPECT().GetString("web.influxdb.org").Return("scrutiny").AnyTimes()
@@ -1,10 +1,11 @@
package database
import (
mock_config "github.com/analogj/scrutiny/webapp/backend/pkg/config/mock"
"github.com/golang/mock/gomock"
"github.com/stretchr/testify/require"
"testing"
mock_config "github.com/analogj/scrutiny/webapp/backend/pkg/config/mock"
"github.com/stretchr/testify/require"
"go.uber.org/mock/gomock"
)
func Test_aggregateTempQuery_Week(t *testing.T) {
@@ -12,7 +13,6 @@ func Test_aggregateTempQuery_Week(t *testing.T) {
//setup
mockCtrl := gomock.NewController(t)
defer mockCtrl.Finish()
fakeConfig := mock_config.NewMockInterface(mockCtrl)
fakeConfig.EXPECT().GetString("web.influxdb.bucket").Return("metrics").AnyTimes()
fakeConfig.EXPECT().GetString("web.influxdb.org").Return("scrutiny").AnyTimes()
@@ -45,7 +45,6 @@ func Test_aggregateTempQuery_Month(t *testing.T) {
//setup
mockCtrl := gomock.NewController(t)
defer mockCtrl.Finish()
fakeConfig := mock_config.NewMockInterface(mockCtrl)
fakeConfig.EXPECT().GetString("web.influxdb.bucket").Return("metrics").AnyTimes()
fakeConfig.EXPECT().GetString("web.influxdb.org").Return("scrutiny").AnyTimes()
@@ -86,7 +85,6 @@ func Test_aggregateTempQuery_Year(t *testing.T) {
//setup
mockCtrl := gomock.NewController(t)
defer mockCtrl.Finish()
fakeConfig := mock_config.NewMockInterface(mockCtrl)
fakeConfig.EXPECT().GetString("web.influxdb.bucket").Return("metrics").AnyTimes()
fakeConfig.EXPECT().GetString("web.influxdb.org").Return("scrutiny").AnyTimes()
@@ -134,7 +132,6 @@ func Test_aggregateTempQuery_Forever(t *testing.T) {
//setup
mockCtrl := gomock.NewController(t)
defer mockCtrl.Finish()
fakeConfig := mock_config.NewMockInterface(mockCtrl)
fakeConfig.EXPECT().GetString("web.influxdb.bucket").Return("metrics").AnyTimes()
fakeConfig.EXPECT().GetString("web.influxdb.org").Return("scrutiny").AnyTimes()
@@ -2,13 +2,14 @@ package measurements
import (
"fmt"
"github.com/analogj/scrutiny/webapp/backend/pkg"
"github.com/analogj/scrutiny/webapp/backend/pkg/models/collector"
"github.com/analogj/scrutiny/webapp/backend/pkg/thresholds"
"log"
"strconv"
"strings"
"time"
"github.com/analogj/scrutiny/webapp/backend/pkg"
"github.com/analogj/scrutiny/webapp/backend/pkg/models/collector"
"github.com/analogj/scrutiny/webapp/backend/pkg/thresholds"
)
type Smart struct {
@@ -102,7 +103,7 @@ func NewSmartFromInfluxDB(attrs map[string]interface{}) (*Smart, error) {
} else if sm.DeviceProtocol == pkg.DeviceProtocolScsi {
sm.Attributes[attributeId] = &SmartScsiAttribute{}
} else {
return nil, fmt.Errorf("Unknown Device Protocol: %s", sm.DeviceProtocol)
return nil, fmt.Errorf("unknown Device Protocol: %s", sm.DeviceProtocol)
}
}
@@ -116,7 +117,7 @@ func NewSmartFromInfluxDB(attrs map[string]interface{}) (*Smart, error) {
return &sm, nil
}
//Parse Collector SMART data results and create Smart object (and associated SmartAtaAttribute entries)
// Parse Collector SMART data results and create Smart object (and associated SmartAtaAttribute entries)
func (sm *Smart) FromCollectorSmartInfo(wwn string, info collector.SmartInfo) error {
sm.DeviceWWN = wwn
sm.Date = time.Unix(info.LocalTime.TimeT, 0)
@@ -143,7 +144,7 @@ func (sm *Smart) FromCollectorSmartInfo(wwn string, info collector.SmartInfo) er
return nil
}
//generate SmartAtaAttribute entries from Scrutiny Collector Smart data.
// generate SmartAtaAttribute entries from Scrutiny Collector Smart data.
func (sm *Smart) ProcessAtaSmartInfo(tableItems []collector.AtaSmartAttributesTableItem) {
for _, collectorAttr := range tableItems {
attrModel := SmartAtaAttribute{
@@ -171,7 +172,7 @@ func (sm *Smart) ProcessAtaSmartInfo(tableItems []collector.AtaSmartAttributesTa
}
}
//generate SmartNvmeAttribute entries from Scrutiny Collector Smart data.
// generate SmartNvmeAttribute entries from Scrutiny Collector Smart data.
func (sm *Smart) ProcessNvmeSmartInfo(nvmeSmartHealthInformationLog collector.NvmeSmartHealthInformationLog) {
sm.Attributes = map[string]SmartAttribute{
@@ -201,7 +202,7 @@ func (sm *Smart) ProcessNvmeSmartInfo(nvmeSmartHealthInformationLog collector.Nv
}
}
//generate SmartScsiAttribute entries from Scrutiny Collector Smart data.
// generate SmartScsiAttribute entries from Scrutiny Collector Smart data.
func (sm *Smart) ProcessScsiSmartInfo(defectGrownList int64, scsiErrorCounterLog collector.ScsiErrorCounterLog) {
sm.Attributes = map[string]SmartAttribute{
"scsi_grown_defect_list": (&SmartScsiAttribute{AttributeId: "scsi_grown_defect_list", Value: defectGrownList, Threshold: 0}).PopulateAttributeStatus(),
@@ -91,7 +91,7 @@ func (sa *SmartAtaAttribute) Inflate(key string, val interface{}) {
}
}
//populate attribute status, using SMART Thresholds & Observed Metadata
// populate attribute status, using SMART Thresholds & Observed Metadata
// Chainable
func (sa *SmartAtaAttribute) PopulateAttributeStatus() *SmartAtaAttribute {
if strings.ToUpper(sa.WhenFailed) == pkg.AttributeWhenFailedFailingNow {
@@ -165,6 +165,4 @@ func (sa *SmartAtaAttribute) ValidateThreshold(smartMetadata thresholds.AtaAttri
sa.Status = pkg.AttributeStatusSet(sa.Status, pkg.AttributeStatusWarningScrutiny)
sa.StatusReason = "Could not determine Observed Failure Rate for Critical Attribute"
}
return
}
@@ -2,14 +2,15 @@ package measurements_test
import (
"encoding/json"
"io"
"os"
"testing"
"time"
"github.com/analogj/scrutiny/webapp/backend/pkg"
"github.com/analogj/scrutiny/webapp/backend/pkg/models/collector"
"github.com/analogj/scrutiny/webapp/backend/pkg/models/measurements"
"github.com/stretchr/testify/require"
"io/ioutil"
"os"
"testing"
"time"
)
func TestSmart_Flatten(t *testing.T) {
@@ -312,7 +313,7 @@ func TestFromCollectorSmartInfo(t *testing.T) {
var smartJson collector.SmartInfo
smartDataBytes, err := ioutil.ReadAll(smartDataFile)
smartDataBytes, err := io.ReadAll(smartDataFile)
require.NoError(t, err)
err = json.Unmarshal(smartDataBytes, &smartJson)
require.NoError(t, err)
@@ -344,7 +345,7 @@ func TestFromCollectorSmartInfo_Fail_Smart(t *testing.T) {
var smartJson collector.SmartInfo
smartDataBytes, err := ioutil.ReadAll(smartDataFile)
smartDataBytes, err := io.ReadAll(smartDataFile)
require.NoError(t, err)
err = json.Unmarshal(smartDataBytes, &smartJson)
require.NoError(t, err)
@@ -368,7 +369,7 @@ func TestFromCollectorSmartInfo_Fail_ScrutinySmart(t *testing.T) {
var smartJson collector.SmartInfo
smartDataBytes, err := ioutil.ReadAll(smartDataFile)
smartDataBytes, err := io.ReadAll(smartDataFile)
require.NoError(t, err)
err = json.Unmarshal(smartDataBytes, &smartJson)
require.NoError(t, err)
@@ -392,7 +393,7 @@ func TestFromCollectorSmartInfo_Fail_ScrutinyNonCriticalFailed(t *testing.T) {
var smartJson collector.SmartInfo
smartDataBytes, err := ioutil.ReadAll(smartDataFile)
smartDataBytes, err := io.ReadAll(smartDataFile)
require.NoError(t, err)
err = json.Unmarshal(smartDataBytes, &smartJson)
require.NoError(t, err)
@@ -425,7 +426,7 @@ func TestFromCollectorSmartInfo_NVMe_Fail_Scrutiny(t *testing.T) {
var smartJson collector.SmartInfo
smartDataBytes, err := ioutil.ReadAll(smartDataFile)
smartDataBytes, err := io.ReadAll(smartDataFile)
require.NoError(t, err)
err = json.Unmarshal(smartDataBytes, &smartJson)
require.NoError(t, err)
@@ -456,7 +457,7 @@ func TestFromCollectorSmartInfo_Nvme(t *testing.T) {
var smartJson collector.SmartInfo
smartDataBytes, err := ioutil.ReadAll(smartDataFile)
smartDataBytes, err := io.ReadAll(smartDataFile)
require.NoError(t, err)
err = json.Unmarshal(smartDataBytes, &smartJson)
require.NoError(t, err)
@@ -483,7 +484,7 @@ func TestFromCollectorSmartInfo_Scsi(t *testing.T) {
var smartJson collector.SmartInfo
smartDataBytes, err := ioutil.ReadAll(smartDataFile)
smartDataBytes, err := io.ReadAll(smartDataFile)
require.NoError(t, err)
err = json.Unmarshal(smartDataBytes, &smartJson)
require.NoError(t, err)
+2 -3
View File
@@ -5,7 +5,6 @@ import (
"encoding/json"
"fmt"
"io"
"io/ioutil"
"log"
"net/http"
"os"
@@ -68,7 +67,7 @@ func SendPostRequest(url string, file io.Reader) ([]byte, error) {
log.Printf("%v\n", response.Status)
return ioutil.ReadAll(response.Body)
return io.ReadAll(response.Body)
}
// InfluxDB will throw an error/ignore any submitted data with a timestamp older than the
@@ -79,7 +78,7 @@ func readSmartDataFileFixTimestamp(daysToSubtract int, smartDataFilepath string)
return nil, err
}
metricsFileData, err := ioutil.ReadAll(metricsfile)
metricsFileData, err := io.ReadAll(metricsfile)
if err != nil {
return nil, err
}
+3 -3
View File
@@ -64,7 +64,7 @@ func ShouldNotify(logger logrus.FieldLogger, device models.Device, smartAttrs me
var failingAttributes []string
// Loop through the attributes to find the failing ones
for attrId, attrData := range smartAttrs.Attributes {
var status pkg.AttributeStatus = attrData.GetStatus()
var status = attrData.GetStatus()
// Skip over passing attributes
if status == pkg.AttributeStatusPassed {
continue
@@ -147,7 +147,7 @@ func NewPayload(device models.Device, test bool, currentTime ...time.Time) Paylo
//validate that the Payload is populated
var sendDate time.Time
if currentTime != nil && len(currentTime) > 0 {
if len(currentTime) > 0 {
sendDate = currentTime[0]
} else {
sendDate = time.Now()
@@ -318,7 +318,7 @@ func (n *Notify) SendScriptNotification(scriptUrl string) error {
if !utils.FileExists(scriptPath) {
n.Logger.Errorf("Script does not exist: %s", scriptPath)
return errors.New(fmt.Sprintf("custom script path does not exist: %s", scriptPath))
return fmt.Errorf("custom script path does not exist: %s", scriptPath)
}
copyEnv := os.Environ()
+1 -13
View File
@@ -12,9 +12,9 @@ import (
"github.com/analogj/scrutiny/webapp/backend/pkg/models"
"github.com/analogj/scrutiny/webapp/backend/pkg/models/measurements"
"github.com/gin-gonic/gin"
"github.com/golang/mock/gomock"
"github.com/sirupsen/logrus"
"github.com/stretchr/testify/require"
"go.uber.org/mock/gomock"
)
func TestShouldNotify_MustSkipPassingDevices(t *testing.T) {
@@ -28,7 +28,6 @@ func TestShouldNotify_MustSkipPassingDevices(t *testing.T) {
notifyFilterAttributes := pkg.MetricsStatusFilterAttributesAll
mockCtrl := gomock.NewController(t)
defer mockCtrl.Finish()
fakeDatabase := mock_database.NewMockDeviceRepo(mockCtrl)
//assert
require.False(t, ShouldNotify(logrus.StandardLogger(), device, smartAttrs, statusThreshold, notifyFilterAttributes, true, &gin.Context{}, fakeDatabase))
@@ -44,7 +43,6 @@ func TestShouldNotify_MetricsStatusThresholdBoth_FailingSmartDevice(t *testing.T
statusThreshold := pkg.MetricsStatusThresholdBoth
notifyFilterAttributes := pkg.MetricsStatusFilterAttributesAll
mockCtrl := gomock.NewController(t)
defer mockCtrl.Finish()
fakeDatabase := mock_database.NewMockDeviceRepo(mockCtrl)
//assert
require.True(t, ShouldNotify(logrus.StandardLogger(), device, smartAttrs, statusThreshold, notifyFilterAttributes, true, &gin.Context{}, fakeDatabase))
@@ -60,7 +58,6 @@ func TestShouldNotify_MetricsStatusThresholdSmart_FailingSmartDevice(t *testing.
statusThreshold := pkg.MetricsStatusThresholdSmart
notifyFilterAttributes := pkg.MetricsStatusFilterAttributesAll
mockCtrl := gomock.NewController(t)
defer mockCtrl.Finish()
fakeDatabase := mock_database.NewMockDeviceRepo(mockCtrl)
//assert
require.True(t, ShouldNotify(logrus.StandardLogger(), device, smartAttrs, statusThreshold, notifyFilterAttributes, true, &gin.Context{}, fakeDatabase))
@@ -76,7 +73,6 @@ func TestShouldNotify_MetricsStatusThresholdScrutiny_FailingSmartDevice(t *testi
statusThreshold := pkg.MetricsStatusThresholdScrutiny
notifyFilterAttributes := pkg.MetricsStatusFilterAttributesAll
mockCtrl := gomock.NewController(t)
defer mockCtrl.Finish()
fakeDatabase := mock_database.NewMockDeviceRepo(mockCtrl)
//assert
require.False(t, ShouldNotify(logrus.StandardLogger(), device, smartAttrs, statusThreshold, notifyFilterAttributes, true, &gin.Context{}, fakeDatabase))
@@ -96,7 +92,6 @@ func TestShouldNotify_MetricsStatusFilterAttributesCritical_WithCriticalAttrs(t
statusThreshold := pkg.MetricsStatusThresholdBoth
notifyFilterAttributes := pkg.MetricsStatusFilterAttributesCritical
mockCtrl := gomock.NewController(t)
defer mockCtrl.Finish()
fakeDatabase := mock_database.NewMockDeviceRepo(mockCtrl)
//assert
@@ -120,7 +115,6 @@ func TestShouldNotify_MetricsStatusFilterAttributesCritical_WithMultipleCritical
statusThreshold := pkg.MetricsStatusThresholdBoth
notifyFilterAttributes := pkg.MetricsStatusFilterAttributesCritical
mockCtrl := gomock.NewController(t)
defer mockCtrl.Finish()
fakeDatabase := mock_database.NewMockDeviceRepo(mockCtrl)
//assert
@@ -141,7 +135,6 @@ func TestShouldNotify_MetricsStatusFilterAttributesCritical_WithNoCriticalAttrs(
statusThreshold := pkg.MetricsStatusThresholdBoth
notifyFilterAttributes := pkg.MetricsStatusFilterAttributesCritical
mockCtrl := gomock.NewController(t)
defer mockCtrl.Finish()
fakeDatabase := mock_database.NewMockDeviceRepo(mockCtrl)
//assert
@@ -162,7 +155,6 @@ func TestShouldNotify_MetricsStatusFilterAttributesCritical_WithNoFailingCritica
statusThreshold := pkg.MetricsStatusThresholdBoth
notifyFilterAttributes := pkg.MetricsStatusFilterAttributesCritical
mockCtrl := gomock.NewController(t)
defer mockCtrl.Finish()
fakeDatabase := mock_database.NewMockDeviceRepo(mockCtrl)
//assert
@@ -186,7 +178,6 @@ func TestShouldNotify_MetricsStatusFilterAttributesCritical_MetricsStatusThresho
statusThreshold := pkg.MetricsStatusThresholdSmart
notifyFilterAttributes := pkg.MetricsStatusFilterAttributesCritical
mockCtrl := gomock.NewController(t)
defer mockCtrl.Finish()
fakeDatabase := mock_database.NewMockDeviceRepo(mockCtrl)
//assert
@@ -206,7 +197,6 @@ func TestShouldNotify_NoRepeat_DatabaseFailure(t *testing.T) {
statusThreshold := pkg.MetricsStatusThresholdBoth
notifyFilterAttributes := pkg.MetricsStatusFilterAttributesAll
mockCtrl := gomock.NewController(t)
defer mockCtrl.Finish()
fakeDatabase := mock_database.NewMockDeviceRepo(mockCtrl)
fakeDatabase.EXPECT().GetSmartAttributeHistory(&gin.Context{}, "", database.DURATION_KEY_FOREVER, 1, 1, []string{"5"}).Return([]measurements.Smart{}, errors.New("")).Times(1)
@@ -228,7 +218,6 @@ func TestShouldNotify_NoRepeat_NoDatabaseData(t *testing.T) {
statusThreshold := pkg.MetricsStatusThresholdBoth
notifyFilterAttributes := pkg.MetricsStatusFilterAttributesAll
mockCtrl := gomock.NewController(t)
defer mockCtrl.Finish()
fakeDatabase := mock_database.NewMockDeviceRepo(mockCtrl)
fakeDatabase.EXPECT().GetSmartAttributeHistory(&gin.Context{}, "", database.DURATION_KEY_FOREVER, 1, 1, []string{"5"}).Return([]measurements.Smart{}, nil).Times(1)
@@ -250,7 +239,6 @@ func TestShouldNotify_NoRepeat(t *testing.T) {
statusThreshold := pkg.MetricsStatusThresholdBoth
notifyFilterAttributes := pkg.MetricsStatusFilterAttributesAll
mockCtrl := gomock.NewController(t)
defer mockCtrl.Finish()
fakeDatabase := mock_database.NewMockDeviceRepo(mockCtrl)
fakeDatabase.EXPECT().GetSmartAttributeHistory(&gin.Context{}, "", database.DURATION_KEY_FOREVER, 1, 1, []string{"5"}).Return([]measurements.Smart{smartAttrs}, nil).Times(1)
+1 -1
View File
@@ -2,4 +2,4 @@ package version
// VERSION is the app-global version string, which will be replaced with a
// new value during packaging
const VERSION = "0.8.1"
const VERSION = "0.8.3"
+6 -6
View File
@@ -3,15 +3,15 @@ package middleware
import (
"bytes"
"fmt"
"github.com/gin-gonic/gin"
"github.com/sirupsen/logrus"
"io"
"io/ioutil"
"math"
"net/http"
"os"
"strings"
"time"
"github.com/gin-gonic/gin"
"github.com/sirupsen/logrus"
)
// Middleware based on https://github.com/toorop/gin-logrus/blob/master/logger.go
@@ -40,9 +40,9 @@ func LoggerMiddleware(logger *logrus.Entry) gin.HandlerFunc {
//clone the request body reader.
var reqBody string
if c.Request.Body != nil {
buf, _ := ioutil.ReadAll(c.Request.Body)
reqBodyReader1 := ioutil.NopCloser(bytes.NewBuffer(buf))
reqBodyReader2 := ioutil.NopCloser(bytes.NewBuffer(buf)) //We have to create a new Buffer, because reqBodyReader1 will be read.
buf, _ := io.ReadAll(c.Request.Body)
reqBodyReader1 := io.NopCloser(bytes.NewBuffer(buf))
reqBodyReader2 := io.NopCloser(bytes.NewBuffer(buf)) //We have to create a new Buffer, because reqBodyReader1 will be read.
c.Request.Body = reqBodyReader2
reqBody = readBody(reqBodyReader1)
}
+12 -23
View File
@@ -5,7 +5,6 @@ import (
"encoding/json"
"fmt"
"io"
"io/ioutil"
"net/http"
"net/http/httptest"
"os"
@@ -20,10 +19,10 @@ import (
"github.com/analogj/scrutiny/webapp/backend/pkg/models"
"github.com/analogj/scrutiny/webapp/backend/pkg/models/collector"
"github.com/analogj/scrutiny/webapp/backend/pkg/web"
"github.com/golang/mock/gomock"
"github.com/sirupsen/logrus"
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite"
"go.uber.org/mock/gomock"
)
/*
@@ -52,7 +51,7 @@ func helperReadSmartDataFileFixTimestamp(t *testing.T, smartDataFilepath string)
metricsfile, err := os.Open(smartDataFilepath)
require.NoError(t, err)
metricsFileData, err := ioutil.ReadAll(metricsfile)
metricsFileData, err := io.ReadAll(metricsfile)
require.NoError(t, err)
//unmarshal because we need to change the timestamp
@@ -87,10 +86,9 @@ func TestServerTestSuite_WithCustomBasePath(t *testing.T) {
func (suite *ServerTestSuite) TestHealthRoute() {
//setup
parentPath, _ := ioutil.TempDir("", "")
parentPath, _ := os.MkdirTemp("", "")
defer os.RemoveAll(parentPath)
mockCtrl := gomock.NewController(suite.T())
defer mockCtrl.Finish()
fakeConfig := mock_config.NewMockInterface(mockCtrl)
fakeConfig.EXPECT().SetDefault(gomock.Any(), gomock.Any()).AnyTimes()
fakeConfig.EXPECT().UnmarshalKey(gomock.Any(), gomock.Any()).AnyTimes().Return(nil)
@@ -131,10 +129,9 @@ func (suite *ServerTestSuite) TestHealthRoute() {
func (suite *ServerTestSuite) TestRegisterDevicesRoute() {
//setup
parentPath, _ := ioutil.TempDir("", "")
parentPath, _ := os.MkdirTemp("", "")
defer os.RemoveAll(parentPath)
mockCtrl := gomock.NewController(suite.T())
defer mockCtrl.Finish()
fakeConfig := mock_config.NewMockInterface(mockCtrl)
fakeConfig.EXPECT().SetDefault(gomock.Any(), gomock.Any()).AnyTimes()
fakeConfig.EXPECT().UnmarshalKey(gomock.Any(), gomock.Any()).AnyTimes().Return(nil)
@@ -174,10 +171,9 @@ func (suite *ServerTestSuite) TestRegisterDevicesRoute() {
func (suite *ServerTestSuite) TestUploadDeviceMetricsRoute() {
//setup
parentPath, _ := ioutil.TempDir("", "")
parentPath, _ := os.MkdirTemp("", "")
defer os.RemoveAll(parentPath)
mockCtrl := gomock.NewController(suite.T())
defer mockCtrl.Finish()
fakeConfig := mock_config.NewMockInterface(mockCtrl)
fakeConfig.EXPECT().SetDefault(gomock.Any(), gomock.Any()).AnyTimes()
fakeConfig.EXPECT().UnmarshalKey(gomock.Any(), gomock.Any()).AnyTimes().Return(nil)
@@ -229,10 +225,9 @@ func (suite *ServerTestSuite) TestUploadDeviceMetricsRoute() {
func (suite *ServerTestSuite) TestPopulateMultiple() {
//setup
parentPath, _ := ioutil.TempDir("", "")
parentPath, _ := os.MkdirTemp("", "")
defer os.RemoveAll(parentPath)
mockCtrl := gomock.NewController(suite.T())
defer mockCtrl.Finish()
fakeConfig := mock_config.NewMockInterface(mockCtrl)
fakeConfig.EXPECT().SetDefault(gomock.Any(), gomock.Any()).AnyTimes()
fakeConfig.EXPECT().UnmarshalKey(gomock.Any(), gomock.Any()).AnyTimes().Return(nil)
@@ -311,10 +306,9 @@ func (suite *ServerTestSuite) TestPopulateMultiple() {
//TODO: this test should use a recorded request/response playback.
//func TestSendTestNotificationRoute(t *testing.T) {
// //setup
// parentPath, _ := ioutil.TempDir("", "")
// parentPath, _ := os.MkdirTemp("", "")
// defer os.RemoveAll(parentPath)
// mockCtrl := gomock.NewController(t)
// defer mockCtrl.Finish()
// fakeConfig := mock_config.NewMockInterface(mockCtrl)
// fakeConfig.EXPECT().GetString("web.database.location").AnyTimes().Return(path.Join(parentPath, "scrutiny_test.db"))
// fakeConfig.EXPECT().GetString("web.src.frontend.path").AnyTimes().Return(parentPath)
@@ -335,10 +329,9 @@ func (suite *ServerTestSuite) TestPopulateMultiple() {
func (suite *ServerTestSuite) TestSendTestNotificationRoute_WebhookFailure() {
//setup
parentPath, _ := ioutil.TempDir("", "")
parentPath, _ := os.MkdirTemp("", "")
defer os.RemoveAll(parentPath)
mockCtrl := gomock.NewController(suite.T())
defer mockCtrl.Finish()
fakeConfig := mock_config.NewMockInterface(mockCtrl)
fakeConfig.EXPECT().SetDefault(gomock.Any(), gomock.Any()).AnyTimes()
fakeConfig.EXPECT().UnmarshalKey(gomock.Any(), gomock.Any()).AnyTimes().Return(nil)
@@ -381,10 +374,9 @@ func (suite *ServerTestSuite) TestSendTestNotificationRoute_WebhookFailure() {
func (suite *ServerTestSuite) TestSendTestNotificationRoute_ScriptFailure() {
//setup
parentPath, _ := ioutil.TempDir("", "")
parentPath, _ := os.MkdirTemp("", "")
defer os.RemoveAll(parentPath)
mockCtrl := gomock.NewController(suite.T())
defer mockCtrl.Finish()
fakeConfig := mock_config.NewMockInterface(mockCtrl)
fakeConfig.EXPECT().SetDefault(gomock.Any(), gomock.Any()).AnyTimes()
fakeConfig.EXPECT().UnmarshalKey(gomock.Any(), gomock.Any()).AnyTimes().Return(nil)
@@ -427,10 +419,9 @@ func (suite *ServerTestSuite) TestSendTestNotificationRoute_ScriptFailure() {
func (suite *ServerTestSuite) TestSendTestNotificationRoute_ScriptSuccess() {
//setup
parentPath, _ := ioutil.TempDir("", "")
parentPath, _ := os.MkdirTemp("", "")
defer os.RemoveAll(parentPath)
mockCtrl := gomock.NewController(suite.T())
defer mockCtrl.Finish()
fakeConfig := mock_config.NewMockInterface(mockCtrl)
fakeConfig.EXPECT().SetDefault(gomock.Any(), gomock.Any()).AnyTimes()
fakeConfig.EXPECT().UnmarshalKey(gomock.Any(), gomock.Any()).AnyTimes().Return(nil)
@@ -473,10 +464,9 @@ func (suite *ServerTestSuite) TestSendTestNotificationRoute_ScriptSuccess() {
func (suite *ServerTestSuite) TestSendTestNotificationRoute_ShoutrrrFailure() {
//setup
parentPath, _ := ioutil.TempDir("", "")
parentPath, _ := os.MkdirTemp("", "")
defer os.RemoveAll(parentPath)
mockCtrl := gomock.NewController(suite.T())
defer mockCtrl.Finish()
fakeConfig := mock_config.NewMockInterface(mockCtrl)
fakeConfig.EXPECT().SetDefault(gomock.Any(), gomock.Any()).AnyTimes()
fakeConfig.EXPECT().UnmarshalKey(gomock.Any(), gomock.Any()).AnyTimes().Return(nil)
@@ -518,10 +508,9 @@ func (suite *ServerTestSuite) TestSendTestNotificationRoute_ShoutrrrFailure() {
func (suite *ServerTestSuite) TestGetDevicesSummaryRoute_Nvme() {
//setup
parentPath, _ := ioutil.TempDir("", "")
parentPath, _ := os.MkdirTemp("", "")
defer os.RemoveAll(parentPath)
mockCtrl := gomock.NewController(suite.T())
defer mockCtrl.Finish()
fakeConfig := mock_config.NewMockInterface(mockCtrl)
fakeConfig.EXPECT().SetDefault(gomock.Any(), gomock.Any()).AnyTimes()
fakeConfig.EXPECT().UnmarshalKey(gomock.Any(), gomock.Any()).AnyTimes().Return(nil)
+1949 -285
View File
File diff suppressed because it is too large Load Diff
+10 -10
View File
@@ -21,24 +21,24 @@
"private": true,
"dependencies": {
"@angular/animations": "v13-lts",
"@angular/cdk": "v13-lts",
"@angular/common": "v13-lts",
"@angular/cdk": "v21-lts",
"@angular/common": "v21-lts",
"@angular/compiler": "v13-lts",
"@angular/core": "v13-lts",
"@angular/forms": "v13-lts",
"@angular/material": "v13-lts",
"@angular/material-moment-adapter": "v13-lts",
"@angular/platform-browser": "v13-lts",
"@angular/platform-browser-dynamic": "v13-lts",
"@angular/router": "v13-lts",
"@angular/forms": "v21-lts",
"@angular/material": "v21-lts",
"@angular/material-moment-adapter": "v21-lts",
"@angular/platform-browser": "v21-lts",
"@angular/platform-browser-dynamic": "v21-lts",
"@angular/router": "v21-lts",
"@types/humanize-duration": "^3.27.1",
"crypto-js": "^4.1.1",
"highlight.js": "^11.6.0",
"humanize-duration": "^3.27.3",
"lodash": "4.17.21",
"lodash": "4.17.23",
"moment": "^2.29.4",
"ng-apexcharts": "^1.7.4",
"ngx-markdown": "^13.1.0",
"ngx-markdown": "^21.1.0",
"perfect-scrollbar": "^1.5.5",
"quill": "^1.3.7",
"rrule": "^2.7.1",
@@ -229,7 +229,10 @@ export class DashboardComponent implements OnInit, AfterViewInit, OnDestroy
}
},
xaxis: {
type: 'datetime'
type: 'datetime',
labels: {
datetimeUTC: false
}
}
};
}
@@ -35,7 +35,7 @@ export class TemperaturePipe implements PipeTransform {
temperature = TemperaturePipe.celsiusToFahrenheit(celsiusTemp)
break
}
return TemperaturePipe.formatTemperature(celsiusTemp, unit, includeUnits)
return TemperaturePipe.formatTemperature(temperature, unit, includeUnits)
}
}