Compare commits

..

99 Commits

Author SHA1 Message Date
packagrio-bot 2e4ba44952 (v0.7.0) Automated packaging of release by Packagr 2023-04-06 05:22:42 +00:00
Jason Kulatunga 4192ac719e Merge pull request #470 from AnalogJ/beta 2023-04-05 22:07:13 -07:00
Jason Kulatunga 539c94595f Merge pull request #469 from AnalogJ/angular-v13-upgrade 2023-04-05 21:09:35 -07:00
Jason Kulatunga de21e611a3 fixing migration for line_stroke setting. 2023-04-05 21:06:09 -07:00
Jason Kulatunga dea362361e Merge pull request #468 from AnalogJ/angular-v13-upgrade 2023-04-05 20:58:56 -07:00
Jason Kulatunga 7b77519f49 trying to fix tests. 2023-04-05 20:48:07 -07:00
Jason Kulatunga 94df7e1ec3 trying to fix tests. 2023-04-05 20:44:07 -07:00
Jason Kulatunga babd8d3089 trying to fix tests. 2023-04-05 20:35:18 -07:00
Jason Kulatunga 52ef28f091 removing NODE_OPTIONS. 2023-04-05 20:34:29 -07:00
Jason Kulatunga 80d72f8a1b regenerate package-lock with angular 13-lts packages. 2023-04-05 20:10:53 -07:00
Jason Kulatunga 2e8f4a0581 update with instructions for adding host id to UI.
- fixes #464
- related #151
2023-04-05 19:50:09 -07:00
Michael Manganiello 7fd2e2b050 Upgrade to Go 1.20
With the release of Go 1.20, version 1.18 is not supported anymore. This
change upgrades the project to Go 1.20.
2023-04-05 19:50:09 -07:00
Jason Kulatunga 8c65166a90 Merge pull request #446 from adamantike/misc/upgrade-go-1.20 2023-04-05 19:48:31 -07:00
Jason Kulatunga 733b49c2c4 Merge branch 'master' into misc/upgrade-go-1.20 2023-04-05 07:53:01 -07:00
Jason Kulatunga 711b5c40b1 update with instructions for adding host id to UI.
- fixes #464
- related #151
2023-04-04 21:53:39 -07:00
Jason Kulatunga f94e616d8d Merge branch 'master' into angular-refactoring 2023-04-04 21:38:57 -07:00
Michael Manganiello 007857afd5 Upgrade to Go 1.20
With the release of Go 1.20, version 1.18 is not supported anymore. This
change upgrades the project to Go 1.20.
2023-02-19 21:18:51 -03:00
Jason Kulatunga e4bbe8c035 Merge pull request #441 from padhi-forks/master
Fixes https://github.com/AnalogJ/scrutiny/issues/440
2023-02-08 21:43:45 -08:00
Saswat Padhi cb5226f6e4 Update backend tests failing on insecure_skip_verify 2023-02-07 08:24:39 +00:00
Saswat Padhi c69770d1dd Add insecure_skip_verify in example.scrutiny.yaml 2023-02-06 22:34:06 +00:00
Saswat Padhi e07a53046f [FEAT] Allow insecure certificates on InfluxDB
This change allows users to skip TLS certificate verification on their
InfluxDB server, if they wish to do so, for instance when using self-
signed certificates.
Without this change, scrutiny failed to start and paniced with a
`x509: certificate signed by unknown authority` error.
2023-02-06 22:26:40 +00:00
Jason Kulatunga 19a0b8c2ac Update TROUBLESHOOTING_INFLUXDB.md
change volume mounts when upgrading from LSIO image
2023-02-04 08:41:47 -08:00
Jason Kulatunga 97f73703b1 update docs with instructions for customizing Influxdb creds. 2023-01-19 22:21:48 -08:00
Jason Kulatunga 4fcd11f497 update coverage makefile with workaround. 2023-01-11 21:31:25 -08:00
Jason Kulatunga 7f1023fa9b temporary fix for #426
using legacy open ssl provider for fixing `"error:0308010C:digital envelope routines::unsupported"` error.

See https://stackoverflow.com/questions/69692842/error-message-error0308010cdigital-envelope-routinesunsupported

We need to upgrade Angular version.
2023-01-11 21:30:16 -08:00
Jason Kulatunga d49497da80 update hub/spoke guide - thanks @TinJoy59
fixes #417
2023-01-11 18:03:49 -08:00
Jason Kulatunga d8c359bd8a instructions documenting added ability to trigger collector at startup.
Disabled by default, (enable by setting `-e COLLECTOR_RUN_STARTUP=true`)

Added COLLECTOR_RUN_STARTUP_SLEEP env variable to specify seconds before calling scrutiny collector on first run, default sleep value = 1s.
2023-01-11 17:49:58 -08:00
Jason Kulatunga ad4b117f6e fixes #412
Added ability to trigger collector at startup.
Disabled by default, (enable by setting `-e COLLECTOR_RUN_STARTUP=true`)

Added COLLECTOR_RUN_STARTUP_SLEEP env variable to specify seconds before calling scrutiny collector on first run, default sleep value = 1s.
2023-01-11 17:41:09 -08:00
Jason Kulatunga 22d2f9847c fixes #418
when using device type `sat,auto`, scrutiny config parser will treat `type: 'sat,auto'` as a list, which will cause it to be treated like a raid disk. force single command execution using `type: ['sat,auto']`
2023-01-11 17:24:01 -08:00
packagrio-bot 1c7f299b98 (v0.6.0) Automated packaging of release by Packagr 2023-01-12 00:42:30 +00:00
Jason Kulatunga 602fdce0ee Merge pull request #425 from AnalogJ/update_shoutrrr 2023-01-11 16:37:01 -08:00
Jason Kulatunga 61fde6a2ca update shoutrrr version.
fixes #355
2023-01-11 16:17:25 -08:00
Jason Kulatunga d843bcc258 fix nightly build. 2022-12-02 07:13:52 -08:00
Jason Kulatunga f2856e0f26 adding a healthcheck endpoint that confirms communication with influxdb and sqlite.
ref: #381
2022-11-30 08:25:27 -08:00
Jason Kulatunga 58ef1aa311 update readme. 2022-11-30 07:56:01 -08:00
Jason Kulatunga 6a6570b8e3 trying to fix nodejs build of frontend. 2022-11-29 22:45:40 -08:00
Jason Kulatunga 29a0860caa trying to fix nodejs build of frontend. 2022-11-29 22:24:16 -08:00
Jason Kulatunga fb760a9f6d make sure we print an error if the config file is invalid.
fixes #408
2022-11-29 22:06:42 -08:00
Jason Kulatunga c9f13f4398 update README to correctly call-out the influxdb requirement in hub-spoke deployments.
references #409
2022-11-29 21:07:51 -08:00
Jason Kulatunga dd03a8cf63 Revert "trying to fix build"
This reverts commit 0a6ade4da9.
2022-11-28 08:16:45 -08:00
Jason Kulatunga 0a6ade4da9 trying to fix build 2022-11-28 08:13:19 -08:00
Jason Kulatunga 5c8c11d78b Merge pull request #407 from StratusFearMe21/patch-1 2022-11-28 07:47:01 -08:00
Jason Kulatunga 00502cc565 Merge pull request #406 from boomam/patch-1 2022-11-28 07:46:35 -08:00
StratusFearMe21 0febe3fda5 Looks like this is already implemented 2022-11-28 05:46:21 +00:00
boomam fcd4bb4561 Added new formatting
....to match existing doc formatting standard.
2022-11-27 19:28:09 -05:00
boomam 89f763e65d Addition of notification testing command to troubleshooting
Addition of notification testing command to troubleshooting.
2022-11-27 19:27:10 -05:00
Jason Kulatunga 075eb94fa2 Merge pull request #394 from AnalogJ/skip_null_temp 2022-11-15 09:07:13 -08:00
adripo e9cf8a9180 fix: igeneric types 2022-11-12 22:27:07 +01:00
adripo 64ad353628 fix: remove fullcalendar 2022-11-12 22:26:03 +01:00
adripo 5518865bc6 fix: remove outdated option 2022-11-12 22:24:38 +01:00
adripo 50321d897a fix: prod build command 2022-11-12 22:24:02 +01:00
adripo e18a7e9ce0 refactor: update dependencies version 2022-11-12 22:23:37 +01:00
adripo 536b590080 feat: dynamic line stroke settings 2022-11-11 00:19:51 +01:00
Jason Kulatunga 098ce0673a Update TROUBLESHOOTING_DEVICE_COLLECTOR.md 2022-11-08 20:21:21 -08:00
Jason Kulatunga 2677796322 fixing bug. Null value for temperatures should be ignored. 2022-11-06 07:54:32 -08:00
Jason Kulatunga 5cc7fb30ed Merge pull request #391 from adripo/patch-1 2022-11-06 07:46:39 -08:00
adripo 222b8103d6 fix: increase timeout 2022-11-05 04:14:44 +01:00
Jason Kulatunga 727d5b0ace Update SUPPORTED_NAS_OS.md 2022-10-12 21:26:04 -07:00
Jason Kulatunga d7b45e5f01 update docs. 2022-10-12 20:58:41 -07:00
Jason Kulatunga 578a262d90 Merge pull request #372 from Robert-Zacchigna/master 2022-09-22 22:13:31 -07:00
Robert-Zacchigna c6e11f88b4 Minor Formatting Fix 2022-09-21 14:58:16 -05:00
Robert-Zacchigna b795331efb Doc for Manual Install on Windows 2022-09-21 14:51:14 -05:00
packagrio-bot f1e5bd3ed4 (v0.5.0) Automated packaging of release by Packagr 2022-08-04 15:11:04 +00:00
Jason Kulatunga d8d56f77f9 Merge pull request #352 from AnalogJ/beta 2022-08-04 08:07:56 -07:00
Jason Kulatunga 26b221532e fix tests. 2022-08-04 07:56:43 -07:00
Jason Kulatunga 15d3206f6f remove settings dialog from Details page. 2022-08-04 07:30:14 -07:00
Jason Kulatunga 59e2e928a8 remove the notify.level and notify.filter_attributes values from the example.scrutiny.yaml, since they are no longer allowed. 2022-08-03 23:12:09 -07:00
Jason Kulatunga 51f59e4fcd docs, added an explanation for why influxdb is required. 2022-08-03 22:59:19 -07:00
Jason Kulatunga f823127825 simplify logger creation (move logic into a function in main packages)
Ensure logger creation is consistent between Web and Collector
Create logger in main, pass down to downstream functions (like gin)
In debug mode, print a copy of AppConfig
Better debugging for logger.
2022-08-03 22:51:44 -07:00
Jason Kulatunga d41d535ab7 make sure that the device host id is provided in notifications (if available).
fixes #337
2022-08-03 20:55:34 -07:00
Jason Kulatunga 9a4a8de341 make sure the settings dialog width is 600px for readability. 2022-08-03 18:38:59 -07:00
Jason Kulatunga 2d6f60abaa attrHistory needs to be reversed, so the newest data is on the right
fixes #339
2022-08-03 18:23:58 -07:00
Jason Kulatunga d201f798fb Merge pull request #351 from AnalogJ/app_db_settings 2022-08-02 22:15:52 -07:00
Jason Kulatunga a1b0108503 Added PRAGMA settings support when connecting to SQLITE db.
When a transaction cannot lock the database, because it is already locked by another one,
SQLite by default throws an error: database is locked. This behavior is usually not appropriate when
concurrent access is needed, typically when multiple processes write to the same database.
PRAGMA busy_timeout lets you set a timeout or a handler for these events. When setting a timeout,
SQLite will try the transaction multiple times within this timeout.
https://rsqlite.r-dbi.org/reference/sqlitesetbusyhandler

retrying for 30000 milliseconds, 30seconds - this would be unreasonable for a distributed multi-tenant application,
but should be fine for local usage.

added mechanism for global settings (PRAGMA and DB level instructions).

fixes #341
2022-08-02 22:14:23 -07:00
Jason Kulatunga f0275d2349 Merge pull request #346 from KF5JWC/patch-3 2022-08-02 20:42:53 -07:00
Jason Kulatunga 9dafde8a43 Merge pull request #350 from MattKobayashi/docs_udev 2022-08-02 20:29:25 -07:00
Matthew Kobayashi fa8f86ab7b Add missing setup command 2022-08-03 11:02:51 +10:00
KF5JWC 41c9daa939 Make run_collect.sh executable
Synology task will fail when not executable:

```
/bin/bash: /volume1/@Entware/scrutiny/bin/run_collect.sh: Permission denied
```
2022-08-01 15:07:28 -05:00
Jason Kulatunga 83186ba36e Merge pull request #345 from KF5JWC/patch-2 2022-07-31 11:00:15 -07:00
KF5JWC 3205e3d022 Update INSTALL_SYNOLOGY_COLLECTOR.md
Typo: Created and loaded config into `conf/`, but specifies `config/` in argument
2022-07-31 00:07:04 -05:00
Jason Kulatunga 3f272b36d4 adding setting to allow users to customize between binary vs SI/Metric units in UI.
fixes #330
2022-07-30 08:50:23 -07:00
Jason Kulatunga b238579fe6 Merge pull request #343 from AnalogJ/app_db_settings
adding tests. Make sure that device status depends on the configured threshold
2022-07-30 08:05:19 -07:00
Jason Kulatunga ce2f990eb1 consolidate device status to string logic in DeviceStatusPipe.
Ensure device status takes into account new settings.
2022-07-29 07:11:57 -07:00
Jason Kulatunga b11b8732aa Merge pull request #342 from MattKobayashi/docs_udev 2022-07-29 06:43:49 -07:00
Matthew Kobayashi 5cd441da7b Add udev troubleshooting doc 2022-07-29 09:33:55 +10:00
Jason Kulatunga 2e768fb491 adding tests. Make sure that device status depends on the configured threshold. 2022-07-25 07:46:44 -07:00
Jason Kulatunga e8755ff617 Merge pull request #338 from AnalogJ/app_db_settings 2022-07-23 16:37:16 -07:00
Jason Kulatunga e41ee47371 filter attributes after notify 2022-07-23 16:21:53 -07:00
Jason Kulatunga 7a68a68e76 frontend, determine the device status by checking against the configured thresholds. 2022-07-23 16:11:49 -07:00
Jason Kulatunga 94594db20a on settings save, return the new settings.
update the frontend to persist settings to the database.
Using ScrutinyConfigService instead of TreoConfigService.
Using snake case settings in frontend.
Make sure we're using AppConfig type where possible.
2022-07-23 14:36:32 -07:00
Jason Kulatunga 7e672e8b8e adding tests for config.MergeConfigMap functionality. (Set vs SetDefault).
Converted all settings keys to snakecase.
2022-07-23 10:19:15 -07:00
Jason Kulatunga 54e2cacb00 move frontend settings into the DB (for consistent settings handling).
Flattened settings object.
2022-07-23 09:32:56 -07:00
Jason Kulatunga c0f1dfdb0b fixing config mock. 2022-07-20 22:38:30 -07:00
Jason Kulatunga 29bc79996b working settings update.
Settings are loaded from the DB and added to the AppConfig during startup.
When updating settings, they are stored in AppConfig, and written do  the database.
2022-07-19 23:12:23 -07:00
Jason Kulatunga 99af2b8b16 WIP settings system.
- updated dbdiagrams schema
- [BREAKING] force failure if `notify.filter_attributes` or `notify.level` is set
- added Settings table (and default values during migration)
- Added Save Settings and Get Settings functions.
- Added web API endpoints for getting and saving settings.
- Deprecated old Notify* constants. Created new MetricsStatus* and MetricsNotifyLevel constants.
2022-07-17 10:32:28 -07:00
Jason Kulatunga dd0c3e6fba rename the migration model package name. 2022-07-16 22:07:50 -07:00
Jason Kulatunga 5b2746f389 initial settings table. 2022-07-16 21:50:48 -07:00
Jason Kulatunga e9c1de9664 update support table in README.
- freebsd binaries for collector and web working
- macos binaries for arm and amd.
2022-07-16 10:12:30 -07:00
Jason Kulatunga 6ca4bd4912 fix the WORKDIR for collector image.
fixes #335
2022-07-13 21:56:58 -07:00
106 changed files with 10535 additions and 28782 deletions
+2 -2
View File
@@ -101,7 +101,7 @@ jobs:
uses: actions/checkout@v2
- uses: actions/setup-go@v3
with:
go-version: '^1.18.3'
go-version: '^1.20.1'
- name: Build Binaries
run: |
make binary-clean binary-all
@@ -111,4 +111,4 @@ jobs:
name: binaries.zip
path: |
scrutiny-web-*
scrutiny-collector-metrics-*
scrutiny-collector-metrics-*
+13 -18
View File
@@ -74,15 +74,13 @@ jobs:
uses: actions/checkout@v2
- name: "Populate frontend version information"
run: "cd webapp/frontend && ./git.version.sh"
- name: "Generate frontend"
uses: addnab/docker-run-action@v3
- name: "Install Node"
uses: actions/setup-node@v3
with:
image: node:lts
options: -v ${{ github.workspace }}:/work
run: |
cd /work
make binary-frontend && echo "print contents of /work/dist" && ls -alt /work/dist
node-version: 16
- name: "Generate frontend"
run: |
make binary-frontend && echo "print contents of ./dist" && ls -alt ./dist
- name: Set up QEMU
uses: docker/setup-qemu-action@v2
with:
@@ -134,16 +132,13 @@ jobs:
uses: actions/checkout@v2
- name: "Populate frontend version information"
run: "cd webapp/frontend && ./git.version.sh"
- name: "Generate frontend & version information"
uses: addnab/docker-run-action@v3
- name: "Install Node"
uses: actions/setup-node@v3
with:
image: node:lts
options: -v ${{ github.workspace }}:/work
run: |
cd /work
make binary-frontend && echo "print contents of /work/dist" && ls -alt /work/dist
node-version: 16
- name: "Generate frontend"
run: |
make binary-frontend && echo "print contents of ./dist" && ls -alt ./dist
- name: Set up QEMU
uses: docker/setup-qemu-action@v2
with:
@@ -181,4 +176,4 @@ jobs:
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
# cache-from: type=gha
# cache-to: type=gha,mode=max
# cache-to: type=gha,mode=max
+6 -9
View File
@@ -19,16 +19,13 @@ jobs:
uses: actions/checkout@v2
- name: "Populate frontend version information"
run: "cd webapp/frontend && ./git.version.sh"
- name: "Generate frontend & version information"
uses: addnab/docker-run-action@v3
- name: "Install Node"
uses: actions/setup-node@v3
with:
image: node:lts
options: -v ${{ github.workspace }}:/work
run: |
cd /work
make binary-frontend && echo "print contents of /work/dist" && ls -alt /work/dist
node-version: 16
- name: "Generate frontend"
run: |
make binary-frontend && echo "print contents of ./dist" && ls -alt ./dist
- name: Set up QEMU
uses: docker/setup-qemu-action@v2
with:
+1 -1
View File
@@ -96,7 +96,7 @@ jobs:
name: workspace
- uses: actions/setup-go@v3
with:
go-version: '1.18.3' # The Go version to download (if necessary) and use.
go-version: '1.20.1' # The Go version to download (if necessary) and use.
- name: Build Binaries
run: |
make binary-clean binary-all
+11 -11
View File
@@ -5,18 +5,18 @@ The Scrutiny repository is a [monorepo](https://en.wikipedia.org/wiki/Monorepo)
- Scrutiny Frontend Angular SPA
- S.M.A.R.T Collector
Depending on the functionality you are adding, you may need to setup a development environment for 1 or more projects.
Depending on the functionality you are adding, you may need to setup a development environment for 1 or more projects.
# Modifying the Scrutiny Backend Server (API)
1. install the [Go runtime](https://go.dev/doc/install) (v1.18+)
1. install the [Go runtime](https://go.dev/doc/install) (v1.20+)
2. download the `scrutiny-web-frontend.tar.gz` for
the [latest release](https://github.com/AnalogJ/scrutiny/releases/latest). Extract to a folder named `dist`
3. create a `scrutiny.yaml` config file
```yaml
# config file for local development. store as scrutiny.yaml
version: 1
web:
listen:
port: 8080
@@ -29,13 +29,13 @@ Depending on the functionality you are adding, you may need to setup a developme
path: ./dist
influxdb:
retention_policy: false
log:
file: 'web.log' #absolute or relative paths allowed, eg. web.log
level: DEBUG
```
4. start a InfluxDB docker container.
4. start a InfluxDB docker container.
```bash
docker run -p 8086:8086 --rm influxdb:2.2
```
@@ -55,21 +55,21 @@ The frontend is written in Angular. If you're working on the frontend and can us
```bash
cd webapp/frontend
npm install
npm run start -- --deploy-url="/web/" --base-href="/web/" --port 4200
npm run start -- --serve-path="/web/" --port 4200
```
3. open your browser and visit [http://localhost:4200/web](http://localhost:4200/web)
# Modifying both Scrutiny Backend and Frontend Applications
If you're developing a feature that requires changes to the backend and the frontend, or a frontend feature that requires real data,
If you're developing a feature that requires changes to the backend and the frontend, or a frontend feature that requires real data,
you'll need to follow the steps below:
1. install the [Go runtime](https://go.dev/doc/install) (v1.18+)
1. install the [Go runtime](https://go.dev/doc/install) (v1.20+)
2. install [NodeJS](https://nodejs.org/en/download/)
3. create a `scrutiny.yaml` config file
```yaml
# config file for local development. store as scrutiny.yaml
version: 1
web:
listen:
port: 8080
@@ -82,7 +82,7 @@ you'll need to follow the steps below:
path: ./dist
influxdb:
retention_policy: false
log:
file: 'web.log' #absolute or relative paths allowed, eg. web.log
level: DEBUG
@@ -185,4 +185,4 @@ docker run -p 8086:8086 -d --rm \
influxdb:2.2
go test ./...
```
```
+11 -7
View File
@@ -46,7 +46,7 @@ Scrutiny is a simple but focused application, with a couple of core features:
- Customized thresholds using real world failure rates
- Temperature tracking
- Provided as an all-in-one Docker image (but can be installed manually)
- Future Configurable Alerting/Notifications via Webhooks
- Configurable Alerting/Notifications via Webhooks
- (Future) Hard Drive performance testing & tracking
# Getting Started
@@ -91,10 +91,14 @@ docker run -it --rm -p 8080:8080 -p 8086:8086 \
### Hub/Spoke Deployment
In addition to the Omnibus image (available under the `latest` tag) there are 2 other Docker images available:
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 scheduler. You can run one collector on each server.
- `ghcr.io/analogj/scrutiny:master-web` - Contains the Web UI, API and Database. Only one container necessary
- `ghcr.io/analogj/scrutiny:master-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
- `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)
> See [docker/example.hubspoke.docker-compose.yml](./docker/example.hubspoke.docker-compose.yml) for a docker-compose file.
@@ -239,9 +243,9 @@ scrutiny-collector-metrics run --debug --log-file /tmp/collector.log
| linux-arm-6 | :white_check_mark: | |
| linux-arm-7 | :white_check_mark: | web/collector only. see [#236](https://github.com/AnalogJ/scrutiny/issues/236) |
| linux-arm64 | :white_check_mark: | :white_check_mark: |
| freebsd-amd64 | collector only. see [#238](https://github.com/AnalogJ/scrutiny/issues/238) | |
| macos-amd64 | | :white_check_mark: |
| macos-arm64 | | :white_check_mark: |
| freebsd-amd64 | :white_check_mark: | |
| macos-amd64 | :white_check_mark: | :white_check_mark: |
| macos-arm64 | :white_check_mark: | :white_check_mark: |
| windows-amd64 | :white_check_mark: | WIP, see [#15](https://github.com/AnalogJ/scrutiny/issues/15) |
| windows-arm64 | :white_check_mark: | |
@@ -1,6 +1,7 @@
package main
import (
"encoding/json"
"fmt"
"github.com/analogj/scrutiny/collector/pkg/collector"
"github.com/analogj/scrutiny/collector/pkg/config"
@@ -120,26 +121,16 @@ OPTIONS:
config.Set("api.endpoint", apiEndpoint)
}
collectorLogger := logrus.WithFields(logrus.Fields{
"type": "metrics",
})
if level, err := logrus.ParseLevel(config.GetString("log.level")); err == nil {
logrus.SetLevel(level)
} else {
logrus.SetLevel(logrus.InfoLevel)
}
if config.IsSet("log.file") && len(config.GetString("log.file")) > 0 {
logFile, err := os.OpenFile(config.GetString("log.file"), os.O_CREATE|os.O_WRONLY, 0644)
if err != nil {
logrus.Errorf("Failed to open log file %s for output: %s", config.GetString("log.file"), err)
return err
}
collectorLogger, logFile, err := CreateLogger(config)
if logFile != nil {
defer logFile.Close()
logrus.SetOutput(io.MultiWriter(os.Stderr, logFile))
}
if err != nil {
return err
}
settingsData, err := json.MarshalIndent(config.AllSettings(), "", "\t")
collectorLogger.Debug(string(settingsData), err)
metricCollector, err := collector.CreateMetricsCollector(
config,
collectorLogger,
@@ -192,5 +183,28 @@ OPTIONS:
if err != nil {
log.Fatal(color.HiRedString("ERROR: %v", err))
}
}
func CreateLogger(appConfig config.Interface) (*logrus.Entry, *os.File, error) {
logger := logrus.WithFields(logrus.Fields{
"type": "metrics",
})
if level, err := logrus.ParseLevel(appConfig.GetString("log.level")); err == nil {
logger.Logger.SetLevel(level)
} else {
logger.Logger.SetLevel(logrus.InfoLevel)
}
var logFile *os.File
var err error
if appConfig.IsSet("log.file") && len(appConfig.GetString("log.file")) > 0 {
logFile, err = os.OpenFile(appConfig.GetString("log.file"), os.O_CREATE|os.O_WRONLY, 0644)
if err != nil {
logger.Logger.Errorf("Failed to open log file %s for output: %s", appConfig.GetString("log.file"), err)
return nil, logFile, err
}
logger.Logger.SetOutput(io.MultiWriter(os.Stderr, logFile))
}
return logger, logFile, nil
}
+1 -1
View File
@@ -8,7 +8,7 @@ import (
"time"
)
var httpClient = &http.Client{Timeout: 10 * time.Second}
var httpClient = &http.Client{Timeout: 60 * time.Second}
type BaseCollector struct {
logger *logrus.Entry
+19
View File
@@ -36,6 +36,25 @@ func TestConfiguration_GetScanOverrides_Simple(t *testing.T) {
require.Equal(t, []models.ScanOverride{{Device: "/dev/sda", DeviceType: []string{"sat"}, Ignore: false}}, scanOverrides)
}
// fixes #418
func TestConfiguration_GetScanOverrides_DeviceTypeComma(t *testing.T) {
t.Parallel()
//setup
testConfig, _ := config.Create()
//test
err := testConfig.ReadConfig(path.Join("testdata", "device_type_comma.yaml"))
require.NoError(t, err, "should correctly load simple device config")
scanOverrides := testConfig.GetDeviceOverrides()
//assert
require.Equal(t, []models.ScanOverride{
{Device: "/dev/sda", DeviceType: []string{"sat", "auto"}, Ignore: false},
{Device: "/dev/sdb", DeviceType: []string{"sat,auto"}, Ignore: false},
}, scanOverrides)
}
func TestConfiguration_GetScanOverrides_Ignore(t *testing.T) {
t.Parallel()
+9
View File
@@ -0,0 +1,9 @@
version: 1
devices:
# the scrutiny config parser will detect `sat,auto` as two separate items in a list. If you want to use `-d sat,auto` you must
# set 'sat,auto' in a list (see eg. /dev/sbd)
- device: /dev/sda
type: 'sat,auto'
- device: /dev/sdb
type:
- sat,auto
+1 -1
View File
@@ -5,7 +5,7 @@
########
FROM golang:1.18-bullseye as backendbuild
FROM golang:1.20-bullseye as backendbuild
WORKDIR /go/src/github.com/analogj/scrutiny
COPY . /go/src/github.com/analogj/scrutiny
+2 -2
View File
@@ -4,7 +4,7 @@
########
FROM golang:1.18-bullseye as backendbuild
FROM golang:1.20-bullseye as backendbuild
WORKDIR /go/src/github.com/analogj/scrutiny
@@ -14,7 +14,7 @@ RUN make binary-clean binary-collector
########
FROM debian:bullseye-slim as runtime
WORKDIR /scrutiny
WORKDIR /opt/scrutiny
ENV PATH="/opt/scrutiny/bin:${PATH}"
RUN apt-get update && apt-get install -y cron smartmontools ca-certificates tzdata && update-ca-certificates
+1 -1
View File
@@ -5,7 +5,7 @@
########
FROM golang:1.18-bullseye as backendbuild
FROM golang:1.20-bullseye as backendbuild
WORKDIR /go/src/github.com/analogj/scrutiny
+9
View File
@@ -7,6 +7,8 @@
# adding ability to customize the cron schedule.
COLLECTOR_CRON_SCHEDULE=${COLLECTOR_CRON_SCHEDULE:-"0 0 * * *"}
COLLECTOR_RUN_STARTUP=${COLLECTOR_RUN_STARTUP:-"false"}
COLLECTOR_RUN_STARTUP_SLEEP=${COLLECTOR_RUN_STARTUP_SLEEP:-"1"}
# if the cron schedule has been overridden via env variable (eg docker-compose) we should make sure to strip quotes
[[ "${COLLECTOR_CRON_SCHEDULE}" == \"*\" || "${COLLECTOR_CRON_SCHEDULE}" == \'*\' ]] && COLLECTOR_CRON_SCHEDULE="${COLLECTOR_CRON_SCHEDULE:1:-1}"
@@ -14,6 +16,13 @@ COLLECTOR_CRON_SCHEDULE=${COLLECTOR_CRON_SCHEDULE:-"0 0 * * *"}
# replace placeholder with correct value
sed -i 's|{COLLECTOR_CRON_SCHEDULE}|'"${COLLECTOR_CRON_SCHEDULE}"'|g' /etc/cron.d/scrutiny
if [[ "${COLLECTOR_RUN_STARTUP}" == "true" ]]; then
sleep ${COLLECTOR_RUN_STARTUP_SLEEP}
echo "starting scrutiny collector (run-once mode. subsequent calls will be triggered via cron service)"
/opt/scrutiny/bin/scrutiny-collector-metrics run
fi
# now that we have the env start cron in the foreground
echo "starting cron"
su -c "cron -f -L 15" root
+181 -1
View File
@@ -1 +1,181 @@
> See [docker/example.hubspoke.docker-compose.yml](https://github.com/AnalogJ/scrutiny/blob/master/docker/example.hubspoke.docker-compose.yml) for a docker-compose file.
>
See [docker/example.hubspoke.docker-compose.yml](https://github.com/AnalogJ/scrutiny/blob/master/docker/example.hubspoke.docker-compose.yml)
for a docker-compose file.
> The following guide was contributed by @TinJoy59 in #417
> It describes how to deploy the Scrutiny in Hub/Spoke mode, where the Hub is running in Docker, and the Spokes (
> collectors) are running as binaries.
> He's using Proxmox & Synology in his guide, however this should be applicable for almost anyone
# S.M.A.R.T. Monitoring with Scrutiny across machines
![drawing-3-1671744407](https://user-images.githubusercontent.com/86809766/209230023-bf1ef9f8-65c4-454e-9e1a-be1293cd737e.png)
### 🤔 The problem:
Scrutiny offers a nice Docker package called "Omnibus" that can monitor HDDs attached to a Docker host with relative
ease. Scrutiny can also be installed in a Hub-Spoke layout where Web interface, Database and Collector come in 3
separate packages. The official documentation assumes that the spokes in the "Hub-Spokes layout" run Docker, which is
not always the case. The third approach is to install Scrutiny manually, entirely outside of Docker.
### 💡 The solution:
This tutorial provides a hybrid configuration where the Hub lives in a Docker instance while the spokes have only
Scrutiny Collector installed manually. The Collector periodically send data to the Hub. It's not mind-boggling hard to
understand but someone might struggle with the setup. This is for them.
### 🖥️ My setup:
I have a Proxmox cluster where one VM runs Docker and all monitoring services - Grafana, Prometheus, various exporters,
InfluxDB and so forth. Another VM runs the NAS - OpenMediaVault v6, where all hard drives reside. The Scrutiny Collector
is triggered every 30min to collect data on the drives. The data is sent to the Docker VM, running InfluxDB.
## Setting up the Hub
![drawing-3-1671744714](https://user-images.githubusercontent.com/86809766/209230113-c954d834-521b-4555-bcd2-eb6b80f343be.png)
The Hub consists of Scrutiny Web - a web interface for viewing the SMART data. And InfluxDB, where the smartmon data is
stored.
[🔗This is the official Hub-Spoke layout in docker-compose.](https://github.com/AnalogJ/scrutiny/blob/master/docker/example.hubspoke.docker-compose.yml)
We are going to reuse parts of it. The ENV variables provide the necessary configuration for the initial setup, both for
InfluxDB and Scrutiny.
If you are working with and existing InfluxDB instance, you can forgo all the `INIT` variables as they already exist.
The official Scrutiny documentation has a
sample [scrutiny.yaml ](https://github.com/AnalogJ/scrutiny/blob/master/example.scrutiny.yaml)file that normally
contains the connection and notification details but I always find it easier to configure as much as possible in the
docker-compose.
```yaml
version: "3.4"
networks:
monitoring: # A common network for all monitoring services to communicate into
external: true
notifications: # To Gotify or another Notification service
external: true
services:
influxdb:
container_name: influxdb
image: influxdb:2.1-alpine
ports:
- 8086:8086
volumes:
- ${DIR_CONFIG}/influxdb2/db:/var/lib/influxdb2
- ${DIR_CONFIG}/influxdb2/config:/etc/influxdb2
environment:
- DOCKER_INFLUXDB_INIT_MODE=setup
- DOCKER_INFLUXDB_INIT_USERNAME=Admin
- DOCKER_INFLUXDB_INIT_PASSWORD=${PASSWORD}
- DOCKER_INFLUXDB_INIT_ORG=homelab
- DOCKER_INFLUXDB_INIT_BUCKET=scrutiny
- DOCKER_INFLUXDB_INIT_ADMIN_TOKEN=your-very-secret-token
restart: unless-stopped
networks:
- monitoring
scrutiny:
container_name: scrutiny
image: ghcr.io/analogj/scrutiny:master-web
ports:
- 8080:8080
volumes:
- ${DIR_CONFIG}/scrutiny/config:/opt/scrutiny/config
environment:
- SCRUTINY_WEB_INFLUXDB_HOST=influxdb
- SCRUTINY_WEB_INFLUXDB_PORT=8086
- SCRUTINY_WEB_INFLUXDB_TOKEN=your-very-secret-token
- SCRUTINY_WEB_INFLUXDB_ORG=homelab
- SCRUTINY_WEB_INFLUXDB_BUCKET=scrutiny
# Optional but highly recommended to notify you in case of a problem
- SCRUTINY_WEB_NOTIFY_URLS=["http://gotify:80/message?token=a-gotify-token"]
depends_on:
- influxdb
restart: unless-stopped
networks:
- notifications
- monitoring
```
A freshly initialized Scrutiny instance can be accessed on port 8080, eg. `192.168.0.100:8080`. The interface will be
empty because no metrics have been collected yet.
## Setting up a Spoke ***without*** Docker
![drawing-3-1671744208](https://user-images.githubusercontent.com/86809766/209230155-386a8644-b506-497f-8245-0d24e15c9063.png)
A spoke consists of the Scrutiny Collector binary that is run on a set interval via crontab and sends the data to the
Hub. The official
documentation [describes the manual setup of the Collector](https://github.com/AnalogJ/scrutiny/blob/master/docs/INSTALL_MANUAL.md#collector)
- dependencies and step by step commands. I have a shortened version that does the same thing but in one line of code.
```bash
# Installing dependencies
apt install smartmontools -y
# 1. Create directory for the binary
# 2. Download the binary into that directory
# 3. Make it exacutable
# 4. List the contents of the library for confirmation
mkdir -p /opt/scrutiny/bin && \
curl -L https://github.com/AnalogJ/scrutiny/releases/download/v0.5.0/scrutiny-collector-metrics-linux-amd64 > /opt/scrutiny/bin/scrutiny-collector-metrics-linux-amd64 && \
chmod +x /opt/scrutiny/bin/scrutiny-collector-metrics-linux-amd64 && \
ls -lha /opt/scrutiny/bin
```
<p class="callout warning">When downloading Github Release Assests, make sure that you have the correct version. The provided example is with Release v0.5.0. [The release list can be found here.](https://github.com/analogj/scrutiny/releases) </p>
Once the Collector is installed, you can run it with the following command. Make sure to add the correct address and
port of your Hub as `--api-endpoint`.
```bash
/opt/scrutiny/bin/scrutiny-collector-metrics-linux-amd64 run --api-endpoint "http://192.168.0.100:8080"
```
This will run the Collector once and populate the Web interface of your Scrutiny instance. In order to collect metrics
for a time series, you need to run the command repeatedly. Here is an example for crontab, running the Collector every
15min.
```bash
# open crontab
crontab -e
# add a line for Scrutiny
*/15 * * * * /opt/scrutiny/bin/scrutiny-collector-metrics-linux-amd64 run --api-endpoint "http://192.168.0.100:8080"
```
The Collector has its own independent config file that lives in `/opt/scrutiny/config/collector.yaml` but I did not find
a need to modify
it. [A default collector.yaml can be found in the official documentation.](https://github.com/AnalogJ/scrutiny/blob/master/example.collector.yaml)
## Setting up a Spoke ***with*** Docker
![drawing-3-1671744277](https://user-images.githubusercontent.com/86809766/209230176-87c9e55a-4e3e-4f5f-9609-335d41529f3d.png)
Setting up a remote Spoke in Docker requires you to split
the [official Hub-Spoke layout docker-compose.yml](https://github.com/AnalogJ/scrutiny/blob/master/docker/example.hubspoke.docker-compose.yml)
. In the following docker-compose you need to provide the `${API_ENDPOINT}`, in my case `http://192.168.0.100:8080`.
Also all drives that you wish to monitor need to be presented to the container under `devices`.
The image handles the periodic scanning of the drives.
```yaml
version: "3.4"
services:
collector:
image: 'ghcr.io/analogj/scrutiny:master-collector'
cap_add:
- SYS_RAWIO
volumes:
- '/run/udev:/run/udev:ro'
environment:
COLLECTOR_API_ENDPOINT: ${API_ENDPOINT}
devices:
- "/dev/sda"
- "/dev/sdb"
```
+59
View File
@@ -0,0 +1,59 @@
# Manual Windows Install
This guide is specifically for people who are on a Windows machine using [WSL](https://learn.microsoft.com/en-us/windows/wsl/about) with Docker.
Scrutiny is made up of three components: an influxdb Database, a collector and a webapp/api. Docker will be used for
the influxdb and webapp/API, the collector component will be facilitated by [Windows Task Scheduler](https://learn.microsoft.com/en-us/windows/win32/taskschd/task-scheduler-start-page).
> **NOTE:** If you are **NOT** using WSL with docker, then the easiest way to get started with [Scrutiny is the omnibus Docker image](https://github.com/AnalogJ/scrutiny#docker).
## InfluxDB and Webapp/API (Docker)
1. Copy the [example.hubspoke.docker-compose.yml](https://github.com/AnalogJ/scrutiny/blob/master/docker/example.hubspoke.docker-compose.yml)
file and delete the collector section near the bottom of the file.
2. Run `docker-compose up -d` to verify that the DB and webapp are working correctly and once its completed, your webapp
should be up and running but the dashboard will be empty (default location is `localhost:8080`)
## Collector (Windows Task Scheduler)
1. Download the latest `scrutiny-collector-metrics-windows-amd64.exe` from the [releases page](https://github.com/AnalogJ/scrutiny/releases) (under assets)
2. On your windows host, open [Windows Task Scheduler](https://www.wikihow.com/Open-Task-Scheduler-in-Windows-10) as **Administrator**
1. In the **Start Menu** (Windows key), type `Task Scheduler` and then right click `Run as Administrator` to open
3. On the status bar (under the `action` tab), click `Create Task...`
4. A new window should open with the `General` Tab open, enter relevant information into the `Name` and `Description` fields
1. Under **Security Options** check:
1. **Run whether user is logged on or not**
2. **Run with highest privileges**
5. Next, click the `Triggers` tab and then click `New...` (bottom left-hand side of the window)
6. Here you can set how often you want this task to run, example settings are the following:
1. **Settings:**
1. `Daily`, start at `TODAYS_DATE` `12:00:00 AM`, Recur every `1` days,
2. **Advanced Settings:**
1. Repeat Task every: `1 hour` for a duration of `Indefinitely`
2. Stop task if it runs longer than: `30 minutes`
3. Click Ok when satisfied with your schedule
> **NOTE:** The above settings will trigger the task **every day at midnight** and then **run every hour after that** (modify as needed)
7. Next, click the `Actions` tab and then click `New...` (bottom left-hand side of the window)
1. **Action Settings:**
1. In the **Program/Script** field, put: `scrutiny-collector-metrics-windows-amd64.exe`
2. In the **Add arguments (optional)** field, put: `run --api-endpoint "http://localhost:8080" --config collector.yaml`
> **NOTE:**
> * Make sure that you put the correct port number (as specified in the docker-compose file) for the webapp (default is `8080`)
> * The `--config` param is optional and is not needed if you just want to use the default collector config, see
[example.collector.yaml](https://github.com/AnalogJ/scrutiny/blob/master/example.collector.yaml) for more info on the collector config.
3. In the **Start in (optional)** field, put: FOLDER_PATH_TO_YOUR `scrutiny-collector-metrics-windows-amd64.exe` file
> **NOTE:** Must be exact and do not include `scrutiny-collector-metrics-windows-amd64.exe` in the path
4. Click Ok when finished
8. Next, click the `Conditions` tab and make sure that everything is unchecked (unless you want to specify otherwise)
9. Next, click the `Settings` tab and check everything except for the last checkbox
1. **Examples for the following settings:**
1. If the task fails, restart every: `5 minutes`
2. Attempt restart up to: `3` times
3. Stop the task if it runs longer than `1 hour`
10. Next, once satisfied with everything, click Ok
11. Then, find your newly created task (by its name) in the scheduler task list and then manually run it (right click it and then click `Run`)
12. Finally, refresh your dashboard after a minute or two and your drive information should have populated the webapp dashboard.
+6 -2
View File
@@ -91,9 +91,13 @@ wget https://raw.githubusercontent.com/smartmontools/smartmontools/master/smartm
```
#!/bin/bash
/volume1/\@Entware/scrutiny/bin/scrutiny-collector-metrics-linux-arm64 run --config /volume1/\@Entware/scrutiny/config/collector.yaml
/volume1/\@Entware/scrutiny/bin/scrutiny-collector-metrics-linux-arm64 run --config /volume1/\@Entware/scrutiny/conf/collector.yaml
```
**Make `run_collect.sh` executable**
`chmod +x /volume1/\@Entware/scrutiny/bin/run_collect.sh`
## Set up Synology to run a scheduled task.
Log in to DSM and do the following:
@@ -131,4 +135,4 @@ Frequency: <Your desired frequency>
## Troubleshooting
If you have any issues with your devices being detected, or incorrect data, please take a look at [TROUBLESHOOTING_DEVICE_COLLECTOR.md](./TROUBLESHOOTING_DEVICE_COLLECTOR.md)
If you have any issues with your devices being detected, or incorrect data, please take a look at [TROUBLESHOOTING_DEVICE_COLLECTOR.md](./TROUBLESHOOTING_DEVICE_COLLECTOR.md)
+5 -2
View File
@@ -7,12 +7,15 @@ in `docs/guides/` or elsewhere) it will be linked here.
- [x] [unraid](./INSTALL_UNRAID.md)
- [ ] ESXI
- [ ] Proxmox
- [x] [Synology](./INSTALL_SYNOLOGY_COLLECTOR.md)
- [x] Synology
- [Hub/Spoke Deployment - Collector](./INSTALL_SYNOLOGY_COLLECTOR.md)
- [Omnibus Deployment](https://drfrankenstein.co.uk/2022/07/28/scrutiny-in-docker-on-a-synology-nas)
- [ ] OMV
- [ ] Amahi
- [ ] Running in a LXC container
- [x] [PFSense](./INSTALL_UNRAID.md)
- [x] [PFSense](./INSTALL_PFSENSE.md)
- [x] QNAP
- [x] [RockStor](https://rockstor.com/docs/interface/docker-based-rock-ons/scrutiny.html)
- [ ] Solaris/OmniOS CE Support
- [ ] Kubernetes
- [x] [Windows](./INSTALL_MANUAL_WINDOWS.md)
+63 -8
View File
@@ -19,6 +19,25 @@ Scrutiny stores and references the devices by their `WWN` which is globally uniq
As such, passing devices to the Scrutiny collector container using `/dev/disk/by-id/`, `/dev/disk/by-label/`, `/dev/disk/by-path/` and `/dev/disk/by-uuid/`
paths are unnecessary, unless you'd like to ensure the docker run command never needs to change.
#### Force /dev/disk/by-id paths
Since Scrutiny uses WWN under the hood, it really doesn't care about `/dev/sd*` vs `/dev/disk/by-id/`. The problem is the interaction between docker and smartmontools when using `--device /dev/disk/by-id` paths.
Basically Scrutiny offloads all device detection to smartmontools, which doesn't seem to detect devices that have been passed into the docker container using `/dev/disk/by-id` paths.
If you must use "static" device references, you can map the host device id/uuid/wwn references to device names within the container:
```
# --device=<Host Device>:<Container Device Mapping>
docker run ....
--device=/dev/disk/by-id/wwn-0x5000xxxxx:/dev/sda
--device=/dev/disk/by-id/wwn-0x5001xxxxx:/dev/sdb
--device=/dev/disk/by-id/wwn-0x5003xxxxx:/dev/sdc
...
```
## Device Detection By Smartctl
@@ -61,7 +80,8 @@ using a collector config file. See [example.collector.yaml](/example.collector.y
> NOTE: If you use docker, you **must** pass though the RAID virtual disk to the container using `--device` (see below)
>
> This device may be in `/dev/*` or `/dev/bus/*`.
> This device may be in `/dev/*` or `/dev/bus/*`.
> If you do not see a virtual device file `/dev/bus/*` you may need to use the `--privileged` flag. See [#366 for more info](https://github.com/AnalogJ/scrutiny/issues/366#issuecomment-1253196407)
>
> If you're unsure, run `smartctl --scan` on your host, and pass all listed devices to the container.
@@ -92,7 +112,7 @@ devices:
type:
- aacraid,0,0,0
- aacraid,0,0,1
# HPE Smart Array example: https://github.com/AnalogJ/scrutiny/issues/213
- device: /dev/sda
type:
@@ -100,8 +120,11 @@ devices:
- 'cciss,1'
```
>
### NVMe Drives
As mentioned in the [README.md](/README.md), NVMe devices require both `--cap-add SYS_RAWIO` and `--cap-add SYS_ADMIN`
As mentioned in the [README.md](/README.md), NVMe devices require both `--cap-add SYS_RAWIO` and `--cap-add SYS_ADMIN`
to allow smartctl permission to query your NVMe device SMART data [#26](https://github.com/AnalogJ/scrutiny/issues/26)
When attaching NVMe devices using `--device=/dev/nvme..`, make sure to provide the device controller (`/dev/nvme0`)
@@ -252,10 +275,12 @@ to disable Scrutiny analysis for them. Both are non-critical, and have low-corre
If this is effecting your drives, you'll need to do the following:
1. Upgrade to v0.4.13+
2. Reset your drive status using the SQLite script in [#device-failed-but-smart--scrutiny-passed](https://github.com/AnalogJ/scrutiny/blob/master/docs/TROUBLESHOOTING_DEVICE_COLLECTOR.md#device-failed-but-smart--scrutiny-passed)
2. Reset your drive status using the SQLite script
in [#device-failed-but-smart--scrutiny-passed](https://github.com/AnalogJ/scrutiny/blob/master/docs/TROUBLESHOOTING_DEVICE_COLLECTOR.md#device-failed-but-smart--scrutiny-passed)
3. Wait for (or manually start) the collector.
If you'd like to learn more about how the Seagate Ironwolf SMART attributes work under the hood, and how they differ from
If you'd like to learn more about how the Seagate Ironwolf SMART attributes work under the hood, and how they differ
from
other drives, please read the following:
- http://www.users.on.net/~fzabkar/HDD/Seagate_SER_RRER_HEC.html
@@ -263,10 +288,23 @@ other drives, please read the following:
## Hub & Spoke model, with multiple Hosts.
When deploying Scrutiny in a hub & spoke model, it can be difficult to determine exactly which node a set of devices are associated with.
Thankfully the collector has a special `--host-id` flag (or `COLLECTOR_HOST_ID` env variable) that can be used to associate devices with a friendly host name.
![multiple-host-ids image](multiple-host-ids.png)
See the [docs/INSTALL_HUB_SPOKE.md](/docs/INSTALL_HUB_SPOKE.md) guide for more information.
When deploying Scrutiny in a hub & spoke model, it can be difficult to determine exactly which node a set of devices are
associated with.
Thankfully the collector has a special `--host-id` flag (or `COLLECTOR_HOST_ID` env variable) that can be used to
associate devices with a friendly host name.
The host-id is passed from the collector to the web-api when SMART device data is uploaded. There's 3 ways you can set
the host-id:
- using the collector config
file: [master/example.collector.yaml#L19-L22](https://github.com/AnalogJ/scrutiny/blob/master/example.collector.yaml?rgh-link-date=2022-05-25T15%3A08%3A56Z#L19-L22)
- using the `--host-id` collector CLI
argument: [master/collector/cmd/collector-metrics/collector-metrics.go#L180-L185](https://github.com/AnalogJ/scrutiny/blob/master/collector/cmd/collector-metrics/collector-metrics.go?rgh-link-date=2022-05-25T15%3A08%3A56Z#L180-L185)
- using the `COLLECTOR_HOST_ID` environmental variable.
See the [docs/INSTALL_HUB_SPOKE.md](/docs/INSTALL_HUB_SPOKE.md) guide for more information.
## Collector DEBUG mode
@@ -282,3 +320,20 @@ Or if you're not using docker, you can pass CLI arguments to the collector durin
```bash
scrutiny-collector-metrics run --debug --log-file /tmp/collector.log
```
## Collector trigger on startup
When the `omnibus` docker image starts up, it will automatically trigger the collector, which will populate the Scrutiny
Webui with your disks.
This is not the case when running the collector docker image in **hub/spoke** mode, as the collector and webui are
running in different containers (and potentially different host machines), so
the web container may not be ready for incoming connections. By default the container will only run the collector at the
time specified in the cron schedule.
You can force the collector to run on startup using the following env variables:
- `-e COLLECTOR_RUN_STARTUP=true` - forces the collector to run on startup (cron will be started after the collector
completes)
- `-e COLLECTOR_RUN_STARTUP_SLEEP=10` - if `COLLECTOR_RUN_STARTUP` is enabled, you can use this env variable to
configure the delay before the collector is run (default: `1` second). Used to ensure the web container has started
successfully.
+44 -2
View File
@@ -1,7 +1,19 @@
# InfluxDB Troubleshooting
## Installation
InfluxDB is a required dependency for Scrutiny v0.4.0+.
## Why??
Scrutiny has many features, but the relevant one to this conversation is the "S.M.A.R.T metric tracking for historical
trends". Basically Scrutiny not only shows you the current SMART values, but how they've changed over weeks, months (or
even years).
To efficiently handle that data at scale (and to make my life easier as a developer) I decided to add InfluxDB as a
dependency. It's a dedicated timeseries database, as opposed to the general purpose sqlite DB I used before. I also did
a bunch of testing and analysis before I made the change. With InfluxDB the memory footprint for Scrutiny (at idle) is ~
100mb, which is still fairly reasonable.
## Installation
InfluxDB is a required dependency for Scrutiny v0.4.0+.
https://docs.influxdata.com/influxdb/v2.2/install/
@@ -70,6 +82,7 @@ this usually related to either:
variables
- remove the `SCRUTINY_WEB=true` and `SCRUTINY_COLLECTOR=true` environmental variables. They were used by the LSIO
image, but are unnecessary and cause issues with the official Scrutiny image.
- Change your volume mappings to `/opt/scrutiny` from `/scrutiny`
- Updated versions of the [LSIO Scrutiny images are broken](https://github.com/linuxserver/docker-scrutiny/issues/22),
as they have not installed InfluxDB which is a required dependency of Scrutiny v0.4.x
- You can revert to an earlier version of the LSIO image (`lscr.io/linuxserver/scrutiny:060ac7b8-ls34`), or just
@@ -382,3 +395,32 @@ After running the Curl command above, you'll see a JSON response that looks like
You must copy the token field from the JSON response, and save it in your `scrutiny.yaml` config file. After that's
done, you can start the Scrutiny server
## Customize InfluxDB Admin Username & Password
The full set of InfluxDB configuration options are available
in [code](https://github.com/AnalogJ/scrutiny/blob/master/webapp/backend/pkg/config/config.go?rgh-link-date=2023-01-19T16%3A23%3A40Z#L49-L51)
.
During first startup Scrutiny will connect to the unprotected InfluxDB server, start the setup process (via API) using a
username and password of `admin`:`password12345` and then create an API token of `scrutiny-default-admin-token`.
After that's complete, it will use the api token for all subsequent communication with InfluxDB.
You can configure the values for the Admin username, password and token using the config file, or env variables:
#### Config File Example
```yaml
web:
influxdb:
token: 'my-custom-token'
init_username: 'my-custom-username'
init_password: 'my-custom-password'
```
#### Environmental Variables Example
`SCRUTINY_WEB_INFLUXDB_TOKEN` , `SCRUTINY_WEB_INFLUXDB_INIT_USERNAME` and `SCRUTINY_WEB_INFLUXDB_INIT_PASSWORD`
It's safe to change the InfluxDB Admin username/password after setup has completed, only the API token is used for
subsequent communication with InfluxDB.
+6
View File
@@ -21,5 +21,11 @@ SCRUTINY_DEVICE_NAME - eg. /dev/sda
SCRUTINY_DEVICE_TYPE - ATA/SCSI/NVMe
SCRUTINY_DEVICE_SERIAL - eg. WDDJ324KSO
SCRUTINY_MESSAGE - eg. "Scrutiny SMART error notification for device: %s\nFailure Type: %s\nDevice Name: %s\nDevice Serial: %s\nDevice Type: %s\nDate: %s"
SCRUTINY_HOST_ID - (optional) eg. "my-custom-host-id"
```
# Testing Notifications
You can test that your notifications are configured correctly by posting an empty payload to the notifications health check API.
```
curl -X POST http://localhost:8080/api/health/notify
```
+18
View File
@@ -0,0 +1,18 @@
# Operating systems without udev
Some operating systems do not come with `udev` out of the box, for example Alpine Linux. In these instances you will not be able to bind `/run/udev` to the container for sharing device metadata. Some operating systems offer `udev` as a package that can be installed separately, or an alternative (such as `eudev` in the case of Alpine Linux) that provides the same functionality.
To install `eudev` in Alpine Linux (run as root):
```
apk add eudev
setup-udev
```
Once your `udev` implementation is installed, create `/run/udev` with the following command:
```
udevadm trigger
```
On Alpine Linux, this also has the benefit of creating symlinks to device serial numbers in `/dev/disk/by-id`.
+70 -44
View File
@@ -1,62 +1,88 @@
// SQLite Table(s)
Table device {
created_at timestamp
wwn varchar [pk]
Table Device {
//GORM attributes, see: http://gorm.io/docs/conventions.html
CreatedAt time
UpdatedAt time
DeletedAt time
//user provided
label varchar
host_id varchar
WWN string
// smartctl provided
device_name varchar
manufacturer varchar
model_name varchar
interface_type varchar
interface_speed varchar
serial_number varchar
firmware varchar
rotational_speed varchar
capacity varchar
form_factor varchar
smart_support varchar
device_protocol varchar
device_type varchar
DeviceName string
DeviceUUID string
DeviceSerialID string
DeviceLabel string
Manufacturer string
ModelName string
InterfaceType string
InterfaceSpeed string
SerialNumber string
Firmware string
RotationSpeed int
Capacity int64
FormFactor string
SmartSupport bool
DeviceProtocol string//protocol determines which smart attribute types are available (ATA, NVMe, SCSI)
DeviceType string//device type is used for querying with -d/t flag, should only be used by collector.
// User provided metadata
Label string
HostId string
// Data set by Scrutiny
DeviceStatus enum
}
Table Setting {
//GORM attributes, see: http://gorm.io/docs/conventions.html
SettingKeyName string
SettingKeyDescription string
SettingDataType string
SettingValueNumeric int64
SettingValueString string
}
// InfluxDB Tables
Table device_temperature {
//timestamp
created_at timestamp
//tags (indexed & queryable)
device_wwn varchar [pk]
//fields
temp bigint
}
Table SmartTemperature {
Date time
DeviceWWN string //(tag)
Temp int64
}
Table smart_ata_results {
//timestamp
created_at timestamp
Table Smart {
Date time
DeviceWWN string //(tag)
DeviceProtocol string
//tags (indexed & queryable)
device_wwn varchar [pk]
smart_status varchar
scrutiny_status varchar
//Metrics (fields)
Temp int64
PowerOnHours int64
PowerCycleCount int64
//Smart Status
Status enum
//fields
temp bigint
power_on_hours bigint
power_cycle_count bigint
//SMART Attributes (fields)
Attr_ID_AttributeId int
Attr_ID_Value int64
Attr_ID_Threshold int64
Attr_ID_Worst int64
Attr_ID_RawValue int64
Attr_ID_RawString string
Attr_ID_WhenFailed string
//Generated data
Attr_ID_TransformedValue int64
Attr_ID_Status enum
Attr_ID_StatusReason string
Attr_ID_FailureRate float64
}
Ref: device.wwn < smart_ata_results.device_wwn
Ref: Device.WWN < Smart.DeviceWWN
Ref: Device.WWN < SmartTemperature.DeviceWWN
Binary file not shown.

After

Width:  |  Height:  |  Size: 78 KiB

+4
View File
@@ -31,6 +31,10 @@ devices:
# - device: /dev/sda
# type: 'sat'
#
# # example for using `-d sat,auto`, notice the square brackets (workaround for #418)
# - device: /dev/sda
# type: ['sat,auto']
#
# # example to show how to ignore a specific disk/device.
# - device: /dev/sda
# ignore: true
+5 -2
View File
@@ -47,6 +47,11 @@ web:
# org: 'my-org'
# bucket: 'bucket'
retention_policy: true
# if you wish to disable TLS certificate verification,
# when using self-signed certificates for example,
# then uncomment the lines below and set `insecure_skip_verify: true`
# tls:
# insecure_skip_verify: false
log:
file: '' #absolute or relative paths allowed, eg. web.log
@@ -73,8 +78,6 @@ log:
# - "join://shoutrrr:api-key@join/?devices=device1[,device2, ...][&icon=icon][&title=title]"
# - "script:///file/path/on/disk"
# - "https://www.example.com/path"
# filter_attributes: 'all' # options: 'all' or 'critical'
# level: 'fail' # options: 'fail', 'fail_scrutiny', 'fail_smart'
########################################################################################################################
# FEATURES COMING SOON
+5 -13
View File
@@ -1,10 +1,10 @@
module github.com/analogj/scrutiny
go 1.18
go 1.20
require (
github.com/analogj/go-util v0.0.0-20190301173314-5295e364eb14
github.com/containrrr/shoutrrr v0.4.4
github.com/containrrr/shoutrrr v0.6.1
github.com/fatih/color v1.10.0
github.com/gin-gonic/gin v1.6.3
github.com/glebarez/sqlite v1.4.5
@@ -24,7 +24,7 @@ require (
require (
github.com/StackExchange/wmi v0.0.0-20190523213315-cbe66965904d // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.0 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.1 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/deepmap/oapi-codegen v1.8.2 // indirect
github.com/fsnotify/fsnotify v1.4.9 // indirect
@@ -43,7 +43,6 @@ require (
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.4 // indirect
github.com/json-iterator/go v1.1.9 // indirect
github.com/klauspost/compress v1.11.7 // indirect
github.com/konsorten/go-windows-terminal-sequences v1.0.2 // indirect
github.com/kvz/logstreamer v0.0.0-20201023134116-02d20f4338f5 // indirect
github.com/leodido/go-urn v1.2.0 // indirect
@@ -53,14 +52,11 @@ require (
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.1 // indirect
github.com/nxadm/tail v1.4.6 // indirect
github.com/onsi/ginkgo v1.14.2 // indirect
github.com/pelletier/go-toml v1.7.0 // 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/russross/blackfriday/v2 v2.0.1 // indirect
github.com/shurcooL/sanitized_anchor_name v1.0.0 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/spf13/afero v1.2.2 // indirect
github.com/spf13/cast v1.3.1 // indirect
github.com/spf13/jwalterweatherman v1.1.0 // indirect
@@ -73,17 +69,13 @@ require (
golang.org/x/sys v0.0.0-20220405052023-b1e9470b6e64 // indirect
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1 // indirect
golang.org/x/text v0.3.5 // indirect
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
google.golang.org/protobuf v1.23.0 // indirect
gopkg.in/ini.v1 v1.55.0 // indirect
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect
gopkg.in/yaml.v2 v2.3.0 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect
gosrc.io/xmpp v0.5.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
nhooyr.io/websocket v1.8.6 // indirect
)
+9 -67
View File
@@ -18,7 +18,6 @@ github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAE
github.com/PuerkitoBio/goquery v1.5.1/go.mod h1:GsLWisAFVj4WgDibEWF4pvYnkVQBpKBKeU+7zCJoLcc=
github.com/StackExchange/wmi v0.0.0-20190523213315-cbe66965904d h1:G0m3OIz70MZUWq3EgK3CesDbo8upS2Vm9/P3FtgI+Jk=
github.com/StackExchange/wmi v0.0.0-20190523213315-cbe66965904d/go.mod h1:3eOhrUMpNV+6aFIbp5/iudMxNCF27Vw2OZgy4xEx0Fg=
github.com/agnivade/wasmbrowsertest v0.3.1/go.mod h1:zQt6ZTdl338xxRaMW395qccVE2eQm0SjC/SDz0mPWQI=
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/analogj/go-util v0.0.0-20190301173314-5295e364eb14 h1:wsrSjiqQtseStRIoLLxS4C5IEtXkazZVEPDHq8jW7r8=
@@ -33,18 +32,11 @@ github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+Ce
github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84=
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
github.com/chromedp/cdproto v0.0.0-20190614062957-d6d2f92b486d/go.mod h1:S8mB5wY3vV+vRIzf39xDXsw3XKYewW9X6rW2aEmkrSw=
github.com/chromedp/cdproto v0.0.0-20190621002710-8cbd498dd7a0/go.mod h1:S8mB5wY3vV+vRIzf39xDXsw3XKYewW9X6rW2aEmkrSw=
github.com/chromedp/cdproto v0.0.0-20190812224334-39ef923dcb8d/go.mod h1:0YChpVzuLJC5CPr+x3xkHN6Z8KOSXjNbL7qV8Wc4GW0=
github.com/chromedp/cdproto v0.0.0-20190926234355-1b4886c6fad6/go.mod h1:0YChpVzuLJC5CPr+x3xkHN6Z8KOSXjNbL7qV8Wc4GW0=
github.com/chromedp/chromedp v0.3.1-0.20190619195644-fd957a4d2901/go.mod h1:mJdvfrVn594N9tfiPecUidF6W5jPRKHymqHfzbobPsM=
github.com/chromedp/chromedp v0.4.0/go.mod h1:DC3QUn4mJ24dwjcaGQLoZrhm4X/uPHZ6spDbS2uFhm4=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ=
github.com/containrrr/shoutrrr v0.4.4 h1:vHZ4E/76pKVY+Jyn/qhBz3X540Bn8NI5ppPHK4PyILY=
github.com/containrrr/shoutrrr v0.4.4/go.mod h1:zqL2BvfC1W4FujrT4b3/ZCLxvD+uoeEpBL7rg9Dqpbg=
github.com/containrrr/shoutrrr v0.6.1 h1:6ih7jA6mo3t6C97MZbd3SxL/kRizOE3bI9CpBQZ6wzg=
github.com/containrrr/shoutrrr v0.6.1/go.mod h1:ye9jGX5YzMnJ76waaNVWlJ4luhMEyt1EWU5unYTQSb0=
github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk=
github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
github.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
@@ -52,8 +44,8 @@ github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7
github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
github.com/cpuguy83/go-md2man/v2 v2.0.0 h1:EoUDS0afbrsXAZ9YQ9jdu/mZ2sXgT1/2yyNng4PGlyM=
github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
github.com/cpuguy83/go-md2man/v2 v2.0.1 h1:r/myEWzV9lfsM1tFLgDyu0atFtJ1fXn261LKYj/3DxU=
github.com/cpuguy83/go-md2man/v2 v2.0.1/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY=
github.com/cyberdelia/templates v0.0.0-20141128023046-ca7fffd4298c/go.mod h1:GyV+0YP4qX0UQ7r2MoYZ+AvYDp12OF5yg4q8rGnyNh4=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@@ -66,8 +58,6 @@ github.com/denisenkom/go-mssqldb v0.0.0-20200428022330-06a60b6afbbc/go.mod h1:xb
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
github.com/edsrzf/mmap-go v1.0.0/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M=
github.com/fatih/color v1.6.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
github.com/fatih/color v1.10.0 h1:s36xzo75JdqLaaWoiEHk767eHiwo0598uUxyfiPkDsg=
github.com/fatih/color v1.10.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM=
@@ -89,8 +79,6 @@ github.com/go-chi/chi/v5 v5.0.0/go.mod h1:BBug9lr0cqtdAhsu6R4AAdvufI0/XBzAQSsUqJ
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
github.com/go-gormigrate/gormigrate/v2 v2.0.0 h1:e2A3Uznk4viUC4UuemuVgsNnvYZyOA8B3awlYk3UioU=
github.com/go-gormigrate/gormigrate/v2 v2.0.0/go.mod h1:YuVJ+D/dNt4HWrThTBnjgZuRbt7AuwINeg4q52ZE3Jw=
github.com/go-interpreter/wagon v0.5.1-0.20190713202023-55a163980b6c/go.mod h1:5+b/MBYkclRZngKF5s6qrgWxSLgE9F5dFdO1hAueZLc=
github.com/go-interpreter/wagon v0.6.0/go.mod h1:5+b/MBYkclRZngKF5s6qrgWxSLgE9F5dFdO1hAueZLc=
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
@@ -109,12 +97,6 @@ github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GO
github.com/go-sql-driver/mysql v1.5.0 h1:ozyZYNQW3x3HtqT1jira07DN2PArx2v7/mN66gGcHOs=
github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee h1:s+21KNqlpePfkah2I+gwHF8xmJWRjooY+5248k6m4A0=
github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo=
github.com/gobwas/pool v0.2.0 h1:QEmUOlnSjWtnpRGHF3SauEiOsy82Cup83Vf2LcMlnc8=
github.com/gobwas/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
github.com/gobwas/ws v1.0.2 h1:CoAavW/wd/kulfZmSIBt6p24n4j7tHgNVCjsfHVNUbo=
github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM=
github.com/gofrs/uuid v3.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
@@ -131,7 +113,6 @@ github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5y
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
@@ -152,10 +133,7 @@ github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/pprof v0.0.0-20190908185732-236ed259b199/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.1.5/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
@@ -164,8 +142,6 @@ github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGa
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc=
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
@@ -272,10 +248,6 @@ github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7V
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/klauspost/compress v1.10.3/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
github.com/klauspost/compress v1.11.7 h1:0hzRabrMN4tSTvMfnL3SCv1ZGeAP23ynzodBgaHeMeg=
github.com/klauspost/compress v1.11.7/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
github.com/knq/sysutil v0.0.0-20181215143952-f05b59f0f307/go.mod h1:BjPj+aVjl9FW/cCGiF3nGh5v+9Gd3VCgBQbod/GlMaQ=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/konsorten/go-windows-terminal-sequences v1.0.2 h1:DB17ag19krx9CFsz4o3enTrPXyIXCl+2iCXH/aMAp9s=
github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
@@ -295,14 +267,10 @@ github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/lib/pq v1.1.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/lib/pq v1.3.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
github.com/magiconair/properties v1.8.1 h1:ZC2Vc7/ZFkGmsVC9KvOjumD+G5lXy2RtTKyzRKO2BQ4=
github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
github.com/mailru/easyjson v0.0.0-20190403194419-1ea4449da983/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/mailru/easyjson v0.0.0-20190620125010-da37f6c1e481/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/mailru/easyjson v0.7.0/go.mod h1:KAzv3t3aY1NaHWoQz1+4F1ccyAH66Jk7yos7ldAVICs=
github.com/matryer/moq v0.0.0-20190312154309-6cfb0558e1bd/go.mod h1:9ELz6aaclSIGnZBoaSLZ3NAl1VTufbOrXBPvtcy6WiQ=
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ=
@@ -348,11 +316,9 @@ github.com/nxadm/tail v1.4.6 h1:11TGpSHY7Esh/i/qnq02Jo5oVrI1Gue8Slbq0ujPZFQ=
github.com/nxadm/tail v1.4.6/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.8.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
github.com/onsi/ginkgo v1.14.2 h1:8mVmC9kjFFmA8H4pKMUhcblgifdkOIXPvbhN1T36q1M=
github.com/onsi/ginkgo v1.14.2/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY=
github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
github.com/onsi/gomega v1.10.1 h1:o0+MgICZLuZ7xjH7Vx6zS/zcu93/BEp1VwkIW1mEXCE=
github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
@@ -383,8 +349,9 @@ github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFR
github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ=
github.com/rs/zerolog v1.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU=
github.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThCjNc=
github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q=
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
github.com/samber/lo v1.25.0 h1:H8F6cB0RotRdgcRCivTByAQePaYhGMdOTJIj2QFS2I0=
github.com/samber/lo v1.25.0/go.mod h1:2I7tgIv8Q1SG2xEIkRq0F2i2zgxVpnyPOP0d3Gj2r+A=
@@ -392,9 +359,7 @@ github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdh
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4=
github.com/shopspring/decimal v0.0.0-20200227202807-02e2044944cc/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo=
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
github.com/sirupsen/logrus v1.0.5/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc=
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=
github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4=
@@ -412,16 +377,14 @@ github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkU
github.com/spf13/cast v1.3.1 h1:nFm6S0SMdyzrzcmThSipiEubIDy8WEXKNZ0UOgiRpng=
github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ=
github.com/spf13/cobra v0.0.7/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHNrgE=
github.com/spf13/cobra v1.4.0/go.mod h1:Wo4iy3BUC+X2Fybo0PDqwJIv3dNRiZLHQymsfxlB84g=
github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk=
github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo=
github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
github.com/spf13/pflag v1.0.2/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE=
github.com/spf13/viper v1.6.3/go.mod h1:jUMtyi0/lB5yZH/FjyGAoH7IMNrIhlBf6pXZmbMDvzw=
github.com/spf13/viper v1.7.0 h1:xVKxvI7ouOI5I+U9s2eeiUfMaWBVoXA3AWskkrqK0VM=
github.com/spf13/viper v1.7.0/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg=
@@ -438,8 +401,6 @@ github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s
github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
github.com/thoas/go-funk v0.9.1 h1:O549iLZqPpTUQ10ykd26sZhzD+rmR5pWhuElrhbC20M=
github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
github.com/twitchyliquid64/golang-asm v0.0.0-20190126203739-365674df15fc/go.mod h1:NoCfSFWosfqMqmmD7hApkirIK9ozpHjxRnRxs1l413A=
github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc=
github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw=
github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs=
github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY=
@@ -452,7 +413,6 @@ github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q=
go.coder.com/go-tools v0.0.0-20190317003359-0c6a35b74a16/go.mod h1:iKV5yK9t+J5nG9O3uF6KYdPEz3dyfMyB15MN1rbQ8Qw=
go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
@@ -464,7 +424,6 @@ go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKY
go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA=
go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
golang.org/x/crypto v0.0.0-20180426230345-b49d69b5da94/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
@@ -506,7 +465,6 @@ golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73r
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181102091132-c10e9556a7bc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@@ -536,7 +494,6 @@ golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9 h1:SQFwaSi55rU7vdNs9Yr0Z324VNlrF+0wMqRXT4St8ck=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@@ -548,7 +505,6 @@ golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5h
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190306220234-b354f8bf4d9e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -556,13 +512,10 @@ golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190618155005-516e3c20635f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190712062909-fae7ac547cb7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190927073244-c990c680b611/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -591,7 +544,6 @@ golang.org/x/text v0.3.5 h1:i6eZZ+zk0SOf0xgBpEpPD18qWcJda6q1sxt3S0kzyUQ=
golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20201208040808-7e3f01d25324/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
@@ -611,7 +563,6 @@ golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgw
golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20190823170909-c4a336ef6a2f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20190920225731-5eefd052ad72/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
@@ -655,14 +606,12 @@ google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miE
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
google.golang.org/protobuf v1.23.0 h1:4MY060fB1DLGMB/7MBTLnwQUY6+F09GEiz6SsrNqyzM=
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
gopkg.in/airbrake/gobrake.v2 v2.0.9/go.mod h1:/h5ZAUhDkGaJfjzjKLSjv6zCL6O0LLBxU4K+aSYdM/U=
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
gopkg.in/gemnasium/logrus-airbrake-hook.v2 v2.1.2/go.mod h1:Xk6kEKp8OKb+X14hQBKWaSkCsqBpgog8nAV2xsGOxlo=
gopkg.in/inconshreveable/log15.v2 v2.0.0-20180818164646-67afb5ed74ec/go.mod h1:aPpfJ7XW+gOuirDoZ8gHhLh3kZ1B08FtV2bbmy7Jv3s=
gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/ini.v1 v1.55.0 h1:E8yzL5unfpW3M6fz/eB7Cb5MQAYSZ7GKo4Qth+N2sgQ=
@@ -675,8 +624,9 @@ gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU=
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
@@ -692,10 +642,6 @@ gorm.io/gorm v1.9.19/go.mod h1:0HFTzE/SqkGTzK6TlDPPQbAYCluiVvhzoA1+aVyzenw=
gorm.io/gorm v1.20.0/go.mod h1:0HFTzE/SqkGTzK6TlDPPQbAYCluiVvhzoA1+aVyzenw=
gorm.io/gorm v1.23.5 h1:TnlF26wScKSvknUC/Rn8t0NLLM22fypYBlvj1+aH6dM=
gorm.io/gorm v1.23.5/go.mod h1:l2lP/RyAtc1ynaTjFksBde/O8v9oOGIApu2/xRitmZk=
gosrc.io/xmpp v0.5.1 h1:Rgrm5s2rt+npGggJH3HakQxQXR8ZZz3+QRzakRQqaq4=
gosrc.io/xmpp v0.5.1/go.mod h1:L3NFMqYOxyLz3JGmgFyWf7r9htE91zVGiK40oW4RwdY=
gotest.tools v2.1.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw=
gotest.tools/gotestsum v0.3.5/go.mod h1:Mnf3e5FUzXbkCfynWBGOwLssY7gTQgCHObK9tMpAriY=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
@@ -728,10 +674,6 @@ modernc.org/strutil v1.1.1/go.mod h1:DE+MQQ/hjKBZS2zNInV5hhcipt5rLPWkmpbGeW5mmdw
modernc.org/tcl v1.13.1/go.mod h1:XOLfOwzhkljL4itZkK6T72ckMgvj0BDsnKNdZVUOecw=
modernc.org/token v1.0.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
modernc.org/z v1.5.1/go.mod h1:eWFB510QWW5Th9YGZT81s+LwvaAs3Q2yr4sP0rmLkv8=
mvdan.cc/sh v2.6.4+incompatible/go.mod h1:IeeQbZq+x2SUGBensq/jge5lLQbS3XT2ktyp3wrt4x8=
nhooyr.io/websocket v1.6.5/go.mod h1:F259lAzPRAH0htX2y3ehpJe09ih1aSHN7udWki1defY=
nhooyr.io/websocket v1.8.6 h1:s+C3xAMLwGmlI31Nyn/eAehUlZPwfYZu2JXM621Q5/k=
nhooyr.io/websocket v1.8.6/go.mod h1:B70DZP8IakI65RVQ51MsWP/8jndNma26DVA/nFSCgW0=
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
+41 -2
View File
@@ -1,12 +1,15 @@
package main
import (
"encoding/json"
"fmt"
"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"
log "github.com/sirupsen/logrus"
"github.com/sirupsen/logrus"
"io"
"log"
"os"
"time"
@@ -31,6 +34,7 @@ func main() {
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))
os.Exit(1)
}
@@ -107,7 +111,18 @@ OPTIONS:
config.Set("log.file", c.String("log-file"))
}
webServer := web.AppEngine{Config: config}
webLogger, logFile, err := CreateLogger(config)
if logFile != nil {
defer logFile.Close()
}
if err != nil {
return err
}
settingsData, err := json.Marshal(config.AllSettings())
webLogger.Debug(string(settingsData), err)
webServer := web.AppEngine{Config: config, Logger: webLogger}
return webServer.Start()
},
@@ -140,3 +155,27 @@ OPTIONS:
}
}
func CreateLogger(appConfig config.Interface) (*logrus.Entry, *os.File, error) {
logger := logrus.WithFields(logrus.Fields{
"type": "web",
})
//set default log level
if level, err := logrus.ParseLevel(appConfig.GetString("log.level")); err == nil {
logger.Logger.SetLevel(level)
} else {
logger.Logger.SetLevel(logrus.InfoLevel)
}
var logFile *os.File
var err error
if appConfig.IsSet("log.file") && len(appConfig.GetString("log.file")) > 0 {
logFile, err = os.OpenFile(appConfig.GetString("log.file"), os.O_CREATE|os.O_WRONLY, 0644)
if err != nil {
logger.Logger.Errorf("Failed to open log file %s for output: %s", appConfig.GetString("log.file"), err)
return nil, logFile, err
}
logger.Logger.SetOutput(io.MultiWriter(os.Stderr, logFile))
}
return logger, logFile, nil
}
+27 -33
View File
@@ -2,7 +2,6 @@ package config
import (
"github.com/analogj/go-util/utils"
"github.com/analogj/scrutiny/webapp/backend/pkg"
"github.com/analogj/scrutiny/webapp/backend/pkg/errors"
"github.com/spf13/viper"
"log"
@@ -10,6 +9,8 @@ import (
"strings"
)
const DB_USER_SETTINGS_SUBKEY = "user"
// When initializing this class the following methods must be called:
// Config.New
// Config.Init
@@ -39,8 +40,6 @@ func (c *configuration) Init() error {
c.SetDefault("log.file", "")
c.SetDefault("notify.urls", []string{})
c.SetDefault("notify.filter_attributes", pkg.NotifyFilterAttributesAll)
c.SetDefault("notify.level", pkg.NotifyLevelFail)
c.SetDefault("web.influxdb.scheme", "http")
c.SetDefault("web.influxdb.host", "localhost")
@@ -50,22 +49,12 @@ func (c *configuration) Init() error {
c.SetDefault("web.influxdb.init_username", "admin")
c.SetDefault("web.influxdb.init_password", "password12345")
c.SetDefault("web.influxdb.token", "scrutiny-default-admin-token")
c.SetDefault("web.influxdb.tls.insecure_skip_verify", false)
c.SetDefault("web.influxdb.retention_policy", true)
//c.SetDefault("disks.include", []string{})
//c.SetDefault("disks.exclude", []string{})
//c.SetDefault("notify.metric.script", "/opt/scrutiny/config/notify-metrics.sh")
//c.SetDefault("notify.long.script", "/opt/scrutiny/config/notify-long-test.sh")
//c.SetDefault("notify.short.script", "/opt/scrutiny/config/notify-short-test.sh")
//c.SetDefault("collect.metric.enable", true)
//c.SetDefault("collect.metric.command", "-a -o on -S on")
//c.SetDefault("collect.long.enable", true)
//c.SetDefault("collect.long.command", "-a -o on -S on")
//c.SetDefault("collect.short.enable", true)
//c.SetDefault("collect.short.command", "-a -o on -S on")
//if you want to load a non-standard location system config file (~/drawbridge.yml), use ReadConfig
c.SetConfigType("yaml")
//c.SetConfigName("drawbridge")
@@ -77,7 +66,18 @@ func (c *configuration) Init() error {
c.AutomaticEnv()
//CLI options will be added via the `Set()` function
return nil
return c.ValidateConfig()
}
func (c *configuration) SubKeys(key string) []string {
return c.Sub(key).AllKeys()
}
func (c *configuration) Sub(key string) Interface {
config := configuration{
Viper: c.Viper.Sub(key),
}
return &config
}
func (c *configuration) ReadConfig(configFilePath string) error {
@@ -120,24 +120,18 @@ func (c *configuration) ReadConfig(configFilePath string) error {
// This function ensures that the merged config works correctly.
func (c *configuration) ValidateConfig() error {
////deserialize Questions
//questionsMap := map[string]Question{}
//err := c.UnmarshalKey("questions", &questionsMap)
//
//if err != nil {
// log.Printf("questions could not be deserialized correctly. %v", err)
// return err
//}
//
//for _, v := range questionsMap {
//
// typeContent, ok := v.Schema["type"].(string)
// if !ok || len(typeContent) == 0 {
// return errors.QuestionSyntaxError("`type` is required for questions")
// }
//}
//
//
//the following keys are deprecated, and no longer supported
/*
- notify.filter_attributes (replaced by metrics.status.filter_attributes SETTING)
- notify.level (replaced by metrics.notify.level and metrics.status.threshold SETTING)
*/
//TODO add docs and upgrade doc.
if c.IsSet("notify.filter_attributes") {
return errors.ConfigValidationError("`notify.filter_attributes` configuration option is deprecated. Replaced by option in Dashboard Settings page")
}
if c.IsSet("notify.level") {
return errors.ConfigValidationError("`notify.level` configuration option is deprecated. Replaced by option in Dashboard Settings page")
}
return nil
}
+34
View File
@@ -0,0 +1,34 @@
package config
import (
"github.com/spf13/viper"
"github.com/stretchr/testify/require"
"testing"
)
func Test_MergeConfigMap(t *testing.T) {
//setup
testConfig := configuration{
Viper: viper.New(),
}
testConfig.Set("user.dashboard_display", "hello")
testConfig.SetDefault("user.layout", "hello")
mergeSettings := map[string]interface{}{
"user": map[string]interface{}{
"dashboard_display": "dashboard_display",
"layout": "layout",
},
}
//test
err := testConfig.MergeConfigMap(mergeSettings)
//verify
require.NoError(t, err)
// if using Set, the MergeConfigMap functionality will not override
// if using SetDefault, the MergeConfigMap will override correctly
require.Equal(t, "hello", testConfig.GetString("user.dashboard_display"))
require.Equal(t, "layout", testConfig.GetString("user.layout"))
}
+5
View File
@@ -12,12 +12,17 @@ type Interface interface {
WriteConfig() error
Set(key string, value interface{})
SetDefault(key string, value interface{})
MergeConfigMap(cfg map[string]interface{}) error
Sub(key string) Interface
AllSettings() map[string]interface{}
AllKeys() []string
SubKeys(key string) []string
IsSet(key string) bool
Get(key string) interface{}
GetBool(key string) bool
GetInt(key string) int
GetInt64(key string) int64
GetString(key string) string
GetStringSlice(key string) []string
UnmarshalKey(key string, rawVal interface{}, decoderOpts ...viper.DecoderConfigOption) error
@@ -7,6 +7,7 @@ package mock_config
import (
reflect "reflect"
config "github.com/analogj/scrutiny/webapp/backend/pkg/config"
gomock "github.com/golang/mock/gomock"
viper "github.com/spf13/viper"
)
@@ -34,6 +35,20 @@ func (m *MockInterface) EXPECT() *MockInterfaceMockRecorder {
return m.recorder
}
// AllKeys mocks base method.
func (m *MockInterface) AllKeys() []string {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "AllKeys")
ret0, _ := ret[0].([]string)
return ret0
}
// AllKeys indicates an expected call of AllKeys.
func (mr *MockInterfaceMockRecorder) AllKeys() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AllKeys", reflect.TypeOf((*MockInterface)(nil).AllKeys))
}
// AllSettings mocks base method.
func (m *MockInterface) AllSettings() map[string]interface{} {
m.ctrl.T.Helper()
@@ -90,6 +105,20 @@ func (mr *MockInterfaceMockRecorder) GetInt(key interface{}) *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetInt", reflect.TypeOf((*MockInterface)(nil).GetInt), key)
}
// GetInt64 mocks base method.
func (m *MockInterface) GetInt64(key string) int64 {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetInt64", key)
ret0, _ := ret[0].(int64)
return ret0
}
// GetInt64 indicates an expected call of GetInt64.
func (mr *MockInterfaceMockRecorder) GetInt64(key interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetInt64", reflect.TypeOf((*MockInterface)(nil).GetInt64), key)
}
// GetString mocks base method.
func (m *MockInterface) GetString(key string) string {
m.ctrl.T.Helper()
@@ -146,6 +175,20 @@ func (mr *MockInterfaceMockRecorder) IsSet(key interface{}) *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IsSet", reflect.TypeOf((*MockInterface)(nil).IsSet), key)
}
// MergeConfigMap mocks base method.
func (m *MockInterface) MergeConfigMap(cfg map[string]interface{}) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "MergeConfigMap", cfg)
ret0, _ := ret[0].(error)
return ret0
}
// MergeConfigMap indicates an expected call of MergeConfigMap.
func (mr *MockInterfaceMockRecorder) MergeConfigMap(cfg interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "MergeConfigMap", reflect.TypeOf((*MockInterface)(nil).MergeConfigMap), cfg)
}
// ReadConfig mocks base method.
func (m *MockInterface) ReadConfig(configFilePath string) error {
m.ctrl.T.Helper()
@@ -184,6 +227,34 @@ func (mr *MockInterfaceMockRecorder) SetDefault(key, value interface{}) *gomock.
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetDefault", reflect.TypeOf((*MockInterface)(nil).SetDefault), key, value)
}
// Sub mocks base method.
func (m *MockInterface) Sub(key string) config.Interface {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Sub", key)
ret0, _ := ret[0].(config.Interface)
return ret0
}
// Sub indicates an expected call of Sub.
func (mr *MockInterfaceMockRecorder) Sub(key interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Sub", reflect.TypeOf((*MockInterface)(nil).Sub), key)
}
// SubKeys mocks base method.
func (m *MockInterface) SubKeys(key string) []string {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "SubKeys", key)
ret0, _ := ret[0].([]string)
return ret0
}
// SubKeys indicates an expected call of SubKeys.
func (mr *MockInterfaceMockRecorder) SubKeys(key interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SubKeys", reflect.TypeOf((*MockInterface)(nil).SubKeys), key)
}
// UnmarshalKey mocks base method.
func (m *MockInterface) UnmarshalKey(key string, rawVal interface{}, decoderOpts ...viper.DecoderConfigOption) error {
m.ctrl.T.Helper()
+30 -9
View File
@@ -4,17 +4,11 @@ const DeviceProtocolAta = "ATA"
const DeviceProtocolScsi = "SCSI"
const DeviceProtocolNvme = "NVMe"
const NotifyFilterAttributesAll = "all"
const NotifyFilterAttributesCritical = "critical"
const NotifyLevelFail = "fail"
const NotifyLevelFailScrutiny = "fail_scrutiny"
const NotifyLevelFailSmart = "fail_smart"
//go:generate stringer -type=AttributeStatus
// AttributeStatus bitwise flag, 1,2,4,8,16,32,etc
type AttributeStatus uint8
const (
// AttributeStatusPassed binary, 1,2,4,8,16,32,etc
AttributeStatusPassed AttributeStatus = 0
AttributeStatusFailedSmart AttributeStatus = 1
AttributeStatusWarningScrutiny AttributeStatus = 2
@@ -30,9 +24,10 @@ func AttributeStatusToggle(b, flag AttributeStatus) AttributeStatus { return b ^
func AttributeStatusHas(b, flag AttributeStatus) bool { return b&flag != 0 }
//go:generate stringer -type=DeviceStatus
// DeviceStatus bitwise flag, 1,2,4,8,16,32,etc
type DeviceStatus uint8
const (
// DeviceStatusPassed binary, 1,2,4,8,16,32,etc
DeviceStatusPassed DeviceStatus = 0
DeviceStatusFailedSmart DeviceStatus = 1
DeviceStatusFailedScrutiny DeviceStatus = 2
@@ -42,3 +37,29 @@ func DeviceStatusSet(b, flag DeviceStatus) DeviceStatus { return b | flag }
func DeviceStatusClear(b, flag DeviceStatus) DeviceStatus { return b &^ flag }
func DeviceStatusToggle(b, flag DeviceStatus) DeviceStatus { return b ^ flag }
func DeviceStatusHas(b, flag DeviceStatus) bool { return b&flag != 0 }
// Metrics Specific Filtering & Threshold Constants
type MetricsNotifyLevel int64
const (
MetricsNotifyLevelWarn MetricsNotifyLevel = 1
MetricsNotifyLevelFail MetricsNotifyLevel = 2
)
type MetricsStatusFilterAttributes int64
const (
MetricsStatusFilterAttributesAll MetricsStatusFilterAttributes = 0
MetricsStatusFilterAttributesCritical MetricsStatusFilterAttributes = 1
)
// MetricsStatusThreshold bitwise flag, 1,2,4,8,16,32,etc
type MetricsStatusThreshold int64
const (
MetricsStatusThresholdSmart MetricsStatusThreshold = 1
MetricsStatusThresholdScrutiny MetricsStatusThreshold = 2
//shortcut
MetricsStatusThresholdBoth MetricsStatusThreshold = 3
)
+4 -3
View File
@@ -10,9 +10,7 @@ import (
type DeviceRepo interface {
Close() error
//GetSettings()
//SaveSetting()
HealthCheck(ctx context.Context) error
RegisterDevice(ctx context.Context, dev models.Device) error
GetDevices(ctx context.Context) ([]models.Device, error)
@@ -28,4 +26,7 @@ type DeviceRepo interface {
GetSummary(ctx context.Context) (map[string]*models.DeviceSummary, error)
GetSmartTemperatureHistory(ctx context.Context, durationKey string) (map[string][]measurements.SmartTemperature, error)
LoadSettings(ctx context.Context) (*models.Settings, error)
SaveSettings(ctx context.Context, settings models.Settings) error
}
@@ -0,0 +1,18 @@
package m20220716214900
import (
"gorm.io/gorm"
)
type Setting struct {
//GORM attributes, see: http://gorm.io/docs/conventions.html
gorm.Model
SettingKeyName string `json:"setting_key_name"`
SettingKeyDescription string `json:"setting_key_description"`
SettingDataType string `json:"setting_data_type"`
SettingValueNumeric int `json:"setting_value_numeric"`
SettingValueString string `json:"setting_value_string"`
SettingValueBool bool `json:"setting_value_bool"`
}
@@ -2,6 +2,7 @@ package database
import (
"context"
"crypto/tls"
"encoding/json"
"fmt"
"github.com/analogj/scrutiny/webapp/backend/pkg/config"
@@ -62,7 +63,20 @@ func NewScrutinyRepository(appConfig config.Interface, globalLogger logrus.Field
// Gorm/SQLite setup
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
globalLogger.Infof("Trying to connect to scrutiny sqlite db: %s\n", appConfig.GetString("web.database.location"))
database, err := gorm.Open(sqlite.Open(appConfig.GetString("web.database.location")), &gorm.Config{
// When a transaction cannot lock the database, because it is already locked by another one,
// SQLite by default throws an error: database is locked. This behavior is usually not appropriate when
// concurrent access is needed, typically when multiple processes write to the same database.
// PRAGMA busy_timeout lets you set a timeout or a handler for these events. When setting a timeout,
// SQLite will try the transaction multiple times within this timeout.
// fixes #341
// https://rsqlite.r-dbi.org/reference/sqlitesetbusyhandler
// retrying for 30000 milliseconds, 30seconds - this would be unreasonable for a distributed multi-tenant application,
// but should be fine for local usage.
pragmaStr := sqlitePragmaString(map[string]string{
"busy_timeout": "30000",
})
database, err := gorm.Open(sqlite.Open(appConfig.GetString("web.database.location")+pragmaStr), &gorm.Config{
//TODO: figure out how to log database queries again.
//Logger: logger
DisableForeignKeyConstraintWhenMigrating: true,
@@ -82,11 +96,20 @@ func NewScrutinyRepository(appConfig config.Interface, globalLogger logrus.Field
influxdbUrl := fmt.Sprintf("%s://%s:%s", appConfig.GetString("web.influxdb.scheme"), appConfig.GetString("web.influxdb.host"), appConfig.GetString("web.influxdb.port"))
globalLogger.Debugf("InfluxDB url: %s", influxdbUrl)
client := influxdb2.NewClient(influxdbUrl, appConfig.GetString("web.influxdb.token"))
tlsConfig := &tls.Config{
InsecureSkipVerify: appConfig.GetBool("web.influxdb.tls.insecure_skip_verify"),
}
globalLogger.Infof("InfluxDB certificate verification: %t\n", !tlsConfig.InsecureSkipVerify)
client := influxdb2.NewClientWithOptions(
influxdbUrl,
appConfig.GetString("web.influxdb.token"),
influxdb2.DefaultOptions().SetTLSConfig(tlsConfig),
)
//if !appConfig.IsSet("web.influxdb.token") {
globalLogger.Debugf("Determine Influxdb setup status...")
influxSetupComplete, err := InfluxSetupComplete(influxdbUrl)
influxSetupComplete, err := InfluxSetupComplete(influxdbUrl, tlsConfig)
if err != nil {
return nil, fmt.Errorf("failed to check influxdb setup status - %w", err)
}
@@ -182,7 +205,30 @@ func (sr *scrutinyRepository) Close() error {
return nil
}
func InfluxSetupComplete(influxEndpoint string) (bool, error) {
func (sr *scrutinyRepository) HealthCheck(ctx context.Context) error {
//check influxdb
status, err := sr.influxClient.Health(ctx)
if err != nil {
return fmt.Errorf("influxdb healthcheck failed: %w", err)
}
if status.Status != "pass" {
return fmt.Errorf("influxdb healthcheckf failed: status=%s", status.Status)
}
//check sqlite db.
database, err := sr.gormClient.DB()
if err != nil {
return fmt.Errorf("sqlite healthcheck failed: %w", err)
}
err = database.Ping()
if err != nil {
return fmt.Errorf("sqlite healthcheck failed during ping: %w", err)
}
return nil
}
func InfluxSetupComplete(influxEndpoint string, tlsConfig *tls.Config) (bool, error) {
influxUri, err := url.Parse(influxEndpoint)
if err != nil {
return false, err
@@ -192,7 +238,8 @@ func InfluxSetupComplete(influxEndpoint string) (bool, error) {
return false, err
}
res, err := http.Get(influxUri.String())
client := &http.Client{Transport: &http.Transport{TLSClientConfig: tlsConfig}}
res, err := client.Get(influxUri.String())
if err != nil {
return false, err
}
@@ -450,3 +497,16 @@ func (sr *scrutinyRepository) lookupNestedDurationKeys(durationKey string) []str
}
return []string{DURATION_KEY_WEEK}
}
func sqlitePragmaString(pragmas map[string]string) string {
q := url.Values{}
for key, val := range pragmas {
q.Add("_pragma", key+"="+val)
}
queryStr := q.Encode()
if len(queryStr) > 0 {
return "?" + queryStr
}
return ""
}
@@ -4,9 +4,11 @@ import (
"context"
"errors"
"fmt"
"github.com/analogj/scrutiny/webapp/backend/pkg"
"github.com/analogj/scrutiny/webapp/backend/pkg/database/migrations/m20201107210306"
"github.com/analogj/scrutiny/webapp/backend/pkg/database/migrations/m20220503120000"
"github.com/analogj/scrutiny/webapp/backend/pkg/database/migrations/m20220509170100"
"github.com/analogj/scrutiny/webapp/backend/pkg/database/migrations/m20220716214900"
"github.com/analogj/scrutiny/webapp/backend/pkg/models"
"github.com/analogj/scrutiny/webapp/backend/pkg/models/collector"
"github.com/analogj/scrutiny/webapp/backend/pkg/models/measurements"
@@ -275,6 +277,98 @@ func (sr *scrutinyRepository) Migrate(ctx context.Context) error {
return tx.Where("wwn = ?", "").Delete(&models.Device{}).Error
},
},
{
ID: "m20220716214900", // add settings table.
Migrate: func(tx *gorm.DB) error {
// adding the settings table.
err := tx.AutoMigrate(m20220716214900.Setting{})
if err != nil {
return err
}
//add defaults.
var defaultSettings = []m20220716214900.Setting{
{
SettingKeyName: "theme",
SettingKeyDescription: "Frontend theme ('light' | 'dark' | 'system')",
SettingDataType: "string",
SettingValueString: "system", // options: 'light' | 'dark' | 'system'
},
{
SettingKeyName: "layout",
SettingKeyDescription: "Frontend layout ('material')",
SettingDataType: "string",
SettingValueString: "material",
},
{
SettingKeyName: "dashboard_display",
SettingKeyDescription: "Frontend device display title ('name' | 'serial_id' | 'uuid' | 'label')",
SettingDataType: "string",
SettingValueString: "name",
},
{
SettingKeyName: "dashboard_sort",
SettingKeyDescription: "Frontend device sort by ('status' | 'title' | 'age')",
SettingDataType: "string",
SettingValueString: "status",
},
{
SettingKeyName: "temperature_unit",
SettingKeyDescription: "Frontend temperature unit ('celsius' | 'fahrenheit')",
SettingDataType: "string",
SettingValueString: "celsius",
},
{
SettingKeyName: "file_size_si_units",
SettingKeyDescription: "File size in SI units (true | false)",
SettingDataType: "bool",
SettingValueBool: false,
},
{
SettingKeyName: "line_stroke",
SettingKeyDescription: "Temperature chart line stroke ('smooth' | 'straight' | 'stepline')",
SettingDataType: "string",
SettingValueString: "smooth",
},
{
SettingKeyName: "metrics.notify_level",
SettingKeyDescription: "Determines which device status will cause a notification (fail or warn)",
SettingDataType: "numeric",
SettingValueNumeric: int(pkg.MetricsNotifyLevelFail), // options: 'fail' or 'warn'
},
{
SettingKeyName: "metrics.status_filter_attributes",
SettingKeyDescription: "Determines which attributes should impact device status",
SettingDataType: "numeric",
SettingValueNumeric: int(pkg.MetricsStatusFilterAttributesAll), // options: 'all' or 'critical'
},
{
SettingKeyName: "metrics.status_threshold",
SettingKeyDescription: "Determines which threshold should impact device status",
SettingDataType: "numeric",
SettingValueNumeric: int(pkg.MetricsStatusThresholdBoth), // options: 'scrutiny', 'smart', 'both'
},
}
return tx.Create(&defaultSettings).Error
},
},
{
ID: "m20221115214900", // add line_stroke setting.
Migrate: func(tx *gorm.DB) error {
//add line_stroke setting default.
var defaultSettings = []m20220716214900.Setting{
{
SettingKeyName: "line_stroke",
SettingKeyDescription: "Temperature chart line stroke ('smooth' | 'straight' | 'stepline')",
SettingDataType: "string",
SettingValueString: "smooth",
},
}
return tx.Create(&defaultSettings).Error
},
},
})
if err := m.Migrate(); err != nil {
@@ -282,6 +376,30 @@ func (sr *scrutinyRepository) Migrate(ctx context.Context) error {
return err
}
sr.logger.Infoln("Database migration completed successfully")
//these migrations cannot be done within a transaction, so they are done as a separate group, with `UseTransaction = false`
sr.logger.Infoln("SQLite global configuration migrations starting. Please wait....")
globalMigrateOptions := gormigrate.DefaultOptions
globalMigrateOptions.UseTransaction = false
gm := gormigrate.New(sr.gormClient, globalMigrateOptions, []*gormigrate.Migration{
{
ID: "g20220802211500",
Migrate: func(tx *gorm.DB) error {
//shrink the Database (maybe necessary after 20220503113100)
if err := tx.Exec("VACUUM;").Error; err != nil {
return err
}
return nil
},
},
})
if err := gm.Migrate(); err != nil {
sr.logger.Errorf("SQLite global configuration migrations failed with error. \n Please open a github issue at https://github.com/AnalogJ/scrutiny and attach a copy of your scrutiny.db file. \n %v", err)
return err
}
sr.logger.Infoln("SQLite global configuration migrations completed successfully")
return nil
}
@@ -0,0 +1,85 @@
package database
import (
"context"
"fmt"
"github.com/analogj/scrutiny/webapp/backend/pkg/config"
"github.com/analogj/scrutiny/webapp/backend/pkg/models"
"github.com/mitchellh/mapstructure"
"strings"
)
// 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)
}
// store retrieved settings in the AppConfig obj
for _, settingsEntry := range settingsEntries {
configKey := fmt.Sprintf("%s.%s", config.DB_USER_SETTINGS_SUBKEY, settingsEntry.SettingKeyName)
if settingsEntry.SettingDataType == "numeric" {
sr.appConfig.SetDefault(configKey, settingsEntry.SettingValueNumeric)
} else if settingsEntry.SettingDataType == "string" {
sr.appConfig.SetDefault(configKey, settingsEntry.SettingValueString)
} else if settingsEntry.SettingDataType == "bool" {
sr.appConfig.SetDefault(configKey, settingsEntry.SettingValueBool)
}
}
// unmarshal the dbsetting object data to a settings object.
var settings models.Settings
err := sr.appConfig.UnmarshalKey(config.DB_USER_SETTINGS_SUBKEY, &settings)
if err != nil {
return nil, err
}
return &settings, nil
}
// testing
// curl -d '{"metrics": { "notify_level": 5, "status_filter_attributes": 5, "status_threshold": 5 }}' -H "Content-Type: application/json" -X POST http://localhost:9090/api/settings
// SaveSettings will update settings in AppConfig object, then save the settings to the database.
func (sr *scrutinyRepository) SaveSettings(ctx context.Context, settings models.Settings) error {
//save the entries to the appconfig
settingsMap := &map[string]interface{}{}
err := mapstructure.Decode(settings, &settingsMap)
if err != nil {
return err
}
settingsWrapperMap := map[string]interface{}{}
settingsWrapperMap[config.DB_USER_SETTINGS_SUBKEY] = *settingsMap
err = sr.appConfig.MergeConfigMap(settingsWrapperMap)
if err != nil {
return err
}
sr.logger.Debugf("after merge settings: %v", sr.appConfig.AllSettings())
//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)
}
//update settingsEntries
for ndx, settingsEntry := range settingsEntries {
configKey := fmt.Sprintf("%s.%s", config.DB_USER_SETTINGS_SUBKEY, strings.ToLower(settingsEntry.SettingKeyName))
if settingsEntry.SettingDataType == "numeric" {
settingsEntries[ndx].SettingValueNumeric = sr.appConfig.GetInt(configKey)
} else if settingsEntry.SettingDataType == "string" {
settingsEntries[ndx].SettingValueString = sr.appConfig.GetString(configKey)
} else if settingsEntry.SettingDataType == "bool" {
settingsEntries[ndx].SettingValueBool = sr.appConfig.GetBool(configKey)
}
// store in database.
//TODO: this should be `sr.gormClient.Updates(&settingsEntries).Error`
err := sr.gormClient.Model(&models.SettingEntry{}).Where([]uint{settingsEntry.ID}).Select("setting_value_numeric", "setting_value_string", "setting_value_bool").Updates(settingsEntries[ndx]).Error
if err != nil {
return err
}
}
return nil
}
@@ -17,6 +17,10 @@ func (sr *scrutinyRepository) SaveSmartTemperature(ctx context.Context, wwn stri
if len(collectorSmartData.AtaSctTemperatureHistory.Table) > 0 {
for ndx, temp := range collectorSmartData.AtaSctTemperatureHistory.Table {
//temp value may be null, we must skip/ignore them. See #393
if temp == 0 {
continue
}
minutesOffset := collectorSmartData.AtaSctTemperatureHistory.LoggingIntervalMinutes * int64(ndx) * 60
smartTemp := measurements.SmartTemperature{
-5
View File
@@ -1,5 +0,0 @@
package models
// Temperature Format
// Date Format
// Device History window
@@ -0,0 +1,23 @@
package models
import (
"gorm.io/gorm"
)
// SettingEntry matches a setting row in the database
type SettingEntry struct {
//GORM attributes, see: http://gorm.io/docs/conventions.html
gorm.Model
SettingKeyName string `json:"setting_key_name" gorm:"unique;not null"`
SettingKeyDescription string `json:"setting_key_description"`
SettingDataType string `json:"setting_data_type"`
SettingValueNumeric int `json:"setting_value_numeric"`
SettingValueString string `json:"setting_value_string"`
SettingValueBool bool `json:"setting_value_bool"`
}
func (s SettingEntry) TableName() string {
return "settings"
}
+24
View File
@@ -0,0 +1,24 @@
package models
// Settings is made up of parsed SettingEntry objects retrieved from the database
//type Settings struct {
// MetricsNotifyLevel pkg.MetricsNotifyLevel `json:"metrics.notify.level" mapstructure:"metrics.notify.level"`
// MetricsStatusFilterAttributes pkg.MetricsStatusFilterAttributes `json:"metrics.status.filter_attributes" mapstructure:"metrics.status.filter_attributes"`
// MetricsStatusThreshold pkg.MetricsStatusThreshold `json:"metrics.status.threshold" mapstructure:"metrics.status.threshold"`
//}
type Settings struct {
Theme string `json:"theme" mapstructure:"theme"`
Layout string `json:"layout" mapstructure:"layout"`
DashboardDisplay string `json:"dashboard_display" mapstructure:"dashboard_display"`
DashboardSort string `json:"dashboard_sort" mapstructure:"dashboard_sort"`
TemperatureUnit string `json:"temperature_unit" mapstructure:"temperature_unit"`
FileSizeSIUnits bool `json:"file_size_si_units" mapstructure:"file_size_si_units"`
LineStroke string `json:"line_stroke" mapstructure:"line_stroke"`
Metrics struct {
NotifyLevel int `json:"notify_level" mapstructure:"notify_level"`
StatusFilterAttributes int `json:"status_filter_attributes" mapstructure:"status_filter_attributes"`
StatusThreshold int `json:"status_threshold" mapstructure:"status_threshold"`
} `json:"metrics" mapstructure:"metrics"`
}
+52 -25
View File
@@ -29,20 +29,22 @@ const NotifyFailureTypeSmartFailure = "SmartFailure"
const NotifyFailureTypeScrutinyFailure = "ScrutinyFailure"
// ShouldNotify check if the error Message should be filtered (level mismatch or filtered_attributes)
func ShouldNotify(device models.Device, smartAttrs measurements.Smart, notifyLevel string, notifyFilterAttributes string) bool {
func ShouldNotify(device models.Device, smartAttrs measurements.Smart, statusThreshold pkg.MetricsStatusThreshold, statusFilterAttributes pkg.MetricsStatusFilterAttributes) bool {
// 1. check if the device is healthy
if device.DeviceStatus == pkg.DeviceStatusPassed {
return false
}
//TODO: cannot check for warning notifyLevel yet.
// setup constants for comparison
var requiredDeviceStatus pkg.DeviceStatus
var requiredAttrStatus pkg.AttributeStatus
if notifyLevel == pkg.NotifyLevelFail {
if statusThreshold == pkg.MetricsStatusThresholdBoth {
// either scrutiny or smart failures should trigger an email
requiredDeviceStatus = pkg.DeviceStatusSet(pkg.DeviceStatusFailedSmart, pkg.DeviceStatusFailedScrutiny)
requiredAttrStatus = pkg.AttributeStatusSet(pkg.AttributeStatusFailedSmart, pkg.AttributeStatusFailedScrutiny)
} else if notifyLevel == pkg.NotifyLevelFailSmart {
} else if statusThreshold == pkg.MetricsStatusThresholdSmart {
//only smart failures
requiredDeviceStatus = pkg.DeviceStatusFailedSmart
requiredAttrStatus = pkg.AttributeStatusFailedSmart
@@ -53,9 +55,9 @@ func ShouldNotify(device models.Device, smartAttrs measurements.Smart, notifyLev
// 2. check if the attributes that are failing should be filtered (non-critical)
// 3. for any unfiltered attribute, store the failure reason (Smart or Scrutiny)
if notifyFilterAttributes == pkg.NotifyFilterAttributesCritical {
if statusFilterAttributes == pkg.MetricsStatusFilterAttributesCritical {
hasFailingCriticalAttr := false
var statusFailingCrtiticalAttr pkg.AttributeStatus
var statusFailingCriticalAttr pkg.AttributeStatus
for attrId, attrData := range smartAttrs.Attributes {
//find failing attribute
@@ -64,7 +66,7 @@ func ShouldNotify(device models.Device, smartAttrs measurements.Smart, notifyLev
}
// merge the status's of all critical attributes
statusFailingCrtiticalAttr = pkg.AttributeStatusSet(statusFailingCrtiticalAttr, attrData.GetStatus())
statusFailingCriticalAttr = pkg.AttributeStatusSet(statusFailingCriticalAttr, attrData.GetStatus())
//found a failing attribute, see if its critical
if device.IsScsi() && thresholds.ScsiMetadata[attrId].Critical {
@@ -89,7 +91,7 @@ func ShouldNotify(device models.Device, smartAttrs measurements.Smart, notifyLev
return false
} else {
// check if any of the critical attributes have a status that we're looking for
return pkg.AttributeStatusHas(statusFailingCrtiticalAttr, requiredAttrStatus)
return pkg.AttributeStatusHas(statusFailingCriticalAttr, requiredAttrStatus)
}
} else {
@@ -99,12 +101,13 @@ func ShouldNotify(device models.Device, smartAttrs measurements.Smart, notifyLev
}
}
// TODO: include host and/or user label for device.
// TODO: include user label for device.
type Payload struct {
DeviceType string `json:"device_type"` //ATA/SCSI/NVMe
DeviceName string `json:"device_name"` //dev/sda
DeviceSerial string `json:"device_serial"` //WDDJ324KSO
Test bool `json:"test"` // false
HostId string `json:"host_id,omitempty"` //host id (optional)
DeviceType string `json:"device_type"` //ATA/SCSI/NVMe
DeviceName string `json:"device_name"` //dev/sda
DeviceSerial string `json:"device_serial"` //WDDJ324KSO
Test bool `json:"test"` // false
//private, populated during init (marked as Public for JSON serialization)
Date string `json:"date"` //populated by Send function.
@@ -113,8 +116,9 @@ type Payload struct {
Message string `json:"message"`
}
func NewPayload(device models.Device, test bool) Payload {
func NewPayload(device models.Device, test bool, currentTime ...time.Time) Payload {
payload := Payload{
HostId: strings.TrimSpace(device.HostId),
DeviceType: device.DeviceType,
DeviceName: device.DeviceName,
DeviceSerial: device.SerialNumber,
@@ -122,7 +126,13 @@ func NewPayload(device models.Device, test bool) Payload {
}
//validate that the Payload is populated
sendDate := time.Now()
var sendDate time.Time
if currentTime != nil && len(currentTime) > 0 {
sendDate = currentTime[0]
} else {
sendDate = time.Now()
}
payload.Date = sendDate.Format(time.RFC3339)
payload.FailureType = payload.GenerateFailureType(device.DeviceStatus)
payload.Subject = payload.GenerateSubject()
@@ -146,25 +156,39 @@ func (p *Payload) GenerateFailureType(deviceStatus pkg.DeviceStatus) string {
func (p *Payload) GenerateSubject() string {
//generate a detailed failure message
return fmt.Sprintf("Scrutiny SMART error (%s) detected on device: %s", p.FailureType, p.DeviceName)
var subject string
if len(p.HostId) > 0 {
subject = fmt.Sprintf("Scrutiny SMART error (%s) detected on [host]device: [%s]%s", p.FailureType, p.HostId, p.DeviceName)
} else {
subject = fmt.Sprintf("Scrutiny SMART error (%s) detected on device: %s", p.FailureType, p.DeviceName)
}
return subject
}
func (p *Payload) GenerateMessage() string {
//generate a detailed failure message
message := fmt.Sprintf(
`Scrutiny SMART error notification for device: %s
Failure Type: %s
Device Name: %s
Device Serial: %s
Device Type: %s
Date: %s`, p.DeviceName, p.FailureType, p.DeviceName, p.DeviceSerial, p.DeviceType, p.Date)
messageParts := []string{}
if p.Test {
message = "TEST NOTIFICATION:\n" + message
messageParts = append(messageParts, fmt.Sprintf("Scrutiny SMART error notification for device: %s", p.DeviceName))
if len(p.HostId) > 0 {
messageParts = append(messageParts, fmt.Sprintf("Host Id: %s", p.HostId))
}
return message
messageParts = append(messageParts,
fmt.Sprintf("Failure Type: %s", p.FailureType),
fmt.Sprintf("Device Name: %s", p.DeviceName),
fmt.Sprintf("Device Serial: %s", p.DeviceSerial),
fmt.Sprintf("Device Type: %s", p.DeviceType),
"",
fmt.Sprintf("Date: %s", p.Date),
)
if p.Test {
messageParts = append([]string{"TEST NOTIFICATION:"}, messageParts...)
}
return strings.Join(messageParts, "\n")
}
func New(logger logrus.FieldLogger, appconfig config.Interface, device models.Device, test bool) Notify {
@@ -285,6 +309,9 @@ func (n *Notify) SendScriptNotification(scriptUrl string) error {
copyEnv = append(copyEnv, fmt.Sprintf("SCRUTINY_DEVICE_TYPE=%s", n.Payload.DeviceType))
copyEnv = append(copyEnv, fmt.Sprintf("SCRUTINY_DEVICE_SERIAL=%s", n.Payload.DeviceSerial))
copyEnv = append(copyEnv, fmt.Sprintf("SCRUTINY_MESSAGE=%s", n.Payload.Message))
if len(n.Payload.HostId) > 0 {
copyEnv = append(copyEnv, fmt.Sprintf("SCRUTINY_HOST_ID=%s", n.Payload.HostId))
}
err := utils.CmdExec(scriptPath, []string{}, "", copyEnv, "")
if err != nil {
n.Logger.Errorf("An error occurred while executing script %s: %v", scriptPath, err)
+118 -35
View File
@@ -1,11 +1,13 @@
package notify
import (
"fmt"
"github.com/analogj/scrutiny/webapp/backend/pkg"
"github.com/analogj/scrutiny/webapp/backend/pkg/models"
"github.com/analogj/scrutiny/webapp/backend/pkg/models/measurements"
"github.com/stretchr/testify/require"
"testing"
"time"
)
func TestShouldNotify_MustSkipPassingDevices(t *testing.T) {
@@ -15,56 +17,56 @@ func TestShouldNotify_MustSkipPassingDevices(t *testing.T) {
DeviceStatus: pkg.DeviceStatusPassed,
}
smartAttrs := measurements.Smart{}
notifyLevel := pkg.NotifyLevelFail
notifyFilterAttributes := pkg.NotifyFilterAttributesAll
statusThreshold := pkg.MetricsStatusThresholdBoth
notifyFilterAttributes := pkg.MetricsStatusFilterAttributesAll
//assert
require.False(t, ShouldNotify(device, smartAttrs, notifyLevel, notifyFilterAttributes))
require.False(t, ShouldNotify(device, smartAttrs, statusThreshold, notifyFilterAttributes))
}
func TestShouldNotify_NotifyLevelFail_FailingSmartDevice(t *testing.T) {
func TestShouldNotify_MetricsStatusThresholdBoth_FailingSmartDevice(t *testing.T) {
t.Parallel()
//setup
device := models.Device{
DeviceStatus: pkg.DeviceStatusFailedSmart,
}
smartAttrs := measurements.Smart{}
notifyLevel := pkg.NotifyLevelFail
notifyFilterAttributes := pkg.NotifyFilterAttributesAll
statusThreshold := pkg.MetricsStatusThresholdBoth
notifyFilterAttributes := pkg.MetricsStatusFilterAttributesAll
//assert
require.True(t, ShouldNotify(device, smartAttrs, notifyLevel, notifyFilterAttributes))
require.True(t, ShouldNotify(device, smartAttrs, statusThreshold, notifyFilterAttributes))
}
func TestShouldNotify_NotifyLevelFailSmart_FailingSmartDevice(t *testing.T) {
func TestShouldNotify_MetricsStatusThresholdSmart_FailingSmartDevice(t *testing.T) {
t.Parallel()
//setup
device := models.Device{
DeviceStatus: pkg.DeviceStatusFailedSmart,
}
smartAttrs := measurements.Smart{}
notifyLevel := pkg.NotifyLevelFailSmart
notifyFilterAttributes := pkg.NotifyFilterAttributesAll
statusThreshold := pkg.MetricsStatusThresholdSmart
notifyFilterAttributes := pkg.MetricsStatusFilterAttributesAll
//assert
require.True(t, ShouldNotify(device, smartAttrs, notifyLevel, notifyFilterAttributes))
require.True(t, ShouldNotify(device, smartAttrs, statusThreshold, notifyFilterAttributes))
}
func TestShouldNotify_NotifyLevelFailScrutiny_FailingSmartDevice(t *testing.T) {
func TestShouldNotify_MetricsStatusThresholdScrutiny_FailingSmartDevice(t *testing.T) {
t.Parallel()
//setup
device := models.Device{
DeviceStatus: pkg.DeviceStatusFailedSmart,
}
smartAttrs := measurements.Smart{}
notifyLevel := pkg.NotifyLevelFailScrutiny
notifyFilterAttributes := pkg.NotifyFilterAttributesAll
statusThreshold := pkg.MetricsStatusThresholdScrutiny
notifyFilterAttributes := pkg.MetricsStatusFilterAttributesAll
//assert
require.False(t, ShouldNotify(device, smartAttrs, notifyLevel, notifyFilterAttributes))
require.False(t, ShouldNotify(device, smartAttrs, statusThreshold, notifyFilterAttributes))
}
func TestShouldNotify_NotifyFilterAttributesCritical_WithCriticalAttrs(t *testing.T) {
func TestShouldNotify_MetricsStatusFilterAttributesCritical_WithCriticalAttrs(t *testing.T) {
t.Parallel()
//setup
device := models.Device{
@@ -75,14 +77,14 @@ func TestShouldNotify_NotifyFilterAttributesCritical_WithCriticalAttrs(t *testin
Status: pkg.AttributeStatusFailedSmart,
},
}}
notifyLevel := pkg.NotifyLevelFail
notifyFilterAttributes := pkg.NotifyFilterAttributesCritical
statusThreshold := pkg.MetricsStatusThresholdBoth
notifyFilterAttributes := pkg.MetricsStatusFilterAttributesCritical
//assert
require.True(t, ShouldNotify(device, smartAttrs, notifyLevel, notifyFilterAttributes))
require.True(t, ShouldNotify(device, smartAttrs, statusThreshold, notifyFilterAttributes))
}
func TestShouldNotify_NotifyFilterAttributesCritical_WithMultipleCriticalAttrs(t *testing.T) {
func TestShouldNotify_MetricsStatusFilterAttributesCritical_WithMultipleCriticalAttrs(t *testing.T) {
t.Parallel()
//setup
device := models.Device{
@@ -96,14 +98,14 @@ func TestShouldNotify_NotifyFilterAttributesCritical_WithMultipleCriticalAttrs(t
Status: pkg.AttributeStatusFailedScrutiny,
},
}}
notifyLevel := pkg.NotifyLevelFail
notifyFilterAttributes := pkg.NotifyFilterAttributesCritical
statusThreshold := pkg.MetricsStatusThresholdBoth
notifyFilterAttributes := pkg.MetricsStatusFilterAttributesCritical
//assert
require.True(t, ShouldNotify(device, smartAttrs, notifyLevel, notifyFilterAttributes))
require.True(t, ShouldNotify(device, smartAttrs, statusThreshold, notifyFilterAttributes))
}
func TestShouldNotify_NotifyFilterAttributesCritical_WithNoCriticalAttrs(t *testing.T) {
func TestShouldNotify_MetricsStatusFilterAttributesCritical_WithNoCriticalAttrs(t *testing.T) {
t.Parallel()
//setup
device := models.Device{
@@ -114,14 +116,14 @@ func TestShouldNotify_NotifyFilterAttributesCritical_WithNoCriticalAttrs(t *test
Status: pkg.AttributeStatusFailedSmart,
},
}}
notifyLevel := pkg.NotifyLevelFail
notifyFilterAttributes := pkg.NotifyFilterAttributesCritical
statusThreshold := pkg.MetricsStatusThresholdBoth
notifyFilterAttributes := pkg.MetricsStatusFilterAttributesCritical
//assert
require.False(t, ShouldNotify(device, smartAttrs, notifyLevel, notifyFilterAttributes))
require.False(t, ShouldNotify(device, smartAttrs, statusThreshold, notifyFilterAttributes))
}
func TestShouldNotify_NotifyFilterAttributesCritical_WithNoFailingCriticalAttrs(t *testing.T) {
func TestShouldNotify_MetricsStatusFilterAttributesCritical_WithNoFailingCriticalAttrs(t *testing.T) {
t.Parallel()
//setup
device := models.Device{
@@ -132,14 +134,14 @@ func TestShouldNotify_NotifyFilterAttributesCritical_WithNoFailingCriticalAttrs(
Status: pkg.AttributeStatusPassed,
},
}}
notifyLevel := pkg.NotifyLevelFail
notifyFilterAttributes := pkg.NotifyFilterAttributesCritical
statusThreshold := pkg.MetricsStatusThresholdBoth
notifyFilterAttributes := pkg.MetricsStatusFilterAttributesCritical
//assert
require.False(t, ShouldNotify(device, smartAttrs, notifyLevel, notifyFilterAttributes))
require.False(t, ShouldNotify(device, smartAttrs, statusThreshold, notifyFilterAttributes))
}
func TestShouldNotify_NotifyFilterAttributesCritical_NotifyLevelFailSmart_WithCriticalAttrsFailingScrutiny(t *testing.T) {
func TestShouldNotify_MetricsStatusFilterAttributesCritical_MetricsStatusThresholdSmart_WithCriticalAttrsFailingScrutiny(t *testing.T) {
t.Parallel()
//setup
device := models.Device{
@@ -153,9 +155,90 @@ func TestShouldNotify_NotifyFilterAttributesCritical_NotifyLevelFailSmart_WithCr
Status: pkg.AttributeStatusFailedScrutiny,
},
}}
notifyLevel := pkg.NotifyLevelFailSmart
notifyFilterAttributes := pkg.NotifyFilterAttributesCritical
statusThreshold := pkg.MetricsStatusThresholdSmart
notifyFilterAttributes := pkg.MetricsStatusFilterAttributesCritical
//assert
require.False(t, ShouldNotify(device, smartAttrs, notifyLevel, notifyFilterAttributes))
require.False(t, ShouldNotify(device, smartAttrs, statusThreshold, notifyFilterAttributes))
}
func TestNewPayload(t *testing.T) {
t.Parallel()
//setup
device := models.Device{
SerialNumber: "FAKEWDDJ324KSO",
DeviceType: pkg.DeviceProtocolAta,
DeviceName: "/dev/sda",
DeviceStatus: pkg.DeviceStatusFailedScrutiny,
}
currentTime := time.Now()
//test
payload := NewPayload(device, false, currentTime)
//assert
require.Equal(t, "Scrutiny SMART error (ScrutinyFailure) detected on device: /dev/sda", payload.Subject)
require.Equal(t, fmt.Sprintf(`Scrutiny SMART error notification for device: /dev/sda
Failure Type: ScrutinyFailure
Device Name: /dev/sda
Device Serial: FAKEWDDJ324KSO
Device Type: ATA
Date: %s`, currentTime.Format(time.RFC3339)), payload.Message)
}
func TestNewPayload_TestMode(t *testing.T) {
t.Parallel()
//setup
device := models.Device{
SerialNumber: "FAKEWDDJ324KSO",
DeviceType: pkg.DeviceProtocolAta,
DeviceName: "/dev/sda",
DeviceStatus: pkg.DeviceStatusFailedScrutiny,
}
currentTime := time.Now()
//test
payload := NewPayload(device, true, currentTime)
//assert
require.Equal(t, "Scrutiny SMART error (EmailTest) detected on device: /dev/sda", payload.Subject)
require.Equal(t, fmt.Sprintf(`TEST NOTIFICATION:
Scrutiny SMART error notification for device: /dev/sda
Failure Type: EmailTest
Device Name: /dev/sda
Device Serial: FAKEWDDJ324KSO
Device Type: ATA
Date: %s`, currentTime.Format(time.RFC3339)), payload.Message)
}
func TestNewPayload_WithHostId(t *testing.T) {
t.Parallel()
//setup
device := models.Device{
SerialNumber: "FAKEWDDJ324KSO",
DeviceType: pkg.DeviceProtocolAta,
DeviceName: "/dev/sda",
DeviceStatus: pkg.DeviceStatusFailedScrutiny,
HostId: "custom-host",
}
currentTime := time.Now()
//test
payload := NewPayload(device, false, currentTime)
//assert
require.Equal(t, "Scrutiny SMART error (ScrutinyFailure) detected on [host]device: [custom-host]/dev/sda", payload.Subject)
require.Equal(t, fmt.Sprintf(`Scrutiny SMART error notification for device: /dev/sda
Host Id: custom-host
Failure Type: ScrutinyFailure
Device Name: /dev/sda
Device Serial: FAKEWDDJ324KSO
Device Type: ATA
Date: %s`, currentTime.Format(time.RFC3339)), payload.Message)
}
+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.4.16"
const VERSION = "0.7.0"
@@ -8,7 +8,7 @@ import (
)
func DeleteDevice(c *gin.Context) {
logger := c.MustGet("LOGGER").(logrus.FieldLogger)
logger := c.MustGet("LOGGER").(*logrus.Entry)
deviceRepo := c.MustGet("DEVICE_REPOSITORY").(database.DeviceRepo)
err := deviceRepo.DeleteDevice(c, c.Param("wwn"))
@@ -9,7 +9,7 @@ import (
)
func GetDeviceDetails(c *gin.Context) {
logger := c.MustGet("LOGGER").(logrus.FieldLogger)
logger := c.MustGet("LOGGER").(*logrus.Entry)
deviceRepo := c.MustGet("DEVICE_REPOSITORY").(database.DeviceRepo)
device, err := deviceRepo.GetDeviceDetails(c, c.Param("wwn"))
@@ -8,7 +8,7 @@ import (
)
func GetDevicesSummary(c *gin.Context) {
logger := c.MustGet("LOGGER").(logrus.FieldLogger)
logger := c.MustGet("LOGGER").(*logrus.Entry)
deviceRepo := c.MustGet("DEVICE_REPOSITORY").(database.DeviceRepo)
summary, err := deviceRepo.GetSummary(c)
@@ -8,7 +8,7 @@ import (
)
func GetDevicesSummaryTempHistory(c *gin.Context) {
logger := c.MustGet("LOGGER").(logrus.FieldLogger)
logger := c.MustGet("LOGGER").(*logrus.Entry)
deviceRepo := c.MustGet("DEVICE_REPOSITORY").(database.DeviceRepo)
durationKey, exists := c.GetQuery("duration_key")
@@ -0,0 +1,25 @@
package handler
import (
"github.com/analogj/scrutiny/webapp/backend/pkg/database"
"github.com/gin-gonic/gin"
"github.com/sirupsen/logrus"
"net/http"
)
func GetSettings(c *gin.Context) {
logger := c.MustGet("LOGGER").(*logrus.Entry)
deviceRepo := c.MustGet("DEVICE_REPOSITORY").(database.DeviceRepo)
settings, err := deviceRepo.LoadSettings(c)
if err != nil {
logger.Errorln("An error occurred while retrieving settings", err)
c.JSON(http.StatusInternalServerError, gin.H{"success": false})
return
}
c.JSON(http.StatusOK, gin.H{
"success": true,
"settings": settings,
})
}
@@ -0,0 +1,29 @@
package handler
import (
"github.com/analogj/scrutiny/webapp/backend/pkg/database"
"github.com/gin-gonic/gin"
"github.com/sirupsen/logrus"
"net/http"
)
func HealthCheck(c *gin.Context) {
logger := c.MustGet("LOGGER").(*logrus.Entry)
deviceRepo := c.MustGet("DEVICE_REPOSITORY").(database.DeviceRepo)
logger.Infof("Checking Influxdb & Sqlite health")
//check sqlite and influxdb health
err := deviceRepo.HealthCheck(c)
if err != nil {
logger.Errorln("An error occurred during healthcheck", err)
c.JSON(http.StatusInternalServerError, gin.H{"success": false, "error": err.Error()})
return
}
//TODO:
// check if the /web folder is populated.
c.JSON(http.StatusOK, gin.H{
"success": true,
})
}
@@ -13,7 +13,7 @@ import (
// This function is run everytime a collector is about to start a run. It can be used to update device metadata.
func RegisterDevices(c *gin.Context) {
deviceRepo := c.MustGet("DEVICE_REPOSITORY").(database.DeviceRepo)
logger := c.MustGet("LOGGER").(logrus.FieldLogger)
logger := c.MustGet("LOGGER").(*logrus.Entry)
var collectorDeviceWrapper models.DeviceWrapper
err := c.BindJSON(&collectorDeviceWrapper)
@@ -0,0 +1,34 @@
package handler
import (
"github.com/analogj/scrutiny/webapp/backend/pkg/database"
"github.com/analogj/scrutiny/webapp/backend/pkg/models"
"github.com/gin-gonic/gin"
"github.com/sirupsen/logrus"
"net/http"
)
func SaveSettings(c *gin.Context) {
logger := c.MustGet("LOGGER").(*logrus.Entry)
deviceRepo := c.MustGet("DEVICE_REPOSITORY").(database.DeviceRepo)
var settings models.Settings
err := c.BindJSON(&settings)
if err != nil {
logger.Errorln("Cannot parse updated settings", err)
c.JSON(http.StatusInternalServerError, gin.H{"success": false})
return
}
err = deviceRepo.SaveSettings(c, settings)
if err != nil {
logger.Errorln("An error occurred while saving settings", err)
c.JSON(http.StatusInternalServerError, gin.H{"success": false})
return
}
c.JSON(http.StatusOK, gin.H{
"success": true,
"settings": settings,
})
}
@@ -13,7 +13,7 @@ import (
// Send test notification
func SendTestNotification(c *gin.Context) {
appConfig := c.MustGet("CONFIG").(config.Interface)
logger := c.MustGet("LOGGER").(logrus.FieldLogger)
logger := c.MustGet("LOGGER").(*logrus.Entry)
testNotify := notify.New(
logger,
@@ -1,6 +1,7 @@
package handler
import (
"fmt"
"github.com/analogj/scrutiny/webapp/backend/pkg"
"github.com/analogj/scrutiny/webapp/backend/pkg/config"
"github.com/analogj/scrutiny/webapp/backend/pkg/database"
@@ -13,7 +14,7 @@ import (
func UploadDeviceMetrics(c *gin.Context) {
//db := c.MustGet("DB").(*gorm.DB)
logger := c.MustGet("LOGGER").(logrus.FieldLogger)
logger := c.MustGet("LOGGER").(*logrus.Entry)
appConfig := c.MustGet("CONFIG").(config.Interface)
//influxWriteDb := c.MustGet("INFLUXDB_WRITE").(*api.WriteAPIBlocking)
deviceRepo := c.MustGet("DEVICE_REPOSITORY").(database.DeviceRepo)
@@ -67,7 +68,12 @@ func UploadDeviceMetrics(c *gin.Context) {
}
//check for error
if notify.ShouldNotify(updatedDevice, smartData, appConfig.GetString("notify.level"), appConfig.GetString("notify.filter_attributes")) {
if notify.ShouldNotify(
updatedDevice,
smartData,
pkg.MetricsStatusThreshold(appConfig.GetInt(fmt.Sprintf("%s.metrics.status_threshold", config.DB_USER_SETTINGS_SUBKEY))),
pkg.MetricsStatusFilterAttributes(appConfig.GetInt(fmt.Sprintf("%s.metrics.status_filter_attributes", config.DB_USER_SETTINGS_SUBKEY))),
) {
//send notifications
liveNotify := notify.New(
+2 -2
View File
@@ -28,11 +28,11 @@ import (
var timeFormat = "02/Jan/2006:15:04:05 -0700"
// Logger is the logrus logger handler
func LoggerMiddleware(logger logrus.FieldLogger) gin.HandlerFunc {
func LoggerMiddleware(logger *logrus.Entry) gin.HandlerFunc {
hostname, err := os.Hostname()
if err != nil {
hostname = "unknow"
hostname = "unknown"
}
return func(c *gin.Context) {
@@ -1,6 +1,7 @@
package middleware
import (
"context"
"github.com/analogj/scrutiny/webapp/backend/pkg/config"
"github.com/analogj/scrutiny/webapp/backend/pkg/database"
"github.com/gin-gonic/gin"
@@ -14,6 +15,14 @@ func RepositoryMiddleware(appConfig config.Interface, globalLogger logrus.FieldL
panic(err)
}
// ensure the settings have been loaded into the app config during startup.
_, err = deviceRepo.LoadSettings(context.Background())
if err != nil {
panic(err)
}
//settings.UpdateSettingEntries()
//TODO: determine where we can call defer deviceRepo.Close()
return func(c *gin.Context) {
c.Set("DEVICE_REPOSITORY", deviceRepo)
+6 -29
View File
@@ -9,18 +9,17 @@ import (
"github.com/analogj/scrutiny/webapp/backend/pkg/web/middleware"
"github.com/gin-gonic/gin"
"github.com/sirupsen/logrus"
"io"
"net/http"
"os"
"path/filepath"
"strings"
)
type AppEngine struct {
Config config.Interface
Logger *logrus.Entry
}
func (ae *AppEngine) Setup(logger logrus.FieldLogger) *gin.Engine {
func (ae *AppEngine) Setup(logger *logrus.Entry) *gin.Engine {
r := gin.New()
r.Use(middleware.LoggerMiddleware(logger))
@@ -35,11 +34,7 @@ func (ae *AppEngine) Setup(logger logrus.FieldLogger) *gin.Engine {
{
api := base.Group("/api")
{
api.GET("/health", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"success": true,
})
})
api.GET("/health", handler.HealthCheck)
api.POST("/health/notify", handler.SendTestNotification) //check if notifications are configured correctly
api.POST("/devices/register", handler.RegisterDevices) //used by Collector to register new devices and retrieve filtered list
@@ -50,6 +45,8 @@ func (ae *AppEngine) Setup(logger logrus.FieldLogger) *gin.Engine {
api.GET("/device/:wwn/details", handler.GetDeviceDetails) //used by Details
api.DELETE("/device/:wwn", handler.DeleteDevice) //used by UI to delete device
api.GET("/settings", handler.GetSettings) //used to get settings
api.POST("/settings", handler.SaveSettings) //used to save settings
}
}
@@ -75,26 +72,6 @@ func (ae *AppEngine) Start() error {
gin.SetMode(gin.DebugMode)
}
logger := logrus.New()
//set default log level
logLevel, err := logrus.ParseLevel(ae.Config.GetString("log.level"))
if err != nil {
return err
}
logger.SetLevel(logLevel)
//set the log file if present
if len(ae.Config.GetString("log.file")) != 0 {
logFile, err := os.OpenFile(ae.Config.GetString("log.file"), os.O_CREATE|os.O_WRONLY, 0644)
defer logFile.Close()
if err != nil {
logrus.Errorf("Failed to open log file %s for output: %s", ae.Config.GetString("log.file"), err)
return err
}
//configure the logrus default
logger.SetOutput(io.MultiWriter(os.Stderr, logFile))
}
//check if the database parent directory exists, fail here rather than in a handler.
if !utils.FileExists(filepath.Dir(ae.Config.GetString("web.database.location"))) {
return errors.ConfigValidationError(fmt.Sprintf(
@@ -102,7 +79,7 @@ func (ae *AppEngine) Start() error {
filepath.Dir(ae.Config.GetString("web.database.location"))))
}
r := ae.Setup(logger)
r := ae.Setup(ae.Logger)
return r.Run(fmt.Sprintf("%s:%s", ae.Config.GetString("web.listen.host"), ae.Config.GetString("web.listen.port")))
}
+60 -23
View File
@@ -3,7 +3,9 @@ package web_test
import (
"bytes"
"encoding/json"
"fmt"
"github.com/analogj/scrutiny/webapp/backend/pkg"
"github.com/analogj/scrutiny/webapp/backend/pkg/config"
mock_config "github.com/analogj/scrutiny/webapp/backend/pkg/config/mock"
"github.com/analogj/scrutiny/webapp/backend/pkg/models"
"github.com/analogj/scrutiny/webapp/backend/pkg/models/collector"
@@ -89,6 +91,8 @@ func (suite *ServerTestSuite) TestHealthRoute() {
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)
fakeConfig.EXPECT().GetString("web.database.location").Return(path.Join(parentPath, "scrutiny_test.db")).AnyTimes()
fakeConfig.EXPECT().GetString("web.src.frontend.path").Return(parentPath).AnyTimes()
fakeConfig.EXPECT().GetString("web.listen.basepath").Return(suite.Basepath).AnyTimes()
@@ -99,6 +103,7 @@ func (suite *ServerTestSuite) TestHealthRoute() {
fakeConfig.EXPECT().GetString("web.influxdb.token").Return("my-super-secret-auth-token").AnyTimes()
fakeConfig.EXPECT().GetString("web.influxdb.org").Return("scrutiny").AnyTimes()
fakeConfig.EXPECT().GetString("web.influxdb.bucket").Return("metrics").AnyTimes()
fakeConfig.EXPECT().GetBool("web.influxdb.tls.insecure_skip_verify").Return(false).AnyTimes()
fakeConfig.EXPECT().GetBool("web.influxdb.retention_policy").Return(false).AnyTimes()
if _, isGithubActions := os.LookupEnv("GITHUB_ACTIONS"); isGithubActions {
// when running test suite in github actions, we run an influxdb service as a sidecar.
@@ -111,7 +116,7 @@ func (suite *ServerTestSuite) TestHealthRoute() {
Config: fakeConfig,
}
router := ae.Setup(logrus.New())
router := ae.Setup(logrus.WithField("test", suite.T().Name()))
//test
w := httptest.NewRecorder()
@@ -130,6 +135,8 @@ func (suite *ServerTestSuite) TestRegisterDevicesRoute() {
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)
fakeConfig.EXPECT().GetString("web.database.location").Return(path.Join(parentPath, "scrutiny_test.db")).AnyTimes()
fakeConfig.EXPECT().GetString("web.src.frontend.path").Return(parentPath).AnyTimes()
fakeConfig.EXPECT().GetString("web.listen.basepath").Return(suite.Basepath).AnyTimes()
@@ -139,6 +146,7 @@ func (suite *ServerTestSuite) TestRegisterDevicesRoute() {
fakeConfig.EXPECT().GetString("web.influxdb.token").Return("my-super-secret-auth-token").AnyTimes()
fakeConfig.EXPECT().GetString("web.influxdb.org").Return("scrutiny").AnyTimes()
fakeConfig.EXPECT().GetString("web.influxdb.bucket").Return("metrics").AnyTimes()
fakeConfig.EXPECT().GetBool("web.influxdb.tls.insecure_skip_verify").Return(false).AnyTimes()
fakeConfig.EXPECT().GetBool("web.influxdb.retention_policy").Return(false).AnyTimes()
if _, isGithubActions := os.LookupEnv("GITHUB_ACTIONS"); isGithubActions {
// when running test suite in github actions, we run an influxdb service as a sidecar.
@@ -150,7 +158,7 @@ func (suite *ServerTestSuite) TestRegisterDevicesRoute() {
ae := web.AppEngine{
Config: fakeConfig,
}
router := ae.Setup(logrus.New())
router := ae.Setup(logrus.WithField("test", suite.T().Name()))
file, err := os.Open("testdata/register-devices-req.json")
require.NoError(suite.T(), err)
@@ -170,6 +178,8 @@ func (suite *ServerTestSuite) TestUploadDeviceMetricsRoute() {
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)
fakeConfig.EXPECT().GetString("web.database.location").AnyTimes().Return(path.Join(parentPath, "scrutiny_test.db"))
fakeConfig.EXPECT().GetString("web.src.frontend.path").AnyTimes().Return(parentPath)
fakeConfig.EXPECT().GetString("web.listen.basepath").Return(suite.Basepath).AnyTimes()
@@ -179,6 +189,7 @@ func (suite *ServerTestSuite) TestUploadDeviceMetricsRoute() {
fakeConfig.EXPECT().GetString("web.influxdb.token").Return("my-super-secret-auth-token").AnyTimes()
fakeConfig.EXPECT().GetString("web.influxdb.org").Return("scrutiny").AnyTimes()
fakeConfig.EXPECT().GetString("web.influxdb.bucket").Return("metrics").AnyTimes()
fakeConfig.EXPECT().GetBool("web.influxdb.tls.insecure_skip_verify").Return(false).AnyTimes()
fakeConfig.EXPECT().GetBool("web.influxdb.retention_policy").Return(false).AnyTimes()
if _, isGithubActions := os.LookupEnv("GITHUB_ACTIONS"); isGithubActions {
// when running test suite in github actions, we run an influxdb service as a sidecar.
@@ -186,13 +197,14 @@ func (suite *ServerTestSuite) TestUploadDeviceMetricsRoute() {
} else {
fakeConfig.EXPECT().GetString("web.influxdb.host").Return("localhost").AnyTimes()
}
fakeConfig.EXPECT().GetString("notify.level").AnyTimes().Return(pkg.NotifyLevelFail)
fakeConfig.EXPECT().GetString("notify.filter_attributes").AnyTimes().Return(pkg.NotifyFilterAttributesAll)
fakeConfig.EXPECT().GetInt(fmt.Sprintf("%s.metrics.notify_level", config.DB_USER_SETTINGS_SUBKEY)).AnyTimes().Return(int(pkg.MetricsNotifyLevelFail))
fakeConfig.EXPECT().GetInt(fmt.Sprintf("%s.metrics.status_filter_attributes", config.DB_USER_SETTINGS_SUBKEY)).AnyTimes().Return(int(pkg.MetricsStatusFilterAttributesAll))
fakeConfig.EXPECT().GetInt(fmt.Sprintf("%s.metrics.status_threshold", config.DB_USER_SETTINGS_SUBKEY)).AnyTimes().Return(int(pkg.MetricsStatusThresholdBoth))
ae := web.AppEngine{
Config: fakeConfig,
}
router := ae.Setup(logrus.New())
router := ae.Setup(logrus.WithField("test", suite.T().Name()))
devicesfile, err := os.Open("testdata/register-devices-single-req.json")
require.NoError(suite.T(), err)
@@ -219,10 +231,13 @@ func (suite *ServerTestSuite) TestPopulateMultiple() {
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)
//fakeConfig.EXPECT().GetString("web.database.location").AnyTimes().Return("testdata/scrutiny_test.db")
fakeConfig.EXPECT().GetStringSlice("notify.urls").Return([]string{}).AnyTimes()
fakeConfig.EXPECT().GetString("notify.level").AnyTimes().Return(pkg.NotifyLevelFail)
fakeConfig.EXPECT().GetString("notify.filter_attributes").AnyTimes().Return(pkg.NotifyFilterAttributesAll)
fakeConfig.EXPECT().GetInt(fmt.Sprintf("%s.metrics.notify_level", config.DB_USER_SETTINGS_SUBKEY)).AnyTimes().Return(int(pkg.MetricsNotifyLevelFail))
fakeConfig.EXPECT().GetInt(fmt.Sprintf("%s.metrics.status_filter_attributes", config.DB_USER_SETTINGS_SUBKEY)).AnyTimes().Return(int(pkg.MetricsStatusFilterAttributesAll))
fakeConfig.EXPECT().GetInt(fmt.Sprintf("%s.metrics.status_threshold", config.DB_USER_SETTINGS_SUBKEY)).AnyTimes().Return(int(pkg.MetricsStatusThresholdBoth))
fakeConfig.EXPECT().GetString("web.database.location").AnyTimes().Return(path.Join(parentPath, "scrutiny_test.db"))
fakeConfig.EXPECT().GetString("web.src.frontend.path").AnyTimes().Return(parentPath)
fakeConfig.EXPECT().GetString("web.listen.basepath").Return(suite.Basepath).AnyTimes()
@@ -232,6 +247,7 @@ func (suite *ServerTestSuite) TestPopulateMultiple() {
fakeConfig.EXPECT().GetString("web.influxdb.token").Return("my-super-secret-auth-token").AnyTimes()
fakeConfig.EXPECT().GetString("web.influxdb.org").Return("scrutiny").AnyTimes()
fakeConfig.EXPECT().GetString("web.influxdb.bucket").Return("metrics").AnyTimes()
fakeConfig.EXPECT().GetBool("web.influxdb.tls.insecure_skip_verify").Return(false).AnyTimes()
fakeConfig.EXPECT().GetBool("web.influxdb.retention_policy").Return(false).AnyTimes()
if _, isGithubActions := os.LookupEnv("GITHUB_ACTIONS"); isGithubActions {
// when running test suite in github actions, we run an influxdb service as a sidecar.
@@ -243,7 +259,7 @@ func (suite *ServerTestSuite) TestPopulateMultiple() {
ae := web.AppEngine{
Config: fakeConfig,
}
router := ae.Setup(logrus.New())
router := ae.Setup(logrus.WithField("test", suite.T().Name()))
devicesfile, err := os.Open("testdata/register-devices-req.json")
require.NoError(suite.T(), err)
@@ -319,6 +335,8 @@ func (suite *ServerTestSuite) TestSendTestNotificationRoute_WebhookFailure() {
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)
fakeConfig.EXPECT().GetString("web.database.location").AnyTimes().Return(path.Join(parentPath, "scrutiny_test.db"))
fakeConfig.EXPECT().GetString("web.src.frontend.path").AnyTimes().Return(parentPath)
fakeConfig.EXPECT().GetString("web.listen.basepath").Return(suite.Basepath).AnyTimes()
@@ -328,10 +346,12 @@ func (suite *ServerTestSuite) TestSendTestNotificationRoute_WebhookFailure() {
fakeConfig.EXPECT().GetString("web.influxdb.token").Return("my-super-secret-auth-token").AnyTimes()
fakeConfig.EXPECT().GetString("web.influxdb.org").Return("scrutiny").AnyTimes()
fakeConfig.EXPECT().GetString("web.influxdb.bucket").Return("metrics").AnyTimes()
fakeConfig.EXPECT().GetBool("web.influxdb.tls.insecure_skip_verify").Return(false).AnyTimes()
fakeConfig.EXPECT().GetBool("web.influxdb.retention_policy").Return(false).AnyTimes()
fakeConfig.EXPECT().GetStringSlice("notify.urls").AnyTimes().Return([]string{"https://unroutable.domain.example.asdfghj"})
fakeConfig.EXPECT().GetString("notify.level").AnyTimes().Return(pkg.NotifyLevelFail)
fakeConfig.EXPECT().GetString("notify.filter_attributes").AnyTimes().Return(pkg.NotifyFilterAttributesAll)
fakeConfig.EXPECT().GetInt(fmt.Sprintf("%s.metrics.notify_level", config.DB_USER_SETTINGS_SUBKEY)).AnyTimes().Return(int(pkg.MetricsNotifyLevelFail))
fakeConfig.EXPECT().GetInt(fmt.Sprintf("%s.metrics.status_filter_attributes", config.DB_USER_SETTINGS_SUBKEY)).AnyTimes().Return(int(pkg.MetricsStatusFilterAttributesAll))
fakeConfig.EXPECT().GetInt(fmt.Sprintf("%s.metrics.status_threshold", config.DB_USER_SETTINGS_SUBKEY)).AnyTimes().Return(int(pkg.MetricsStatusThresholdBoth))
if _, isGithubActions := os.LookupEnv("GITHUB_ACTIONS"); isGithubActions {
// when running test suite in github actions, we run an influxdb service as a sidecar.
@@ -343,7 +363,7 @@ func (suite *ServerTestSuite) TestSendTestNotificationRoute_WebhookFailure() {
ae := web.AppEngine{
Config: fakeConfig,
}
router := ae.Setup(logrus.New())
router := ae.Setup(logrus.WithField("test", suite.T().Name()))
//test
wr := httptest.NewRecorder()
@@ -361,6 +381,8 @@ func (suite *ServerTestSuite) TestSendTestNotificationRoute_ScriptFailure() {
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)
fakeConfig.EXPECT().GetString("web.database.location").AnyTimes().Return(path.Join(parentPath, "scrutiny_test.db"))
fakeConfig.EXPECT().GetString("web.src.frontend.path").AnyTimes().Return(parentPath)
fakeConfig.EXPECT().GetString("web.listen.basepath").Return(suite.Basepath).AnyTimes()
@@ -370,10 +392,12 @@ func (suite *ServerTestSuite) TestSendTestNotificationRoute_ScriptFailure() {
fakeConfig.EXPECT().GetString("web.influxdb.token").Return("my-super-secret-auth-token").AnyTimes()
fakeConfig.EXPECT().GetString("web.influxdb.org").Return("scrutiny").AnyTimes()
fakeConfig.EXPECT().GetString("web.influxdb.bucket").Return("metrics").AnyTimes()
fakeConfig.EXPECT().GetBool("web.influxdb.tls.insecure_skip_verify").Return(false).AnyTimes()
fakeConfig.EXPECT().GetBool("web.influxdb.retention_policy").Return(false).AnyTimes()
fakeConfig.EXPECT().GetStringSlice("notify.urls").AnyTimes().Return([]string{"script:///missing/path/on/disk"})
fakeConfig.EXPECT().GetString("notify.level").AnyTimes().Return(pkg.NotifyLevelFail)
fakeConfig.EXPECT().GetString("notify.filter_attributes").AnyTimes().Return(pkg.NotifyFilterAttributesAll)
fakeConfig.EXPECT().GetInt(fmt.Sprintf("%s.metrics.notify_level", config.DB_USER_SETTINGS_SUBKEY)).AnyTimes().Return(int(pkg.MetricsNotifyLevelFail))
fakeConfig.EXPECT().GetInt(fmt.Sprintf("%s.metrics.status_filter_attributes", config.DB_USER_SETTINGS_SUBKEY)).AnyTimes().Return(int(pkg.MetricsStatusFilterAttributesAll))
fakeConfig.EXPECT().GetInt(fmt.Sprintf("%s.metrics.status_threshold", config.DB_USER_SETTINGS_SUBKEY)).AnyTimes().Return(int(pkg.MetricsStatusThresholdBoth))
if _, isGithubActions := os.LookupEnv("GITHUB_ACTIONS"); isGithubActions {
// when running test suite in github actions, we run an influxdb service as a sidecar.
@@ -385,7 +409,7 @@ func (suite *ServerTestSuite) TestSendTestNotificationRoute_ScriptFailure() {
ae := web.AppEngine{
Config: fakeConfig,
}
router := ae.Setup(logrus.New())
router := ae.Setup(logrus.WithField("test", suite.T().Name()))
//test
wr := httptest.NewRecorder()
@@ -403,6 +427,8 @@ func (suite *ServerTestSuite) TestSendTestNotificationRoute_ScriptSuccess() {
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)
fakeConfig.EXPECT().GetString("web.database.location").AnyTimes().Return(path.Join(parentPath, "scrutiny_test.db"))
fakeConfig.EXPECT().GetString("web.src.frontend.path").AnyTimes().Return(parentPath)
fakeConfig.EXPECT().GetString("web.listen.basepath").Return(suite.Basepath).AnyTimes()
@@ -412,10 +438,12 @@ func (suite *ServerTestSuite) TestSendTestNotificationRoute_ScriptSuccess() {
fakeConfig.EXPECT().GetString("web.influxdb.token").Return("my-super-secret-auth-token").AnyTimes()
fakeConfig.EXPECT().GetString("web.influxdb.org").Return("scrutiny").AnyTimes()
fakeConfig.EXPECT().GetString("web.influxdb.bucket").Return("metrics").AnyTimes()
fakeConfig.EXPECT().GetBool("web.influxdb.tls.insecure_skip_verify").Return(false).AnyTimes()
fakeConfig.EXPECT().GetBool("web.influxdb.retention_policy").Return(false).AnyTimes()
fakeConfig.EXPECT().GetStringSlice("notify.urls").AnyTimes().Return([]string{"script:///usr/bin/env"})
fakeConfig.EXPECT().GetString("notify.level").AnyTimes().Return(pkg.NotifyLevelFail)
fakeConfig.EXPECT().GetString("notify.filter_attributes").AnyTimes().Return(pkg.NotifyFilterAttributesAll)
fakeConfig.EXPECT().GetInt(fmt.Sprintf("%s.metrics.notify_level", config.DB_USER_SETTINGS_SUBKEY)).AnyTimes().Return(int(pkg.MetricsNotifyLevelFail))
fakeConfig.EXPECT().GetInt(fmt.Sprintf("%s.metrics.status_filter_attributes", config.DB_USER_SETTINGS_SUBKEY)).AnyTimes().Return(int(pkg.MetricsStatusFilterAttributesAll))
fakeConfig.EXPECT().GetInt(fmt.Sprintf("%s.metrics.status_threshold", config.DB_USER_SETTINGS_SUBKEY)).AnyTimes().Return(int(pkg.MetricsStatusThresholdBoth))
if _, isGithubActions := os.LookupEnv("GITHUB_ACTIONS"); isGithubActions {
// when running test suite in github actions, we run an influxdb service as a sidecar.
@@ -427,7 +455,7 @@ func (suite *ServerTestSuite) TestSendTestNotificationRoute_ScriptSuccess() {
ae := web.AppEngine{
Config: fakeConfig,
}
router := ae.Setup(logrus.New())
router := ae.Setup(logrus.WithField("test", suite.T().Name()))
//test
wr := httptest.NewRecorder()
@@ -445,6 +473,8 @@ func (suite *ServerTestSuite) TestSendTestNotificationRoute_ShoutrrrFailure() {
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)
fakeConfig.EXPECT().GetString("web.database.location").AnyTimes().Return(path.Join(parentPath, "scrutiny_test.db"))
fakeConfig.EXPECT().GetString("web.src.frontend.path").AnyTimes().Return(parentPath)
fakeConfig.EXPECT().GetString("web.listen.basepath").Return(suite.Basepath).AnyTimes()
@@ -454,10 +484,12 @@ func (suite *ServerTestSuite) TestSendTestNotificationRoute_ShoutrrrFailure() {
fakeConfig.EXPECT().GetString("web.influxdb.token").Return("my-super-secret-auth-token").AnyTimes()
fakeConfig.EXPECT().GetString("web.influxdb.org").Return("scrutiny").AnyTimes()
fakeConfig.EXPECT().GetString("web.influxdb.bucket").Return("metrics").AnyTimes()
fakeConfig.EXPECT().GetBool("web.influxdb.tls.insecure_skip_verify").Return(false).AnyTimes()
fakeConfig.EXPECT().GetBool("web.influxdb.retention_policy").Return(false).AnyTimes()
fakeConfig.EXPECT().GetStringSlice("notify.urls").AnyTimes().Return([]string{"discord://invalidtoken@channel"})
fakeConfig.EXPECT().GetString("notify.level").AnyTimes().Return(pkg.NotifyLevelFail)
fakeConfig.EXPECT().GetString("notify.filter_attributes").AnyTimes().Return(pkg.NotifyFilterAttributesAll)
fakeConfig.EXPECT().GetInt(fmt.Sprintf("%s.metrics.notify_level", config.DB_USER_SETTINGS_SUBKEY)).AnyTimes().Return(int(pkg.MetricsNotifyLevelFail))
fakeConfig.EXPECT().GetInt(fmt.Sprintf("%s.metrics.status_filter_attributes", config.DB_USER_SETTINGS_SUBKEY)).AnyTimes().Return(int(pkg.MetricsStatusFilterAttributesAll))
fakeConfig.EXPECT().GetInt(fmt.Sprintf("%s.metrics.status_threshold", config.DB_USER_SETTINGS_SUBKEY)).AnyTimes().Return(int(pkg.MetricsStatusThresholdBoth))
if _, isGithubActions := os.LookupEnv("GITHUB_ACTIONS"); isGithubActions {
// when running test suite in github actions, we run an influxdb service as a sidecar.
@@ -468,7 +500,7 @@ func (suite *ServerTestSuite) TestSendTestNotificationRoute_ShoutrrrFailure() {
ae := web.AppEngine{
Config: fakeConfig,
}
router := ae.Setup(logrus.New())
router := ae.Setup(logrus.WithField("test", suite.T().Name()))
//test
wr := httptest.NewRecorder()
@@ -486,6 +518,8 @@ func (suite *ServerTestSuite) TestGetDevicesSummaryRoute_Nvme() {
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)
fakeConfig.EXPECT().GetString("web.database.location").AnyTimes().Return(path.Join(parentPath, "scrutiny_test.db"))
fakeConfig.EXPECT().GetString("web.src.frontend.path").AnyTimes().Return(parentPath)
fakeConfig.EXPECT().GetString("web.listen.basepath").Return(suite.Basepath).AnyTimes()
@@ -495,10 +529,13 @@ func (suite *ServerTestSuite) TestGetDevicesSummaryRoute_Nvme() {
fakeConfig.EXPECT().GetString("web.influxdb.token").Return("my-super-secret-auth-token").AnyTimes()
fakeConfig.EXPECT().GetString("web.influxdb.org").Return("scrutiny").AnyTimes()
fakeConfig.EXPECT().GetString("web.influxdb.bucket").Return("metrics").AnyTimes()
fakeConfig.EXPECT().GetBool("web.influxdb.tls.insecure_skip_verify").Return(false).AnyTimes()
fakeConfig.EXPECT().GetBool("web.influxdb.retention_policy").Return(false).AnyTimes()
fakeConfig.EXPECT().GetStringSlice("notify.urls").AnyTimes().Return([]string{})
fakeConfig.EXPECT().GetString("notify.level").AnyTimes().Return(pkg.NotifyLevelFail)
fakeConfig.EXPECT().GetString("notify.filter_attributes").AnyTimes().Return(pkg.NotifyFilterAttributesAll)
fakeConfig.EXPECT().GetInt(fmt.Sprintf("%s.metrics.notify_level", config.DB_USER_SETTINGS_SUBKEY)).AnyTimes().Return(int(pkg.MetricsNotifyLevelFail))
fakeConfig.EXPECT().GetInt(fmt.Sprintf("%s.metrics.status_filter_attributes", config.DB_USER_SETTINGS_SUBKEY)).AnyTimes().Return(int(pkg.MetricsStatusFilterAttributesAll))
fakeConfig.EXPECT().GetInt(fmt.Sprintf("%s.metrics.status_threshold", config.DB_USER_SETTINGS_SUBKEY)).AnyTimes().Return(int(pkg.MetricsStatusThresholdBoth))
if _, isGithubActions := os.LookupEnv("GITHUB_ACTIONS"); isGithubActions {
// when running test suite in github actions, we run an influxdb service as a sidecar.
fakeConfig.EXPECT().GetString("web.influxdb.host").Return("influxdb").AnyTimes()
@@ -509,7 +546,7 @@ func (suite *ServerTestSuite) TestGetDevicesSummaryRoute_Nvme() {
ae := web.AppEngine{
Config: fakeConfig,
}
router := ae.Setup(logrus.New())
router := ae.Setup(logrus.WithField("test", suite.T().Name()))
devicesfile, err := os.Open("testdata/register-devices-req-2.json")
require.NoError(suite.T(), err)
-2
View File
@@ -52,7 +52,6 @@
"optimization": true,
"outputHashing": "all",
"sourceMap": false,
"extractCss": true,
"namedChunks": false,
"extractLicenses": true,
"vendorChunk": false,
@@ -91,7 +90,6 @@
},
"test": {
"builder": "@angular-devkit/build-angular:karma",
"defaultConfiguration": "production",
"options": {
"main": "src/test.ts",
"polyfills": "src/polyfills.ts",
+10 -10
View File
@@ -10,10 +10,10 @@ module.exports = function (config)
require('karma-jasmine'),
require('karma-chrome-launcher'),
require('karma-jasmine-html-reporter'),
require('karma-coverage-istanbul-reporter'),
require('karma-coverage'),
require('@angular-devkit/build-angular/plugins/karma')
],
client : {
client: {
clearContext: false // leave Jasmine Spec Runner output visible in browser
},
coverageIstanbulReporter: {
@@ -21,13 +21,13 @@ module.exports = function (config)
reports: ['html', 'lcovonly', 'text-summary'],
fixWebpackSourcePaths: true
},
reporters : ['progress', 'kjhtml'],
port : 9876,
colors : true,
logLevel : config.LOG_INFO,
autoWatch : true,
browsers : ['Chrome'],
singleRun : false,
restartOnFileChange : true
reporters: ['progress', 'kjhtml'],
port: 9876,
colors: true,
logLevel: config.LOG_INFO,
autoWatch: true,
browsers: ['Chrome'],
singleRun: false,
restartOnFileChange: true
});
};
+8291 -27143
View File
File diff suppressed because it is too large Load Diff
+49 -60
View File
@@ -7,7 +7,7 @@
"start": "ng serve --open",
"start:mem": "node --max_old_space_size=6144 ./node_modules/@angular/cli/bin/ng serve --open",
"build": "ng build",
"build:prod": "ng build --prod",
"build:prod": "ng build --configuration production",
"build:prod:mem": "node --max_old_space_size=6144 ./node_modules/@angular/cli/bin/ng build --prod",
"test": "ng test",
"lint": "ng lint",
@@ -20,66 +20,55 @@
},
"private": true,
"dependencies": {
"@angular/animations": "9.1.4",
"@angular/cdk": "9.2.2",
"@angular/common": "9.1.4",
"@angular/compiler": "9.1.4",
"@angular/core": "9.1.4",
"@angular/forms": "9.1.4",
"@angular/material": "9.2.2",
"@angular/material-moment-adapter": "9.2.2",
"@angular/platform-browser": "9.1.4",
"@angular/platform-browser-dynamic": "9.1.4",
"@angular/router": "9.1.4",
"@fullcalendar/angular": "4.4.5-beta",
"@fullcalendar/core": "4.4.0",
"@fullcalendar/daygrid": "4.4.0",
"@fullcalendar/interaction": "4.4.0",
"@fullcalendar/list": "4.4.0",
"@fullcalendar/moment": "4.4.0",
"@fullcalendar/rrule": "4.4.0",
"@fullcalendar/timegrid": "4.4.0",
"@types/humanize-duration": "^3.18.1",
"apexcharts": "3.19.2",
"crypto-js": "3.3.0",
"highlight.js": "10.0.1",
"humanize-duration": "^3.24.0",
"lodash": "4.17.15",
"moment": "2.24.0",
"ng-apexcharts": "1.5.12",
"ngx-markdown": "9.0.0",
"ngx-quill": "9.1.0",
"perfect-scrollbar": "1.5.0",
"quill": "1.3.7",
"rrule": "2.6.4",
"rxjs": "6.5.5",
"tslib": "1.11.1",
"web-animations-js": "2.3.2",
"zone.js": "0.10.3"
"@angular/animations": "v13-lts",
"@angular/cdk": "v13-lts",
"@angular/common": "v13-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",
"@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",
"moment": "^2.29.4",
"ng-apexcharts": "^1.7.4",
"ngx-markdown": "^13.1.0",
"perfect-scrollbar": "^1.5.5",
"quill": "^1.3.7",
"rrule": "^2.7.1",
"rxjs": "^7.5.7",
"tslib": "^2.4.1",
"web-animations-js": "^2.3.2"
},
"devDependencies": {
"@angular-devkit/build-angular": "0.901.4",
"@angular/cli": "9.1.4",
"@angular/compiler-cli": "9.1.4",
"@angular/language-service": "9.1.4",
"@types/crypto-js": "3.1.45",
"@types/highlight.js": "9.12.3",
"@types/jasmine": "3.5.10",
"@types/jasminewd2": "2.0.8",
"@types/lodash": "4.14.150",
"@types/node": "12.12.37",
"codelyzer": "5.2.2",
"jasmine-core": "3.5.0",
"jasmine-spec-reporter": "4.2.1",
"karma": "5.0.4",
"karma-chrome-launcher": "3.1.0",
"karma-coverage-istanbul-reporter": "2.1.1",
"karma-jasmine": "3.0.3",
"karma-jasmine-html-reporter": "1.5.3",
"protractor": "5.4.4",
"tailwindcss": "1.4.4",
"ts-node": "8.3.0",
"tslint": "6.1.2",
"typescript": "3.8.3"
"@angular-devkit/build-angular": "v13-lts",
"@angular/cli": "v13-lts",
"@angular/compiler-cli": "v13-lts",
"@angular/language-service": "v13-lts",
"@types/crypto-js": "^4.1.1",
"@types/highlight.js": "^10.1.0",
"@types/jasmine": "^4.3.0",
"@types/jasminewd2": "^2.0.10",
"@types/lodash": "^4.14.188",
"@types/node": "^18.11.9",
"codelyzer": "^6.0.2",
"jasmine-core": "^4.5.0",
"jasmine-spec-reporter": "^7.0.0",
"karma": "^6.4.1",
"karma-chrome-launcher": "^3.1.1",
"karma-coverage": "^2.2.0",
"karma-jasmine": "^5.1.0",
"karma-jasmine-html-reporter": "^2.0.0",
"protractor": "^7.0.0",
"tailwindcss": "^3.2.3",
"ts-node": "^10.9.1",
"tslint": "^6.1.3",
"typescript": "^4.6.4"
}
}
@@ -56,7 +56,7 @@ export class TreoDateRangeComponent implements ControlValueAccessor, OnInit, OnD
private _timeFormat: string;
private _timeRange: boolean;
private readonly _timeRegExp: RegExp;
private _unsubscribeAll: Subject<any>;
private _unsubscribeAll: Subject<void>;
/**
* Constructor
@@ -31,7 +31,7 @@ export class TreoMessageComponent implements OnInit, OnDestroy
private _dismissed: null | boolean;
private _showIcon: boolean;
private _type: TreoMessageType;
private _unsubscribeAll: Subject<any>;
private _unsubscribeAll: Subject<void>;
/**
* Constructor
@@ -23,7 +23,7 @@ export class TreoHorizontalNavigationBasicItemComponent implements OnInit, OnDes
// Private
private _treoHorizontalNavigationComponent: TreoHorizontalNavigationComponent;
private _unsubscribeAll: Subject<any>;
private _unsubscribeAll: Subject<void>;
/**
* Constructor
@@ -32,7 +32,7 @@ export class TreoHorizontalNavigationBranchItemComponent implements OnInit, OnDe
// Private
private _treoHorizontalNavigationComponent: TreoHorizontalNavigationComponent;
private _unsubscribeAll: Subject<any>;
private _unsubscribeAll: Subject<void>;
/**
* Constructor
@@ -23,7 +23,7 @@ export class TreoHorizontalNavigationDividerItemComponent implements OnInit, OnD
// Private
private _treoHorizontalNavigationComponent: TreoHorizontalNavigationComponent;
private _unsubscribeAll: Subject<any>;
private _unsubscribeAll: Subject<void>;
/**
* Constructor
@@ -23,7 +23,7 @@ export class TreoHorizontalNavigationSpacerItemComponent implements OnInit, OnDe
// Private
private _treoHorizontalNavigationComponent: TreoHorizontalNavigationComponent;
private _unsubscribeAll: Subject<any>;
private _unsubscribeAll: Subject<void>;
/**
* Constructor
@@ -23,7 +23,7 @@ export class TreoHorizontalNavigationComponent implements OnInit, OnDestroy
// Private
private _navigation: TreoNavigationItem[];
private _unsubscribeAll: Subject<any>;
private _unsubscribeAll: Subject<void>;
/**
* Constructor
@@ -35,7 +35,7 @@ export class TreoVerticalNavigationAsideItemComponent implements OnInit, OnDestr
// Private
private _treoVerticalNavigationComponent: TreoVerticalNavigationComponent;
private _unsubscribeAll: Subject<any>;
private _unsubscribeAll: Subject<void>;
/**
* Constructor
@@ -23,7 +23,7 @@ export class TreoVerticalNavigationBasicItemComponent implements OnInit, OnDestr
// Private
private _treoVerticalNavigationComponent: TreoVerticalNavigationComponent;
private _unsubscribeAll: Subject<any>;
private _unsubscribeAll: Subject<void>;
/**
* Constructor
@@ -38,7 +38,7 @@ export class TreoVerticalNavigationCollapsableItemComponent implements OnInit, O
// Private
private _treoVerticalNavigationComponent: TreoVerticalNavigationComponent;
private _unsubscribeAll: Subject<any>;
private _unsubscribeAll: Subject<void>;
/**
* Constructor
@@ -23,7 +23,7 @@ export class TreoVerticalNavigationDividerItemComponent implements OnInit, OnDes
// Private
private _treoVerticalNavigationComponent: TreoVerticalNavigationComponent;
private _unsubscribeAll: Subject<any>;
private _unsubscribeAll: Subject<void>;
/**
* Constructor
@@ -27,7 +27,7 @@ export class TreoVerticalNavigationGroupItemComponent implements OnInit, OnDestr
// Private
private _treoVerticalNavigationComponent: TreoVerticalNavigationComponent;
private _unsubscribeAll: Subject<any>;
private _unsubscribeAll: Subject<void>;
/**
* Constructor
@@ -23,7 +23,7 @@ export class TreoVerticalNavigationSpacerItemComponent implements OnInit, OnDest
// Private
private _treoVerticalNavigationComponent: TreoVerticalNavigationComponent;
private _unsubscribeAll: Subject<any>;
private _unsubscribeAll: Subject<void>;
/**
* Constructor
@@ -65,7 +65,7 @@ export class TreoVerticalNavigationComponent implements OnInit, AfterViewInit, O
private _position: TreoVerticalNavigationPosition;
private _scrollStrategy: ScrollStrategy;
private _transparentOverlay: boolean | '';
private _unsubscribeAll: Subject<any>;
private _unsubscribeAll: Subject<void>;
@HostBinding('class.treo-vertical-navigation-animations-enabled')
private _animationsEnabled: boolean;
@@ -12,7 +12,7 @@ export class TreoAutogrowDirective implements OnInit, OnDestroy
// Private
private _padding: number;
private _unsubscribeAll: Subject<any>;
private _unsubscribeAll: Subject<void>;
/**
* Constructor
@@ -24,7 +24,7 @@ export class TreoScrollbarDirective implements OnInit, OnDestroy
private _animation: number | null;
private _enabled: boolean;
private _options: any;
private _unsubscribeAll: Subject<any>;
private _unsubscribeAll: Subject<void>;
/**
* Constructor
@@ -20,7 +20,7 @@ export class TreoMockApiModule
*
* @param mockDataServices
*/
static forRoot(mockDataServices: any[]): ModuleWithProviders
static forRoot(mockDataServices: any[]): ModuleWithProviders<TreoMockApiModule>
{
return {
ngModule : TreoMockApiModule,
@@ -26,7 +26,6 @@
// 6. Overrides
@import 'overrides/angular-material';
@import 'overrides/fullcalendar';
@import 'overrides/highlightjs';
@import 'overrides/perfect-scrollbar';
@import 'overrides/quill';
@@ -1,878 +0,0 @@
// -----------------------------------------------------------------------------------------------------
// @ FullCalendar overrides
// -----------------------------------------------------------------------------------------------------
.fc {
.fc-view-container {
// Day Grid - Month view
.fc-view.fc-dayGridMonth-view {
.fc-head {
> tr > .fc-head-container {
border: none;
.fc-row {
.fc-day-header {
span {
display: flex;
align-items: center;
justify-content: center;
padding-top: 8px;
font-size: 12px;
font-weight: 500;
text-transform: uppercase;
}
}
}
}
}
.fc-body {
> tr > .fc-widget-content {
border: none;
.fc-day-grid {
.fc-week {
.fc-content-skeleton {
.fc-day-top {
text-align: center;
&.fc-other-month {
opacity: 1;
}
.fc-day-number {
display: inline-flex;
align-items: center;
justify-content: center;
width: 22px;
height: 21px;
margin: 4px 0;
font-size: 12px;
border-radius: 50%;
float: none;
}
}
.fc-event-container {
.fc-day-grid-event {
display: flex;
align-items: center;
height: 22px;
min-height: 22px;
max-height: 22px;
margin: 0 6px 4px 6px;
padding: 0 8px;
font-size: 12px;
border-radius: 4px;
border: none;
cursor: pointer;
@include treo-breakpoint('xs') {
padding: 0 5px;
}
}
}
.fc-more {
padding: 0 6px;
font-size: 12px;
font-weight: 500;
white-space: nowrap;
@include treo-breakpoint('xs') {
padding: 0 3px;
}
}
}
.fc-highlight-skeleton {
.fc-highlight {
position: relative;
}
}
}
}
}
}
.fc-popover {
&.fc-more-popover {
border: none;
border-radius: 4px;
@include treo-elevation('2xl');
.fc-header {
height: 32px;
min-height: 32px;
max-height: 32px;
padding: 0 8px;
.fc-title {
margin: 0;
padding: 0;
font-size: 12px;
}
}
.fc-body {
max-height: 160px;
overflow: hidden auto;
.fc-event-container {
padding: 8px;
.fc-day-grid-event {
display: flex;
align-items: center;
height: 22px;
min-height: 22px;
max-height: 22px;
margin: 0 0 6px 0;
padding: 0 8px;
font-size: 12px;
line-height: 1;
border-radius: 4px;
border: none;
cursor: pointer;
&:last-child {
margin-bottom: 0;
}
}
}
}
}
}
}
// Time Grid - Week view
.fc-view.fc-timeGridWeek-view {
.fc-head {
> tr > .fc-head-container {
border: none;
.fc-row {
.fc-axis {
width: 48px !important;
}
.fc-day-header {
span {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
width: 100%;
&.fc-weekday {
padding-top: 16px;
font-size: 12px;
letter-spacing: 0.055em;
text-transform: uppercase;
font-weight: 400;
}
&.fc-date {
padding-bottom: 12px;
font-size: 26px;
font-weight: 300;
}
}
}
}
}
}
.fc-body {
> tr > .fc-widget-content {
border: none;
.fc-day-grid {
.fc-row {
min-height: 0;
.fc-bg {
.fc-axis {
width: 48px !important;
}
}
.fc-content-skeleton {
padding-bottom: 0;
.fc-axis {
width: 48px !important;
}
.fc-event-container {
.fc-day-grid-event {
display: flex;
align-items: center;
height: 22px;
min-height: 22px;
max-height: 22px;
margin: 0 6px 6px 6px;
padding: 0 8px;
font-size: 12px;
line-height: 1;
border-radius: 4px;
border: none;
cursor: pointer;
}
}
}
}
}
.fc-divider {
border: none;
}
.fc-time-grid {
.fc-bg {
.fc-axis {
border: none;
width: 48px !important;
+ .fc-day {
border: none;
}
}
}
.fc-slats {
.fc-axis {
width: 48px !important;
height: 48px;
text-align: center;
span {
font-size: 12px;
width: 48px;
min-width: 48px;
}
}
}
.fc-content-skeleton {
.fc-axis {
width: 48px !important;
}
.fc-event-container {
margin: 0 12px 0 0;
.fc-time-grid-event {
display: flex;
padding: 8px;
border-radius: 4px;
border: none;
cursor: pointer;
.fc-time,
.fc-title {
font-size: 12px;
}
}
}
}
}
}
}
}
// Time Grid - Day view
.fc-view.fc-timeGridDay-view {
.fc-head {
> tr > .fc-head-container {
border: none;
.fc-row {
.fc-axis {
width: 48px !important;
}
.fc-day-header {
span {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
width: 100%;
&.fc-weekday {
padding-top: 16px;
font-size: 12px;
letter-spacing: 0.055em;
text-transform: uppercase;
font-weight: 400;
}
&.fc-date {
padding-bottom: 12px;
font-size: 26px;
font-weight: 300;
}
}
}
}
}
}
.fc-body {
> tr > .fc-widget-content {
border: none;
.fc-day-grid {
.fc-row {
min-height: 0;
.fc-bg {
.fc-axis {
width: 48px !important;
}
}
.fc-content-skeleton {
padding-bottom: 0;
.fc-axis {
width: 48px !important;
}
.fc-event-container {
.fc-day-grid-event {
display: flex;
align-items: center;
height: 22px;
min-height: 22px;
max-height: 22px;
margin: 0 6px 6px 6px;
padding: 0 8px;
font-size: 12px;
line-height: 1;
border-radius: 4px;
border: none;
cursor: pointer;
}
}
}
}
}
.fc-divider {
border: none;
}
.fc-time-grid {
.fc-bg {
.fc-axis {
border: none;
width: 48px !important;
+ .fc-day {
border: none;
}
}
}
.fc-slats {
.fc-axis {
width: 48px !important;
height: 48px;
text-align: center;
span {
font-size: 12px;
width: 48px;
min-width: 48px;
}
}
}
.fc-content-skeleton {
.fc-axis {
width: 48px !important;
}
.fc-event-container {
margin: 0 12px 0 0;
.fc-time-grid-event {
display: flex;
padding: 8px;
border-radius: 4px;
border: none;
cursor: pointer;
.fc-time,
.fc-title {
font-size: 12px;
}
}
}
}
}
}
}
}
// List - Year view
.fc-view.fc-listYear-view {
border: none;
.fc-list-table {
.fc-list-heading {
display: none;
}
.fc-list-item {
display: flex;
cursor: pointer;
td {
display: flex;
align-items: center;
width: auto;
height: 48px;
min-height: 48px;
padding: 0 8px;
border-width: 0 0 1px 0;
&.fc-list-item-date {
order: 1;
padding-left: 16px;
width: 120px;
min-width: 120px;
max-width: 120px;
@include treo-breakpoint('xs') {
width: 100px;
min-width: 100px;
max-width: 100px;
}
> span {
display: flex;
align-items: baseline;
span {
&:first-child {
display: flex;
justify-content: center;
padding-right: 8px;
width: 32px;
min-width: 32px;
max-width: 32px;
font-size: 18px;
@include treo-breakpoint('xs') {
padding-right: 2px;
}
+ span {
display: flex;
font-size: 11px;
font-weight: 500;
letter-spacing: 0.055em;
text-transform: uppercase;
}
}
}
}
}
&.fc-list-item-time {
flex: 0 0 auto;
order: 3;
width: 160px;
min-width: 160px;
max-width: 160px;
@include treo-breakpoint('xs') {
width: 120px;
min-width: 120px;
max-width: 120px;
}
}
&.fc-list-item-marker {
flex: 0 0 auto;
order: 2;
.fc-event-dot {
width: 12px;
height: 12px;
border-radius: 50%;
}
}
&.fc-list-item-title {
flex: 1 1 auto;
order: 4;
padding-right: 24px;
font-weight: 500;
}
}
}
}
}
}
// Day grid event - Dragging
.fc-day-grid-event {
&.fc-dragging,
&.fc-resizing {
display: flex;
align-items: center;
height: 22px;
min-height: 22px;
max-height: 22px;
margin: 0 6px 4px 6px;
padding: 0 8px;
font-size: 12px;
line-height: 1;
border-radius: 4px;
border: none;
}
}
}
// -----------------------------------------------------------------------------------------------------
// @ Theming
// -----------------------------------------------------------------------------------------------------
@include treo-theme {
$background: map-get($theme, background);
$foreground: map-get($theme, foreground);
$primary: map-get($theme, primary);
.fc {
.fc-view-container {
// Day Grid - Month view
.fc-view.fc-dayGridMonth-view {
.fc-head {
> tr > .fc-head-container {
.fc-row {
.fc-day-header {
border-color: map-get($foreground, divider);
span {
color: map-get($foreground, secondary-text);
}
}
}
}
}
.fc-body {
> tr > .fc-widget-content {
.fc-day-grid {
.fc-week {
.fc-bg {
.fc-day {
border-color: map-get($foreground, divider);
&.fc-today {
background: none;
}
}
}
.fc-content-skeleton {
.fc-day-top {
&.fc-other-month {
.fc-day-number {
color: map-get($foreground, hint-text);
}
}
&.fc-today {
.fc-day-number {
background: map-get($primary, default);
color: map-get($primary, default-contrast);
}
}
}
.fc-more {
color: map-get($foreground, secondary-text);
}
}
.fc-highlight-skeleton {
.fc-highlight {
background: treo-color('cool-gray', 100);
opacity: 1;
}
}
}
}
}
}
.fc-popover {
background: map-get($background, card);
&.fc-more-popover {
.fc-header {
background: map-get($background, hover);
}
}
}
}
// Time Grid - Week view
.fc-view.fc-timeGridWeek-view {
.fc-head {
> tr > .fc-head-container {
.fc-row {
.fc-axis {
border-color: map-get($foreground, divider);
}
.fc-day-header {
border-color: map-get($foreground, divider);
span {
color: map-get($foreground, secondary-text);
}
}
}
}
}
.fc-body {
> tr > .fc-widget-content {
border: none;
.fc-day-grid {
.fc-bg {
.fc-axis {
border-color: map-get($foreground, divider);
}
.fc-day {
border-color: map-get($foreground, divider);
&.fc-today {
background: none;
}
}
}
}
.fc-divider {
background: map-get($foreground, divider);
}
.fc-time-grid {
.fc-bg {
.fc-day {
border-color: map-get($foreground, divider);
&.fc-today {
background: none;
}
}
}
.fc-slats {
.fc-time {
border-color: map-get($foreground, divider);
}
.fc-widget-content {
border-color: map-get($foreground, divider);
}
}
}
}
}
}
// Time Grid - Day view
.fc-view.fc-timeGridDay-view {
.fc-head {
> tr > .fc-head-container {
.fc-row {
.fc-axis {
border-color: map-get($foreground, divider);
}
.fc-day-header {
border-color: map-get($foreground, divider);
span {
color: map-get($foreground, secondary-text);
}
}
}
}
}
.fc-body {
> tr > .fc-widget-content {
border: none;
.fc-day-grid {
.fc-bg {
.fc-axis {
border-color: map-get($foreground, divider);
}
.fc-day {
border-color: map-get($foreground, divider);
&.fc-today {
background: none;
}
}
}
}
.fc-divider {
background: map-get($foreground, divider);
}
.fc-time-grid {
.fc-bg {
.fc-day {
border-color: map-get($foreground, divider);
&.fc-today {
background: none;
}
}
}
.fc-slats {
.fc-time {
border-color: map-get($foreground, divider);
}
.fc-widget-content {
border-color: map-get($foreground, divider);
}
}
}
}
}
}
// List - Year view
.fc-view.fc-listYear-view {
.fc-list-table {
.fc-list-item {
&:hover {
td {
background-color: map-get($background, hover);
}
}
td {
border-color: map-get($foreground, divider);
&.fc-list-item-date {
> span {
span {
&:first-child {
+ span {
color: map-get($foreground, secondary-text);
}
}
}
}
}
}
}
}
}
}
}
}
+17 -17
View File
@@ -1,22 +1,22 @@
import { NgModule, enableProdMode } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { ExtraOptions, PreloadAllModules, RouterModule } from '@angular/router';
import { APP_BASE_HREF } from '@angular/common';
import { MarkdownModule } from 'ngx-markdown';
import { TreoModule } from '@treo';
import { TreoConfigModule } from '@treo/services/config';
import { TreoMockApiModule } from '@treo/lib/mock-api';
import { CoreModule } from 'app/core/core.module';
import { appConfig } from 'app/core/config/app.config';
import { mockDataServices } from 'app/data/mock';
import { LayoutModule } from 'app/layout/layout.module';
import { AppComponent } from 'app/app.component';
import { appRoutes, getAppBaseHref } from 'app/app.routing';
import {enableProdMode, NgModule} from '@angular/core';
import {BrowserModule} from '@angular/platform-browser';
import {BrowserAnimationsModule} from '@angular/platform-browser/animations';
import {ExtraOptions, PreloadAllModules, RouterModule} from '@angular/router';
import {APP_BASE_HREF} from '@angular/common';
import {MarkdownModule} from 'ngx-markdown';
import {TreoModule} from '@treo';
import {ScrutinyConfigModule} from 'app/core/config/scrutiny-config.module';
import {TreoMockApiModule} from '@treo/lib/mock-api';
import {CoreModule} from 'app/core/core.module';
import {appConfig} from 'app/core/config/app.config';
import {mockDataServices} from 'app/data/mock';
import {LayoutModule} from 'app/layout/layout.module';
import {AppComponent} from 'app/app.component';
import {appRoutes, getAppBaseHref} from 'app/app.routing';
const routerConfig: ExtraOptions = {
scrollPositionRestoration: 'enabled',
preloadingStrategy : PreloadAllModules
preloadingStrategy: PreloadAllModules
};
let dev = [
@@ -41,7 +41,7 @@ if (process.env.NODE_ENV === 'production') {
// Treo & Treo Mock API
TreoModule,
TreoConfigModule.forRoot(appConfig),
ScrutinyConfigModule.forRoot(appConfig),
...dev,
// Core
@@ -10,19 +10,53 @@ export type DashboardSort = 'status' | 'title' | 'age'
export type TemperatureUnit = 'celsius' | 'fahrenheit'
export type LineStroke = 'smooth' | 'straight' | 'stepline'
export enum MetricsNotifyLevel {
Warn = 1,
Fail = 2
}
export enum MetricsStatusFilterAttributes {
All = 0,
Critical = 1
}
export enum MetricsStatusThreshold {
Smart = 1,
Scrutiny = 2,
// shortcut
Both = 3
}
/**
* AppConfig interface. Update this interface to strictly type your config
* object.
*/
export interface AppConfig {
theme: Theme;
layout: Layout;
theme?: Theme;
layout?: Layout;
// Dashboard options
dashboardDisplay: DashboardDisplay;
dashboardSort: DashboardSort;
dashboard_display?: DashboardDisplay;
dashboard_sort?: DashboardSort;
temperature_unit?: TemperatureUnit;
file_size_si_units?: boolean;
line_stroke?: LineStroke;
// Settings from Scrutiny API
metrics?: {
notify_level?: MetricsNotifyLevel
status_filter_attributes?: MetricsStatusFilterAttributes
status_threshold?: MetricsStatusThreshold
}
temperatureUnit: TemperatureUnit;
}
/**
@@ -34,12 +68,21 @@ export interface AppConfig {
* "ConfigService".
*/
export const appConfig: AppConfig = {
theme : 'light',
theme: 'light',
layout: 'material',
dashboardDisplay: 'name',
dashboardSort: 'status',
dashboard_display: 'name',
dashboard_sort: 'status',
temperatureUnit: 'celsius',
temperature_unit: 'celsius',
file_size_si_units: false,
line_stroke: 'smooth',
metrics: {
notify_level: MetricsNotifyLevel.Fail,
status_filter_attributes: MetricsStatusFilterAttributes.All,
status_threshold: MetricsStatusThreshold.Both
}
};
@@ -0,0 +1,33 @@
import {ModuleWithProviders, NgModule} from '@angular/core';
import {ScrutinyConfigService} from 'app/core/config/scrutiny-config.service';
import {TREO_APP_CONFIG} from '@treo/services/config/config.constants';
@NgModule()
export class ScrutinyConfigModule {
/**
* Constructor
*
* @param {ScrutinyConfigService} _scrutinyConfigService
*/
constructor(
private _scrutinyConfigService: ScrutinyConfigService
) {
}
/**
* forRoot method for setting user configuration
*
* @param config
*/
static forRoot(config: any): ModuleWithProviders<ScrutinyConfigModule> {
return {
ngModule: ScrutinyConfigModule,
providers: [
{
provide: TREO_APP_CONFIG,
useValue: config
}
]
};
}
}
@@ -0,0 +1,84 @@
import {Inject, Injectable} from '@angular/core';
import {HttpClient} from '@angular/common/http';
import {TREO_APP_CONFIG} from '@treo/services/config/config.constants';
import {BehaviorSubject, Observable} from 'rxjs';
import {getBasePath} from '../../app.routing';
import {map, tap} from 'rxjs/operators';
import {AppConfig} from './app.config';
import {merge} from 'lodash';
@Injectable({
providedIn: 'root'
})
export class ScrutinyConfigService {
// Private
private _config: BehaviorSubject<AppConfig>;
private _defaultConfig: AppConfig;
constructor(
private _httpClient: HttpClient,
@Inject(TREO_APP_CONFIG) defaultConfig: AppConfig
) {
// Set the private defaults
this._defaultConfig = defaultConfig
this._config = new BehaviorSubject(null);
}
// -----------------------------------------------------------------------------------------------------
// @ Accessors
// -----------------------------------------------------------------------------------------------------
/**
* Setter & getter for config
*/
set config(value: AppConfig) {
// get the current config, merge the new values, and then submit. (setTheme only sets a single key, not the whole obj)
const mergedSettings = merge({}, this._config.getValue(), value);
console.log('saving settings...', mergedSettings)
this._httpClient.post(getBasePath() + '/api/settings', mergedSettings).pipe(
map((response: any) => {
console.log('settings resp')
return response.settings
}),
tap((settings: AppConfig) => {
this._config.next(settings);
return settings
})
).subscribe(resp => {
console.log('updated settings', resp)
})
}
get config$(): Observable<AppConfig> {
if (this._config.getValue()) {
console.log('using cached settings:', this._config.getValue())
return this._config.asObservable()
} else {
console.log('retrieving settings')
return this._httpClient.get(getBasePath() + '/api/settings').pipe(
map((response: any) => {
return response.settings
}),
tap((settings: AppConfig) => {
this._config.next(settings);
return this._config.asObservable()
})
);
}
}
// -----------------------------------------------------------------------------------------------------
// @ Public methods
// -----------------------------------------------------------------------------------------------------
/**
* Resets the config to the default
*/
reset(): void {
// Set the config
this.config = this._defaultConfig
}
}
@@ -1,21 +1,21 @@
<div [ngClass]="{ 'border-green': deviceSummary.device.device_status == 0 && deviceSummary.smart,
'border-red': deviceSummary.device.device_status != 0 }"
<div [ngClass]="{ 'border-green': deviceStatusForModelWithThreshold(deviceSummary.device, !!deviceSummary.smart, config.metrics.status_threshold) == 'passed',
'border-red': deviceStatusForModelWithThreshold(deviceSummary.device, !!deviceSummary.smart, config.metrics.status_threshold) == 'failed' }"
class="relative flex flex-col flex-auto p-6 pr-3 pb-3 bg-card rounded border-l-4 shadow-md overflow-hidden">
<div class="absolute bottom-0 right-0 w-24 h-24 -m-6">
<mat-icon class="icon-size-96 opacity-12 text-green"
*ngIf="deviceSummary.device.device_status == 0 && deviceSummary.smart"
*ngIf="deviceStatusForModelWithThreshold(deviceSummary.device, !!deviceSummary.smart, config.metrics.status_threshold) == 'passed'"
[svgIcon]="'heroicons_outline:check-circle'"></mat-icon>
<mat-icon class="icon-size-96 opacity-12 text-red"
*ngIf="deviceSummary.device.device_status != 0"
*ngIf="deviceStatusForModelWithThreshold(deviceSummary.device, !!deviceSummary.smart, config.metrics.status_threshold) == 'failed'"
[svgIcon]="'heroicons_outline:exclamation-circle'"></mat-icon>
<mat-icon class="icon-size-96 opacity-12 text-yellow"
*ngIf="!deviceSummary.smart"
*ngIf="deviceStatusForModelWithThreshold(deviceSummary.device, !!deviceSummary.smart, config.metrics.status_threshold) == 'unknown'"
[svgIcon]="'heroicons_outline:question-mark-circle'"></mat-icon>
</div>
<div class="flex items-center">
<div class="flex flex-col">
<a [routerLink]="'/device/'+ deviceSummary.device.wwn"
class="font-bold text-md text-secondary uppercase tracking-wider">{{deviceSummary.device | deviceTitle:config.dashboardDisplay}}</a>
class="font-bold text-md text-secondary uppercase tracking-wider">{{deviceSummary.device | deviceTitle:config.dashboard_display}}</a>
<div [ngClass]="classDeviceLastUpdatedOn(deviceSummary)" class="font-medium text-sm" *ngIf="deviceSummary.smart">
Last Updated on {{deviceSummary.smart.collector_date | date:'MMMM dd, yyyy - HH:mm' }}
</div>
@@ -46,17 +46,20 @@
<div class="flex flex-row flex-wrap mt-4 -mx-6">
<div class="flex flex-col mx-6 my-3 xs:w-full">
<div class="font-semibold text-xs text-hint uppercase tracking-wider leading-none">Status</div>
<div class="mt-2 font-medium text-3xl leading-none" *ngIf="deviceSummary.smart?.collector_date; else unknownStatus">{{ deviceStatusString(deviceSummary.device.device_status) | titlecase}}</div>
<div class="mt-2 font-medium text-3xl leading-none"
*ngIf="deviceSummary.smart?.collector_date; else unknownStatus">{{ deviceStatusForModelWithThreshold(deviceSummary.device, !!deviceSummary.smart, config.metrics.status_threshold) | titlecase}}</div>
<ng-template #unknownStatus><div class="mt-2 font-medium text-3xl leading-none">No Data</div></ng-template>
</div>
<div class="flex flex-col mx-6 my-3 xs:w-full">
<div class="font-semibold text-xs text-hint uppercase tracking-wider leading-none">Temperature</div>
<div class="mt-2 font-medium text-3xl leading-none" *ngIf="deviceSummary.smart?.collector_date; else unknownTemp">{{ deviceSummary.smart?.temp | temperature:config.temperatureUnit:true }}</div>
<div class="mt-2 font-medium text-3xl leading-none"
*ngIf="deviceSummary.smart?.collector_date; else unknownTemp">{{ deviceSummary.smart?.temp | temperature:config.temperature_unit:true }}</div>
<ng-template #unknownTemp><div class="mt-2 font-medium text-3xl leading-none">--</div></ng-template>
</div>
<div class="flex flex-col mx-6 my-3 xs:w-full">
<div class="font-semibold text-xs text-hint uppercase tracking-wider leading-none">Capacity</div>
<div class="mt-2 font-medium text-3xl leading-none">{{ deviceSummary.device.capacity | fileSize}}</div>
<div
class="mt-2 font-medium text-3xl leading-none">{{ deviceSummary.device.capacity | fileSize:config.file_size_si_units}}</div>
</div>
<div class="flex flex-col mx-6 my-3 xs:w-full">
<div class="font-semibold text-xs text-hint uppercase tracking-wider leading-none">Powered On</div>
@@ -9,26 +9,38 @@ import {MatMenuModule} from '@angular/material/menu';
import {TREO_APP_CONFIG} from '@treo/services/config/config.constants';
import {DeviceSummaryModel} from 'app/core/models/device-summary-model';
import * as moment from 'moment';
import {HttpClientTestingModule} from '@angular/common/http/testing';
import {HttpClient} from '@angular/common/http';
import {ScrutinyConfigService} from 'app/core/config/scrutiny-config.service';
import {of} from 'rxjs';
import {MetricsStatusThreshold} from 'app/core/config/app.config';
describe('DashboardDeviceComponent', () => {
let component: DashboardDeviceComponent;
let fixture: ComponentFixture<DashboardDeviceComponent>;
const matDialogSpy = jasmine.createSpyObj('MatDialog', ['open']);
// const configServiceSpy = jasmine.createSpyObj('TreoConfigService', ['config$']);
// const configServiceSpy = jasmine.createSpyObj('ScrutinyConfigService', ['config$']);
let configService: ScrutinyConfigService;
let httpClientSpy: jasmine.SpyObj<HttpClient>;
beforeEach(async(() => {
httpClientSpy = jasmine.createSpyObj('HttpClient', ['get']);
configService = new ScrutinyConfigService(httpClientSpy, {});
TestBed.configureTestingModule({
imports: [
MatButtonModule,
MatIconModule,
MatMenuModule,
SharedModule,
HttpClientTestingModule,
],
providers: [
{provide: MatDialog, useValue: matDialogSpy},
{provide: TREO_APP_CONFIG, useValue: {dashboardDisplay: 'name'}}
{provide: TREO_APP_CONFIG, useValue: {dashboard_display: 'name', metrics: {status_threshold: 3}}},
{provide: ScrutinyConfigService, useValue: configService}
],
declarations: [DashboardDeviceComponent]
})
@@ -48,25 +60,53 @@ describe('DashboardDeviceComponent', () => {
describe('#classDeviceLastUpdatedOn()', () => {
it('if non-zero device status, should be red', () => {
httpClientSpy.get.and.returnValue(of({
settings: {
metrics: {
status_threshold: MetricsStatusThreshold.Both,
}
}
}));
component.ngOnInit()
// component.deviceSummary = summary.data.summary['0x5000c500673e6b5f'] as DeviceSummaryModel
expect(component.classDeviceLastUpdatedOn({
device: {
device_status: 2
}
device_status: 2,
},
smart: {
collector_date: moment().subtract(13, 'days').toISOString()
},
} as DeviceSummaryModel)).toBe('text-red')
});
it('if non-zero device status, should be red', () => {
// component.deviceSummary = summary.data.summary['0x5000c500673e6b5f'] as DeviceSummaryModel
httpClientSpy.get.and.returnValue(of({
settings: {
metrics: {
status_threshold: MetricsStatusThreshold.Both,
}
}
}));
component.ngOnInit()
expect(component.classDeviceLastUpdatedOn({
device: {
device_status: 2
}
},
smart: {
collector_date: moment().subtract(13, 'days').toISOString()
},
} as DeviceSummaryModel)).toBe('text-red')
});
it('if healthy device status and updated in the last two weeks, should be green', () => {
// component.deviceSummary = summary.data.summary['0x5000c500673e6b5f'] as DeviceSummaryModel
httpClientSpy.get.and.returnValue(of({
settings: {
metrics: {
status_threshold: MetricsStatusThreshold.Both,
}
}
}));
component.ngOnInit()
expect(component.classDeviceLastUpdatedOn({
device: {
device_status: 0
@@ -78,7 +118,14 @@ describe('DashboardDeviceComponent', () => {
});
it('if healthy device status and updated more than two weeks ago, but less than 1 month, should be yellow', () => {
// component.deviceSummary = summary.data.summary['0x5000c500673e6b5f'] as DeviceSummaryModel
httpClientSpy.get.and.returnValue(of({
settings: {
metrics: {
status_threshold: MetricsStatusThreshold.Both,
}
}
}));
component.ngOnInit()
expect(component.classDeviceLastUpdatedOn({
device: {
device_status: 0
@@ -90,7 +137,14 @@ describe('DashboardDeviceComponent', () => {
});
it('if healthy device status and updated more 1 month ago, should be red', () => {
// component.deviceSummary = summary.data.summary['0x5000c500673e6b5f'] as DeviceSummaryModel
httpClientSpy.get.and.returnValue(of({
settings: {
metrics: {
status_threshold: MetricsStatusThreshold.Both,
}
}
}));
component.ngOnInit()
expect(component.classDeviceLastUpdatedOn({
device: {
device_status: 0
@@ -101,5 +155,4 @@ describe('DashboardDeviceComponent', () => {
} as DeviceSummaryModel)).toBe('text-red')
});
})
});
@@ -2,13 +2,14 @@ import {Component, EventEmitter, Input, OnInit, Output} from '@angular/core';
import * as moment from 'moment';
import {takeUntil} from 'rxjs/operators';
import {AppConfig} from 'app/core/config/app.config';
import {TreoConfigService} from '@treo/services/config';
import {ScrutinyConfigService} from 'app/core/config/scrutiny-config.service';
import {Subject} from 'rxjs';
import humanizeDuration from 'humanize-duration'
import {MatDialog} from '@angular/material/dialog';
import {DashboardDeviceDeleteDialogComponent} from 'app/layout/common/dashboard-device-delete-dialog/dashboard-device-delete-dialog.component';
import {DeviceTitlePipe} from 'app/shared/device-title.pipe';
import {DeviceSummaryModel} from 'app/core/models/device-summary-model';
import {DeviceStatusPipe} from 'app/shared/device-status.pipe';
@Component({
selector: 'app-dashboard-device',
@@ -18,7 +19,7 @@ import {DeviceSummaryModel} from 'app/core/models/device-summary-model';
export class DashboardDeviceComponent implements OnInit {
constructor(
private _configService: TreoConfigService,
private _configService: ScrutinyConfigService,
public dialog: MatDialog,
) {
// Set the private defaults
@@ -31,10 +32,12 @@ export class DashboardDeviceComponent implements OnInit {
config: AppConfig;
private _unsubscribeAll: Subject<any>;
private _unsubscribeAll: Subject<void>;
readonly humanizeDuration = humanizeDuration;
deviceStatusForModelWithThreshold = DeviceStatusPipe.deviceStatusForModelWithThreshold
ngOnInit(): void {
// Subscribe to config changes
this._configService.config$
@@ -50,9 +53,10 @@ export class DashboardDeviceComponent implements OnInit {
// -----------------------------------------------------------------------------------------------------
classDeviceLastUpdatedOn(deviceSummary: DeviceSummaryModel): string {
if (deviceSummary.device.device_status !== 0) {
const deviceStatus = DeviceStatusPipe.deviceStatusForModelWithThreshold(deviceSummary.device, !!deviceSummary.smart, this.config.metrics.status_threshold)
if (deviceStatus === 'failed') {
return 'text-red' // if the device has failed, always highlight in red
} else if (deviceSummary.device.device_status === 0 && deviceSummary.smart) {
} else if (deviceStatus === 'passed') {
if (moment().subtract(14, 'days').isBefore(deviceSummary.smart.collector_date)) {
// this device was updated in the last 2 weeks.
return 'text-green'
@@ -68,21 +72,12 @@ export class DashboardDeviceComponent implements OnInit {
}
}
deviceStatusString(deviceStatus: number): string {
if (deviceStatus === 0) {
return 'passed'
} else {
return 'failed'
}
}
openDeleteDialog(): void {
const dialogRef = this.dialog.open(DashboardDeviceDeleteDialogComponent, {
// width: '250px',
data: {
wwn: this.deviceWWN,
title: DeviceTitlePipe.deviceTitleWithFallback(this.deviceSummary.device, this.config.dashboardDisplay)
title: DeviceTitlePipe.deviceTitleWithFallback(this.deviceSummary.device, this.config.dashboard_display)
}
});
@@ -37,69 +37,52 @@
<div class="flex flex-col mt-5 gt-md:flex-row">
<mat-form-field class="flex-auto gt-xs:pr-3 gt-md:pr-3">
<mat-label>Temperature Display Unit</mat-label>
<mat-label>Temperature</mat-label>
<mat-select [(ngModel)]="temperatureUnit">
<mat-option value="celsius">Celsius</mat-option>
<mat-option value="fahrenheit">Fahrenheit</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field class="flex-auto gt-xs:pr-3 gt-md:pr-3">
<mat-label>File Size</mat-label>
<mat-select [(ngModel)]="fileSizeSIUnits">
<mat-option [value]=true>SI Units (GB)</mat-option>
<mat-option [value]=false>Binary Units (GiB)</mat-option>
</mat-select>
</mat-form-field>
</div>
<div class="flex">
<mat-tab-group mat-align-tabs="start">
<mat-tab label="Ata">
<div class="flex flex-col mt-5 gt-md:flex-row">
<mat-form-field class="flex-auto gt-xs:pr-3 gt-md:pr-3">
<mat-label>Line stroke</mat-label>
<mat-select [(ngModel)]="lineStroke">
<mat-option value="smooth">Smooth</mat-option>
<mat-option value="straight">Straight</mat-option>
<mat-option value="stepline">Stepline</mat-option>
</mat-select>
</mat-form-field>
</div>
<div matTooltip="not yet implemented" class="flex flex-col mt-5 gt-md:flex-row">
<mat-form-field class="flex-auto gt-md:pr-3">
<mat-label class="text-hint">Critical Error Threshold</mat-label>
<input disabled matInput [value]="'10%'">
</mat-form-field>
<mat-form-field class="flex-auto gt-md:pl-3">
<mat-label class="text-hint">Critical Warning Threshold</mat-label>
<input disabled matInput>
</mat-form-field>
</div>
<div class="flex flex-col mt-5 gt-md:flex-row">
<mat-form-field class="flex-auto gt-xs:pr-3 gt-md:pr-3">
<mat-label>Device Status - Thresholds</mat-label>
<mat-select [(ngModel)]=statusThreshold>
<mat-option [value]=1>Smart</mat-option>
<mat-option [value]=2>Scrutiny</mat-option>
<mat-option [value]=3>Both</mat-option>
</mat-select>
</mat-form-field>
</div>
<div matTooltip="not yet implemented" class="flex flex-col gt-md:flex-row">
<mat-form-field class="flex-auto gt-md:pr-3">
<mat-label class="text-hint">Error Threshold</mat-label>
<input disabled matInput [value]="'20%'">
</mat-form-field>
<mat-form-field class="flex-auto gt-md:pl-3">
<mat-label class="text-hint">Warning Threshold</mat-label>
<input disabled matInput [value]="'10%'">
</mat-form-field>
</div>
</mat-tab>
<mat-tab label="NVMe">
<div matTooltip="not yet implemented" class="flex flex-col mt-5 gt-md:flex-row">
<mat-form-field class="flex-auto gt-md:pr-3">
<mat-label class="text-hint">Critical Error Threshold</mat-label>
<input disabled matInput [value]="'enabled'">
</mat-form-field>
<mat-form-field class="flex-auto gt-md:pl-3">
<mat-label class="text-hint">Critical Warning Threshold</mat-label>
<input disabled matInput>
</mat-form-field>
</div>
</mat-tab>
<mat-tab label="SCSI">
<div matTooltip="not yet implemented" class="flex flex-col mt-5 gt-md:flex-row">
<mat-form-field class="flex-auto gt-md:pr-3">
<mat-label class="text-hint">Critical Error Threshold</mat-label>
<input disabled matInput [value]="'enabled'">
</mat-form-field>
<mat-form-field class="flex-auto gt-md:pl-3">
<mat-label class="text-hint">Critical Warning Threshold</mat-label>
<input disabled matInput>
</mat-form-field>
</div>
</mat-tab>
</mat-tab-group>
<div class="flex flex-col mt-5 gt-md:flex-row">
<mat-form-field class="flex-auto gt-xs:pr-3 gt-md:pr-3">
<mat-label>Notify - Filter Attributes</mat-label>
<mat-select [(ngModel)]=statusFilterAttributes>
<mat-option [value]=0>All</mat-option>
<mat-option [value]=1>Critical</mat-option>
</mat-select>
</mat-form-field>
</div>
</div>
@@ -1,6 +1,15 @@
import {Component, OnInit} from '@angular/core';
import {AppConfig} from 'app/core/config/app.config';
import {TreoConfigService} from '@treo/services/config';
import {
AppConfig,
DashboardDisplay,
DashboardSort,
MetricsStatusFilterAttributes,
MetricsStatusThreshold,
TemperatureUnit,
LineStroke,
Theme
} from 'app/core/config/app.config';
import {ScrutinyConfigService} from 'app/core/config/scrutiny-config.service';
import {Subject} from 'rxjs';
import {takeUntil} from 'rxjs/operators';
@@ -14,13 +23,17 @@ export class DashboardSettingsComponent implements OnInit {
dashboardDisplay: string;
dashboardSort: string;
temperatureUnit: string;
fileSizeSIUnits: boolean;
lineStroke: string;
theme: string;
statusThreshold: number;
statusFilterAttributes: number;
// Private
private _unsubscribeAll: Subject<any>;
private _unsubscribeAll: Subject<void>;
constructor(
private _configService: TreoConfigService,
private _configService: ScrutinyConfigService,
) {
// Set the private defaults
this._unsubscribeAll = new Subject();
@@ -33,21 +46,32 @@ export class DashboardSettingsComponent implements OnInit {
.subscribe((config: AppConfig) => {
// Store the config
this.dashboardDisplay = config.dashboardDisplay;
this.dashboardSort = config.dashboardSort;
this.temperatureUnit = config.temperatureUnit;
this.dashboardDisplay = config.dashboard_display;
this.dashboardSort = config.dashboard_sort;
this.temperatureUnit = config.temperature_unit;
this.fileSizeSIUnits = config.file_size_si_units;
this.lineStroke = config.line_stroke;
this.theme = config.theme;
this.statusFilterAttributes = config.metrics.status_filter_attributes;
this.statusThreshold = config.metrics.status_threshold;
});
}
saveSettings(): void {
const newSettings = {
dashboardDisplay: this.dashboardDisplay,
dashboardSort: this.dashboardSort,
temperatureUnit: this.temperatureUnit,
theme: this.theme
const newSettings: AppConfig = {
dashboard_display: this.dashboardDisplay as DashboardDisplay,
dashboard_sort: this.dashboardSort as DashboardSort,
temperature_unit: this.temperatureUnit as TemperatureUnit,
file_size_si_units: this.fileSizeSIUnits,
line_stroke: this.lineStroke as LineStroke,
theme: this.theme as Theme,
metrics: {
status_filter_attributes: this.statusFilterAttributes as MetricsStatusFilterAttributes,
status_threshold: this.statusThreshold as MetricsStatusThreshold
}
}
this._configService.config = newSettings
console.log(`Saved Settings: ${JSON.stringify(newSettings)}`)
@@ -35,7 +35,7 @@ export class SearchComponent implements OnInit, OnDestroy
// Private
private _appearance: 'basic' | 'bar';
private _opened: boolean;
private _unsubscribeAll: Subject<any>;
private _unsubscribeAll: Subject<void>;
/**
* Constructor
@@ -1,42 +1,41 @@
import { Component, Inject, OnDestroy, OnInit, ViewEncapsulation } from '@angular/core';
import { DOCUMENT } from '@angular/common';
import { ActivatedRoute, NavigationEnd, Router } from '@angular/router';
import { MatSlideToggleChange } from '@angular/material/slide-toggle';
import { Subject } from 'rxjs';
import { filter, takeUntil } from 'rxjs/operators';
import { TreoConfigService } from '@treo/services/config';
import { TreoDrawerService } from '@treo/components/drawer';
import { Layout } from 'app/layout/layout.types';
import { AppConfig, Theme } from 'app/core/config/app.config';
import {Component, Inject, OnDestroy, OnInit, ViewEncapsulation} from '@angular/core';
import {DOCUMENT} from '@angular/common';
import {ActivatedRoute, NavigationEnd, Router} from '@angular/router';
import {MatSlideToggleChange} from '@angular/material/slide-toggle';
import {Subject} from 'rxjs';
import {filter, takeUntil} from 'rxjs/operators';
import {ScrutinyConfigService} from 'app/core/config/scrutiny-config.service';
import {TreoDrawerService} from '@treo/components/drawer';
import {Layout} from 'app/layout/layout.types';
import {AppConfig, Theme} from 'app/core/config/app.config';
@Component({
selector : 'layout',
templateUrl : './layout.component.html',
styleUrls : ['./layout.component.scss'],
selector: 'layout',
templateUrl: './layout.component.html',
styleUrls: ['./layout.component.scss'],
encapsulation: ViewEncapsulation.None
})
export class LayoutComponent implements OnInit, OnDestroy
{
export class LayoutComponent implements OnInit, OnDestroy {
config: AppConfig;
layout: Layout;
theme: Theme;
// Private
private _unsubscribeAll: Subject<any>;
private _unsubscribeAll: Subject<void>;
private systemPrefersDark: boolean;
/**
* Constructor
*
* @param {ActivatedRoute} _activatedRoute
* @param {TreoConfigService} _treoConfigService
* @param {ScrutinyConfigService} _scrutinyConfigService
* @param {TreoDrawerService} _treoDrawerService
* @param {DOCUMENT} _document
* @param {Router} _router
*/
constructor(
private _activatedRoute: ActivatedRoute,
private _treoConfigService: TreoConfigService,
private _scrutinyConfigService: ScrutinyConfigService,
private _treoDrawerService: TreoDrawerService,
@Inject(DOCUMENT) private _document: any,
private _router: Router
@@ -59,7 +58,7 @@ export class LayoutComponent implements OnInit, OnDestroy
ngOnInit(): void
{
// Subscribe to config changes
this._treoConfigService.config$
this._scrutinyConfigService.config$
.pipe(takeUntil(this._unsubscribeAll))
.subscribe((config: AppConfig) => {
@@ -180,18 +179,17 @@ export class LayoutComponent implements OnInit, OnDestroy
*
* @param layout
*/
setLayout(layout: string): void
{
setLayout(layout: Layout): void {
// Clear the 'layout' query param to allow layout changes
this._router.navigate([], {
queryParams : {
queryParams: {
layout: null
},
queryParamsHandling: 'merge'
}).then(() => {
// Set the config
this._treoConfigService.config = {layout};
this._scrutinyConfigService.config = {layout};
});
}
@@ -202,6 +200,6 @@ export class LayoutComponent implements OnInit, OnDestroy
*/
setTheme(change: MatSlideToggleChange): void
{
this._treoConfigService.config = {theme: change.checked ? 'dark' : 'light'};
this._scrutinyConfigService.config = {theme: change.checked ? 'dark' : 'light'};
}
}
@@ -10,7 +10,7 @@ import { Subject } from 'rxjs';
export class EmptyLayoutComponent implements OnInit, OnDestroy
{
// Private
private _unsubscribeAll: Subject<any>;
private _unsubscribeAll: Subject<void>;
/**
* Constructor
@@ -25,7 +25,7 @@ export class MaterialLayoutComponent implements OnInit, OnDestroy
fixedFooter: boolean;
// Private
private _unsubscribeAll: Subject<any>;
private _unsubscribeAll: Subject<void>;
/**
* Constructor
@@ -51,7 +51,11 @@
<div class="flex flex-wrap w-full" *ngFor="let hostId of hostGroups | keyvalue">
<h3 class="ml-4" *ngIf="hostId.key">{{hostId.key}}</h3>
<div class="flex flex-wrap w-full">
<app-dashboard-device (deviceDeleted)="onDeviceDeleted($event)" class="flex gt-sm:w-1/2 min-w-80 p-4" *ngFor="let deviceSummary of (deviceSummariesForHostGroup(hostId.value) | deviceSort:config.dashboardSort:config.dashboardDisplay )" [deviceWWN]="deviceSummary.device.wwn" [deviceSummary]="deviceSummary"></app-dashboard-device>
<app-dashboard-device (deviceDeleted)="onDeviceDeleted($event)"
class="flex gt-sm:w-1/2 min-w-80 p-4"
*ngFor="let deviceSummary of (deviceSummariesForHostGroup(hostId.value) | deviceSort:config.dashboard_sort:config.dashboard_display )"
[deviceWWN]="deviceSummary.device.wwn"
[deviceSummary]="deviceSummary"></app-dashboard-device>
</div>
</div>
@@ -14,7 +14,7 @@ import {DashboardService} from 'app/modules/dashboard/dashboard.service';
import {MatDialog} from '@angular/material/dialog';
import {DashboardSettingsComponent} from 'app/layout/common/dashboard-settings/dashboard-settings.component';
import {AppConfig} from 'app/core/config/app.config';
import {TreoConfigService} from '@treo/services/config';
import {ScrutinyConfigService} from 'app/core/config/scrutiny-config.service';
import {Router} from '@angular/router';
import {TemperaturePipe} from 'app/shared/temperature.pipe';
import {DeviceTitlePipe} from 'app/shared/device-title.pipe';
@@ -36,20 +36,20 @@ export class DashboardComponent implements OnInit, AfterViewInit, OnDestroy
config: AppConfig;
// Private
private _unsubscribeAll: Subject<any>;
private _unsubscribeAll: Subject<void>;
@ViewChild('tempChart', { static: false }) tempChart: ChartComponent;
/**
* Constructor
*
* @param {DashboardService} _dashboardService
* @param {TreoConfigService} _configService
* @param {ScrutinyConfigService} _configService
* @param {MatDialog} dialog
* @param {Router} router
*/
constructor(
private _dashboardService: DashboardService,
private _configService: TreoConfigService,
private _configService: ScrutinyConfigService,
public dialog: MatDialog,
private router: Router,
)
@@ -150,7 +150,7 @@ export class DashboardComponent implements OnInit, AfterViewInit, OnDestroy
continue
}
const deviceName = DeviceTitlePipe.deviceTitleWithFallback(deviceSummary.device, this.config.dashboardDisplay)
const deviceName = DeviceTitlePipe.deviceTitleWithFallback(deviceSummary.device, this.config.dashboard_display)
const deviceSeriesMetadata = {
name: deviceName,
@@ -161,7 +161,7 @@ export class DashboardComponent implements OnInit, AfterViewInit, OnDestroy
const newDate = new Date(tempHistory.date);
deviceSeriesMetadata.data.push({
x: newDate,
y: TemperaturePipe.formatTemperature(tempHistory.temp, this.config.temperatureUnit, false)
y: TemperaturePipe.formatTemperature(tempHistory.temp, this.config.temperature_unit, false)
})
}
deviceTemperatureSeries.push(deviceSeriesMetadata)
@@ -193,15 +193,15 @@ export class DashboardComponent implements OnInit, AfterViewInit, OnDestroy
enabled: true
}
},
colors : ['#A3BFFA', '#667EEA'],
colors : ['#667eea', '#9066ea', '#66c0ea', '#66ead2', '#d266ea', '#66ea90'],
fill : {
colors : ['#CED9FB', '#AECDFD'],
colors : ['#b2bef4', '#c7b2f4', '#b2dff4', '#b2f4e8', '#e8b2f4', '#b2f4c7'],
opacity: 0.5,
type : 'solid'
type : 'gradient'
},
series : this._deviceDataTemperatureSeries(),
stroke : {
curve: 'straight',
curve: this.config.line_stroke,
width: 2
},
tooltip: {
@@ -212,7 +212,7 @@ export class DashboardComponent implements OnInit, AfterViewInit, OnDestroy
y : {
formatter: (value) => {
return TemperaturePipe.formatTemperature(value, this.config.temperatureUnit, true) as string;
return TemperaturePipe.formatTemperature(value, this.config.temperature_unit, true) as string;
}
}
},
@@ -237,7 +237,7 @@ export class DashboardComponent implements OnInit, AfterViewInit, OnDestroy
}
openDialog(): void {
const dialogRef = this.dialog.open(DashboardSettingsComponent);
const dialogRef = this.dialog.open(DashboardSettingsComponent, {width: '600px',});
dialogRef.afterClosed().subscribe(result => {
console.log(`Dialog result: ${result}`);
@@ -4,7 +4,7 @@
<div class="flex items-center justify-between w-full my-4 px-4 xs:pr-0">
<div class="mr-6">
<h2 class="m-0">Drive Details - {{device | deviceTitle:config.dashboardDisplay}} </h2>
<h2 class="m-0">Drive Details - {{device | deviceTitle:config.dashboard_display}} </h2>
<div class="text-secondary tracking-tight">Dive into S.M.A.R.T data</div>
</div>
<!-- Action buttons -->
@@ -17,7 +17,7 @@
<span class="ml-2">Export</span>
</button>
<button class="ml-2 xs:hidden"
(click)="openDialog()"
matTooltip="not yet implemented"
mat-stroked-button>
<mat-icon class="icon-size-20 rotate-90 mirror"
[svgIcon]="'tune'"></mat-icon>
@@ -56,12 +56,13 @@
<div *ngIf="device" class="my-2 col-span-2 lt-md:col-span-1">
<div>
<span class="inline-flex items-center font-bold text-xs px-2 py-2px rounded-full tracking-wide uppercase"
[ngClass]="{'red-200': device?.device_status != 0,
[ngClass]="{'red-200': deviceStatusForModelWithThreshold(device, !!smart_results, config.metrics.status_threshold) == 'failed',
'green-200': device?.device_status == 0}">
<span class="w-2 h-2 rounded-full mr-2"
[ngClass]="{'bg-red': device?.device_status != 0,
'bg-green': device?.device_status == 0}"></span>
<span class="pr-2px leading-relaxed whitespace-no-wrap">{{device?.device_status | deviceStatus}}</span>
<span
class="pr-2px leading-relaxed whitespace-no-wrap">{{device | deviceStatus:!!smart_results:config.metrics.status_threshold:true}}</span>
</span>
</div>
<div class="text-secondary text-md">Status</div>
@@ -106,7 +107,7 @@
<div class="text-secondary text-md">Firmware Version</div>
</div>
<div class="my-2 col-span-2 lt-md:col-span-1">
<div>{{device?.capacity | fileSize}}</div>
<div>{{device?.capacity | fileSize:config.file_size_si_units}}</div>
<div class="text-secondary text-md">Capacity</div>
</div>
<div *ngIf="device?.rotational_speed" class="my-2 col-span-2 lt-md:col-span-1">
@@ -126,7 +127,7 @@
<div class="text-secondary text-md">Powered On</div>
</div>
<div class="my-2 col-span-2 lt-md:col-span-1">
<div>{{smart_results[0]?.temp | temperature:config.temperatureUnit:true}}</div>
<div>{{smart_results[0]?.temp | temperature:config.temperature_unit:true}}</div>
<div class="text-secondary text-md">Temperature</div>
</div>
</div>

Some files were not shown because too many files have changed in this diff Show More