Compare commits

..

1 Commits

Author SHA1 Message Date
packagrio-bot fc09df19f5 (v0.4.15) Automated packaging of release by Packagr 2022-07-07 05:23:20 +00:00
107 changed files with 1085 additions and 4747 deletions
+7 -43
View File
@@ -3,25 +3,11 @@ name: CI
on: [pull_request] on: [pull_request]
jobs: jobs:
test-frontend: test:
name: Test Frontend name: Test
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Test Frontend
run: |
make binary-frontend-test-coverage
- name: Upload coverage
uses: actions/upload-artifact@v3
with:
name: coverage
path: ${{ github.workspace }}/webapp/frontend/coverage/lcov.info
retention-days: 1
test-backend:
name: Test Backend
runs-on: ubuntu-latest runs-on: ubuntu-latest
container: ghcr.io/packagrio/packagr:latest-golang container: ghcr.io/packagrio/packagr:latest-golang
# Service containers to run with `build` (Required for end-to-end testing) # Service containers to run with `build` (Required for end-to-end testing)
services: services:
influxdb: influxdb:
@@ -36,6 +22,7 @@ jobs:
ports: ports:
- 8086:8086 - 8086:8086
env: env:
PROJECT_PATH: /go/src/github.com/analogj/scrutiny
STATIC: true STATIC: true
steps: steps:
- name: Git - name: Git
@@ -45,36 +32,16 @@ jobs:
git --version git --version
- name: Checkout - name: Checkout
uses: actions/checkout@v2 uses: actions/checkout@v2
- name: Test Backend - name: Test
run: | run: |
make binary-clean binary-test-coverage make binary-clean binary-test-coverage
- name: Upload coverage - name: Generate coverage report
uses: actions/upload-artifact@v3
with:
name: coverage
path: ${{ github.workspace }}/coverage.txt
retention-days: 1
test-coverage:
name: Test Coverage Upload
needs:
- test-backend
- test-frontend
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Download coverage reports
uses: actions/download-artifact@v3
with:
name: coverage
- name: Upload coverage reports
uses: codecov/codecov-action@v2 uses: codecov/codecov-action@v2
with: with:
files: ${{ github.workspace }}/coverage.txt,${{ github.workspace }}/lcov.info files: ${{ github.workspace }}/coverage.txt
flags: unittests flags: unittests
fail_ci_if_error: true fail_ci_if_error: true
verbose: true verbose: true
build: build:
name: Build ${{ matrix.cfg.goos }}/${{ matrix.cfg.goarch }} name: Build ${{ matrix.cfg.goos }}/${{ matrix.cfg.goarch }}
runs-on: ${{ matrix.cfg.on }} runs-on: ${{ matrix.cfg.on }}
@@ -99,9 +66,6 @@ jobs:
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v2 uses: actions/checkout@v2
- uses: actions/setup-go@v3
with:
go-version: '^1.18.3'
- name: Build Binaries - name: Build Binaries
run: | run: |
make binary-clean binary-all make binary-clean binary-all
+15 -10
View File
@@ -74,13 +74,15 @@ jobs:
uses: actions/checkout@v2 uses: actions/checkout@v2
- name: "Populate frontend version information" - name: "Populate frontend version information"
run: "cd webapp/frontend && ./git.version.sh" run: "cd webapp/frontend && ./git.version.sh"
- name: "Install Node"
uses: actions/setup-node@v3
with:
node-version: 16
- name: "Generate frontend" - name: "Generate frontend"
uses: addnab/docker-run-action@v3
with:
image: node:lts
options: -v ${{ github.workspace }}:/work
run: | run: |
make binary-frontend && echo "print contents of ./dist" && ls -alt ./dist cd /work
make binary-frontend && echo "print contents of /work/dist" && ls -alt /work/dist
- name: Set up QEMU - name: Set up QEMU
uses: docker/setup-qemu-action@v2 uses: docker/setup-qemu-action@v2
with: with:
@@ -132,13 +134,16 @@ jobs:
uses: actions/checkout@v2 uses: actions/checkout@v2
- name: "Populate frontend version information" - name: "Populate frontend version information"
run: "cd webapp/frontend && ./git.version.sh" run: "cd webapp/frontend && ./git.version.sh"
- name: "Install Node" - name: "Generate frontend & version information"
uses: actions/setup-node@v3 uses: addnab/docker-run-action@v3
with: with:
node-version: 16 image: node:lts
- name: "Generate frontend" options: -v ${{ github.workspace }}:/work
run: | run: |
make binary-frontend && echo "print contents of ./dist" && ls -alt ./dist cd /work
make binary-frontend && echo "print contents of /work/dist" && ls -alt /work/dist
- name: Set up QEMU - name: Set up QEMU
uses: docker/setup-qemu-action@v2 uses: docker/setup-qemu-action@v2
with: with:
+8 -5
View File
@@ -19,13 +19,16 @@ jobs:
uses: actions/checkout@v2 uses: actions/checkout@v2
- name: "Populate frontend version information" - name: "Populate frontend version information"
run: "cd webapp/frontend && ./git.version.sh" run: "cd webapp/frontend && ./git.version.sh"
- name: "Install Node" - name: "Generate frontend & version information"
uses: actions/setup-node@v3 uses: addnab/docker-run-action@v3
with: with:
node-version: 16 image: node:lts
- name: "Generate frontend" options: -v ${{ github.workspace }}:/work
run: | run: |
make binary-frontend && echo "print contents of ./dist" && ls -alt ./dist cd /work
make binary-frontend && echo "print contents of /work/dist" && ls -alt /work/dist
- name: Set up QEMU - name: Set up QEMU
uses: docker/setup-qemu-action@v2 uses: docker/setup-qemu-action@v2
with: with:
-1
View File
@@ -19,7 +19,6 @@ jobs:
run: "cd webapp/frontend && ./git.version.sh" run: "cd webapp/frontend && ./git.version.sh"
- name: Build Frontend - name: Build Frontend
run: | run: |
apt-get update && apt-get install -y make
make binary-frontend make binary-frontend
tar -czf scrutiny-web-frontend.tar.gz dist tar -czf scrutiny-web-frontend.tar.gz dist
- name: Upload Frontend Asset - name: Upload Frontend Asset
+3 -4
View File
@@ -9,9 +9,8 @@ Depending on the functionality you are adding, you may need to setup a developme
# Modifying the Scrutiny Backend Server (API) # 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.17+)
2. download the `scrutiny-web-frontend.tar.gz` for 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`
the [latest release](https://github.com/AnalogJ/scrutiny/releases/latest). Extract to a folder named `dist`
3. create a `scrutiny.yaml` config file 3. create a `scrutiny.yaml` config file
```yaml ```yaml
# config file for local development. store as scrutiny.yaml # config file for local development. store as scrutiny.yaml
@@ -63,7 +62,7 @@ The frontend is written in Angular. If you're working on the frontend and can us
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: 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.17+)
2. install [NodeJS](https://nodejs.org/en/download/) 2. install [NodeJS](https://nodejs.org/en/download/)
3. create a `scrutiny.yaml` config file 3. create a `scrutiny.yaml` config file
```yaml ```yaml
-11
View File
@@ -1,5 +1,4 @@
.ONESHELL: # Applies to every targets in the file! .ONESHELL instructs make to invoke a single instance of the shell and provide it with the entire recipe, regardless of how many lines it contains. .ONESHELL: # Applies to every targets in the file! .ONESHELL instructs make to invoke a single instance of the shell and provide it with the entire recipe, regardless of how many lines it contains.
.SHELLFLAGS = -ec
######################################################################################################################## ########################################################################################################################
# Global Env Settings # Global Env Settings
@@ -90,10 +89,6 @@ ifneq ($(OS),Windows_NT)
./$(WEB_BINARY_NAME) || true ./$(WEB_BINARY_NAME) || true
endif endif
########################################################################################################################
# Binary
########################################################################################################################
.PHONY: binary-frontend .PHONY: binary-frontend
# reduce logging, disable angular-cli analytics for ci environment # reduce logging, disable angular-cli analytics for ci environment
binary-frontend: export NPM_CONFIG_LOGLEVEL = warn binary-frontend: export NPM_CONFIG_LOGLEVEL = warn
@@ -105,12 +100,6 @@ binary-frontend:
npm ci npm ci
npm run build:prod -- --output-path=$(CURDIR)/dist npm run build:prod -- --output-path=$(CURDIR)/dist
.PHONY: binary-frontend-test-coverage
# reduce logging, disable angular-cli analytics for ci environment
binary-frontend-test-coverage:
cd webapp/frontend
npm ci
npx ng test --watch=false --browsers=ChromeHeadless --code-coverage
######################################################################################################################## ########################################################################################################################
# Docker # Docker
+7 -11
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 - Customized thresholds using real world failure rates
- Temperature tracking - Temperature tracking
- Provided as an all-in-one Docker image (but can be installed manually) - Provided as an all-in-one Docker image (but can be installed manually)
- Configurable Alerting/Notifications via Webhooks - Future Configurable Alerting/Notifications via Webhooks
- (Future) Hard Drive performance testing & tracking - (Future) Hard Drive performance testing & tracking
# Getting Started # Getting Started
@@ -91,14 +91,10 @@ docker run -it --rm -p 8080:8080 -p 8086:8086 \
### Hub/Spoke Deployment ### Hub/Spoke Deployment
In addition to the Omnibus image (available under the `latest` tag) you can deploy in Hub/Spoke mode, which requires 3 In addition to the Omnibus image (available under the `latest` tag) there are 2 other Docker images available:
other Docker images:
- `ghcr.io/analogj/scrutiny:master-collector` - Contains the Scrutiny data collector, `smartctl` binary and cron-like - `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.
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-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. > See [docker/example.hubspoke.docker-compose.yml](./docker/example.hubspoke.docker-compose.yml) for a docker-compose file.
@@ -243,9 +239,9 @@ scrutiny-collector-metrics run --debug --log-file /tmp/collector.log
| linux-arm-6 | :white_check_mark: | | | linux-arm-6 | :white_check_mark: | |
| linux-arm-7 | :white_check_mark: | web/collector only. see [#236](https://github.com/AnalogJ/scrutiny/issues/236) | | linux-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: | | linux-arm64 | :white_check_mark: | :white_check_mark: |
| freebsd-amd64 | :white_check_mark: | | | freebsd-amd64 | collector only. see [#238](https://github.com/AnalogJ/scrutiny/issues/238) | |
| macos-amd64 | :white_check_mark: | :white_check_mark: | | macos-amd64 | | :white_check_mark: |
| macos-arm64 | :white_check_mark: | :white_check_mark: | | macos-arm64 | | :white_check_mark: |
| windows-amd64 | :white_check_mark: | WIP, see [#15](https://github.com/AnalogJ/scrutiny/issues/15) | | windows-amd64 | :white_check_mark: | WIP, see [#15](https://github.com/AnalogJ/scrutiny/issues/15) |
| windows-arm64 | :white_check_mark: | | | windows-arm64 | :white_check_mark: | |
@@ -1,7 +1,6 @@
package main package main
import ( import (
"encoding/json"
"fmt" "fmt"
"github.com/analogj/scrutiny/collector/pkg/collector" "github.com/analogj/scrutiny/collector/pkg/collector"
"github.com/analogj/scrutiny/collector/pkg/config" "github.com/analogj/scrutiny/collector/pkg/config"
@@ -121,16 +120,26 @@ OPTIONS:
config.Set("api.endpoint", apiEndpoint) config.Set("api.endpoint", apiEndpoint)
} }
collectorLogger, logFile, err := CreateLogger(config) collectorLogger := logrus.WithFields(logrus.Fields{
if logFile != nil { "type": "metrics",
defer logFile.Close() })
}
if err != nil { if level, err := logrus.ParseLevel(config.GetString("log.level")); err == nil {
return err 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
}
defer logFile.Close()
logrus.SetOutput(io.MultiWriter(os.Stderr, logFile))
} }
settingsData, err := json.MarshalIndent(config.AllSettings(), "", "\t")
collectorLogger.Debug(string(settingsData), err)
metricCollector, err := collector.CreateMetricsCollector( metricCollector, err := collector.CreateMetricsCollector(
config, config,
collectorLogger, collectorLogger,
@@ -183,28 +192,5 @@ OPTIONS:
if err != nil { if err != nil {
log.Fatal(color.HiRedString("ERROR: %v", err)) 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" "time"
) )
var httpClient = &http.Client{Timeout: 60 * time.Second} var httpClient = &http.Client{Timeout: 10 * time.Second}
type BaseCollector struct { type BaseCollector struct {
logger *logrus.Entry logger *logrus.Entry
+1 -7
View File
@@ -9,7 +9,6 @@ import (
"github.com/analogj/scrutiny/collector/pkg/detect" "github.com/analogj/scrutiny/collector/pkg/detect"
"github.com/analogj/scrutiny/collector/pkg/errors" "github.com/analogj/scrutiny/collector/pkg/errors"
"github.com/analogj/scrutiny/collector/pkg/models" "github.com/analogj/scrutiny/collector/pkg/models"
"github.com/samber/lo"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"net/url" "net/url"
"os" "os"
@@ -57,16 +56,11 @@ func (mc *MetricsCollector) Run() error {
Logger: mc.logger, Logger: mc.logger,
Config: mc.config, Config: mc.config,
} }
rawDetectedStorageDevices, err := deviceDetector.Start() detectedStorageDevices, err := deviceDetector.Start()
if err != nil { if err != nil {
return err return err
} }
//filter any device with empty wwn (they are invalid)
detectedStorageDevices := lo.Filter[models.Device](rawDetectedStorageDevices, func(dev models.Device, _ int) bool {
return len(dev.WWN) > 0
})
mc.logger.Infoln("Sending detected devices to API, for filtering & validation") mc.logger.Infoln("Sending detected devices to API, for filtering & validation")
jsonObj, _ := json.Marshal(detectedStorageDevices) jsonObj, _ := json.Marshal(detectedStorageDevices)
mc.logger.Debugf("Detected devices: %v", string(jsonObj)) mc.logger.Debugf("Detected devices: %v", string(jsonObj))
+1 -1
View File
@@ -5,7 +5,7 @@
######## ########
FROM golang:1.18-bullseye as backendbuild FROM golang:1.17-bullseye as backendbuild
WORKDIR /go/src/github.com/analogj/scrutiny WORKDIR /go/src/github.com/analogj/scrutiny
COPY . /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.17-bullseye as backendbuild
WORKDIR /go/src/github.com/analogj/scrutiny WORKDIR /go/src/github.com/analogj/scrutiny
@@ -14,7 +14,7 @@ RUN make binary-clean binary-collector
######## ########
FROM debian:bullseye-slim as runtime FROM debian:bullseye-slim as runtime
WORKDIR /opt/scrutiny WORKDIR /scrutiny
ENV PATH="/opt/scrutiny/bin:${PATH}" ENV PATH="/opt/scrutiny/bin:${PATH}"
RUN apt-get update && apt-get install -y cron smartmontools ca-certificates tzdata && update-ca-certificates 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.17-bullseye as backendbuild
WORKDIR /go/src/github.com/analogj/scrutiny WORKDIR /go/src/github.com/analogj/scrutiny
-59
View File
@@ -1,59 +0,0 @@
# 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.
+1 -5
View File
@@ -91,13 +91,9 @@ wget https://raw.githubusercontent.com/smartmontools/smartmontools/master/smartm
``` ```
#!/bin/bash #!/bin/bash
/volume1/\@Entware/scrutiny/bin/scrutiny-collector-metrics-linux-arm64 run --config /volume1/\@Entware/scrutiny/conf/collector.yaml /volume1/\@Entware/scrutiny/bin/scrutiny-collector-metrics-linux-arm64 run --config /volume1/\@Entware/scrutiny/config/collector.yaml
``` ```
**Make `run_collect.sh` executable**
`chmod +x /volume1/\@Entware/scrutiny/bin/run_collect.sh`
## Set up Synology to run a scheduled task. ## Set up Synology to run a scheduled task.
Log in to DSM and do the following: Log in to DSM and do the following:
+9 -13
View File
@@ -1,21 +1,17 @@
# Officially Supported NAS/OS's # Officially Supported NAS OS's
These are the officially supported NAS OS's (with documentation and setup guides). Once a guide is created ( These are the officially supported NAS OS's (with documentation and setup guides).
in `docs/guides/` or elsewhere) it will be linked here. Once a guide is created (in `docs/guides/`) it will be linked here.
- [x] [freenas/truenas](https://blog.stefandroid.com/2022/01/14/smart-scrutiny.html) - [ ] freenas/truenas
- [x] [unraid](./INSTALL_UNRAID.md) - [x] [unraid](./INSTALL_UNRAID.md)
- [ ] ESXI - [ ] ESXI
- [ ] Proxmox - [ ] Proxmox
- [x] Synology - [x] Synology(./INSTALL_SYNOLOGY_COLLECTOR.md)
- [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 - [ ] OMV
- [ ] Amahi - [ ] Amahi
- [ ] Running in a LXC container - [ ] Running in a LXC container
- [x] [PFSense](./INSTALL_PFSENSE.md) - [x] [PFSense](./INSTALL_UNRAID.md)
- [x] QNAP - [ ] QNAP
- [x] [RockStor](https://rockstor.com/docs/interface/docker-based-rock-ons/scrutiny.html) - [ ] RockStor
- [ ] Solaris/OmniOS CE Support
- [ ] Kubernetes
- [x] [Windows](./INSTALL_MANUAL_WINDOWS.md)
+2 -39
View File
@@ -19,25 +19,6 @@ 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/` 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. 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 ## Device Detection By Smartctl
@@ -81,7 +62,6 @@ 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) > 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. > If you're unsure, run `smartctl --scan` on your host, and pass all listed devices to the container.
@@ -120,10 +100,7 @@ devices:
- 'cciss,1' - 'cciss,1'
``` ```
>
### NVMe Drives ### 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) to allow smartctl permission to query your NVMe device SMART data [#26](https://github.com/AnalogJ/scrutiny/issues/26)
@@ -136,25 +113,11 @@ instead of the block device (`/dev/nvme0n1`). See [#209](https://github.com/Anal
### ATA ### ATA
### USB Devices
The following information is extracted from [#266](https://github.com/AnalogJ/scrutiny/issues/266)
External HDDs support two modes of operation usb-storage (old, slower, stable) and uas (new, faster, sometimes unstable)
. On some external HDDs, uas mode does not properly pass through SMART information, or even causes hardware issues, so
it has been disabled by the kernel. No amount of smartctl parameters will fix this, as it is being rejected by the
kernel. This is especially true with Seagate HDDs. One solution is to force these devices into usb-storage mode, which
will incur some performance penalty, but may work well enough for you. More info:
- https://smartmontools.org/wiki/Supported_USB-Devices
- https://smartmontools.org/wiki/SAT-with-UAS-Linux
- https://forums.raspberrypi.com/viewtopic.php?t=245931
### Exit Codes ### Exit Codes
If you see an error message similar to `smartctl returned an error code (2) while processing /dev/sda`, this means that If you see an error message similar to `smartctl returned an error code (2) while processing /dev/sda`, this means that
`smartctl` (not Scrutiny) exited with an error code. Scrutiny will attempt to print a helpful error message to help you `smartctl` (not Scrutiny) exited with an error code. Scrutiny will attempt to print a helpful error message to help you debug,
debug, but you can look at the table (and associated links) below to debug `smartctl`. but you can look at the table (and associated links) below to debug `smartctl`.
> smartctl Return Values > smartctl Return Values
> The return values of smartctl are defined by a bitmask. If all is well with the disk, the return value (exit status) of > The return values of smartctl are defined by a bitmask. If all is well with the disk, the return value (exit status) of
+4 -323
View File
@@ -1,18 +1,6 @@
# InfluxDB Troubleshooting # InfluxDB Troubleshooting
## 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 ## Installation
InfluxDB is a required dependency for Scrutiny v0.4.0+. InfluxDB is a required dependency for Scrutiny v0.4.0+.
https://docs.influxdata.com/influxdb/v2.2/install/ https://docs.influxdata.com/influxdb/v2.2/install/
@@ -78,319 +66,12 @@ panic: failed to check influxdb setup status - parse "://:": missing protocol sc
As discussed in [#248](https://github.com/AnalogJ/scrutiny/issues/248) and [#234](https://github.com/AnalogJ/scrutiny/issues/234), As discussed in [#248](https://github.com/AnalogJ/scrutiny/issues/248) and [#234](https://github.com/AnalogJ/scrutiny/issues/234),
this usually related to either: this usually related to either:
- Upgrading from the LSIO Scrutiny image to the Official Scrutiny image, without removing LSIO specific environmental - Upgrading from the LSIO Scrutiny image to the Official Scrutiny image, without removing LSIO specific environmental variables
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.
- remove the `SCRUTINY_WEB=true` and `SCRUTINY_COLLECTOR=true` environmental variables. They were used by the LSIO - 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
image, but are unnecessary and cause issues with the official Scrutiny image. - You can revert to an earlier version of the LSIO image (`lscr.io/linuxserver/scrutiny:060ac7b8-ls34`), or just change to the official Scrutiny image (`ghcr.io/analogj/scrutiny:master-omnibus`)
- 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
change to the official Scrutiny image (`ghcr.io/analogj/scrutiny:master-omnibus`)
Here's a couple of confirmed working docker-compose files that you may want to look at: Here's a couple of confirmed working docker-compose files that you may want to look at:
- https://github.com/AnalogJ/scrutiny/blob/master/docker/example.hubspoke.docker-compose.yml - https://github.com/AnalogJ/scrutiny/blob/master/docker/example.hubspoke.docker-compose.yml
- https://github.com/AnalogJ/scrutiny/blob/master/docker/example.omnibus.docker-compose.yml - https://github.com/AnalogJ/scrutiny/blob/master/docker/example.omnibus.docker-compose.yml
## Bring your own InfluxDB
> WARNING: Most users should not follow these steps. This is ONLY for users who have an EXISTING InfluxDB installation which contains data from multiple services.
> The Scrutiny Docker omnibus image includes an empty InfluxDB instance which it can configure.
> If you're deploying manually or via Hub/Spoke, you can just follow the installation instructions, Scrutiny knows how
> to run the first-time setup automatically.
The goal here is to create an InfluxDB API key with minimal permissions for use by Scrutiny.
- Create Scrutiny buckets (`metrics`, `metrics_weekly`, `metrics_monthly`, `metrics_yearly`) with placeholder config
- Create Downsampling tasks (`tsk-weekly-aggr`, `tsk-monthly-aggr`, `tsk-yearly-aggr`) with placeholder script.
- Create API token with restricted scope
- NOTE: Placeholder bucket & task configuration will be replaced automatically by Scrutiny during startup
The placeholder buckets and tasks need to be created before the API token can be created, as the resource ID's need to
exist for the scope restriction to work.
Scopes:
- `orgs`: read - required for scrutiny to find it's configured org_id
- `tasks`: scrutiny specific read/write access - Scrutiny only needs access to the downsampling tasks you created above
- `buckets`: scrutiny specific read/write access - Scrutiny only needs access to the buckets you created above
### Setup Environmental Variables
```bash
# replace the following values with correct values for your InfluxDB installation
export INFLUXDB_ADMIN_TOKEN=pCqRq7xxxxxx-FZgNLfstIs0w==
export INFLUXDB_ORG_ID=b2495xxxxx
export INFLUXDB_HOSTNAME=http://localhost:8086
# if you want to change the bucket name prefix below, you'll also need to update the setting in the scrutiny.yaml config file.
export INFLUXDB_SCRUTINY_BUCKET_BASENAME=metrics
```
### Create placeholder buckets
<details>
<summary>Click to expand!</summary>
```bash
curl -sS -X POST ${INFLUXDB_HOSTNAME}/api/v2/buckets \
-H "Content-Type: application/json" \
-H "Authorization: Token ${INFLUXDB_ADMIN_TOKEN}" \
--data-binary @- << EOF
{
"name": "${INFLUXDB_SCRUTINY_BUCKET_BASENAME}",
"orgID": "${INFLUXDB_ORG_ID}",
"retentionRules": []
}
EOF
curl -sS -X POST ${INFLUXDB_HOSTNAME}/api/v2/buckets \
-H "Content-Type: application/json" \
-H "Authorization: Token ${INFLUXDB_ADMIN_TOKEN}" \
--data-binary @- << EOF
{
"name": "${INFLUXDB_SCRUTINY_BUCKET_BASENAME}_weekly",
"orgID": "${INFLUXDB_ORG_ID}",
"retentionRules": []
}
EOF
curl -sS -X POST ${INFLUXDB_HOSTNAME}/api/v2/buckets \
-H "Content-Type: application/json" \
-H "Authorization: Token ${INFLUXDB_ADMIN_TOKEN}" \
--data-binary @- << EOF
{
"name": "${INFLUXDB_SCRUTINY_BUCKET_BASENAME}_monthly",
"orgID": "${INFLUXDB_ORG_ID}",
"retentionRules": []
}
EOF
curl -sS -X POST ${INFLUXDB_HOSTNAME}/api/v2/buckets \
-H "Content-Type: application/json" \
-H "Authorization: Token ${INFLUXDB_ADMIN_TOKEN}" \
--data-binary @- << EOF
{
"name": "${INFLUXDB_SCRUTINY_BUCKET_BASENAME}_yearly",
"orgID": "${INFLUXDB_ORG_ID}",
"retentionRules": []
}
EOF
```
</details>
### Create placeholder tasks
<details>
<summary>Click to expand!</summary>
```bash
curl -sS -X POST ${INFLUXDB_HOSTNAME}/api/v2/tasks \
-H "Content-Type: application/json" \
-H "Authorization: Token ${INFLUXDB_ADMIN_TOKEN}" \
--data-binary @- << EOF
{
"orgID": "${INFLUXDB_ORG_ID}",
"flux": "option task = {name: \"tsk-weekly-aggr\", every: 1y} \nyield now()"
}
EOF
curl -sS -X POST ${INFLUXDB_HOSTNAME}/api/v2/tasks \
-H "Content-Type: application/json" \
-H "Authorization: Token ${INFLUXDB_ADMIN_TOKEN}" \
--data-binary @- << EOF
{
"orgID": "${INFLUXDB_ORG_ID}",
"flux": "option task = {name: \"tsk-monthly-aggr\", every: 1y} \nyield now()"
}
EOF
curl -sS -X POST ${INFLUXDB_HOSTNAME}/api/v2/tasks \
-H "Content-Type: application/json" \
-H "Authorization: Token ${INFLUXDB_ADMIN_TOKEN}" \
--data-binary @- << EOF
{
"orgID": "${INFLUXDB_ORG_ID}",
"flux": "option task = {name: \"tsk-yearly-aggr\", every: 1y} \nyield now()"
}
EOF
```
</details>
### Create InfluxDB API Token
<details>
<summary>Click to expand!</summary>
```bash
# replace these values with placeholder bucket and task ids from your InfluxDB installation.
export INFLUXDB_SCRUTINY_BASE_BUCKET_ID=1e0709xxxx
export INFLUXDB_SCRUTINY_WEEKLY_BUCKET_ID=1af03dexxxxx
export INFLUXDB_SCRUTINY_MONTHLY_BUCKET_ID=b3c59c7xxxxx
export INFLUXDB_SCRUTINY_YEARLY_BUCKET_ID=f381d8cxxxxx
export INFLUXDB_SCRUTINY_WEEKLY_TASK_ID=09a64ecxxxxx
export INFLUXDB_SCRUTINY_MONTHLY_TASK_ID=09a64xxxxx
export INFLUXDB_SCRUTINY_YEARLY_TASK_ID=09a64ecxxxxx
curl -sS -X POST ${INFLUXDB_HOSTNAME}/api/v2/authorizations \
-H "Content-Type: application/json" \
-H "Authorization: Token ${INFLUXDB_ADMIN_TOKEN}" \
--data-binary @- << EOF
{
"description": "scrutiny - restricted scope token",
"orgID": "${INFLUXDB_ORG_ID}",
"permissions": [
{
"action": "read",
"resource": {
"type": "orgs"
}
},
{
"action": "read",
"resource": {
"type": "tasks"
}
},
{
"action": "write",
"resource": {
"type": "tasks",
"id": "${INFLUXDB_SCRUTINY_WEEKLY_TASK_ID}",
"orgID": "${INFLUXDB_ORG_ID}"
}
},
{
"action": "write",
"resource": {
"type": "tasks",
"id": "${INFLUXDB_SCRUTINY_MONTHLY_TASK_ID}",
"orgID": "${INFLUXDB_ORG_ID}"
}
},
{
"action": "write",
"resource": {
"type": "tasks",
"id": "${INFLUXDB_SCRUTINY_YEARLY_TASK_ID}",
"orgID": "${INFLUXDB_ORG_ID}"
}
},
{
"action": "read",
"resource": {
"type": "buckets",
"id": "${INFLUXDB_SCRUTINY_BASE_BUCKET_ID}",
"orgID": "${INFLUXDB_ORG_ID}"
}
},
{
"action": "write",
"resource": {
"type": "buckets",
"id": "${INFLUXDB_SCRUTINY_BASE_BUCKET_ID}",
"orgID": "${INFLUXDB_ORG_ID}"
}
},
{
"action": "read",
"resource": {
"type": "buckets",
"id": "${INFLUXDB_SCRUTINY_WEEKLY_BUCKET_ID}",
"orgID": "${INFLUXDB_ORG_ID}"
}
},
{
"action": "write",
"resource": {
"type": "buckets",
"id": "${INFLUXDB_SCRUTINY_WEEKLY_BUCKET_ID}",
"orgID": "${INFLUXDB_ORG_ID}"
}
},
{
"action": "read",
"resource": {
"type": "buckets",
"id": "${INFLUXDB_SCRUTINY_MONTHLY_BUCKET_ID}",
"orgID": "${INFLUXDB_ORG_ID}"
}
},
{
"action": "write",
"resource": {
"type": "buckets",
"id": "${INFLUXDB_SCRUTINY_MONTHLY_BUCKET_ID}",
"orgID": "${INFLUXDB_ORG_ID}"
}
},
{
"action": "read",
"resource": {
"type": "buckets",
"id": "${INFLUXDB_SCRUTINY_YEARLY_BUCKET_ID}",
"orgID": "${INFLUXDB_ORG_ID}"
}
},
{
"action": "write",
"resource": {
"type": "buckets",
"id": "${INFLUXDB_SCRUTINY_YEARLY_BUCKET_ID}",
"orgID": "${INFLUXDB_ORG_ID}"
}
}
]
}
EOF
```
</details>
### Save InfluxDB API Token
After running the Curl command above, you'll see a JSON response that looks like the following:
```json
{
"token": "ksVU2t5SkQwYkvIxxxxxxxYt2xUt0uRKSbSF1Po0UQ==",
"status": "active",
"description": "scrutiny - restricted scope token",
"orgID": "b2495586xxxx",
"org": "my-org",
"user": "admin",
"permissions": [
{
"action": "read",
"resource": {
"type": "orgs"
}
},
{
"action": "read",
"resource": {
"type": "tasks"
}
},
{
"action": "write",
"resource": {
"type": "tasks",
"id": "09a64exxxxx",
"orgID": "b24955860xxxxx",
"org": "my-org"
}
},
...
]
}
```
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
-6
View File
@@ -21,11 +21,5 @@ SCRUTINY_DEVICE_NAME - eg. /dev/sda
SCRUTINY_DEVICE_TYPE - ATA/SCSI/NVMe SCRUTINY_DEVICE_TYPE - ATA/SCSI/NVMe
SCRUTINY_DEVICE_SERIAL - eg. WDDJ324KSO 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_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
@@ -1,18 +0,0 @@
# 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`.
+43 -69
View File
@@ -1,88 +1,62 @@
// SQLite Table(s) // SQLite Table(s)
Table device {
created_at timestamp
Table Device { wwn varchar [pk]
//GORM attributes, see: http://gorm.io/docs/conventions.html
CreatedAt time
UpdatedAt time
DeletedAt time
WWN string //user provided
label varchar
host_id varchar
DeviceName string // smartctl provided
DeviceUUID string device_name varchar
DeviceSerialID string manufacturer varchar
DeviceLabel string 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
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 // InfluxDB Tables
Table SmartTemperature { Table device_temperature {
Date time //timestamp
DeviceWWN string //(tag) created_at timestamp
Temp int64
//tags (indexed & queryable)
device_wwn varchar [pk]
//fields
temp bigint
} }
Table Smart { Table smart_ata_results {
Date time //timestamp
DeviceWWN string //(tag) created_at timestamp
DeviceProtocol string
//Metrics (fields) //tags (indexed & queryable)
Temp int64 device_wwn varchar [pk]
PowerOnHours int64 smart_status varchar
PowerCycleCount int64 scrutiny_status varchar
//Smart Status
Status enum
//SMART Attributes (fields)
Attr_ID_AttributeId int //fields
Attr_ID_Value int64 temp bigint
Attr_ID_Threshold int64 power_on_hours bigint
Attr_ID_Worst int64 power_cycle_count bigint
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.DeviceWWN Ref: device.wwn < smart_ata_results.device_wwn
Ref: Device.WWN < SmartTemperature.DeviceWWN
+2
View File
@@ -73,6 +73,8 @@ log:
# - "join://shoutrrr:api-key@join/?devices=device1[,device2, ...][&icon=icon][&title=title]" # - "join://shoutrrr:api-key@join/?devices=device1[,device2, ...][&icon=icon][&title=title]"
# - "script:///file/path/on/disk" # - "script:///file/path/on/disk"
# - "https://www.example.com/path" # - "https://www.example.com/path"
# filter_attributes: 'all' # options: 'all' or 'critical'
# level: 'fail' # options: 'fail', 'fail_scrutiny', 'fail_smart'
######################################################################################################################## ########################################################################################################################
# FEATURES COMING SOON # FEATURES COMING SOON
+15 -8
View File
@@ -1,10 +1,10 @@
module github.com/analogj/scrutiny module github.com/analogj/scrutiny
go 1.18 go 1.17
require ( require (
github.com/analogj/go-util v0.0.0-20190301173314-5295e364eb14 github.com/analogj/go-util v0.0.0-20190301173314-5295e364eb14
github.com/containrrr/shoutrrr v0.6.1 github.com/containrrr/shoutrrr v0.4.4
github.com/fatih/color v1.10.0 github.com/fatih/color v1.10.0
github.com/gin-gonic/gin v1.6.3 github.com/gin-gonic/gin v1.6.3
github.com/glebarez/sqlite v1.4.5 github.com/glebarez/sqlite v1.4.5
@@ -13,7 +13,6 @@ require (
github.com/influxdata/influxdb-client-go/v2 v2.9.0 github.com/influxdata/influxdb-client-go/v2 v2.9.0
github.com/jaypipes/ghw v0.6.1 github.com/jaypipes/ghw v0.6.1
github.com/mitchellh/mapstructure v1.2.2 github.com/mitchellh/mapstructure v1.2.2
github.com/samber/lo v1.25.0
github.com/sirupsen/logrus v1.4.2 github.com/sirupsen/logrus v1.4.2
github.com/spf13/viper v1.7.0 github.com/spf13/viper v1.7.0
github.com/stretchr/testify v1.7.1 github.com/stretchr/testify v1.7.1
@@ -24,7 +23,8 @@ require (
require ( require (
github.com/StackExchange/wmi v0.0.0-20190523213315-cbe66965904d // indirect github.com/StackExchange/wmi v0.0.0-20190523213315-cbe66965904d // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.1 // indirect github.com/citilinkru/libudev v1.0.0 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect github.com/davecgh/go-spew v1.1.1 // indirect
github.com/deepmap/oapi-codegen v1.8.2 // indirect github.com/deepmap/oapi-codegen v1.8.2 // indirect
github.com/fsnotify/fsnotify v1.4.9 // indirect github.com/fsnotify/fsnotify v1.4.9 // indirect
@@ -43,6 +43,7 @@ require (
github.com/jinzhu/inflection v1.0.0 // indirect github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.4 // indirect github.com/jinzhu/now v1.1.4 // indirect
github.com/json-iterator/go v1.1.9 // 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/konsorten/go-windows-terminal-sequences v1.0.2 // indirect
github.com/kvz/logstreamer v0.0.0-20201023134116-02d20f4338f5 // indirect github.com/kvz/logstreamer v0.0.0-20201023134116-02d20f4338f5 // indirect
github.com/leodido/go-urn v1.2.0 // indirect github.com/leodido/go-urn v1.2.0 // indirect
@@ -52,11 +53,14 @@ require (
github.com/mitchellh/go-homedir v1.1.0 // indirect github.com/mitchellh/go-homedir v1.1.0 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.1 // 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/pelletier/go-toml v1.7.0 // indirect
github.com/pkg/errors v0.9.1 // indirect github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0 // indirect github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/russross/blackfriday/v2 v2.0.1 // indirect
github.com/shurcooL/sanitized_anchor_name v1.0.0 // indirect
github.com/spf13/afero v1.2.2 // indirect github.com/spf13/afero v1.2.2 // indirect
github.com/spf13/cast v1.3.1 // indirect github.com/spf13/cast v1.3.1 // indirect
github.com/spf13/jwalterweatherman v1.1.0 // indirect github.com/spf13/jwalterweatherman v1.1.0 // indirect
@@ -64,18 +68,21 @@ require (
github.com/subosito/gotenv v1.2.0 // indirect github.com/subosito/gotenv v1.2.0 // indirect
github.com/ugorji/go/codec v1.1.7 // indirect github.com/ugorji/go/codec v1.1.7 // indirect
golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad // indirect golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad // indirect
golang.org/x/exp v0.0.0-20220303212507-bbda1eaf7a17 // indirect
golang.org/x/net v0.0.0-20210119194325-5f4716e94777 // indirect golang.org/x/net v0.0.0-20210119194325-5f4716e94777 // indirect
golang.org/x/sys v0.0.0-20220405052023-b1e9470b6e64 // indirect golang.org/x/sys v0.0.0-20220405052023-b1e9470b6e64 // indirect
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1 // indirect golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1 // indirect
golang.org/x/text v0.3.5 // 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 google.golang.org/protobuf v1.23.0 // indirect
gopkg.in/ini.v1 v1.55.0 // indirect gopkg.in/ini.v1 v1.55.0 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect gopkg.in/yaml.v2 v2.3.0 // indirect
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c // indirect
gosrc.io/xmpp v0.5.1 // indirect
howett.net/plist v0.0.0-20181124034731-591f970eefbb // indirect howett.net/plist v0.0.0-20181124034731-591f970eefbb // indirect
modernc.org/libc v1.16.8 // indirect modernc.org/libc v1.16.8 // indirect
modernc.org/mathutil v1.4.1 // indirect modernc.org/mathutil v1.4.1 // indirect
modernc.org/memory v1.1.1 // indirect modernc.org/memory v1.1.1 // indirect
modernc.org/sqlite v1.17.2 // indirect modernc.org/sqlite v1.17.2 // indirect
nhooyr.io/websocket v1.8.6 // indirect
) )
+74 -19
View File
@@ -18,6 +18,7 @@ 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/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 h1:G0m3OIz70MZUWq3EgK3CesDbo8upS2Vm9/P3FtgI+Jk=
github.com/StackExchange/wmi v0.0.0-20190523213315-cbe66965904d/go.mod h1:3eOhrUMpNV+6aFIbp5/iudMxNCF27Vw2OZgy4xEx0Fg= 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/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/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/analogj/go-util v0.0.0-20190301173314-5295e364eb14 h1:wsrSjiqQtseStRIoLLxS4C5IEtXkazZVEPDHq8jW7r8= github.com/analogj/go-util v0.0.0-20190301173314-5295e364eb14 h1:wsrSjiqQtseStRIoLLxS4C5IEtXkazZVEPDHq8jW7r8=
@@ -32,11 +33,20 @@ 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/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/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/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/citilinkru/libudev v1.0.0 h1:upErSdhsJGdiKxwxPmvcz43fwJJD9R+y1j8BqU4wHog=
github.com/citilinkru/libudev v1.0.0/go.mod h1:yaNdhdtfJMs5flqeXzUOMO0mT9QnyNh/U/jdY4WhA/I=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= 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/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ=
github.com/containrrr/shoutrrr v0.6.1 h1:6ih7jA6mo3t6C97MZbd3SxL/kRizOE3bI9CpBQZ6wzg= github.com/containrrr/shoutrrr v0.4.4 h1:vHZ4E/76pKVY+Jyn/qhBz3X540Bn8NI5ppPHK4PyILY=
github.com/containrrr/shoutrrr v0.6.1/go.mod h1:ye9jGX5YzMnJ76waaNVWlJ4luhMEyt1EWU5unYTQSb0= github.com/containrrr/shoutrrr v0.4.4/go.mod h1:zqL2BvfC1W4FujrT4b3/ZCLxvD+uoeEpBL7rg9Dqpbg=
github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= 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/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.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
@@ -44,8 +54,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/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/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-20190314233015-f79a8a8ca69d/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.0 h1:EoUDS0afbrsXAZ9YQ9jdu/mZ2sXgT1/2yyNng4PGlyM=
github.com/cpuguy83/go-md2man/v2 v2.0.1/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= 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/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= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@@ -58,6 +68,8 @@ 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/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/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/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.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
github.com/fatih/color v1.10.0 h1:s36xzo75JdqLaaWoiEHk767eHiwo0598uUxyfiPkDsg= github.com/fatih/color v1.10.0 h1:s36xzo75JdqLaaWoiEHk767eHiwo0598uUxyfiPkDsg=
github.com/fatih/color v1.10.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM= github.com/fatih/color v1.10.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM=
@@ -79,6 +91,8 @@ 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-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 h1:e2A3Uznk4viUC4UuemuVgsNnvYZyOA8B3awlYk3UioU=
github.com/go-gormigrate/gormigrate/v2 v2.0.0/go.mod h1:YuVJ+D/dNt4HWrThTBnjgZuRbt7AuwINeg4q52ZE3Jw= 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-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.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
@@ -97,6 +111,12 @@ 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 h1:ozyZYNQW3x3HtqT1jira07DN2PArx2v7/mN66gGcHOs=
github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= 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/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/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.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
@@ -113,6 +133,7 @@ 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.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.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.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/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.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
@@ -133,7 +154,10 @@ 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/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-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-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/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 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 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= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
@@ -142,6 +166,8 @@ 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/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/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.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/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-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= github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
@@ -248,15 +274,20 @@ 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/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/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 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.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 h1:DB17ag19krx9CFsz4o3enTrPXyIXCl+2iCXH/aMAp9s=
github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw= github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kvz/logstreamer v0.0.0-20201023134116-02d20f4338f5 h1:dkCjlgGN81ahDFtM9R1x16gFGTa7ZvgZfdtAfM9lWOs= github.com/kvz/logstreamer v0.0.0-20201023134116-02d20f4338f5 h1:dkCjlgGN81ahDFtM9R1x16gFGTa7ZvgZfdtAfM9lWOs=
github.com/kvz/logstreamer v0.0.0-20201023134116-02d20f4338f5/go.mod h1:8/LTPeDLaklcUjgSQBHbhBF1ibKAFxzS5o+H7USfMSA= github.com/kvz/logstreamer v0.0.0-20201023134116-02d20f4338f5/go.mod h1:8/LTPeDLaklcUjgSQBHbhBF1ibKAFxzS5o+H7USfMSA=
github.com/labstack/echo/v4 v4.2.1/go.mod h1:AA49e0DZ8kk5jTOOCKNuPR6oTnBS0dYiM4FW1e6jwpg= github.com/labstack/echo/v4 v4.2.1/go.mod h1:AA49e0DZ8kk5jTOOCKNuPR6oTnBS0dYiM4FW1e6jwpg=
@@ -267,10 +298,14 @@ 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.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.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/lib/pq v1.3.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 h1:ZC2Vc7/ZFkGmsVC9KvOjumD+G5lXy2RtTKyzRKO2BQ4=
github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= 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-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.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/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.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ= github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ=
@@ -310,15 +345,16 @@ github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lN
github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI= github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI=
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
github.com/nxadm/tail v1.4.6 h1:11TGpSHY7Esh/i/qnq02Jo5oVrI1Gue8Slbq0ujPZFQ= 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/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/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.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.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
github.com/onsi/ginkgo v1.14.2 h1:8mVmC9kjFFmA8H4pKMUhcblgifdkOIXPvbhN1T36q1M= 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/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.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 h1:o0+MgICZLuZ7xjH7Vx6zS/zcu93/BEp1VwkIW1mEXCE=
github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
@@ -349,17 +385,16 @@ 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/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.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU=
github.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThCjNc= 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.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/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=
github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= 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-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4=
github.com/shopspring/decimal v0.0.0-20200227202807-02e2044944cc/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= 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/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.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.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=
github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4= github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4=
@@ -377,14 +412,16 @@ 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 h1:nFm6S0SMdyzrzcmThSipiEubIDy8WEXKNZ0UOgiRpng=
github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= 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.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ=
github.com/spf13/cobra v1.4.0/go.mod h1:Wo4iy3BUC+X2Fybo0PDqwJIv3dNRiZLHQymsfxlB84g= github.com/spf13/cobra v0.0.7/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHNrgE=
github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= 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 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk=
github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= 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.2/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
github.com/spf13/pflag v1.0.3/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 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 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.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 h1:xVKxvI7ouOI5I+U9s2eeiUfMaWBVoXA3AWskkrqK0VM=
github.com/spf13/viper v1.7.0/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg= github.com/spf13/viper v1.7.0/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg=
@@ -399,8 +436,10 @@ github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMT
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s= 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/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/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 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo=
github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= 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 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs=
github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY=
@@ -413,6 +452,7 @@ 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/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/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q= 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.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.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
@@ -424,6 +464,7 @@ 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/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.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
go.uber.org/zap v1.10.0/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-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-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
@@ -444,8 +485,6 @@ golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek=
golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=
golang.org/x/exp v0.0.0-20220303212507-bbda1eaf7a17 h1:3MTrJm4PyNL9NBqvYDSj3DHl46qQakyfqfWo4jgfaEM=
golang.org/x/exp v0.0.0-20220303212507-bbda1eaf7a17/go.mod h1:lgLbSvA5ygNOMpwM/9anMpWVlVJ7Z+cHWq/eFuinpGE=
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
@@ -465,6 +504,7 @@ 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-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-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-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-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-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@@ -494,6 +534,7 @@ 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-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-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-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 h1:SQFwaSi55rU7vdNs9Yr0Z324VNlrF+0wMqRXT4St8ck=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 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= golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@@ -505,6 +546,7 @@ 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-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-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-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-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-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -512,10 +554,13 @@ 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-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-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-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-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-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-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-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-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-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -544,6 +589,7 @@ 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/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-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-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-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/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= golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
@@ -563,6 +609,7 @@ 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-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-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-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-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-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
@@ -606,12 +653,14 @@ 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.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
google.golang.org/protobuf v1.23.0 h1:4MY060fB1DLGMB/7MBTLnwQUY6+F09GEiz6SsrNqyzM= google.golang.org/protobuf v1.23.0 h1:4MY060fB1DLGMB/7MBTLnwQUY6+F09GEiz6SsrNqyzM=
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 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/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 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/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/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= 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/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.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/ini.v1 v1.55.0 h1:E8yzL5unfpW3M6fz/eB7Cb5MQAYSZ7GKo4Qth+N2sgQ= gopkg.in/ini.v1 v1.55.0 h1:E8yzL5unfpW3M6fz/eB7Cb5MQAYSZ7GKo4Qth+N2sgQ=
@@ -624,12 +673,10 @@ 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.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.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.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.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
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-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=
gorm.io/driver/mysql v1.0.1 h1:omJoilUzyrAp0xNoio88lGJCroGdIOen9hq2A/+3ifw= gorm.io/driver/mysql v1.0.1 h1:omJoilUzyrAp0xNoio88lGJCroGdIOen9hq2A/+3ifw=
gorm.io/driver/mysql v1.0.1/go.mod h1:KtqSthtg55lFp3S5kUXqlGaelnWpKitn4k1xZTnoiPw= gorm.io/driver/mysql v1.0.1/go.mod h1:KtqSthtg55lFp3S5kUXqlGaelnWpKitn4k1xZTnoiPw=
gorm.io/driver/postgres v1.0.0 h1:Yh4jyFQ0a7F+JPU0Gtiam/eKmpT/XFc1FKxotGqc6FM= gorm.io/driver/postgres v1.0.0 h1:Yh4jyFQ0a7F+JPU0Gtiam/eKmpT/XFc1FKxotGqc6FM=
@@ -642,6 +689,10 @@ 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.20.0/go.mod h1:0HFTzE/SqkGTzK6TlDPPQbAYCluiVvhzoA1+aVyzenw=
gorm.io/gorm v1.23.5 h1:TnlF26wScKSvknUC/Rn8t0NLLM22fypYBlvj1+aH6dM= gorm.io/gorm v1.23.5 h1:TnlF26wScKSvknUC/Rn8t0NLLM22fypYBlvj1+aH6dM=
gorm.io/gorm v1.23.5/go.mod h1:l2lP/RyAtc1ynaTjFksBde/O8v9oOGIApu2/xRitmZk= 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-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-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
@@ -674,6 +725,10 @@ modernc.org/strutil v1.1.1/go.mod h1:DE+MQQ/hjKBZS2zNInV5hhcipt5rLPWkmpbGeW5mmdw
modernc.org/tcl v1.13.1/go.mod h1:XOLfOwzhkljL4itZkK6T72ckMgvj0BDsnKNdZVUOecw= modernc.org/tcl v1.13.1/go.mod h1:XOLfOwzhkljL4itZkK6T72ckMgvj0BDsnKNdZVUOecw=
modernc.org/token v1.0.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= modernc.org/token v1.0.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
modernc.org/z v1.5.1/go.mod h1:eWFB510QWW5Th9YGZT81s+LwvaAs3Q2yr4sP0rmLkv8= 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/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/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
+2 -41
View File
@@ -1,15 +1,12 @@
package main package main
import ( import (
"encoding/json"
"fmt" "fmt"
"github.com/analogj/scrutiny/webapp/backend/pkg/config" "github.com/analogj/scrutiny/webapp/backend/pkg/config"
"github.com/analogj/scrutiny/webapp/backend/pkg/errors" "github.com/analogj/scrutiny/webapp/backend/pkg/errors"
"github.com/analogj/scrutiny/webapp/backend/pkg/version" "github.com/analogj/scrutiny/webapp/backend/pkg/version"
"github.com/analogj/scrutiny/webapp/backend/pkg/web" "github.com/analogj/scrutiny/webapp/backend/pkg/web"
"github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
"io"
"log"
"os" "os"
"time" "time"
@@ -34,7 +31,6 @@ func main() {
if _, ok := err.(errors.ConfigFileMissingError); ok { // Handle errors reading the config file if _, ok := err.(errors.ConfigFileMissingError); ok { // Handle errors reading the config file
//ignore "could not find config file" //ignore "could not find config file"
} else if err != nil { } else if err != nil {
log.Print(color.HiRedString("CONFIG ERROR: %v", err))
os.Exit(1) os.Exit(1)
} }
@@ -111,18 +107,7 @@ OPTIONS:
config.Set("log.file", c.String("log-file")) config.Set("log.file", c.String("log-file"))
} }
webLogger, logFile, err := CreateLogger(config) webServer := web.AppEngine{Config: 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() return webServer.Start()
}, },
@@ -155,27 +140,3 @@ 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
}
+33 -26
View File
@@ -2,6 +2,7 @@ package config
import ( import (
"github.com/analogj/go-util/utils" "github.com/analogj/go-util/utils"
"github.com/analogj/scrutiny/webapp/backend/pkg"
"github.com/analogj/scrutiny/webapp/backend/pkg/errors" "github.com/analogj/scrutiny/webapp/backend/pkg/errors"
"github.com/spf13/viper" "github.com/spf13/viper"
"log" "log"
@@ -9,8 +10,6 @@ import (
"strings" "strings"
) )
const DB_USER_SETTINGS_SUBKEY = "user"
// When initializing this class the following methods must be called: // When initializing this class the following methods must be called:
// Config.New // Config.New
// Config.Init // Config.Init
@@ -40,6 +39,8 @@ func (c *configuration) Init() error {
c.SetDefault("log.file", "") c.SetDefault("log.file", "")
c.SetDefault("notify.urls", []string{}) 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.scheme", "http")
c.SetDefault("web.influxdb.host", "localhost") c.SetDefault("web.influxdb.host", "localhost")
@@ -54,6 +55,17 @@ func (c *configuration) Init() error {
//c.SetDefault("disks.include", []string{}) //c.SetDefault("disks.include", []string{})
//c.SetDefault("disks.exclude", []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 //if you want to load a non-standard location system config file (~/drawbridge.yml), use ReadConfig
c.SetConfigType("yaml") c.SetConfigType("yaml")
//c.SetConfigName("drawbridge") //c.SetConfigName("drawbridge")
@@ -65,18 +77,7 @@ func (c *configuration) Init() error {
c.AutomaticEnv() c.AutomaticEnv()
//CLI options will be added via the `Set()` function //CLI options will be added via the `Set()` function
return c.ValidateConfig() return nil
}
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 { func (c *configuration) ReadConfig(configFilePath string) error {
@@ -119,18 +120,24 @@ func (c *configuration) ReadConfig(configFilePath string) error {
// This function ensures that the merged config works correctly. // This function ensures that the merged config works correctly.
func (c *configuration) ValidateConfig() error { func (c *configuration) ValidateConfig() error {
//the following keys are deprecated, and no longer supported ////deserialize Questions
/* //questionsMap := map[string]Question{}
- notify.filter_attributes (replaced by metrics.status.filter_attributes SETTING) //err := c.UnmarshalKey("questions", &questionsMap)
- notify.level (replaced by metrics.notify.level and metrics.status.threshold SETTING) //
*/ //if err != nil {
//TODO add docs and upgrade doc. // log.Printf("questions could not be deserialized correctly. %v", err)
if c.IsSet("notify.filter_attributes") { // return err
return errors.ConfigValidationError("`notify.filter_attributes` configuration option is deprecated. Replaced by option in Dashboard Settings page") //}
} //
if c.IsSet("notify.level") { //for _, v := range questionsMap {
return errors.ConfigValidationError("`notify.level` configuration option is deprecated. Replaced by option in Dashboard Settings page") //
} // typeContent, ok := v.Schema["type"].(string)
// if !ok || len(typeContent) == 0 {
// return errors.QuestionSyntaxError("`type` is required for questions")
// }
//}
//
//
return nil return nil
} }
-34
View File
@@ -1,34 +0,0 @@
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,17 +12,12 @@ type Interface interface {
WriteConfig() error WriteConfig() error
Set(key string, value interface{}) Set(key string, value interface{})
SetDefault(key string, value interface{}) SetDefault(key string, value interface{})
MergeConfigMap(cfg map[string]interface{}) error
Sub(key string) Interface
AllSettings() map[string]interface{} AllSettings() map[string]interface{}
AllKeys() []string
SubKeys(key string) []string
IsSet(key string) bool IsSet(key string) bool
Get(key string) interface{} Get(key string) interface{}
GetBool(key string) bool GetBool(key string) bool
GetInt(key string) int GetInt(key string) int
GetInt64(key string) int64
GetString(key string) string GetString(key string) string
GetStringSlice(key string) []string GetStringSlice(key string) []string
UnmarshalKey(key string, rawVal interface{}, decoderOpts ...viper.DecoderConfigOption) error UnmarshalKey(key string, rawVal interface{}, decoderOpts ...viper.DecoderConfigOption) error
@@ -7,7 +7,6 @@ package mock_config
import ( import (
reflect "reflect" reflect "reflect"
config "github.com/analogj/scrutiny/webapp/backend/pkg/config"
gomock "github.com/golang/mock/gomock" gomock "github.com/golang/mock/gomock"
viper "github.com/spf13/viper" viper "github.com/spf13/viper"
) )
@@ -35,20 +34,6 @@ func (m *MockInterface) EXPECT() *MockInterfaceMockRecorder {
return m.recorder 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. // AllSettings mocks base method.
func (m *MockInterface) AllSettings() map[string]interface{} { func (m *MockInterface) AllSettings() map[string]interface{} {
m.ctrl.T.Helper() m.ctrl.T.Helper()
@@ -105,20 +90,6 @@ func (mr *MockInterfaceMockRecorder) GetInt(key interface{}) *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetInt", reflect.TypeOf((*MockInterface)(nil).GetInt), key) 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. // GetString mocks base method.
func (m *MockInterface) GetString(key string) string { func (m *MockInterface) GetString(key string) string {
m.ctrl.T.Helper() m.ctrl.T.Helper()
@@ -175,20 +146,6 @@ func (mr *MockInterfaceMockRecorder) IsSet(key interface{}) *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IsSet", reflect.TypeOf((*MockInterface)(nil).IsSet), key) 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. // ReadConfig mocks base method.
func (m *MockInterface) ReadConfig(configFilePath string) error { func (m *MockInterface) ReadConfig(configFilePath string) error {
m.ctrl.T.Helper() m.ctrl.T.Helper()
@@ -227,34 +184,6 @@ func (mr *MockInterfaceMockRecorder) SetDefault(key, value interface{}) *gomock.
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetDefault", reflect.TypeOf((*MockInterface)(nil).SetDefault), key, value) 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. // UnmarshalKey mocks base method.
func (m *MockInterface) UnmarshalKey(key string, rawVal interface{}, decoderOpts ...viper.DecoderConfigOption) error { func (m *MockInterface) UnmarshalKey(key string, rawVal interface{}, decoderOpts ...viper.DecoderConfigOption) error {
m.ctrl.T.Helper() m.ctrl.T.Helper()
+10 -31
View File
@@ -4,11 +4,17 @@ const DeviceProtocolAta = "ATA"
const DeviceProtocolScsi = "SCSI" const DeviceProtocolScsi = "SCSI"
const DeviceProtocolNvme = "NVMe" const DeviceProtocolNvme = "NVMe"
//go:generate stringer -type=AttributeStatus const NotifyFilterAttributesAll = "all"
// AttributeStatus bitwise flag, 1,2,4,8,16,32,etc const NotifyFilterAttributesCritical = "critical"
type AttributeStatus uint8
const NotifyLevelFail = "fail"
const NotifyLevelFailScrutiny = "fail_scrutiny"
const NotifyLevelFailSmart = "fail_smart"
//go:generate stringer -type=AttributeStatus
type AttributeStatus uint8
const ( const (
// AttributeStatusPassed binary, 1,2,4,8,16,32,etc
AttributeStatusPassed AttributeStatus = 0 AttributeStatusPassed AttributeStatus = 0
AttributeStatusFailedSmart AttributeStatus = 1 AttributeStatusFailedSmart AttributeStatus = 1
AttributeStatusWarningScrutiny AttributeStatus = 2 AttributeStatusWarningScrutiny AttributeStatus = 2
@@ -24,10 +30,9 @@ func AttributeStatusToggle(b, flag AttributeStatus) AttributeStatus { return b ^
func AttributeStatusHas(b, flag AttributeStatus) bool { return b&flag != 0 } func AttributeStatusHas(b, flag AttributeStatus) bool { return b&flag != 0 }
//go:generate stringer -type=DeviceStatus //go:generate stringer -type=DeviceStatus
// DeviceStatus bitwise flag, 1,2,4,8,16,32,etc
type DeviceStatus uint8 type DeviceStatus uint8
const ( const (
// DeviceStatusPassed binary, 1,2,4,8,16,32,etc
DeviceStatusPassed DeviceStatus = 0 DeviceStatusPassed DeviceStatus = 0
DeviceStatusFailedSmart DeviceStatus = 1 DeviceStatusFailedSmart DeviceStatus = 1
DeviceStatusFailedScrutiny DeviceStatus = 2 DeviceStatusFailedScrutiny DeviceStatus = 2
@@ -37,29 +42,3 @@ func DeviceStatusSet(b, flag DeviceStatus) DeviceStatus { return b | flag }
func DeviceStatusClear(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 DeviceStatusToggle(b, flag DeviceStatus) DeviceStatus { return b ^ flag }
func DeviceStatusHas(b, flag DeviceStatus) bool { return b&flag != 0 } 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
)
+3 -4
View File
@@ -10,7 +10,9 @@ import (
type DeviceRepo interface { type DeviceRepo interface {
Close() error Close() error
HealthCheck(ctx context.Context) error
//GetSettings()
//SaveSetting()
RegisterDevice(ctx context.Context, dev models.Device) error RegisterDevice(ctx context.Context, dev models.Device) error
GetDevices(ctx context.Context) ([]models.Device, error) GetDevices(ctx context.Context) ([]models.Device, error)
@@ -26,7 +28,4 @@ type DeviceRepo interface {
GetSummary(ctx context.Context) (map[string]*models.DeviceSummary, error) GetSummary(ctx context.Context) (map[string]*models.DeviceSummary, error)
GetSmartTemperatureHistory(ctx context.Context, durationKey string) (map[string][]measurements.SmartTemperature, 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
} }
@@ -1,18 +0,0 @@
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"`
}
@@ -62,20 +62,7 @@ func NewScrutinyRepository(appConfig config.Interface, globalLogger logrus.Field
// Gorm/SQLite setup // Gorm/SQLite setup
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
globalLogger.Infof("Trying to connect to scrutiny sqlite db: %s\n", appConfig.GetString("web.database.location")) 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. //TODO: figure out how to log database queries again.
//Logger: logger //Logger: logger
DisableForeignKeyConstraintWhenMigrating: true, DisableForeignKeyConstraintWhenMigrating: true,
@@ -195,29 +182,6 @@ func (sr *scrutinyRepository) Close() error {
return nil return nil
} }
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) (bool, error) { func InfluxSetupComplete(influxEndpoint string) (bool, error) {
influxUri, err := url.Parse(influxEndpoint) influxUri, err := url.Parse(influxEndpoint)
if err != nil { if err != nil {
@@ -278,29 +242,21 @@ func (sr *scrutinyRepository) EnsureBuckets(ctx context.Context, org *domain.Org
//create buckets (used for downsampling) //create buckets (used for downsampling)
weeklyBucket := fmt.Sprintf("%s_weekly", sr.appConfig.GetString("web.influxdb.bucket")) weeklyBucket := fmt.Sprintf("%s_weekly", sr.appConfig.GetString("web.influxdb.bucket"))
if foundWeeklyBucket, foundErr := sr.influxClient.BucketsAPI().FindBucketByName(ctx, weeklyBucket); foundErr != nil { if _, foundErr := sr.influxClient.BucketsAPI().FindBucketByName(ctx, weeklyBucket); foundErr != nil {
// metrics_weekly bucket will have a retention period of 8+1 weeks (since it will be down-sampled once a month) // metrics_weekly bucket will have a retention period of 8+1 weeks (since it will be down-sampled once a month)
_, err := sr.influxClient.BucketsAPI().CreateBucketWithName(ctx, org, weeklyBucket, weeklyBucketRetentionRule) _, err := sr.influxClient.BucketsAPI().CreateBucketWithName(ctx, org, weeklyBucket, weeklyBucketRetentionRule)
if err != nil { if err != nil {
return err return err
} }
} else if sr.appConfig.GetBool("web.influxdb.retention_policy") {
//correctly set the retention period for the bucket (may not be able to do it during setup/creation)
foundWeeklyBucket.RetentionRules = domain.RetentionRules{weeklyBucketRetentionRule}
sr.influxClient.BucketsAPI().UpdateBucket(ctx, foundWeeklyBucket)
} }
monthlyBucket := fmt.Sprintf("%s_monthly", sr.appConfig.GetString("web.influxdb.bucket")) monthlyBucket := fmt.Sprintf("%s_monthly", sr.appConfig.GetString("web.influxdb.bucket"))
if foundMonthlyBucket, foundErr := sr.influxClient.BucketsAPI().FindBucketByName(ctx, monthlyBucket); foundErr != nil { if _, foundErr := sr.influxClient.BucketsAPI().FindBucketByName(ctx, monthlyBucket); foundErr != nil {
// metrics_monthly bucket will have a retention period of 24+1 months (since it will be down-sampled once a year) // metrics_monthly bucket will have a retention period of 24+1 months (since it will be down-sampled once a year)
_, err := sr.influxClient.BucketsAPI().CreateBucketWithName(ctx, org, monthlyBucket, monthlyBucketRetentionRule) _, err := sr.influxClient.BucketsAPI().CreateBucketWithName(ctx, org, monthlyBucket, monthlyBucketRetentionRule)
if err != nil { if err != nil {
return err return err
} }
} else if sr.appConfig.GetBool("web.influxdb.retention_policy") {
//correctly set the retention period for the bucket (may not be able to do it during setup/creation)
foundMonthlyBucket.RetentionRules = domain.RetentionRules{monthlyBucketRetentionRule}
sr.influxClient.BucketsAPI().UpdateBucket(ctx, foundMonthlyBucket)
} }
yearlyBucket := fmt.Sprintf("%s_yearly", sr.appConfig.GetString("web.influxdb.bucket")) yearlyBucket := fmt.Sprintf("%s_yearly", sr.appConfig.GetString("web.influxdb.bucket"))
@@ -486,16 +442,3 @@ func (sr *scrutinyRepository) lookupNestedDurationKeys(durationKey string) []str
} }
return []string{DURATION_KEY_WEEK} 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,11 +4,9 @@ import (
"context" "context"
"errors" "errors"
"fmt" "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/m20201107210306"
"github.com/analogj/scrutiny/webapp/backend/pkg/database/migrations/m20220503120000" "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/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"
"github.com/analogj/scrutiny/webapp/backend/pkg/models/collector" "github.com/analogj/scrutiny/webapp/backend/pkg/models/collector"
"github.com/analogj/scrutiny/webapp/backend/pkg/models/measurements" "github.com/analogj/scrutiny/webapp/backend/pkg/models/measurements"
@@ -269,85 +267,6 @@ func (sr *scrutinyRepository) Migrate(ctx context.Context) error {
return tx.AutoMigrate(m20220509170100.Device{}) return tx.AutoMigrate(m20220509170100.Device{})
}, },
}, },
{
ID: "m20220709181300",
Migrate: func(tx *gorm.DB) error {
// delete devices with empty `wwn` field (they are impossible to delete manually), and are invalid.
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: "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
},
},
}) })
if err := m.Migrate(); err != nil { if err := m.Migrate(); err != nil {
@@ -355,30 +274,6 @@ func (sr *scrutinyRepository) Migrate(ctx context.Context) error {
return err return err
} }
sr.logger.Infoln("Database migration completed successfully") 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 return nil
} }
@@ -1,85 +0,0 @@
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
}
@@ -11,71 +11,35 @@ import (
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
func (sr *scrutinyRepository) EnsureTasks(ctx context.Context, orgID string) error { func (sr *scrutinyRepository) EnsureTasks(ctx context.Context, orgID string) error {
weeklyTaskName := "tsk-weekly-aggr" weeklyTaskName := "tsk-weekly-aggr"
weeklyTaskScript := sr.DownsampleScript("weekly", weeklyTaskName, "0 1 * * 0")
if found, findErr := sr.influxTaskApi.FindTasks(ctx, &api.TaskFilter{Name: weeklyTaskName}); findErr == nil && len(found) == 0 { if found, findErr := sr.influxTaskApi.FindTasks(ctx, &api.TaskFilter{Name: weeklyTaskName}); findErr == nil && len(found) == 0 {
//weekly on Sunday at 1:00am //weekly on Sunday at 1:00am
_, err := sr.influxTaskApi.CreateTaskByFlux(ctx, weeklyTaskScript, orgID) _, err := sr.influxTaskApi.CreateTaskWithCron(ctx, weeklyTaskName, sr.DownsampleScript("weekly"), "0 1 * * 0", orgID)
if err != nil { if err != nil {
return err return err
} }
} else if len(found) == 1 {
//check if we should update
task := &found[0]
if weeklyTaskScript != task.Flux {
sr.logger.Infoln("updating weekly task script")
task.Flux = weeklyTaskScript
_, err := sr.influxTaskApi.UpdateTask(ctx, task)
if err != nil {
return err
}
}
} }
monthlyTaskName := "tsk-monthly-aggr" monthlyTaskName := "tsk-monthly-aggr"
monthlyTaskScript := sr.DownsampleScript("monthly", monthlyTaskName, "30 1 1 * *")
if found, findErr := sr.influxTaskApi.FindTasks(ctx, &api.TaskFilter{Name: monthlyTaskName}); findErr == nil && len(found) == 0 { if found, findErr := sr.influxTaskApi.FindTasks(ctx, &api.TaskFilter{Name: monthlyTaskName}); findErr == nil && len(found) == 0 {
//monthly on first day of the month at 1:30am //monthly on first day of the month at 1:30am
_, err := sr.influxTaskApi.CreateTaskByFlux(ctx, monthlyTaskScript, orgID) _, err := sr.influxTaskApi.CreateTaskWithCron(ctx, monthlyTaskName, sr.DownsampleScript("monthly"), "30 1 1 * *", orgID)
if err != nil { if err != nil {
return err return err
} }
} else if len(found) == 1 {
//check if we should update
task := &found[0]
if monthlyTaskScript != task.Flux {
sr.logger.Infoln("updating monthly task script")
task.Flux = monthlyTaskScript
_, err := sr.influxTaskApi.UpdateTask(ctx, task)
if err != nil {
return err
}
}
} }
yearlyTaskName := "tsk-yearly-aggr" yearlyTaskName := "tsk-yearly-aggr"
yearlyTaskScript := sr.DownsampleScript("yearly", yearlyTaskName, "0 2 1 1 *")
if found, findErr := sr.influxTaskApi.FindTasks(ctx, &api.TaskFilter{Name: yearlyTaskName}); findErr == nil && len(found) == 0 { if found, findErr := sr.influxTaskApi.FindTasks(ctx, &api.TaskFilter{Name: yearlyTaskName}); findErr == nil && len(found) == 0 {
//yearly on the first day of the year at 2:00am //yearly on the first day of the year at 2:00am
_, err := sr.influxTaskApi.CreateTaskByFlux(ctx, yearlyTaskScript, orgID) _, err := sr.influxTaskApi.CreateTaskWithCron(ctx, yearlyTaskName, sr.DownsampleScript("yearly"), "0 2 1 1 *", orgID)
if err != nil { if err != nil {
return err return err
} }
} else if len(found) == 1 {
//check if we should update
task := &found[0]
if yearlyTaskScript != task.Flux {
sr.logger.Infoln("updating yearly task script")
task.Flux = yearlyTaskScript
_, err := sr.influxTaskApi.UpdateTask(ctx, task)
if err != nil {
return err
}
}
} }
return nil return nil
} }
func (sr *scrutinyRepository) DownsampleScript(aggregationType string, name string, cron string) string { func (sr *scrutinyRepository) DownsampleScript(aggregationType string) string {
var sourceBucket string // the source of the data var sourceBucket string // the source of the data
var destBucket string // the destination for the aggregated data var destBucket string // the destination for the aggregated data
var rangeStart string var rangeStart string
@@ -124,11 +88,6 @@ func (sr *scrutinyRepository) DownsampleScript(aggregationType string, name stri
*/ */
return fmt.Sprintf(` return fmt.Sprintf(`
option task = {
name: "%s",
cron: "%s",
}
sourceBucket = "%s" sourceBucket = "%s"
rangeStart = %s rangeStart = %s
rangeEnd = %s rangeEnd = %s
@@ -143,18 +102,16 @@ from(bucket: sourceBucket)
|> aggregateWindow(every: aggWindow, fn: last, createEmpty: false) |> aggregateWindow(every: aggWindow, fn: last, createEmpty: false)
|> to(bucket: destBucket, org: destOrg) |> to(bucket: destBucket, org: destOrg)
from(bucket: sourceBucket) temp_data = from(bucket: sourceBucket)
|> range(start: rangeStart, stop: rangeEnd) |> range(start: rangeStart, stop: rangeEnd)
|> filter(fn: (r) => r["_measurement"] == "temp") |> filter(fn: (r) => r["_measurement"] == "temp")
|> group(columns: ["device_wwn"]) |> group(columns: ["device_wwn"])
|> toInt() |> toInt()
temp_data
|> aggregateWindow(fn: mean, every: aggWindow, createEmpty: false) |> aggregateWindow(fn: mean, every: aggWindow, createEmpty: false)
|> set(key: "_measurement", value: "temp")
|> set(key: "_field", value: "temp")
|> to(bucket: destBucket, org: destOrg) |> to(bucket: destBucket, org: destOrg)
`, `,
name,
cron,
sourceBucket, sourceBucket,
rangeStart, rangeStart,
rangeEnd, rangeEnd,
@@ -1,164 +0,0 @@
package database
import (
mock_config "github.com/analogj/scrutiny/webapp/backend/pkg/config/mock"
"github.com/golang/mock/gomock"
"github.com/stretchr/testify/require"
"testing"
)
func Test_DownsampleScript_Weekly(t *testing.T) {
t.Parallel()
//setup
mockCtrl := gomock.NewController(t)
defer mockCtrl.Finish()
fakeConfig := mock_config.NewMockInterface(mockCtrl)
fakeConfig.EXPECT().GetString("web.influxdb.bucket").Return("metrics").AnyTimes()
fakeConfig.EXPECT().GetString("web.influxdb.org").Return("scrutiny").AnyTimes()
deviceRepo := scrutinyRepository{
appConfig: fakeConfig,
}
aggregationType := "weekly"
//test
influxDbScript := deviceRepo.DownsampleScript(aggregationType, "tsk-weekly-aggr", "0 1 * * 0")
//assert
require.Equal(t, `
option task = {
name: "tsk-weekly-aggr",
cron: "0 1 * * 0",
}
sourceBucket = "metrics"
rangeStart = -2w
rangeEnd = -1w
aggWindow = 1w
destBucket = "metrics_weekly"
destOrg = "scrutiny"
from(bucket: sourceBucket)
|> range(start: rangeStart, stop: rangeEnd)
|> filter(fn: (r) => r["_measurement"] == "smart" )
|> group(columns: ["device_wwn", "_field"])
|> aggregateWindow(every: aggWindow, fn: last, createEmpty: false)
|> to(bucket: destBucket, org: destOrg)
from(bucket: sourceBucket)
|> range(start: rangeStart, stop: rangeEnd)
|> filter(fn: (r) => r["_measurement"] == "temp")
|> group(columns: ["device_wwn"])
|> toInt()
|> aggregateWindow(fn: mean, every: aggWindow, createEmpty: false)
|> set(key: "_measurement", value: "temp")
|> set(key: "_field", value: "temp")
|> to(bucket: destBucket, org: destOrg)
`, influxDbScript)
}
func Test_DownsampleScript_Monthly(t *testing.T) {
t.Parallel()
//setup
mockCtrl := gomock.NewController(t)
defer mockCtrl.Finish()
fakeConfig := mock_config.NewMockInterface(mockCtrl)
fakeConfig.EXPECT().GetString("web.influxdb.bucket").Return("metrics").AnyTimes()
fakeConfig.EXPECT().GetString("web.influxdb.org").Return("scrutiny").AnyTimes()
deviceRepo := scrutinyRepository{
appConfig: fakeConfig,
}
aggregationType := "monthly"
//test
influxDbScript := deviceRepo.DownsampleScript(aggregationType, "tsk-monthly-aggr", "30 1 1 * *")
//assert
require.Equal(t, `
option task = {
name: "tsk-monthly-aggr",
cron: "30 1 1 * *",
}
sourceBucket = "metrics_weekly"
rangeStart = -2mo
rangeEnd = -1mo
aggWindow = 1mo
destBucket = "metrics_monthly"
destOrg = "scrutiny"
from(bucket: sourceBucket)
|> range(start: rangeStart, stop: rangeEnd)
|> filter(fn: (r) => r["_measurement"] == "smart" )
|> group(columns: ["device_wwn", "_field"])
|> aggregateWindow(every: aggWindow, fn: last, createEmpty: false)
|> to(bucket: destBucket, org: destOrg)
from(bucket: sourceBucket)
|> range(start: rangeStart, stop: rangeEnd)
|> filter(fn: (r) => r["_measurement"] == "temp")
|> group(columns: ["device_wwn"])
|> toInt()
|> aggregateWindow(fn: mean, every: aggWindow, createEmpty: false)
|> set(key: "_measurement", value: "temp")
|> set(key: "_field", value: "temp")
|> to(bucket: destBucket, org: destOrg)
`, influxDbScript)
}
func Test_DownsampleScript_Yearly(t *testing.T) {
t.Parallel()
//setup
mockCtrl := gomock.NewController(t)
defer mockCtrl.Finish()
fakeConfig := mock_config.NewMockInterface(mockCtrl)
fakeConfig.EXPECT().GetString("web.influxdb.bucket").Return("metrics").AnyTimes()
fakeConfig.EXPECT().GetString("web.influxdb.org").Return("scrutiny").AnyTimes()
deviceRepo := scrutinyRepository{
appConfig: fakeConfig,
}
aggregationType := "yearly"
//test
influxDbScript := deviceRepo.DownsampleScript(aggregationType, "tsk-yearly-aggr", "0 2 1 1 *")
//assert
require.Equal(t, `
option task = {
name: "tsk-yearly-aggr",
cron: "0 2 1 1 *",
}
sourceBucket = "metrics_monthly"
rangeStart = -2y
rangeEnd = -1y
aggWindow = 1y
destBucket = "metrics_yearly"
destOrg = "scrutiny"
from(bucket: sourceBucket)
|> range(start: rangeStart, stop: rangeEnd)
|> filter(fn: (r) => r["_measurement"] == "smart" )
|> group(columns: ["device_wwn", "_field"])
|> aggregateWindow(every: aggWindow, fn: last, createEmpty: false)
|> to(bucket: destBucket, org: destOrg)
from(bucket: sourceBucket)
|> range(start: rangeStart, stop: rangeEnd)
|> filter(fn: (r) => r["_measurement"] == "temp")
|> group(columns: ["device_wwn"])
|> toInt()
|> aggregateWindow(fn: mean, every: aggWindow, createEmpty: false)
|> set(key: "_measurement", value: "temp")
|> set(key: "_field", value: "temp")
|> to(bucket: destBucket, org: destOrg)
`, influxDbScript)
}
@@ -17,10 +17,6 @@ func (sr *scrutinyRepository) SaveSmartTemperature(ctx context.Context, wwn stri
if len(collectorSmartData.AtaSctTemperatureHistory.Table) > 0 { if len(collectorSmartData.AtaSctTemperatureHistory.Table) > 0 {
for ndx, temp := range collectorSmartData.AtaSctTemperatureHistory.Table { 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 minutesOffset := collectorSmartData.AtaSctTemperatureHistory.LoggingIntervalMinutes * int64(ndx) * 60
smartTemp := measurements.SmartTemperature{ smartTemp := measurements.SmartTemperature{
@@ -1,185 +0,0 @@
package database
import (
mock_config "github.com/analogj/scrutiny/webapp/backend/pkg/config/mock"
"github.com/golang/mock/gomock"
"github.com/stretchr/testify/require"
"testing"
)
func Test_aggregateTempQuery_Week(t *testing.T) {
t.Parallel()
//setup
mockCtrl := gomock.NewController(t)
defer mockCtrl.Finish()
fakeConfig := mock_config.NewMockInterface(mockCtrl)
fakeConfig.EXPECT().GetString("web.influxdb.bucket").Return("metrics").AnyTimes()
fakeConfig.EXPECT().GetString("web.influxdb.org").Return("scrutiny").AnyTimes()
deviceRepo := scrutinyRepository{
appConfig: fakeConfig,
}
aggregationType := DURATION_KEY_WEEK
//test
influxDbScript := deviceRepo.aggregateTempQuery(aggregationType)
//assert
require.Equal(t, `import "influxdata/influxdb/schema"
weekData = from(bucket: "metrics")
|> range(start: -1w, stop: now())
|> filter(fn: (r) => r["_measurement"] == "temp" )
|> aggregateWindow(every: 1h, fn: mean, createEmpty: false)
|> group(columns: ["device_wwn"])
|> toInt()
weekData
|> schema.fieldsAsCols()
|> yield()`, influxDbScript)
}
func Test_aggregateTempQuery_Month(t *testing.T) {
t.Parallel()
//setup
mockCtrl := gomock.NewController(t)
defer mockCtrl.Finish()
fakeConfig := mock_config.NewMockInterface(mockCtrl)
fakeConfig.EXPECT().GetString("web.influxdb.bucket").Return("metrics").AnyTimes()
fakeConfig.EXPECT().GetString("web.influxdb.org").Return("scrutiny").AnyTimes()
deviceRepo := scrutinyRepository{
appConfig: fakeConfig,
}
aggregationType := DURATION_KEY_MONTH
//test
influxDbScript := deviceRepo.aggregateTempQuery(aggregationType)
//assert
require.Equal(t, `import "influxdata/influxdb/schema"
weekData = from(bucket: "metrics")
|> range(start: -1w, stop: now())
|> filter(fn: (r) => r["_measurement"] == "temp" )
|> aggregateWindow(every: 1h, fn: mean, createEmpty: false)
|> group(columns: ["device_wwn"])
|> toInt()
monthData = from(bucket: "metrics_weekly")
|> range(start: -1mo, stop: -1w)
|> filter(fn: (r) => r["_measurement"] == "temp" )
|> aggregateWindow(every: 1h, fn: mean, createEmpty: false)
|> group(columns: ["device_wwn"])
|> toInt()
union(tables: [weekData, monthData])
|> group(columns: ["device_wwn"])
|> sort(columns: ["_time"], desc: false)
|> schema.fieldsAsCols()`, influxDbScript)
}
func Test_aggregateTempQuery_Year(t *testing.T) {
t.Parallel()
//setup
mockCtrl := gomock.NewController(t)
defer mockCtrl.Finish()
fakeConfig := mock_config.NewMockInterface(mockCtrl)
fakeConfig.EXPECT().GetString("web.influxdb.bucket").Return("metrics").AnyTimes()
fakeConfig.EXPECT().GetString("web.influxdb.org").Return("scrutiny").AnyTimes()
deviceRepo := scrutinyRepository{
appConfig: fakeConfig,
}
aggregationType := DURATION_KEY_YEAR
//test
influxDbScript := deviceRepo.aggregateTempQuery(aggregationType)
//assert
require.Equal(t, `import "influxdata/influxdb/schema"
weekData = from(bucket: "metrics")
|> range(start: -1w, stop: now())
|> filter(fn: (r) => r["_measurement"] == "temp" )
|> aggregateWindow(every: 1h, fn: mean, createEmpty: false)
|> group(columns: ["device_wwn"])
|> toInt()
monthData = from(bucket: "metrics_weekly")
|> range(start: -1mo, stop: -1w)
|> filter(fn: (r) => r["_measurement"] == "temp" )
|> aggregateWindow(every: 1h, fn: mean, createEmpty: false)
|> group(columns: ["device_wwn"])
|> toInt()
yearData = from(bucket: "metrics_monthly")
|> range(start: -1y, stop: -1mo)
|> filter(fn: (r) => r["_measurement"] == "temp" )
|> aggregateWindow(every: 1h, fn: mean, createEmpty: false)
|> group(columns: ["device_wwn"])
|> toInt()
union(tables: [weekData, monthData, yearData])
|> group(columns: ["device_wwn"])
|> sort(columns: ["_time"], desc: false)
|> schema.fieldsAsCols()`, influxDbScript)
}
func Test_aggregateTempQuery_Forever(t *testing.T) {
t.Parallel()
//setup
mockCtrl := gomock.NewController(t)
defer mockCtrl.Finish()
fakeConfig := mock_config.NewMockInterface(mockCtrl)
fakeConfig.EXPECT().GetString("web.influxdb.bucket").Return("metrics").AnyTimes()
fakeConfig.EXPECT().GetString("web.influxdb.org").Return("scrutiny").AnyTimes()
deviceRepo := scrutinyRepository{
appConfig: fakeConfig,
}
aggregationType := DURATION_KEY_FOREVER
//test
influxDbScript := deviceRepo.aggregateTempQuery(aggregationType)
//assert
require.Equal(t, `import "influxdata/influxdb/schema"
weekData = from(bucket: "metrics")
|> range(start: -1w, stop: now())
|> filter(fn: (r) => r["_measurement"] == "temp" )
|> aggregateWindow(every: 1h, fn: mean, createEmpty: false)
|> group(columns: ["device_wwn"])
|> toInt()
monthData = from(bucket: "metrics_weekly")
|> range(start: -1mo, stop: -1w)
|> filter(fn: (r) => r["_measurement"] == "temp" )
|> aggregateWindow(every: 1h, fn: mean, createEmpty: false)
|> group(columns: ["device_wwn"])
|> toInt()
yearData = from(bucket: "metrics_monthly")
|> range(start: -1y, stop: -1mo)
|> filter(fn: (r) => r["_measurement"] == "temp" )
|> aggregateWindow(every: 1h, fn: mean, createEmpty: false)
|> group(columns: ["device_wwn"])
|> toInt()
foreverData = from(bucket: "metrics_yearly")
|> range(start: -10y, stop: -1y)
|> filter(fn: (r) => r["_measurement"] == "temp" )
|> aggregateWindow(every: 1h, fn: mean, createEmpty: false)
|> group(columns: ["device_wwn"])
|> toInt()
union(tables: [weekData, monthData, yearData, foreverData])
|> group(columns: ["device_wwn"])
|> sort(columns: ["_time"], desc: false)
|> schema.fieldsAsCols()`, influxDbScript)
}
+5
View File
@@ -0,0 +1,5 @@
package models
// Temperature Format
// Date Format
// Device History window
@@ -1,23 +0,0 @@
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"
}
-23
View File
@@ -1,23 +0,0 @@
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"`
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"`
}
+20 -47
View File
@@ -29,22 +29,20 @@ const NotifyFailureTypeSmartFailure = "SmartFailure"
const NotifyFailureTypeScrutinyFailure = "ScrutinyFailure" const NotifyFailureTypeScrutinyFailure = "ScrutinyFailure"
// ShouldNotify check if the error Message should be filtered (level mismatch or filtered_attributes) // ShouldNotify check if the error Message should be filtered (level mismatch or filtered_attributes)
func ShouldNotify(device models.Device, smartAttrs measurements.Smart, statusThreshold pkg.MetricsStatusThreshold, statusFilterAttributes pkg.MetricsStatusFilterAttributes) bool { func ShouldNotify(device models.Device, smartAttrs measurements.Smart, notifyLevel string, notifyFilterAttributes string) bool {
// 1. check if the device is healthy // 1. check if the device is healthy
if device.DeviceStatus == pkg.DeviceStatusPassed { if device.DeviceStatus == pkg.DeviceStatusPassed {
return false return false
} }
//TODO: cannot check for warning notifyLevel yet.
// setup constants for comparison // setup constants for comparison
var requiredDeviceStatus pkg.DeviceStatus var requiredDeviceStatus pkg.DeviceStatus
var requiredAttrStatus pkg.AttributeStatus var requiredAttrStatus pkg.AttributeStatus
if statusThreshold == pkg.MetricsStatusThresholdBoth { if notifyLevel == pkg.NotifyLevelFail {
// either scrutiny or smart failures should trigger an email // either scrutiny or smart failures should trigger an email
requiredDeviceStatus = pkg.DeviceStatusSet(pkg.DeviceStatusFailedSmart, pkg.DeviceStatusFailedScrutiny) requiredDeviceStatus = pkg.DeviceStatusSet(pkg.DeviceStatusFailedSmart, pkg.DeviceStatusFailedScrutiny)
requiredAttrStatus = pkg.AttributeStatusSet(pkg.AttributeStatusFailedSmart, pkg.AttributeStatusFailedScrutiny) requiredAttrStatus = pkg.AttributeStatusSet(pkg.AttributeStatusFailedSmart, pkg.AttributeStatusFailedScrutiny)
} else if statusThreshold == pkg.MetricsStatusThresholdSmart { } else if notifyLevel == pkg.NotifyLevelFailSmart {
//only smart failures //only smart failures
requiredDeviceStatus = pkg.DeviceStatusFailedSmart requiredDeviceStatus = pkg.DeviceStatusFailedSmart
requiredAttrStatus = pkg.AttributeStatusFailedSmart requiredAttrStatus = pkg.AttributeStatusFailedSmart
@@ -55,9 +53,9 @@ func ShouldNotify(device models.Device, smartAttrs measurements.Smart, statusThr
// 2. check if the attributes that are failing should be filtered (non-critical) // 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) // 3. for any unfiltered attribute, store the failure reason (Smart or Scrutiny)
if statusFilterAttributes == pkg.MetricsStatusFilterAttributesCritical { if notifyFilterAttributes == pkg.NotifyFilterAttributesCritical {
hasFailingCriticalAttr := false hasFailingCriticalAttr := false
var statusFailingCriticalAttr pkg.AttributeStatus var statusFailingCrtiticalAttr pkg.AttributeStatus
for attrId, attrData := range smartAttrs.Attributes { for attrId, attrData := range smartAttrs.Attributes {
//find failing attribute //find failing attribute
@@ -66,7 +64,7 @@ func ShouldNotify(device models.Device, smartAttrs measurements.Smart, statusThr
} }
// merge the status's of all critical attributes // merge the status's of all critical attributes
statusFailingCriticalAttr = pkg.AttributeStatusSet(statusFailingCriticalAttr, attrData.GetStatus()) statusFailingCrtiticalAttr = pkg.AttributeStatusSet(statusFailingCrtiticalAttr, attrData.GetStatus())
//found a failing attribute, see if its critical //found a failing attribute, see if its critical
if device.IsScsi() && thresholds.ScsiMetadata[attrId].Critical { if device.IsScsi() && thresholds.ScsiMetadata[attrId].Critical {
@@ -91,7 +89,7 @@ func ShouldNotify(device models.Device, smartAttrs measurements.Smart, statusThr
return false return false
} else { } else {
// check if any of the critical attributes have a status that we're looking for // check if any of the critical attributes have a status that we're looking for
return pkg.AttributeStatusHas(statusFailingCriticalAttr, requiredAttrStatus) return pkg.AttributeStatusHas(statusFailingCrtiticalAttr, requiredAttrStatus)
} }
} else { } else {
@@ -101,9 +99,8 @@ func ShouldNotify(device models.Device, smartAttrs measurements.Smart, statusThr
} }
} }
// TODO: include user label for device. // TODO: include host and/or user label for device.
type Payload struct { type Payload struct {
HostId string `json:"host_id,omitempty"` //host id (optional)
DeviceType string `json:"device_type"` //ATA/SCSI/NVMe DeviceType string `json:"device_type"` //ATA/SCSI/NVMe
DeviceName string `json:"device_name"` //dev/sda DeviceName string `json:"device_name"` //dev/sda
DeviceSerial string `json:"device_serial"` //WDDJ324KSO DeviceSerial string `json:"device_serial"` //WDDJ324KSO
@@ -116,9 +113,8 @@ type Payload struct {
Message string `json:"message"` Message string `json:"message"`
} }
func NewPayload(device models.Device, test bool, currentTime ...time.Time) Payload { func NewPayload(device models.Device, test bool) Payload {
payload := Payload{ payload := Payload{
HostId: strings.TrimSpace(device.HostId),
DeviceType: device.DeviceType, DeviceType: device.DeviceType,
DeviceName: device.DeviceName, DeviceName: device.DeviceName,
DeviceSerial: device.SerialNumber, DeviceSerial: device.SerialNumber,
@@ -126,13 +122,7 @@ func NewPayload(device models.Device, test bool, currentTime ...time.Time) Paylo
} }
//validate that the Payload is populated //validate that the Payload is populated
var sendDate time.Time sendDate := time.Now()
if currentTime != nil && len(currentTime) > 0 {
sendDate = currentTime[0]
} else {
sendDate = time.Now()
}
payload.Date = sendDate.Format(time.RFC3339) payload.Date = sendDate.Format(time.RFC3339)
payload.FailureType = payload.GenerateFailureType(device.DeviceStatus) payload.FailureType = payload.GenerateFailureType(device.DeviceStatus)
payload.Subject = payload.GenerateSubject() payload.Subject = payload.GenerateSubject()
@@ -156,39 +146,25 @@ func (p *Payload) GenerateFailureType(deviceStatus pkg.DeviceStatus) string {
func (p *Payload) GenerateSubject() string { func (p *Payload) GenerateSubject() string {
//generate a detailed failure message //generate a detailed failure message
var subject string return fmt.Sprintf("Scrutiny SMART error (%s) detected on device: %s", p.FailureType, p.DeviceName)
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 { func (p *Payload) GenerateMessage() string {
//generate a detailed failure message //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
messageParts := []string{} Date: %s`, p.DeviceName, p.FailureType, p.DeviceName, p.DeviceSerial, p.DeviceType, p.Date)
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))
}
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 { if p.Test {
messageParts = append([]string{"TEST NOTIFICATION:"}, messageParts...) message = "TEST NOTIFICATION:\n" + message
} }
return strings.Join(messageParts, "\n") return message
} }
func New(logger logrus.FieldLogger, appconfig config.Interface, device models.Device, test bool) Notify { func New(logger logrus.FieldLogger, appconfig config.Interface, device models.Device, test bool) Notify {
@@ -309,9 +285,6 @@ 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_TYPE=%s", n.Payload.DeviceType))
copyEnv = append(copyEnv, fmt.Sprintf("SCRUTINY_DEVICE_SERIAL=%s", n.Payload.DeviceSerial)) copyEnv = append(copyEnv, fmt.Sprintf("SCRUTINY_DEVICE_SERIAL=%s", n.Payload.DeviceSerial))
copyEnv = append(copyEnv, fmt.Sprintf("SCRUTINY_MESSAGE=%s", n.Payload.Message)) 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, "") err := utils.CmdExec(scriptPath, []string{}, "", copyEnv, "")
if err != nil { if err != nil {
n.Logger.Errorf("An error occurred while executing script %s: %v", scriptPath, err) n.Logger.Errorf("An error occurred while executing script %s: %v", scriptPath, err)
+35 -118
View File
@@ -1,13 +1,11 @@
package notify package notify
import ( import (
"fmt"
"github.com/analogj/scrutiny/webapp/backend/pkg" "github.com/analogj/scrutiny/webapp/backend/pkg"
"github.com/analogj/scrutiny/webapp/backend/pkg/models" "github.com/analogj/scrutiny/webapp/backend/pkg/models"
"github.com/analogj/scrutiny/webapp/backend/pkg/models/measurements" "github.com/analogj/scrutiny/webapp/backend/pkg/models/measurements"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"testing" "testing"
"time"
) )
func TestShouldNotify_MustSkipPassingDevices(t *testing.T) { func TestShouldNotify_MustSkipPassingDevices(t *testing.T) {
@@ -17,56 +15,56 @@ func TestShouldNotify_MustSkipPassingDevices(t *testing.T) {
DeviceStatus: pkg.DeviceStatusPassed, DeviceStatus: pkg.DeviceStatusPassed,
} }
smartAttrs := measurements.Smart{} smartAttrs := measurements.Smart{}
statusThreshold := pkg.MetricsStatusThresholdBoth notifyLevel := pkg.NotifyLevelFail
notifyFilterAttributes := pkg.MetricsStatusFilterAttributesAll notifyFilterAttributes := pkg.NotifyFilterAttributesAll
//assert //assert
require.False(t, ShouldNotify(device, smartAttrs, statusThreshold, notifyFilterAttributes)) require.False(t, ShouldNotify(device, smartAttrs, notifyLevel, notifyFilterAttributes))
} }
func TestShouldNotify_MetricsStatusThresholdBoth_FailingSmartDevice(t *testing.T) { func TestShouldNotify_NotifyLevelFail_FailingSmartDevice(t *testing.T) {
t.Parallel() t.Parallel()
//setup //setup
device := models.Device{ device := models.Device{
DeviceStatus: pkg.DeviceStatusFailedSmart, DeviceStatus: pkg.DeviceStatusFailedSmart,
} }
smartAttrs := measurements.Smart{} smartAttrs := measurements.Smart{}
statusThreshold := pkg.MetricsStatusThresholdBoth notifyLevel := pkg.NotifyLevelFail
notifyFilterAttributes := pkg.MetricsStatusFilterAttributesAll notifyFilterAttributes := pkg.NotifyFilterAttributesAll
//assert //assert
require.True(t, ShouldNotify(device, smartAttrs, statusThreshold, notifyFilterAttributes)) require.True(t, ShouldNotify(device, smartAttrs, notifyLevel, notifyFilterAttributes))
} }
func TestShouldNotify_MetricsStatusThresholdSmart_FailingSmartDevice(t *testing.T) { func TestShouldNotify_NotifyLevelFailSmart_FailingSmartDevice(t *testing.T) {
t.Parallel() t.Parallel()
//setup //setup
device := models.Device{ device := models.Device{
DeviceStatus: pkg.DeviceStatusFailedSmart, DeviceStatus: pkg.DeviceStatusFailedSmart,
} }
smartAttrs := measurements.Smart{} smartAttrs := measurements.Smart{}
statusThreshold := pkg.MetricsStatusThresholdSmart notifyLevel := pkg.NotifyLevelFailSmart
notifyFilterAttributes := pkg.MetricsStatusFilterAttributesAll notifyFilterAttributes := pkg.NotifyFilterAttributesAll
//assert //assert
require.True(t, ShouldNotify(device, smartAttrs, statusThreshold, notifyFilterAttributes)) require.True(t, ShouldNotify(device, smartAttrs, notifyLevel, notifyFilterAttributes))
} }
func TestShouldNotify_MetricsStatusThresholdScrutiny_FailingSmartDevice(t *testing.T) { func TestShouldNotify_NotifyLevelFailScrutiny_FailingSmartDevice(t *testing.T) {
t.Parallel() t.Parallel()
//setup //setup
device := models.Device{ device := models.Device{
DeviceStatus: pkg.DeviceStatusFailedSmart, DeviceStatus: pkg.DeviceStatusFailedSmart,
} }
smartAttrs := measurements.Smart{} smartAttrs := measurements.Smart{}
statusThreshold := pkg.MetricsStatusThresholdScrutiny notifyLevel := pkg.NotifyLevelFailScrutiny
notifyFilterAttributes := pkg.MetricsStatusFilterAttributesAll notifyFilterAttributes := pkg.NotifyFilterAttributesAll
//assert //assert
require.False(t, ShouldNotify(device, smartAttrs, statusThreshold, notifyFilterAttributes)) require.False(t, ShouldNotify(device, smartAttrs, notifyLevel, notifyFilterAttributes))
} }
func TestShouldNotify_MetricsStatusFilterAttributesCritical_WithCriticalAttrs(t *testing.T) { func TestShouldNotify_NotifyFilterAttributesCritical_WithCriticalAttrs(t *testing.T) {
t.Parallel() t.Parallel()
//setup //setup
device := models.Device{ device := models.Device{
@@ -77,14 +75,14 @@ func TestShouldNotify_MetricsStatusFilterAttributesCritical_WithCriticalAttrs(t
Status: pkg.AttributeStatusFailedSmart, Status: pkg.AttributeStatusFailedSmart,
}, },
}} }}
statusThreshold := pkg.MetricsStatusThresholdBoth notifyLevel := pkg.NotifyLevelFail
notifyFilterAttributes := pkg.MetricsStatusFilterAttributesCritical notifyFilterAttributes := pkg.NotifyFilterAttributesCritical
//assert //assert
require.True(t, ShouldNotify(device, smartAttrs, statusThreshold, notifyFilterAttributes)) require.True(t, ShouldNotify(device, smartAttrs, notifyLevel, notifyFilterAttributes))
} }
func TestShouldNotify_MetricsStatusFilterAttributesCritical_WithMultipleCriticalAttrs(t *testing.T) { func TestShouldNotify_NotifyFilterAttributesCritical_WithMultipleCriticalAttrs(t *testing.T) {
t.Parallel() t.Parallel()
//setup //setup
device := models.Device{ device := models.Device{
@@ -98,14 +96,14 @@ func TestShouldNotify_MetricsStatusFilterAttributesCritical_WithMultipleCritical
Status: pkg.AttributeStatusFailedScrutiny, Status: pkg.AttributeStatusFailedScrutiny,
}, },
}} }}
statusThreshold := pkg.MetricsStatusThresholdBoth notifyLevel := pkg.NotifyLevelFail
notifyFilterAttributes := pkg.MetricsStatusFilterAttributesCritical notifyFilterAttributes := pkg.NotifyFilterAttributesCritical
//assert //assert
require.True(t, ShouldNotify(device, smartAttrs, statusThreshold, notifyFilterAttributes)) require.True(t, ShouldNotify(device, smartAttrs, notifyLevel, notifyFilterAttributes))
} }
func TestShouldNotify_MetricsStatusFilterAttributesCritical_WithNoCriticalAttrs(t *testing.T) { func TestShouldNotify_NotifyFilterAttributesCritical_WithNoCriticalAttrs(t *testing.T) {
t.Parallel() t.Parallel()
//setup //setup
device := models.Device{ device := models.Device{
@@ -116,14 +114,14 @@ func TestShouldNotify_MetricsStatusFilterAttributesCritical_WithNoCriticalAttrs(
Status: pkg.AttributeStatusFailedSmart, Status: pkg.AttributeStatusFailedSmart,
}, },
}} }}
statusThreshold := pkg.MetricsStatusThresholdBoth notifyLevel := pkg.NotifyLevelFail
notifyFilterAttributes := pkg.MetricsStatusFilterAttributesCritical notifyFilterAttributes := pkg.NotifyFilterAttributesCritical
//assert //assert
require.False(t, ShouldNotify(device, smartAttrs, statusThreshold, notifyFilterAttributes)) require.False(t, ShouldNotify(device, smartAttrs, notifyLevel, notifyFilterAttributes))
} }
func TestShouldNotify_MetricsStatusFilterAttributesCritical_WithNoFailingCriticalAttrs(t *testing.T) { func TestShouldNotify_NotifyFilterAttributesCritical_WithNoFailingCriticalAttrs(t *testing.T) {
t.Parallel() t.Parallel()
//setup //setup
device := models.Device{ device := models.Device{
@@ -134,14 +132,14 @@ func TestShouldNotify_MetricsStatusFilterAttributesCritical_WithNoFailingCritica
Status: pkg.AttributeStatusPassed, Status: pkg.AttributeStatusPassed,
}, },
}} }}
statusThreshold := pkg.MetricsStatusThresholdBoth notifyLevel := pkg.NotifyLevelFail
notifyFilterAttributes := pkg.MetricsStatusFilterAttributesCritical notifyFilterAttributes := pkg.NotifyFilterAttributesCritical
//assert //assert
require.False(t, ShouldNotify(device, smartAttrs, statusThreshold, notifyFilterAttributes)) require.False(t, ShouldNotify(device, smartAttrs, notifyLevel, notifyFilterAttributes))
} }
func TestShouldNotify_MetricsStatusFilterAttributesCritical_MetricsStatusThresholdSmart_WithCriticalAttrsFailingScrutiny(t *testing.T) { func TestShouldNotify_NotifyFilterAttributesCritical_NotifyLevelFailSmart_WithCriticalAttrsFailingScrutiny(t *testing.T) {
t.Parallel() t.Parallel()
//setup //setup
device := models.Device{ device := models.Device{
@@ -155,90 +153,9 @@ func TestShouldNotify_MetricsStatusFilterAttributesCritical_MetricsStatusThresho
Status: pkg.AttributeStatusFailedScrutiny, Status: pkg.AttributeStatusFailedScrutiny,
}, },
}} }}
statusThreshold := pkg.MetricsStatusThresholdSmart notifyLevel := pkg.NotifyLevelFailSmart
notifyFilterAttributes := pkg.MetricsStatusFilterAttributesCritical notifyFilterAttributes := pkg.NotifyFilterAttributesCritical
//assert //assert
require.False(t, ShouldNotify(device, smartAttrs, statusThreshold, notifyFilterAttributes)) require.False(t, ShouldNotify(device, smartAttrs, notifyLevel, 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 // VERSION is the app-global version string, which will be replaced with a
// new value during packaging // new value during packaging
const VERSION = "0.6.0" const VERSION = "0.4.15"
@@ -8,7 +8,7 @@ import (
) )
func DeleteDevice(c *gin.Context) { func DeleteDevice(c *gin.Context) {
logger := c.MustGet("LOGGER").(*logrus.Entry) logger := c.MustGet("LOGGER").(logrus.FieldLogger)
deviceRepo := c.MustGet("DEVICE_REPOSITORY").(database.DeviceRepo) deviceRepo := c.MustGet("DEVICE_REPOSITORY").(database.DeviceRepo)
err := deviceRepo.DeleteDevice(c, c.Param("wwn")) err := deviceRepo.DeleteDevice(c, c.Param("wwn"))
@@ -9,7 +9,7 @@ import (
) )
func GetDeviceDetails(c *gin.Context) { func GetDeviceDetails(c *gin.Context) {
logger := c.MustGet("LOGGER").(*logrus.Entry) logger := c.MustGet("LOGGER").(logrus.FieldLogger)
deviceRepo := c.MustGet("DEVICE_REPOSITORY").(database.DeviceRepo) deviceRepo := c.MustGet("DEVICE_REPOSITORY").(database.DeviceRepo)
device, err := deviceRepo.GetDeviceDetails(c, c.Param("wwn")) device, err := deviceRepo.GetDeviceDetails(c, c.Param("wwn"))
@@ -8,7 +8,7 @@ import (
) )
func GetDevicesSummary(c *gin.Context) { func GetDevicesSummary(c *gin.Context) {
logger := c.MustGet("LOGGER").(*logrus.Entry) logger := c.MustGet("LOGGER").(logrus.FieldLogger)
deviceRepo := c.MustGet("DEVICE_REPOSITORY").(database.DeviceRepo) deviceRepo := c.MustGet("DEVICE_REPOSITORY").(database.DeviceRepo)
summary, err := deviceRepo.GetSummary(c) summary, err := deviceRepo.GetSummary(c)
@@ -18,7 +18,6 @@ func GetDevicesSummary(c *gin.Context) {
return return
} }
//this must match DeviceSummaryWrapper (webapp/backend/pkg/models/device_summary.go)
c.JSON(http.StatusOK, gin.H{ c.JSON(http.StatusOK, gin.H{
"success": true, "success": true,
"data": map[string]interface{}{ "data": map[string]interface{}{
@@ -8,7 +8,7 @@ import (
) )
func GetDevicesSummaryTempHistory(c *gin.Context) { func GetDevicesSummaryTempHistory(c *gin.Context) {
logger := c.MustGet("LOGGER").(*logrus.Entry) logger := c.MustGet("LOGGER").(logrus.FieldLogger)
deviceRepo := c.MustGet("DEVICE_REPOSITORY").(database.DeviceRepo) deviceRepo := c.MustGet("DEVICE_REPOSITORY").(database.DeviceRepo)
durationKey, exists := c.GetQuery("duration_key") durationKey, exists := c.GetQuery("duration_key")
@@ -1,25 +0,0 @@
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,
})
}
@@ -1,29 +0,0 @@
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,
})
}
@@ -4,7 +4,6 @@ import (
"github.com/analogj/scrutiny/webapp/backend/pkg/database" "github.com/analogj/scrutiny/webapp/backend/pkg/database"
"github.com/analogj/scrutiny/webapp/backend/pkg/models" "github.com/analogj/scrutiny/webapp/backend/pkg/models"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/samber/lo"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"net/http" "net/http"
) )
@@ -13,7 +12,7 @@ import (
// This function is run everytime a collector is about to start a run. It can be used to update device metadata. // 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) { func RegisterDevices(c *gin.Context) {
deviceRepo := c.MustGet("DEVICE_REPOSITORY").(database.DeviceRepo) deviceRepo := c.MustGet("DEVICE_REPOSITORY").(database.DeviceRepo)
logger := c.MustGet("LOGGER").(*logrus.Entry) logger := c.MustGet("LOGGER").(logrus.FieldLogger)
var collectorDeviceWrapper models.DeviceWrapper var collectorDeviceWrapper models.DeviceWrapper
err := c.BindJSON(&collectorDeviceWrapper) err := c.BindJSON(&collectorDeviceWrapper)
@@ -23,13 +22,8 @@ func RegisterDevices(c *gin.Context) {
return return
} }
//filter any device with empty wwn (they are invalid)
detectedStorageDevices := lo.Filter[models.Device](collectorDeviceWrapper.Data, func(dev models.Device, _ int) bool {
return len(dev.WWN) > 0
})
errs := []error{} errs := []error{}
for _, dev := range detectedStorageDevices { for _, dev := range collectorDeviceWrapper.Data {
//insert devices into DB (and update specified columns if device is already registered) //insert devices into DB (and update specified columns if device is already registered)
// update device fields that may change: (DeviceType, HostID) // update device fields that may change: (DeviceType, HostID)
if err := deviceRepo.RegisterDevice(c, dev); err != nil { if err := deviceRepo.RegisterDevice(c, dev); err != nil {
@@ -46,7 +40,7 @@ func RegisterDevices(c *gin.Context) {
} else { } else {
c.JSON(http.StatusOK, models.DeviceWrapper{ c.JSON(http.StatusOK, models.DeviceWrapper{
Success: true, Success: true,
Data: detectedStorageDevices, Data: collectorDeviceWrapper.Data,
}) })
return return
} }
@@ -1,34 +0,0 @@
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 // Send test notification
func SendTestNotification(c *gin.Context) { func SendTestNotification(c *gin.Context) {
appConfig := c.MustGet("CONFIG").(config.Interface) appConfig := c.MustGet("CONFIG").(config.Interface)
logger := c.MustGet("LOGGER").(*logrus.Entry) logger := c.MustGet("LOGGER").(logrus.FieldLogger)
testNotify := notify.New( testNotify := notify.New(
logger, logger,
@@ -1,7 +1,6 @@
package handler package handler
import ( import (
"fmt"
"github.com/analogj/scrutiny/webapp/backend/pkg" "github.com/analogj/scrutiny/webapp/backend/pkg"
"github.com/analogj/scrutiny/webapp/backend/pkg/config" "github.com/analogj/scrutiny/webapp/backend/pkg/config"
"github.com/analogj/scrutiny/webapp/backend/pkg/database" "github.com/analogj/scrutiny/webapp/backend/pkg/database"
@@ -14,17 +13,13 @@ import (
func UploadDeviceMetrics(c *gin.Context) { func UploadDeviceMetrics(c *gin.Context) {
//db := c.MustGet("DB").(*gorm.DB) //db := c.MustGet("DB").(*gorm.DB)
logger := c.MustGet("LOGGER").(*logrus.Entry) logger := c.MustGet("LOGGER").(logrus.FieldLogger)
appConfig := c.MustGet("CONFIG").(config.Interface) appConfig := c.MustGet("CONFIG").(config.Interface)
//influxWriteDb := c.MustGet("INFLUXDB_WRITE").(*api.WriteAPIBlocking) //influxWriteDb := c.MustGet("INFLUXDB_WRITE").(*api.WriteAPIBlocking)
deviceRepo := c.MustGet("DEVICE_REPOSITORY").(database.DeviceRepo) deviceRepo := c.MustGet("DEVICE_REPOSITORY").(database.DeviceRepo)
//appConfig := c.MustGet("CONFIG").(config.Interface) //appConfig := c.MustGet("CONFIG").(config.Interface)
if c.Param("wwn") == "" {
c.JSON(http.StatusBadRequest, gin.H{"success": false})
}
var collectorSmartData collector.SmartInfo var collectorSmartData collector.SmartInfo
err := c.BindJSON(&collectorSmartData) err := c.BindJSON(&collectorSmartData)
if err != nil { if err != nil {
@@ -68,12 +63,7 @@ func UploadDeviceMetrics(c *gin.Context) {
} }
//check for error //check for error
if notify.ShouldNotify( if notify.ShouldNotify(updatedDevice, smartData, appConfig.GetString("notify.level"), appConfig.GetString("notify.filter_attributes")) {
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 //send notifications
liveNotify := notify.New( liveNotify := notify.New(
+2 -2
View File
@@ -28,11 +28,11 @@ import (
var timeFormat = "02/Jan/2006:15:04:05 -0700" var timeFormat = "02/Jan/2006:15:04:05 -0700"
// Logger is the logrus logger handler // Logger is the logrus logger handler
func LoggerMiddleware(logger *logrus.Entry) gin.HandlerFunc { func LoggerMiddleware(logger logrus.FieldLogger) gin.HandlerFunc {
hostname, err := os.Hostname() hostname, err := os.Hostname()
if err != nil { if err != nil {
hostname = "unknown" hostname = "unknow"
} }
return func(c *gin.Context) { return func(c *gin.Context) {
@@ -1,7 +1,6 @@
package middleware package middleware
import ( import (
"context"
"github.com/analogj/scrutiny/webapp/backend/pkg/config" "github.com/analogj/scrutiny/webapp/backend/pkg/config"
"github.com/analogj/scrutiny/webapp/backend/pkg/database" "github.com/analogj/scrutiny/webapp/backend/pkg/database"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
@@ -15,14 +14,6 @@ func RepositoryMiddleware(appConfig config.Interface, globalLogger logrus.FieldL
panic(err) 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() //TODO: determine where we can call defer deviceRepo.Close()
return func(c *gin.Context) { return func(c *gin.Context) {
c.Set("DEVICE_REPOSITORY", deviceRepo) c.Set("DEVICE_REPOSITORY", deviceRepo)
+29 -6
View File
@@ -9,17 +9,18 @@ import (
"github.com/analogj/scrutiny/webapp/backend/pkg/web/middleware" "github.com/analogj/scrutiny/webapp/backend/pkg/web/middleware"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"io"
"net/http" "net/http"
"os"
"path/filepath" "path/filepath"
"strings" "strings"
) )
type AppEngine struct { type AppEngine struct {
Config config.Interface Config config.Interface
Logger *logrus.Entry
} }
func (ae *AppEngine) Setup(logger *logrus.Entry) *gin.Engine { func (ae *AppEngine) Setup(logger logrus.FieldLogger) *gin.Engine {
r := gin.New() r := gin.New()
r.Use(middleware.LoggerMiddleware(logger)) r.Use(middleware.LoggerMiddleware(logger))
@@ -34,7 +35,11 @@ func (ae *AppEngine) Setup(logger *logrus.Entry) *gin.Engine {
{ {
api := base.Group("/api") api := base.Group("/api")
{ {
api.GET("/health", handler.HealthCheck) api.GET("/health", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"success": true,
})
})
api.POST("/health/notify", handler.SendTestNotification) //check if notifications are configured correctly 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 api.POST("/devices/register", handler.RegisterDevices) //used by Collector to register new devices and retrieve filtered list
@@ -45,8 +50,6 @@ func (ae *AppEngine) Setup(logger *logrus.Entry) *gin.Engine {
api.GET("/device/:wwn/details", handler.GetDeviceDetails) //used by Details api.GET("/device/:wwn/details", handler.GetDeviceDetails) //used by Details
api.DELETE("/device/:wwn", handler.DeleteDevice) //used by UI to delete device 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
} }
} }
@@ -72,6 +75,26 @@ func (ae *AppEngine) Start() error {
gin.SetMode(gin.DebugMode) 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. //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"))) { if !utils.FileExists(filepath.Dir(ae.Config.GetString("web.database.location"))) {
return errors.ConfigValidationError(fmt.Sprintf( return errors.ConfigValidationError(fmt.Sprintf(
@@ -79,7 +102,7 @@ func (ae *AppEngine) Start() error {
filepath.Dir(ae.Config.GetString("web.database.location")))) filepath.Dir(ae.Config.GetString("web.database.location"))))
} }
r := ae.Setup(ae.Logger) r := ae.Setup(logger)
return r.Run(fmt.Sprintf("%s:%s", ae.Config.GetString("web.listen.host"), ae.Config.GetString("web.listen.port"))) return r.Run(fmt.Sprintf("%s:%s", ae.Config.GetString("web.listen.host"), ae.Config.GetString("web.listen.port")))
} }
+23 -51
View File
@@ -3,9 +3,7 @@ package web_test
import ( import (
"bytes" "bytes"
"encoding/json" "encoding/json"
"fmt"
"github.com/analogj/scrutiny/webapp/backend/pkg" "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" 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"
"github.com/analogj/scrutiny/webapp/backend/pkg/models/collector" "github.com/analogj/scrutiny/webapp/backend/pkg/models/collector"
@@ -91,8 +89,6 @@ func (suite *ServerTestSuite) TestHealthRoute() {
mockCtrl := gomock.NewController(suite.T()) mockCtrl := gomock.NewController(suite.T())
defer mockCtrl.Finish() defer mockCtrl.Finish()
fakeConfig := mock_config.NewMockInterface(mockCtrl) 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.database.location").Return(path.Join(parentPath, "scrutiny_test.db")).AnyTimes()
fakeConfig.EXPECT().GetString("web.src.frontend.path").Return(parentPath).AnyTimes() fakeConfig.EXPECT().GetString("web.src.frontend.path").Return(parentPath).AnyTimes()
fakeConfig.EXPECT().GetString("web.listen.basepath").Return(suite.Basepath).AnyTimes() fakeConfig.EXPECT().GetString("web.listen.basepath").Return(suite.Basepath).AnyTimes()
@@ -115,7 +111,7 @@ func (suite *ServerTestSuite) TestHealthRoute() {
Config: fakeConfig, Config: fakeConfig,
} }
router := ae.Setup(logrus.WithField("test", suite.T().Name())) router := ae.Setup(logrus.New())
//test //test
w := httptest.NewRecorder() w := httptest.NewRecorder()
@@ -134,8 +130,6 @@ func (suite *ServerTestSuite) TestRegisterDevicesRoute() {
mockCtrl := gomock.NewController(suite.T()) mockCtrl := gomock.NewController(suite.T())
defer mockCtrl.Finish() defer mockCtrl.Finish()
fakeConfig := mock_config.NewMockInterface(mockCtrl) 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.database.location").Return(path.Join(parentPath, "scrutiny_test.db")).AnyTimes()
fakeConfig.EXPECT().GetString("web.src.frontend.path").Return(parentPath).AnyTimes() fakeConfig.EXPECT().GetString("web.src.frontend.path").Return(parentPath).AnyTimes()
fakeConfig.EXPECT().GetString("web.listen.basepath").Return(suite.Basepath).AnyTimes() fakeConfig.EXPECT().GetString("web.listen.basepath").Return(suite.Basepath).AnyTimes()
@@ -156,7 +150,7 @@ func (suite *ServerTestSuite) TestRegisterDevicesRoute() {
ae := web.AppEngine{ ae := web.AppEngine{
Config: fakeConfig, Config: fakeConfig,
} }
router := ae.Setup(logrus.WithField("test", suite.T().Name())) router := ae.Setup(logrus.New())
file, err := os.Open("testdata/register-devices-req.json") file, err := os.Open("testdata/register-devices-req.json")
require.NoError(suite.T(), err) require.NoError(suite.T(), err)
@@ -176,8 +170,6 @@ func (suite *ServerTestSuite) TestUploadDeviceMetricsRoute() {
mockCtrl := gomock.NewController(suite.T()) mockCtrl := gomock.NewController(suite.T())
defer mockCtrl.Finish() defer mockCtrl.Finish()
fakeConfig := mock_config.NewMockInterface(mockCtrl) 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.database.location").AnyTimes().Return(path.Join(parentPath, "scrutiny_test.db"))
fakeConfig.EXPECT().GetString("web.src.frontend.path").AnyTimes().Return(parentPath) fakeConfig.EXPECT().GetString("web.src.frontend.path").AnyTimes().Return(parentPath)
fakeConfig.EXPECT().GetString("web.listen.basepath").Return(suite.Basepath).AnyTimes() fakeConfig.EXPECT().GetString("web.listen.basepath").Return(suite.Basepath).AnyTimes()
@@ -194,14 +186,13 @@ func (suite *ServerTestSuite) TestUploadDeviceMetricsRoute() {
} else { } else {
fakeConfig.EXPECT().GetString("web.influxdb.host").Return("localhost").AnyTimes() fakeConfig.EXPECT().GetString("web.influxdb.host").Return("localhost").AnyTimes()
} }
fakeConfig.EXPECT().GetInt(fmt.Sprintf("%s.metrics.notify_level", config.DB_USER_SETTINGS_SUBKEY)).AnyTimes().Return(int(pkg.MetricsNotifyLevelFail)) fakeConfig.EXPECT().GetString("notify.level").AnyTimes().Return(pkg.NotifyLevelFail)
fakeConfig.EXPECT().GetInt(fmt.Sprintf("%s.metrics.status_filter_attributes", config.DB_USER_SETTINGS_SUBKEY)).AnyTimes().Return(int(pkg.MetricsStatusFilterAttributesAll)) fakeConfig.EXPECT().GetString("notify.filter_attributes").AnyTimes().Return(pkg.NotifyFilterAttributesAll)
fakeConfig.EXPECT().GetInt(fmt.Sprintf("%s.metrics.status_threshold", config.DB_USER_SETTINGS_SUBKEY)).AnyTimes().Return(int(pkg.MetricsStatusThresholdBoth))
ae := web.AppEngine{ ae := web.AppEngine{
Config: fakeConfig, Config: fakeConfig,
} }
router := ae.Setup(logrus.WithField("test", suite.T().Name())) router := ae.Setup(logrus.New())
devicesfile, err := os.Open("testdata/register-devices-single-req.json") devicesfile, err := os.Open("testdata/register-devices-single-req.json")
require.NoError(suite.T(), err) require.NoError(suite.T(), err)
@@ -228,13 +219,10 @@ func (suite *ServerTestSuite) TestPopulateMultiple() {
mockCtrl := gomock.NewController(suite.T()) mockCtrl := gomock.NewController(suite.T())
defer mockCtrl.Finish() defer mockCtrl.Finish()
fakeConfig := mock_config.NewMockInterface(mockCtrl) 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().GetString("web.database.location").AnyTimes().Return("testdata/scrutiny_test.db")
fakeConfig.EXPECT().GetStringSlice("notify.urls").Return([]string{}).AnyTimes() fakeConfig.EXPECT().GetStringSlice("notify.urls").Return([]string{}).AnyTimes()
fakeConfig.EXPECT().GetInt(fmt.Sprintf("%s.metrics.notify_level", config.DB_USER_SETTINGS_SUBKEY)).AnyTimes().Return(int(pkg.MetricsNotifyLevelFail)) fakeConfig.EXPECT().GetString("notify.level").AnyTimes().Return(pkg.NotifyLevelFail)
fakeConfig.EXPECT().GetInt(fmt.Sprintf("%s.metrics.status_filter_attributes", config.DB_USER_SETTINGS_SUBKEY)).AnyTimes().Return(int(pkg.MetricsStatusFilterAttributesAll)) fakeConfig.EXPECT().GetString("notify.filter_attributes").AnyTimes().Return(pkg.NotifyFilterAttributesAll)
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.database.location").AnyTimes().Return(path.Join(parentPath, "scrutiny_test.db"))
fakeConfig.EXPECT().GetString("web.src.frontend.path").AnyTimes().Return(parentPath) fakeConfig.EXPECT().GetString("web.src.frontend.path").AnyTimes().Return(parentPath)
fakeConfig.EXPECT().GetString("web.listen.basepath").Return(suite.Basepath).AnyTimes() fakeConfig.EXPECT().GetString("web.listen.basepath").Return(suite.Basepath).AnyTimes()
@@ -255,7 +243,7 @@ func (suite *ServerTestSuite) TestPopulateMultiple() {
ae := web.AppEngine{ ae := web.AppEngine{
Config: fakeConfig, Config: fakeConfig,
} }
router := ae.Setup(logrus.WithField("test", suite.T().Name())) router := ae.Setup(logrus.New())
devicesfile, err := os.Open("testdata/register-devices-req.json") devicesfile, err := os.Open("testdata/register-devices-req.json")
require.NoError(suite.T(), err) require.NoError(suite.T(), err)
@@ -331,8 +319,6 @@ func (suite *ServerTestSuite) TestSendTestNotificationRoute_WebhookFailure() {
mockCtrl := gomock.NewController(suite.T()) mockCtrl := gomock.NewController(suite.T())
defer mockCtrl.Finish() defer mockCtrl.Finish()
fakeConfig := mock_config.NewMockInterface(mockCtrl) 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.database.location").AnyTimes().Return(path.Join(parentPath, "scrutiny_test.db"))
fakeConfig.EXPECT().GetString("web.src.frontend.path").AnyTimes().Return(parentPath) fakeConfig.EXPECT().GetString("web.src.frontend.path").AnyTimes().Return(parentPath)
fakeConfig.EXPECT().GetString("web.listen.basepath").Return(suite.Basepath).AnyTimes() fakeConfig.EXPECT().GetString("web.listen.basepath").Return(suite.Basepath).AnyTimes()
@@ -344,9 +330,8 @@ func (suite *ServerTestSuite) TestSendTestNotificationRoute_WebhookFailure() {
fakeConfig.EXPECT().GetString("web.influxdb.bucket").Return("metrics").AnyTimes() fakeConfig.EXPECT().GetString("web.influxdb.bucket").Return("metrics").AnyTimes()
fakeConfig.EXPECT().GetBool("web.influxdb.retention_policy").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().GetStringSlice("notify.urls").AnyTimes().Return([]string{"https://unroutable.domain.example.asdfghj"})
fakeConfig.EXPECT().GetInt(fmt.Sprintf("%s.metrics.notify_level", config.DB_USER_SETTINGS_SUBKEY)).AnyTimes().Return(int(pkg.MetricsNotifyLevelFail)) fakeConfig.EXPECT().GetString("notify.level").AnyTimes().Return(pkg.NotifyLevelFail)
fakeConfig.EXPECT().GetInt(fmt.Sprintf("%s.metrics.status_filter_attributes", config.DB_USER_SETTINGS_SUBKEY)).AnyTimes().Return(int(pkg.MetricsStatusFilterAttributesAll)) fakeConfig.EXPECT().GetString("notify.filter_attributes").AnyTimes().Return(pkg.NotifyFilterAttributesAll)
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 { if _, isGithubActions := os.LookupEnv("GITHUB_ACTIONS"); isGithubActions {
// when running test suite in github actions, we run an influxdb service as a sidecar. // when running test suite in github actions, we run an influxdb service as a sidecar.
@@ -358,7 +343,7 @@ func (suite *ServerTestSuite) TestSendTestNotificationRoute_WebhookFailure() {
ae := web.AppEngine{ ae := web.AppEngine{
Config: fakeConfig, Config: fakeConfig,
} }
router := ae.Setup(logrus.WithField("test", suite.T().Name())) router := ae.Setup(logrus.New())
//test //test
wr := httptest.NewRecorder() wr := httptest.NewRecorder()
@@ -376,8 +361,6 @@ func (suite *ServerTestSuite) TestSendTestNotificationRoute_ScriptFailure() {
mockCtrl := gomock.NewController(suite.T()) mockCtrl := gomock.NewController(suite.T())
defer mockCtrl.Finish() defer mockCtrl.Finish()
fakeConfig := mock_config.NewMockInterface(mockCtrl) 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.database.location").AnyTimes().Return(path.Join(parentPath, "scrutiny_test.db"))
fakeConfig.EXPECT().GetString("web.src.frontend.path").AnyTimes().Return(parentPath) fakeConfig.EXPECT().GetString("web.src.frontend.path").AnyTimes().Return(parentPath)
fakeConfig.EXPECT().GetString("web.listen.basepath").Return(suite.Basepath).AnyTimes() fakeConfig.EXPECT().GetString("web.listen.basepath").Return(suite.Basepath).AnyTimes()
@@ -389,9 +372,8 @@ func (suite *ServerTestSuite) TestSendTestNotificationRoute_ScriptFailure() {
fakeConfig.EXPECT().GetString("web.influxdb.bucket").Return("metrics").AnyTimes() fakeConfig.EXPECT().GetString("web.influxdb.bucket").Return("metrics").AnyTimes()
fakeConfig.EXPECT().GetBool("web.influxdb.retention_policy").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().GetStringSlice("notify.urls").AnyTimes().Return([]string{"script:///missing/path/on/disk"})
fakeConfig.EXPECT().GetInt(fmt.Sprintf("%s.metrics.notify_level", config.DB_USER_SETTINGS_SUBKEY)).AnyTimes().Return(int(pkg.MetricsNotifyLevelFail)) fakeConfig.EXPECT().GetString("notify.level").AnyTimes().Return(pkg.NotifyLevelFail)
fakeConfig.EXPECT().GetInt(fmt.Sprintf("%s.metrics.status_filter_attributes", config.DB_USER_SETTINGS_SUBKEY)).AnyTimes().Return(int(pkg.MetricsStatusFilterAttributesAll)) fakeConfig.EXPECT().GetString("notify.filter_attributes").AnyTimes().Return(pkg.NotifyFilterAttributesAll)
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 { if _, isGithubActions := os.LookupEnv("GITHUB_ACTIONS"); isGithubActions {
// when running test suite in github actions, we run an influxdb service as a sidecar. // when running test suite in github actions, we run an influxdb service as a sidecar.
@@ -403,7 +385,7 @@ func (suite *ServerTestSuite) TestSendTestNotificationRoute_ScriptFailure() {
ae := web.AppEngine{ ae := web.AppEngine{
Config: fakeConfig, Config: fakeConfig,
} }
router := ae.Setup(logrus.WithField("test", suite.T().Name())) router := ae.Setup(logrus.New())
//test //test
wr := httptest.NewRecorder() wr := httptest.NewRecorder()
@@ -421,8 +403,6 @@ func (suite *ServerTestSuite) TestSendTestNotificationRoute_ScriptSuccess() {
mockCtrl := gomock.NewController(suite.T()) mockCtrl := gomock.NewController(suite.T())
defer mockCtrl.Finish() defer mockCtrl.Finish()
fakeConfig := mock_config.NewMockInterface(mockCtrl) 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.database.location").AnyTimes().Return(path.Join(parentPath, "scrutiny_test.db"))
fakeConfig.EXPECT().GetString("web.src.frontend.path").AnyTimes().Return(parentPath) fakeConfig.EXPECT().GetString("web.src.frontend.path").AnyTimes().Return(parentPath)
fakeConfig.EXPECT().GetString("web.listen.basepath").Return(suite.Basepath).AnyTimes() fakeConfig.EXPECT().GetString("web.listen.basepath").Return(suite.Basepath).AnyTimes()
@@ -434,9 +414,8 @@ func (suite *ServerTestSuite) TestSendTestNotificationRoute_ScriptSuccess() {
fakeConfig.EXPECT().GetString("web.influxdb.bucket").Return("metrics").AnyTimes() fakeConfig.EXPECT().GetString("web.influxdb.bucket").Return("metrics").AnyTimes()
fakeConfig.EXPECT().GetBool("web.influxdb.retention_policy").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().GetStringSlice("notify.urls").AnyTimes().Return([]string{"script:///usr/bin/env"})
fakeConfig.EXPECT().GetInt(fmt.Sprintf("%s.metrics.notify_level", config.DB_USER_SETTINGS_SUBKEY)).AnyTimes().Return(int(pkg.MetricsNotifyLevelFail)) fakeConfig.EXPECT().GetString("notify.level").AnyTimes().Return(pkg.NotifyLevelFail)
fakeConfig.EXPECT().GetInt(fmt.Sprintf("%s.metrics.status_filter_attributes", config.DB_USER_SETTINGS_SUBKEY)).AnyTimes().Return(int(pkg.MetricsStatusFilterAttributesAll)) fakeConfig.EXPECT().GetString("notify.filter_attributes").AnyTimes().Return(pkg.NotifyFilterAttributesAll)
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 { if _, isGithubActions := os.LookupEnv("GITHUB_ACTIONS"); isGithubActions {
// when running test suite in github actions, we run an influxdb service as a sidecar. // when running test suite in github actions, we run an influxdb service as a sidecar.
@@ -448,7 +427,7 @@ func (suite *ServerTestSuite) TestSendTestNotificationRoute_ScriptSuccess() {
ae := web.AppEngine{ ae := web.AppEngine{
Config: fakeConfig, Config: fakeConfig,
} }
router := ae.Setup(logrus.WithField("test", suite.T().Name())) router := ae.Setup(logrus.New())
//test //test
wr := httptest.NewRecorder() wr := httptest.NewRecorder()
@@ -466,8 +445,6 @@ func (suite *ServerTestSuite) TestSendTestNotificationRoute_ShoutrrrFailure() {
mockCtrl := gomock.NewController(suite.T()) mockCtrl := gomock.NewController(suite.T())
defer mockCtrl.Finish() defer mockCtrl.Finish()
fakeConfig := mock_config.NewMockInterface(mockCtrl) 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.database.location").AnyTimes().Return(path.Join(parentPath, "scrutiny_test.db"))
fakeConfig.EXPECT().GetString("web.src.frontend.path").AnyTimes().Return(parentPath) fakeConfig.EXPECT().GetString("web.src.frontend.path").AnyTimes().Return(parentPath)
fakeConfig.EXPECT().GetString("web.listen.basepath").Return(suite.Basepath).AnyTimes() fakeConfig.EXPECT().GetString("web.listen.basepath").Return(suite.Basepath).AnyTimes()
@@ -479,9 +456,8 @@ func (suite *ServerTestSuite) TestSendTestNotificationRoute_ShoutrrrFailure() {
fakeConfig.EXPECT().GetString("web.influxdb.bucket").Return("metrics").AnyTimes() fakeConfig.EXPECT().GetString("web.influxdb.bucket").Return("metrics").AnyTimes()
fakeConfig.EXPECT().GetBool("web.influxdb.retention_policy").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().GetStringSlice("notify.urls").AnyTimes().Return([]string{"discord://invalidtoken@channel"})
fakeConfig.EXPECT().GetInt(fmt.Sprintf("%s.metrics.notify_level", config.DB_USER_SETTINGS_SUBKEY)).AnyTimes().Return(int(pkg.MetricsNotifyLevelFail)) fakeConfig.EXPECT().GetString("notify.level").AnyTimes().Return(pkg.NotifyLevelFail)
fakeConfig.EXPECT().GetInt(fmt.Sprintf("%s.metrics.status_filter_attributes", config.DB_USER_SETTINGS_SUBKEY)).AnyTimes().Return(int(pkg.MetricsStatusFilterAttributesAll)) fakeConfig.EXPECT().GetString("notify.filter_attributes").AnyTimes().Return(pkg.NotifyFilterAttributesAll)
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 { if _, isGithubActions := os.LookupEnv("GITHUB_ACTIONS"); isGithubActions {
// when running test suite in github actions, we run an influxdb service as a sidecar. // when running test suite in github actions, we run an influxdb service as a sidecar.
@@ -492,7 +468,7 @@ func (suite *ServerTestSuite) TestSendTestNotificationRoute_ShoutrrrFailure() {
ae := web.AppEngine{ ae := web.AppEngine{
Config: fakeConfig, Config: fakeConfig,
} }
router := ae.Setup(logrus.WithField("test", suite.T().Name())) router := ae.Setup(logrus.New())
//test //test
wr := httptest.NewRecorder() wr := httptest.NewRecorder()
@@ -510,8 +486,6 @@ func (suite *ServerTestSuite) TestGetDevicesSummaryRoute_Nvme() {
mockCtrl := gomock.NewController(suite.T()) mockCtrl := gomock.NewController(suite.T())
defer mockCtrl.Finish() defer mockCtrl.Finish()
fakeConfig := mock_config.NewMockInterface(mockCtrl) 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.database.location").AnyTimes().Return(path.Join(parentPath, "scrutiny_test.db"))
fakeConfig.EXPECT().GetString("web.src.frontend.path").AnyTimes().Return(parentPath) fakeConfig.EXPECT().GetString("web.src.frontend.path").AnyTimes().Return(parentPath)
fakeConfig.EXPECT().GetString("web.listen.basepath").Return(suite.Basepath).AnyTimes() fakeConfig.EXPECT().GetString("web.listen.basepath").Return(suite.Basepath).AnyTimes()
@@ -523,10 +497,8 @@ func (suite *ServerTestSuite) TestGetDevicesSummaryRoute_Nvme() {
fakeConfig.EXPECT().GetString("web.influxdb.bucket").Return("metrics").AnyTimes() fakeConfig.EXPECT().GetString("web.influxdb.bucket").Return("metrics").AnyTimes()
fakeConfig.EXPECT().GetBool("web.influxdb.retention_policy").Return(false).AnyTimes() fakeConfig.EXPECT().GetBool("web.influxdb.retention_policy").Return(false).AnyTimes()
fakeConfig.EXPECT().GetStringSlice("notify.urls").AnyTimes().Return([]string{}) fakeConfig.EXPECT().GetStringSlice("notify.urls").AnyTimes().Return([]string{})
fakeConfig.EXPECT().GetInt(fmt.Sprintf("%s.metrics.notify_level", config.DB_USER_SETTINGS_SUBKEY)).AnyTimes().Return(int(pkg.MetricsNotifyLevelFail)) fakeConfig.EXPECT().GetString("notify.level").AnyTimes().Return(pkg.NotifyLevelFail)
fakeConfig.EXPECT().GetInt(fmt.Sprintf("%s.metrics.status_filter_attributes", config.DB_USER_SETTINGS_SUBKEY)).AnyTimes().Return(int(pkg.MetricsStatusFilterAttributesAll)) fakeConfig.EXPECT().GetString("notify.filter_attributes").AnyTimes().Return(pkg.NotifyFilterAttributesAll)
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 { if _, isGithubActions := os.LookupEnv("GITHUB_ACTIONS"); isGithubActions {
// when running test suite in github actions, we run an influxdb service as a sidecar. // when running test suite in github actions, we run an influxdb service as a sidecar.
fakeConfig.EXPECT().GetString("web.influxdb.host").Return("influxdb").AnyTimes() fakeConfig.EXPECT().GetString("web.influxdb.host").Return("influxdb").AnyTimes()
@@ -537,7 +509,7 @@ func (suite *ServerTestSuite) TestGetDevicesSummaryRoute_Nvme() {
ae := web.AppEngine{ ae := web.AppEngine{
Config: fakeConfig, Config: fakeConfig,
} }
router := ae.Setup(logrus.WithField("test", suite.T().Name())) router := ae.Setup(logrus.New())
devicesfile, err := os.Open("testdata/register-devices-req-2.json") devicesfile, err := os.Open("testdata/register-devices-req-2.json")
require.NoError(suite.T(), err) require.NoError(suite.T(), err)
-2
View File
@@ -46,5 +46,3 @@ testem.log
Thumbs.db Thumbs.db
/dist /dist
/coverage
+2 -15
View File
@@ -91,7 +91,6 @@
}, },
"test": { "test": {
"builder": "@angular-devkit/build-angular:karma", "builder": "@angular-devkit/build-angular:karma",
"defaultConfiguration": "production",
"options": { "options": {
"main": "src/test.ts", "main": "src/test.ts",
"polyfills": "src/polyfills.ts", "polyfills": "src/polyfills.ts",
@@ -102,22 +101,10 @@
"src/favicon-32x32.png", "src/favicon-32x32.png",
"src/assets" "src/assets"
], ],
"stylePreprocessorOptions": {
"includePaths": [
"src/@treo/styles"
]
},
"styles": [ "styles": [
"src/styles/vendors.scss", "src/styles.scss"
"src/@treo/styles/main.scss",
"src/styles/styles.scss",
"src/styles/tailwind.scss"
], ],
"scripts": [], "scripts": []
"fileReplacements": [{
"replace": "src/environments/environment.ts",
"with": "src/environments/environment.prod.ts"
}]
} }
}, },
"lint": { "lint": {
+1 -1
View File
@@ -17,7 +17,7 @@ module.exports = function (config)
clearContext: false // leave Jasmine Spec Runner output visible in browser clearContext: false // leave Jasmine Spec Runner output visible in browser
}, },
coverageIstanbulReporter: { coverageIstanbulReporter: {
dir: require('path').join(__dirname, './coverage'), dir : require('path').join(__dirname, './coverage/treo'),
reports : ['html', 'lcovonly', 'text-summary'], reports : ['html', 'lcovonly', 'text-summary'],
fixWebpackSourcePaths: true fixWebpackSourcePaths: true
}, },
+3 -3
View File
@@ -1,11 +1,11 @@
import {enableProdMode, NgModule} from '@angular/core'; import { NgModule, enableProdMode } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser'; import { BrowserModule } from '@angular/platform-browser';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { ExtraOptions, PreloadAllModules, RouterModule } from '@angular/router'; import { ExtraOptions, PreloadAllModules, RouterModule } from '@angular/router';
import { APP_BASE_HREF } from '@angular/common'; import { APP_BASE_HREF } from '@angular/common';
import { MarkdownModule } from 'ngx-markdown'; import { MarkdownModule } from 'ngx-markdown';
import { TreoModule } from '@treo'; import { TreoModule } from '@treo';
import {ScrutinyConfigModule} from 'app/core/config/scrutiny-config.module'; import { TreoConfigModule } from '@treo/services/config';
import { TreoMockApiModule } from '@treo/lib/mock-api'; import { TreoMockApiModule } from '@treo/lib/mock-api';
import { CoreModule } from 'app/core/core.module'; import { CoreModule } from 'app/core/core.module';
import { appConfig } from 'app/core/config/app.config'; import { appConfig } from 'app/core/config/app.config';
@@ -41,7 +41,7 @@ if (process.env.NODE_ENV === 'production') {
// Treo & Treo Mock API // Treo & Treo Mock API
TreoModule, TreoModule,
ScrutinyConfigModule.forRoot(appConfig), TreoConfigModule.forRoot(appConfig),
...dev, ...dev,
// Core // Core
@@ -3,56 +3,20 @@ import {Layout} from 'app/layout/layout.types';
// Theme type // Theme type
export type Theme = 'light' | 'dark' | 'system'; export type Theme = 'light' | 'dark' | 'system';
// Device title to display on the dashboard
export type DashboardDisplay = 'name' | 'serial_id' | 'uuid' | 'label'
export type DashboardSort = 'status' | 'title' | 'age'
export type TemperatureUnit = 'celsius' | 'fahrenheit'
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 * AppConfig interface. Update this interface to strictly type your config
* object. * object.
*/ */
export interface AppConfig { export interface AppConfig
theme?: Theme; {
layout?: Layout; theme: Theme;
layout: Layout;
// Dashboard options // Dashboard options
dashboard_display?: DashboardDisplay; dashboardDisplay: string;
dashboard_sort?: DashboardSort; dashboardSort: string;
temperature_unit?: TemperatureUnit;
file_size_si_units?: boolean;
// Settings from Scrutiny API
metrics?: {
notify_level?: MetricsNotifyLevel
status_filter_attributes?: MetricsStatusFilterAttributes
status_threshold?: MetricsStatusThreshold
}
temperatureUnit: string;
} }
/** /**
@@ -67,16 +31,9 @@ export const appConfig: AppConfig = {
theme : 'light', theme : 'light',
layout: 'material', layout: 'material',
dashboard_display: 'name', dashboardDisplay: 'name',
dashboard_sort: 'status', dashboardSort: 'status',
temperature_unit: 'celsius', temperatureUnit: 'celsius',
file_size_si_units: false,
metrics: {
notify_level: MetricsNotifyLevel.Fail,
status_filter_attributes: MetricsStatusFilterAttributes.All,
status_threshold: MetricsStatusThreshold.Both
}
}; };
@@ -1,33 +0,0 @@
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 {
return {
ngModule: ScrutinyConfigModule,
providers: [
{
provide: TREO_APP_CONFIG,
useValue: config
}
]
};
}
}
@@ -1,84 +0,0 @@
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,14 +0,0 @@
import {DeviceModel} from 'app/core/models/device-model';
import {SmartModel} from 'app/core/models/measurements/smart-model';
import {AttributeMetadataModel} from 'app/core/models/thresholds/attribute-metadata-model';
// maps to webapp/backend/pkg/models/device_summary.go
export interface DeviceDetailsResponseWrapper {
success: boolean;
errors?: any[];
data: {
device: DeviceModel;
smart_results: SmartModel[];
},
metadata: { [key: string]: AttributeMetadataModel } | { [key: number]: AttributeMetadataModel };
}
@@ -1,26 +0,0 @@
// maps to webapp/backend/pkg/models/device.go
export interface DeviceModel {
wwn: string;
device_name?: string;
device_uuid?: string;
device_serial_id?: string;
device_label?: string;
manufacturer: string;
model_name: string;
interface_type: string;
interface_speed: string;
serial_number: string;
firmware: string;
rotational_speed: number;
capacity: number;
form_factor: string;
smart_support: boolean;
device_protocol: string;
device_type: string;
label: string;
host_id: string;
device_status: number;
}
@@ -1,16 +0,0 @@
import {DeviceModel} from 'app/core/models/device-model';
import {SmartTemperatureModel} from 'app/core/models/measurements/smart-temperature-model';
// maps to webapp/backend/pkg/models/device_summary.go
export interface DeviceSummaryModel {
device: DeviceModel;
smart?: SmartSummary;
temp_history?: SmartTemperatureModel[];
}
export interface SmartSummary {
collector_date?: string,
temp?: number
power_on_hours?: number
}
@@ -1,10 +0,0 @@
import {DeviceSummaryModel} from 'app/core/models/device-summary-model';
// maps to webapp/backend/pkg/models/device_summary.go
export interface DeviceSummaryResponseWrapper {
success: boolean;
errors: any[];
data: {
summary: { [key: string]: DeviceSummaryModel }
}
}
@@ -1,9 +0,0 @@
import {SmartTemperatureModel} from './measurements/smart-temperature-model';
export interface DeviceSummaryTempResponseWrapper {
success: boolean;
errors: any[];
data: {
temp_history: { [key: string]: SmartTemperatureModel[]; }
}
}
@@ -1,19 +0,0 @@
// maps to webapp/backend/pkg/models/measurements/smart_ata_attribute.go
// maps to webapp/backend/pkg/models/measurements/smart_nvme_attribute.go
// maps to webapp/backend/pkg/models/measurements/smart_scsi_attribute.go
export interface SmartAttributeModel {
attribute_id: number | string
value: number
thresh: number
worst?: number
raw_value?: number
raw_string?: string
when_failed?: string
transformed_value: number
status: number
status_reason?: string
failure_rate?: number
chartData?: any[]
}
@@ -1,13 +0,0 @@
// maps to webapp/backend/pkg/models/measurements/smart.go
import {SmartAttributeModel} from './smart-attribute-model';
export interface SmartModel {
date: string;
device_wwn: string;
device_protocol: string;
temp: number;
power_on_hours: number;
power_cycle_count: number
attrs: { [key: string]: SmartAttributeModel }
}
@@ -1,6 +0,0 @@
// maps to webapp/backend/pkg/models/measurements/smart_temperature.go
export interface SmartTemperatureModel {
date: string;
temp: number;
}
@@ -1,13 +0,0 @@
// map to webapp/backend/pkg/thresholds/ata_attribute_metadata.go
// map to webapp/backend/pkg/thresholds/nvme_attribute_metadata.go
// map to webapp/backend/pkg/thresholds/scsi_attribute_metadata.go
export interface AttributeMetadataModel {
display_name: string
ideal: string
critical: boolean
description: string
transform_value_unit?: string
observed_thresholds?: any[]
display_type: string
}
File diff suppressed because it is too large Load Diff
@@ -1,39 +1,16 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing'; import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { DashboardDeviceDeleteDialogComponent } from './dashboard-device-delete-dialog.component'; import { DashboardDeviceDeleteDialogComponent } from './dashboard-device-delete-dialog.component';
import {HttpClientModule} from '@angular/common/http';
import {MAT_DIALOG_DATA, MatDialogModule, MatDialogRef} from '@angular/material/dialog';
import {MatButtonModule} from '@angular/material/button';
import {MatIconModule} from '@angular/material/icon';
import {SharedModule} from '../../../shared/shared.module';
import {DashboardDeviceDeleteDialogService} from './dashboard-device-delete-dialog.service';
import {of} from 'rxjs';
describe('DashboardDeviceDeleteDialogComponent', () => { describe('DashboardDeviceDeleteDialogComponent', () => {
let component: DashboardDeviceDeleteDialogComponent; let component: DashboardDeviceDeleteDialogComponent;
let fixture: ComponentFixture<DashboardDeviceDeleteDialogComponent>; let fixture: ComponentFixture<DashboardDeviceDeleteDialogComponent>;
const matDialogRefSpy = jasmine.createSpyObj('MatDialogRef', ['closeDialog', 'close']);
const dashboardDeviceDeleteDialogServiceSpy = jasmine.createSpyObj('DashboardDeviceDeleteDialogService', ['deleteDevice']);
beforeEach(async(() => { beforeEach(async(() => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
imports: [
HttpClientModule,
MatDialogModule,
MatButtonModule,
MatIconModule,
SharedModule,
],
providers: [
{provide: MatDialogRef, useValue: matDialogRefSpy},
{provide: MAT_DIALOG_DATA, useValue: {wwn: 'test-wwn', title: 'my-test-device-title'}},
{provide: DashboardDeviceDeleteDialogService, useValue: dashboardDeviceDeleteDialogServiceSpy}
],
declarations: [ DashboardDeviceDeleteDialogComponent ] declarations: [ DashboardDeviceDeleteDialogComponent ]
}) })
.compileComponents() .compileComponents();
})); }));
beforeEach(() => { beforeEach(() => {
@@ -45,20 +22,4 @@ describe('DashboardDeviceDeleteDialogComponent', () => {
it('should create', () => { it('should create', () => {
expect(component).toBeTruthy(); expect(component).toBeTruthy();
}); });
it('should close the component if cancel is clicked', () => {
matDialogRefSpy.closeDialog.calls.reset();
matDialogRefSpy.closeDialog()
expect(matDialogRefSpy.closeDialog).toHaveBeenCalled();
});
it('should attempt to delete device if delete is clicked', () => {
dashboardDeviceDeleteDialogServiceSpy.deleteDevice.and.returnValue(of({'success': true}));
component.onDeleteClick()
expect(dashboardDeviceDeleteDialogServiceSpy.deleteDevice).toHaveBeenCalledWith('test-wwn');
expect(dashboardDeviceDeleteDialogServiceSpy.deleteDevice.calls.count())
.withContext('one call')
.toBe(1);
});
}); });
@@ -1,6 +1,7 @@
import {Component, Inject, OnInit} from '@angular/core'; import { Component, OnInit, Inject } from '@angular/core';
import {MAT_DIALOG_DATA, MatDialogRef} from '@angular/material/dialog'; import {MAT_DIALOG_DATA, MatDialogRef} from '@angular/material/dialog';
import {DashboardDeviceDeleteDialogService} from 'app/layout/common/dashboard-device-delete-dialog/dashboard-device-delete-dialog.service'; import {DashboardDeviceDeleteDialogService} from 'app/layout/common/dashboard-device-delete-dialog/dashboard-device-delete-dialog.service';
import {Subject} from 'rxjs';
@Component({ @Component({
selector: 'app-dashboard-device-delete-dialog', selector: 'app-dashboard-device-delete-dialog',
@@ -1,10 +1,26 @@
import { NgModule } from '@angular/core'; import { NgModule } from '@angular/core';
import { RouterModule } from '@angular/router'; import { RouterModule } from '@angular/router';
import { Overlay } from '@angular/cdk/overlay';
import { MAT_AUTOCOMPLETE_SCROLL_STRATEGY, MatAutocompleteModule } from '@angular/material/autocomplete';
import { MatButtonModule } from '@angular/material/button'; import { MatButtonModule } from '@angular/material/button';
import { MatSelectModule } from '@angular/material/select';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatIconModule } from '@angular/material/icon'; import { MatIconModule } from '@angular/material/icon';
import { MatInputModule } from '@angular/material/input';
import { SharedModule } from 'app/shared/shared.module'; import { SharedModule } from 'app/shared/shared.module';
import {DashboardDeviceDeleteDialogComponent} from 'app/layout/common/dashboard-device-delete-dialog/dashboard-device-delete-dialog.component' import {DashboardDeviceDeleteDialogComponent} from 'app/layout/common/dashboard-device-delete-dialog/dashboard-device-delete-dialog.component'
import { MatButtonToggleModule} from '@angular/material/button-toggle';
import {MatTabsModule} from '@angular/material/tabs';
import {MatSliderModule} from '@angular/material/slider';
import {MatSlideToggleModule} from '@angular/material/slide-toggle';
import {MatTooltipModule} from '@angular/material/tooltip';
import {dashboardRoutes} from 'app/modules/dashboard/dashboard.routing'; import {dashboardRoutes} from 'app/modules/dashboard/dashboard.routing';
import {MatDividerModule} from '@angular/material/divider';
import {MatMenuModule} from '@angular/material/menu';
import {MatProgressBarModule} from '@angular/material/progress-bar';
import {MatSortModule} from '@angular/material/sort';
import {MatTableModule} from '@angular/material/table';
import {NgApexchartsModule} from 'ng-apexcharts';
import { MatDialogModule } from '@angular/material/dialog'; import { MatDialogModule } from '@angular/material/dialog';
@NgModule({ @NgModule({
@@ -15,7 +31,14 @@ import {MatDialogModule} from '@angular/material/dialog';
RouterModule.forChild([]), RouterModule.forChild([]),
RouterModule.forChild(dashboardRoutes), RouterModule.forChild(dashboardRoutes),
MatButtonModule, MatButtonModule,
MatDividerModule,
MatTooltipModule,
MatIconModule, MatIconModule,
MatMenuModule,
MatProgressBarModule,
MatSortModule,
MatTableModule,
NgApexchartsModule,
SharedModule, SharedModule,
MatDialogModule MatDialogModule
], ],
@@ -1,21 +1,21 @@
<div [ngClass]="{ 'border-green': deviceStatusForModelWithThreshold(deviceSummary.device, !!deviceSummary.smart, config.metrics.status_threshold) == 'passed', <div [ngClass]="{ 'border-green': deviceSummary.device.device_status == 0 && deviceSummary.smart,
'border-red': deviceStatusForModelWithThreshold(deviceSummary.device, !!deviceSummary.smart, config.metrics.status_threshold) == 'failed' }" 'border-red': deviceSummary.device.device_status != 0 }"
class="relative flex flex-col flex-auto p-6 pr-3 pb-3 bg-card rounded border-l-4 shadow-md overflow-hidden"> 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"> <div class="absolute bottom-0 right-0 w-24 h-24 -m-6">
<mat-icon class="icon-size-96 opacity-12 text-green" <mat-icon class="icon-size-96 opacity-12 text-green"
*ngIf="deviceStatusForModelWithThreshold(deviceSummary.device, !!deviceSummary.smart, config.metrics.status_threshold) == 'passed'" *ngIf="deviceSummary.device.device_status == 0 && deviceSummary.smart"
[svgIcon]="'heroicons_outline:check-circle'"></mat-icon> [svgIcon]="'heroicons_outline:check-circle'"></mat-icon>
<mat-icon class="icon-size-96 opacity-12 text-red" <mat-icon class="icon-size-96 opacity-12 text-red"
*ngIf="deviceStatusForModelWithThreshold(deviceSummary.device, !!deviceSummary.smart, config.metrics.status_threshold) == 'failed'" *ngIf="deviceSummary.device.device_status != 0"
[svgIcon]="'heroicons_outline:exclamation-circle'"></mat-icon> [svgIcon]="'heroicons_outline:exclamation-circle'"></mat-icon>
<mat-icon class="icon-size-96 opacity-12 text-yellow" <mat-icon class="icon-size-96 opacity-12 text-yellow"
*ngIf="deviceStatusForModelWithThreshold(deviceSummary.device, !!deviceSummary.smart, config.metrics.status_threshold) == 'unknown'" *ngIf="!deviceSummary.smart"
[svgIcon]="'heroicons_outline:question-mark-circle'"></mat-icon> [svgIcon]="'heroicons_outline:question-mark-circle'"></mat-icon>
</div> </div>
<div class="flex items-center"> <div class="flex items-center">
<div class="flex flex-col"> <div class="flex flex-col">
<a [routerLink]="'/device/'+ deviceSummary.device.wwn" <a [routerLink]="'/device/'+ deviceSummary.device.wwn"
class="font-bold text-md text-secondary uppercase tracking-wider">{{deviceSummary.device | deviceTitle:config.dashboard_display}}</a> class="font-bold text-md text-secondary uppercase tracking-wider">{{deviceSummary.device | deviceTitle:config.dashboardDisplay}}</a>
<div [ngClass]="classDeviceLastUpdatedOn(deviceSummary)" class="font-medium text-sm" *ngIf="deviceSummary.smart"> <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' }} Last Updated on {{deviceSummary.smart.collector_date | date:'MMMM dd, yyyy - HH:mm' }}
</div> </div>
@@ -46,20 +46,17 @@
<div class="flex flex-row flex-wrap mt-4 -mx-6"> <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="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="font-semibold text-xs text-hint uppercase tracking-wider leading-none">Status</div>
<div class="mt-2 font-medium text-3xl leading-none" <div class="mt-2 font-medium text-3xl leading-none" *ngIf="deviceSummary.smart?.collector_date; else unknownStatus">{{ deviceStatusString(deviceSummary.device.device_status) | titlecase}}</div>
*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> <ng-template #unknownStatus><div class="mt-2 font-medium text-3xl leading-none">No Data</div></ng-template>
</div> </div>
<div class="flex flex-col mx-6 my-3 xs:w-full"> <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="font-semibold text-xs text-hint uppercase tracking-wider leading-none">Temperature</div>
<div class="mt-2 font-medium text-3xl leading-none" <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>
*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> <ng-template #unknownTemp><div class="mt-2 font-medium text-3xl leading-none">--</div></ng-template>
</div> </div>
<div class="flex flex-col mx-6 my-3 xs:w-full"> <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="font-semibold text-xs text-hint uppercase tracking-wider leading-none">Capacity</div>
<div <div class="mt-2 font-medium text-3xl leading-none">{{ deviceSummary.device.capacity | fileSize}}</div>
class="mt-2 font-medium text-3xl leading-none">{{ deviceSummary.device.capacity | fileSize:config.file_size_si_units}}</div>
</div> </div>
<div class="flex flex-col mx-6 my-3 xs:w-full"> <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> <div class="font-semibold text-xs text-hint uppercase tracking-wider leading-none">Powered On</div>
@@ -1,158 +1,25 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing'; import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { DashboardDeviceComponent } from './dashboard-device.component'; import { DashboardDeviceComponent } from './dashboard-device.component';
import {MatDialog} from '@angular/material/dialog';
import {MatButtonModule} from '@angular/material/button';
import {MatIconModule} from '@angular/material/icon';
import {SharedModule} from 'app/shared/shared.module';
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', () => { describe('DashboardDeviceComponent', () => {
let component: DashboardDeviceComponent; let component: DashboardDeviceComponent;
let fixture: ComponentFixture<DashboardDeviceComponent>; let fixture: ComponentFixture<DashboardDeviceComponent>;
const matDialogSpy = jasmine.createSpyObj('MatDialog', ['open']);
// const configServiceSpy = jasmine.createSpyObj('ScrutinyConfigService', ['config$']);
let configService: ScrutinyConfigService;
let httpClientSpy: jasmine.SpyObj<HttpClient>;
beforeEach(async(() => { beforeEach(async(() => {
httpClientSpy = jasmine.createSpyObj('HttpClient', ['get']);
configService = new ScrutinyConfigService(httpClientSpy, {});
TestBed.configureTestingModule({ TestBed.configureTestingModule({
imports: [
MatButtonModule,
MatIconModule,
MatMenuModule,
SharedModule,
HttpClientTestingModule,
],
providers: [
{provide: MatDialog, useValue: matDialogSpy},
{provide: TREO_APP_CONFIG, useValue: {dashboard_display: 'name', metrics: {status_threshold: 3}}},
{provide: ScrutinyConfigService, useValue: configService}
],
declarations: [ DashboardDeviceComponent ] declarations: [ DashboardDeviceComponent ]
}) })
.compileComponents(); .compileComponents();
})); }));
beforeEach(() => { beforeEach(() => {
// configServiceSpy.config$.and.returnValue(of({'success': true}));
fixture = TestBed.createComponent(DashboardDeviceComponent); fixture = TestBed.createComponent(DashboardDeviceComponent);
component = fixture.componentInstance; component = fixture.componentInstance;
fixture.detectChanges();
}); });
it('should create', () => { it('should create', () => {
expect(component).toBeTruthy(); expect(component).toBeTruthy();
}); });
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,
},
smart: {
collector_date: moment().subtract(13, 'days').toISOString()
},
} as DeviceSummaryModel)).toBe('text-red')
});
it('if non-zero device status, should be red', () => {
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', () => {
httpClientSpy.get.and.returnValue(of({
settings: {
metrics: {
status_threshold: MetricsStatusThreshold.Both,
}
}
}));
component.ngOnInit()
expect(component.classDeviceLastUpdatedOn({
device: {
device_status: 0
},
smart: {
collector_date: moment().subtract(13, 'days').toISOString()
}
} as DeviceSummaryModel)).toBe('text-green')
});
it('if healthy device status and updated more than two weeks ago, but less than 1 month, should be yellow', () => {
httpClientSpy.get.and.returnValue(of({
settings: {
metrics: {
status_threshold: MetricsStatusThreshold.Both,
}
}
}));
component.ngOnInit()
expect(component.classDeviceLastUpdatedOn({
device: {
device_status: 0
},
smart: {
collector_date: moment().subtract(3, 'weeks').toISOString()
}
} as DeviceSummaryModel)).toBe('text-yellow')
});
it('if healthy device status and updated more 1 month ago, should be red', () => {
httpClientSpy.get.and.returnValue(of({
settings: {
metrics: {
status_threshold: MetricsStatusThreshold.Both,
}
}
}));
component.ngOnInit()
expect(component.classDeviceLastUpdatedOn({
device: {
device_status: 0
},
smart: {
collector_date: moment().subtract(5, 'weeks').toISOString()
}
} as DeviceSummaryModel)).toBe('text-red')
});
})
}); });
@@ -1,15 +1,13 @@
import {Component, EventEmitter, Input, OnInit, Output} from '@angular/core'; import { Component, Input, Output, OnInit, EventEmitter} from '@angular/core';
import * as moment from 'moment'; import * as moment from 'moment';
import {takeUntil} from 'rxjs/operators'; import {takeUntil} from 'rxjs/operators';
import {AppConfig} from 'app/core/config/app.config'; import {AppConfig} from 'app/core/config/app.config';
import {ScrutinyConfigService} from 'app/core/config/scrutiny-config.service'; import {TreoConfigService} from '@treo/services/config';
import {Subject} from 'rxjs'; import {Subject} from 'rxjs';
import humanizeDuration from 'humanize-duration' import humanizeDuration from 'humanize-duration'
import {MatDialog} from '@angular/material/dialog'; import {MatDialog} from '@angular/material/dialog';
import {DashboardDeviceDeleteDialogComponent} from 'app/layout/common/dashboard-device-delete-dialog/dashboard-device-delete-dialog.component'; import {DashboardDeviceDeleteDialogComponent} from 'app/layout/common/dashboard-device-delete-dialog/dashboard-device-delete-dialog.component';
import {DeviceTitlePipe} from 'app/shared/device-title.pipe'; 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({ @Component({
selector: 'app-dashboard-device', selector: 'app-dashboard-device',
@@ -19,14 +17,13 @@ import {DeviceStatusPipe} from 'app/shared/device-status.pipe';
export class DashboardDeviceComponent implements OnInit { export class DashboardDeviceComponent implements OnInit {
constructor( constructor(
private _configService: ScrutinyConfigService, private _configService: TreoConfigService,
public dialog: MatDialog, public dialog: MatDialog,
) { ) {
// Set the private defaults // Set the private defaults
this._unsubscribeAll = new Subject(); this._unsubscribeAll = new Subject();
} }
@Input() deviceSummary: any;
@Input() deviceSummary: DeviceSummaryModel;
@Input() deviceWWN: string; @Input() deviceWWN: string;
@Output() deviceDeleted = new EventEmitter<string>(); @Output() deviceDeleted = new EventEmitter<string>();
@@ -36,8 +33,6 @@ export class DashboardDeviceComponent implements OnInit {
readonly humanizeDuration = humanizeDuration; readonly humanizeDuration = humanizeDuration;
deviceStatusForModelWithThreshold = DeviceStatusPipe.deviceStatusForModelWithThreshold
ngOnInit(): void { ngOnInit(): void {
// Subscribe to config changes // Subscribe to config changes
this._configService.config$ this._configService.config$
@@ -52,33 +47,40 @@ export class DashboardDeviceComponent implements OnInit {
// @ Public methods // @ Public methods
// ----------------------------------------------------------------------------------------------------- // -----------------------------------------------------------------------------------------------------
classDeviceLastUpdatedOn(deviceSummary: DeviceSummaryModel): string { classDeviceLastUpdatedOn(deviceSummary): string {
const deviceStatus = DeviceStatusPipe.deviceStatusForModelWithThreshold(deviceSummary.device, !!deviceSummary.smart, this.config.metrics.status_threshold) if (deviceSummary.device.device_status !== 0) {
if (deviceStatus === 'failed') {
return 'text-red' // if the device has failed, always highlight in red return 'text-red' // if the device has failed, always highlight in red
} else if (deviceStatus === 'passed') { } else if(deviceSummary.device.device_status === 0 && deviceSummary.smart){
if (moment().subtract(14, 'days').isBefore(deviceSummary.smart.collector_date)) { if(moment().subtract(14, 'd').isBefore(deviceSummary.smart.collector_date)){
// this device was updated in the last 2 weeks. // this device was updated in the last 2 weeks.
return 'text-green' return 'text-green'
} else if (moment().subtract(1, 'months').isBefore(deviceSummary.smart.collector_date)) { } else if(moment().subtract(1, 'm').isBefore(deviceSummary.smart.collector_date)){
// this device was updated in the last month // this device was updated in the last month
return 'text-yellow' return 'text-yellow'
} else{ } else{
// last updated more than a month ago. // last updated more than a month ago.
return 'text-red' return 'text-red'
} }
} else { } else {
return '' return ''
} }
} }
deviceStatusString(deviceStatus): string {
if(deviceStatus === 0){
return 'passed'
} else {
return 'failed'
}
}
openDeleteDialog(): void { openDeleteDialog(): void {
const dialogRef = this.dialog.open(DashboardDeviceDeleteDialogComponent, { const dialogRef = this.dialog.open(DashboardDeviceDeleteDialogComponent, {
// width: '250px', // width: '250px',
data: { data: {wwn: this.deviceWWN, title: DeviceTitlePipe.deviceTitleWithFallback(this.deviceSummary.device, this.config.dashboardDisplay)}
wwn: this.deviceWWN,
title: DeviceTitlePipe.deviceTitleWithFallback(this.deviceSummary.device, this.config.dashboard_display)
}
}); });
dialogRef.afterClosed().subscribe(result => { dialogRef.afterClosed().subscribe(result => {
@@ -1,11 +1,27 @@
import { NgModule } from '@angular/core'; import { NgModule } from '@angular/core';
import { RouterModule } from '@angular/router'; import { RouterModule } from '@angular/router';
import { Overlay } from '@angular/cdk/overlay';
import { MAT_AUTOCOMPLETE_SCROLL_STRATEGY, MatAutocompleteModule } from '@angular/material/autocomplete';
import { MatButtonModule } from '@angular/material/button'; import { MatButtonModule } from '@angular/material/button';
import { MatSelectModule } from '@angular/material/select';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatIconModule } from '@angular/material/icon'; import { MatIconModule } from '@angular/material/icon';
import { MatInputModule } from '@angular/material/input';
import { SharedModule } from 'app/shared/shared.module'; import { SharedModule } from 'app/shared/shared.module';
import {DashboardDeviceComponent} from 'app/layout/common/dashboard-device/dashboard-device.component' import {DashboardDeviceComponent} from 'app/layout/common/dashboard-device/dashboard-device.component'
import { MatDialogModule } from '@angular/material/dialog';
import { MatButtonToggleModule} from '@angular/material/button-toggle';
import {MatTabsModule} from '@angular/material/tabs';
import {MatSliderModule} from '@angular/material/slider';
import {MatSlideToggleModule} from '@angular/material/slide-toggle';
import {MatTooltipModule} from '@angular/material/tooltip';
import {dashboardRoutes} from '../../../modules/dashboard/dashboard.routing'; import {dashboardRoutes} from '../../../modules/dashboard/dashboard.routing';
import {MatDividerModule} from '@angular/material/divider';
import {MatMenuModule} from '@angular/material/menu'; import {MatMenuModule} from '@angular/material/menu';
import {MatProgressBarModule} from '@angular/material/progress-bar';
import {MatSortModule} from '@angular/material/sort';
import {MatTableModule} from '@angular/material/table';
import {NgApexchartsModule} from 'ng-apexcharts';
import {DashboardDeviceDeleteDialogModule} from 'app/layout/common/dashboard-device-delete-dialog/dashboard-device-delete-dialog.module'; import {DashboardDeviceDeleteDialogModule} from 'app/layout/common/dashboard-device-delete-dialog/dashboard-device-delete-dialog.module';
@NgModule({ @NgModule({
@@ -16,8 +32,14 @@ import {DashboardDeviceDeleteDialogModule} from 'app/layout/common/dashboard-dev
RouterModule.forChild([]), RouterModule.forChild([]),
RouterModule.forChild(dashboardRoutes), RouterModule.forChild(dashboardRoutes),
MatButtonModule, MatButtonModule,
MatDividerModule,
MatTooltipModule,
MatIconModule, MatIconModule,
MatMenuModule, MatMenuModule,
MatProgressBarModule,
MatSortModule,
MatTableModule,
NgApexchartsModule,
SharedModule, SharedModule,
DashboardDeviceDeleteDialogModule DashboardDeviceDeleteDialogModule
], ],
@@ -26,5 +48,6 @@ import {DashboardDeviceDeleteDialogModule} from 'app/layout/common/dashboard-dev
], ],
providers : [] providers : []
}) })
export class DashboardDeviceModule { export class DashboardDeviceModule
{
} }
@@ -37,41 +37,69 @@
<div class="flex flex-col mt-5 gt-md:flex-row"> <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-form-field class="flex-auto gt-xs:pr-3 gt-md:pr-3">
<mat-label>Temperature</mat-label> <mat-label>Temperature Display Unit</mat-label>
<mat-select [(ngModel)]="temperatureUnit"> <mat-select [(ngModel)]="temperatureUnit">
<mat-option value="celsius">Celsius</mat-option> <mat-option value="celsius">Celsius</mat-option>
<mat-option value="fahrenheit">Fahrenheit</mat-option> <mat-option value="fahrenheit">Fahrenheit</mat-option>
</mat-select> </mat-select>
</mat-form-field> </mat-form-field>
<mat-form-field class="flex-auto gt-xs:pr-3 gt-md:pr-3"> </div>
<mat-label>File Size</mat-label>
<mat-select [(ngModel)]="fileSizeSIUnits"> <div class="flex">
<mat-option [value]=true>SI Units (GB)</mat-option> <mat-tab-group mat-align-tabs="start">
<mat-option [value]=false>Binary Units (GiB)</mat-option> <mat-tab label="Ata">
</mat-select>
<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> </mat-form-field>
</div> </div>
<div class="flex flex-col mt-5 gt-md:flex-row"> <div matTooltip="not yet implemented" class="flex flex-col gt-md:flex-row">
<mat-form-field class="flex-auto gt-xs:pr-3 gt-md:pr-3"> <mat-form-field class="flex-auto gt-md:pr-3">
<mat-label>Device Status - Thresholds</mat-label> <mat-label class="text-hint">Error Threshold</mat-label>
<mat-select [(ngModel)]=statusThreshold> <input disabled matInput [value]="'20%'">
<mat-option [value]=1>Smart</mat-option> </mat-form-field>
<mat-option [value]=2>Scrutiny</mat-option> <mat-form-field class="flex-auto gt-md:pl-3">
<mat-option [value]=3>Both</mat-option> <mat-label class="text-hint">Warning Threshold</mat-label>
</mat-select> <input disabled matInput [value]="'10%'">
</mat-form-field> </mat-form-field>
</div> </div>
<div class="flex flex-col mt-5 gt-md:flex-row"> </mat-tab>
<mat-form-field class="flex-auto gt-xs:pr-3 gt-md:pr-3"> <mat-tab label="NVMe">
<mat-label>Notify - Filter Attributes</mat-label>
<mat-select [(ngModel)]=statusFilterAttributes> <div matTooltip="not yet implemented" class="flex flex-col mt-5 gt-md:flex-row">
<mat-option [value]=0>All</mat-option> <mat-form-field class="flex-auto gt-md:pr-3">
<mat-option [value]=1>Critical</mat-option> <mat-label class="text-hint">Critical Error Threshold</mat-label>
</mat-select> <input disabled matInput [value]="'enabled'">
</mat-form-field> </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> </div>
</div> </div>
@@ -0,0 +1,25 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { DashboardSettingsComponent } from './dashboard-settings.component';
describe('DashboardSettingsComponent', () => {
let component: DashboardSettingsComponent;
let fixture: ComponentFixture<DashboardSettingsComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ DashboardSettingsComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(DashboardSettingsComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -1,14 +1,6 @@
import { Component, OnInit } from '@angular/core'; import { Component, OnInit } from '@angular/core';
import { import {AppConfig} from 'app/core/config/app.config';
AppConfig, import { TreoConfigService } from '@treo/services/config';
DashboardDisplay,
DashboardSort,
MetricsStatusFilterAttributes,
MetricsStatusThreshold,
TemperatureUnit,
Theme
} from 'app/core/config/app.config';
import {ScrutinyConfigService} from 'app/core/config/scrutiny-config.service';
import {Subject} from 'rxjs'; import {Subject} from 'rxjs';
import {takeUntil} from 'rxjs/operators'; import {takeUntil} from 'rxjs/operators';
@@ -22,16 +14,13 @@ export class DashboardSettingsComponent implements OnInit {
dashboardDisplay: string; dashboardDisplay: string;
dashboardSort: string; dashboardSort: string;
temperatureUnit: string; temperatureUnit: string;
fileSizeSIUnits: boolean;
theme: string; theme: string;
statusThreshold: number;
statusFilterAttributes: number;
// Private // Private
private _unsubscribeAll: Subject<any>; private _unsubscribeAll: Subject<any>;
constructor( constructor(
private _configService: ScrutinyConfigService, private _configService: TreoConfigService,
) { ) {
// Set the private defaults // Set the private defaults
this._unsubscribeAll = new Subject(); this._unsubscribeAll = new Subject();
@@ -44,30 +33,23 @@ export class DashboardSettingsComponent implements OnInit {
.subscribe((config: AppConfig) => { .subscribe((config: AppConfig) => {
// Store the config // Store the config
this.dashboardDisplay = config.dashboard_display; this.dashboardDisplay = config.dashboardDisplay;
this.dashboardSort = config.dashboard_sort; this.dashboardSort = config.dashboardSort;
this.temperatureUnit = config.temperature_unit; this.temperatureUnit = config.temperatureUnit;
this.fileSizeSIUnits = config.file_size_si_units;
this.theme = config.theme; this.theme = config.theme;
this.statusFilterAttributes = config.metrics.status_filter_attributes;
this.statusThreshold = config.metrics.status_threshold;
}); });
} }
saveSettings(): void { saveSettings(): void {
const newSettings: AppConfig = {
dashboard_display: this.dashboardDisplay as DashboardDisplay,
dashboard_sort: this.dashboardSort as DashboardSort, const newSettings = {
temperature_unit: this.temperatureUnit as TemperatureUnit, dashboardDisplay: this.dashboardDisplay,
file_size_si_units: this.fileSizeSIUnits, dashboardSort: this.dashboardSort,
theme: this.theme as Theme, temperatureUnit: this.temperatureUnit,
metrics: { theme: this.theme
status_filter_attributes: this.statusFilterAttributes as MetricsStatusFilterAttributes,
status_threshold: this.statusThreshold as MetricsStatusThreshold
}
} }
this._configService.config = newSettings this._configService.config = newSettings
console.log(`Saved Settings: ${JSON.stringify(newSettings)}`) console.log(`Saved Settings: ${JSON.stringify(newSettings)}`)
@@ -1,6 +1,7 @@
import { NgModule } from '@angular/core'; import { NgModule } from '@angular/core';
import { RouterModule } from '@angular/router'; import { RouterModule } from '@angular/router';
import {MatAutocompleteModule} from '@angular/material/autocomplete'; import { Overlay } from '@angular/cdk/overlay';
import { MAT_AUTOCOMPLETE_SCROLL_STRATEGY, MatAutocompleteModule } from '@angular/material/autocomplete';
import { MatButtonModule } from '@angular/material/button'; import { MatButtonModule } from '@angular/material/button';
import { MatSelectModule } from '@angular/material/select'; import { MatSelectModule } from '@angular/material/select';
import { MatFormFieldModule } from '@angular/material/form-field'; import { MatFormFieldModule } from '@angular/material/form-field';
@@ -40,5 +41,6 @@ import {MatTooltipModule} from '@angular/material/tooltip';
], ],
providers : [] providers : []
}) })
export class DetailSettingsModule { export class DetailSettingsModule
{
} }
@@ -4,7 +4,7 @@ import {ActivatedRoute, NavigationEnd, Router} from '@angular/router';
import { MatSlideToggleChange } from '@angular/material/slide-toggle'; import { MatSlideToggleChange } from '@angular/material/slide-toggle';
import { Subject } from 'rxjs'; import { Subject } from 'rxjs';
import { filter, takeUntil } from 'rxjs/operators'; import { filter, takeUntil } from 'rxjs/operators';
import {ScrutinyConfigService} from 'app/core/config/scrutiny-config.service'; import { TreoConfigService } from '@treo/services/config';
import { TreoDrawerService } from '@treo/components/drawer'; import { TreoDrawerService } from '@treo/components/drawer';
import { Layout } from 'app/layout/layout.types'; import { Layout } from 'app/layout/layout.types';
import { AppConfig, Theme } from 'app/core/config/app.config'; import { AppConfig, Theme } from 'app/core/config/app.config';
@@ -15,7 +15,8 @@ import {AppConfig, Theme} from 'app/core/config/app.config';
styleUrls : ['./layout.component.scss'], styleUrls : ['./layout.component.scss'],
encapsulation: ViewEncapsulation.None encapsulation: ViewEncapsulation.None
}) })
export class LayoutComponent implements OnInit, OnDestroy { export class LayoutComponent implements OnInit, OnDestroy
{
config: AppConfig; config: AppConfig;
layout: Layout; layout: Layout;
theme: Theme; theme: Theme;
@@ -28,14 +29,14 @@ export class LayoutComponent implements OnInit, OnDestroy {
* Constructor * Constructor
* *
* @param {ActivatedRoute} _activatedRoute * @param {ActivatedRoute} _activatedRoute
* @param {ScrutinyConfigService} _scrutinyConfigService * @param {TreoConfigService} _treoConfigService
* @param {TreoDrawerService} _treoDrawerService * @param {TreoDrawerService} _treoDrawerService
* @param {DOCUMENT} _document * @param {DOCUMENT} _document
* @param {Router} _router * @param {Router} _router
*/ */
constructor( constructor(
private _activatedRoute: ActivatedRoute, private _activatedRoute: ActivatedRoute,
private _scrutinyConfigService: ScrutinyConfigService, private _treoConfigService: TreoConfigService,
private _treoDrawerService: TreoDrawerService, private _treoDrawerService: TreoDrawerService,
@Inject(DOCUMENT) private _document: any, @Inject(DOCUMENT) private _document: any,
private _router: Router private _router: Router
@@ -58,7 +59,7 @@ export class LayoutComponent implements OnInit, OnDestroy {
ngOnInit(): void ngOnInit(): void
{ {
// Subscribe to config changes // Subscribe to config changes
this._scrutinyConfigService.config$ this._treoConfigService.config$
.pipe(takeUntil(this._unsubscribeAll)) .pipe(takeUntil(this._unsubscribeAll))
.subscribe((config: AppConfig) => { .subscribe((config: AppConfig) => {
@@ -179,7 +180,8 @@ export class LayoutComponent implements OnInit, OnDestroy {
* *
* @param layout * @param layout
*/ */
setLayout(layout: Layout): void { setLayout(layout: string): void
{
// Clear the 'layout' query param to allow layout changes // Clear the 'layout' query param to allow layout changes
this._router.navigate([], { this._router.navigate([], {
queryParams : { queryParams : {
@@ -189,7 +191,7 @@ export class LayoutComponent implements OnInit, OnDestroy {
}).then(() => { }).then(() => {
// Set the config // Set the config
this._scrutinyConfigService.config = {layout}; this._treoConfigService.config = {layout};
}); });
} }
@@ -200,6 +202,6 @@ export class LayoutComponent implements OnInit, OnDestroy {
*/ */
setTheme(change: MatSlideToggleChange): void setTheme(change: MatSlideToggleChange): void
{ {
this._scrutinyConfigService.config = {theme: change.checked ? 'dark' : 'light'}; this._treoConfigService.config = {theme: change.checked ? 'dark' : 'light'};
} }
} }
@@ -1,5 +1,5 @@
<div *ngIf="summaryData; else emptyDashboard"> <div *ngIf="data && data.data && data.data.summary; else emptyDashboard">
<div class="flex flex-col flex-auto w-full p-8 xs:p-2"> <div class="flex flex-col flex-auto w-full p-8 xs:p-2">
<div class="flex flex-wrap w-full"> <div class="flex flex-wrap w-full">
@@ -51,11 +51,7 @@
<div class="flex flex-wrap w-full" *ngFor="let hostId of hostGroups | keyvalue"> <div class="flex flex-wrap w-full" *ngFor="let hostId of hostGroups | keyvalue">
<h3 class="ml-4" *ngIf="hostId.key">{{hostId.key}}</h3> <h3 class="ml-4" *ngIf="hostId.key">{{hostId.key}}</h3>
<div class="flex flex-wrap w-full"> <div class="flex flex-wrap w-full">
<app-dashboard-device (deviceDeleted)="onDeviceDeleted($event)" <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>
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>
</div> </div>
@@ -1,12 +1,6 @@
import { import { AfterViewInit, ChangeDetectionStrategy, Component, OnDestroy, OnInit, ViewChild, ViewEncapsulation } from '@angular/core';
AfterViewInit, import { MatSort } from '@angular/material/sort';
ChangeDetectionStrategy, import { MatTableDataSource } from '@angular/material/table';
Component,
OnDestroy,
OnInit,
ViewChild,
ViewEncapsulation
} from '@angular/core';
import { Subject } from 'rxjs'; import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators'; import { takeUntil } from 'rxjs/operators';
import {ApexOptions, ChartComponent} from 'ng-apexcharts'; import {ApexOptions, ChartComponent} from 'ng-apexcharts';
@@ -14,11 +8,10 @@ import {DashboardService} from 'app/modules/dashboard/dashboard.service';
import {MatDialog} from '@angular/material/dialog'; import {MatDialog} from '@angular/material/dialog';
import { DashboardSettingsComponent } from 'app/layout/common/dashboard-settings/dashboard-settings.component'; import { DashboardSettingsComponent } from 'app/layout/common/dashboard-settings/dashboard-settings.component';
import {AppConfig} from 'app/core/config/app.config'; import {AppConfig} from 'app/core/config/app.config';
import {ScrutinyConfigService} from 'app/core/config/scrutiny-config.service'; import {TreoConfigService} from '@treo/services/config';
import {Router} from '@angular/router'; import {Router} from '@angular/router';
import {TemperaturePipe} from 'app/shared/temperature.pipe'; import {TemperaturePipe} from 'app/shared/temperature.pipe';
import {DeviceTitlePipe} from 'app/shared/device-title.pipe'; import {DeviceTitlePipe} from 'app/shared/device-title.pipe';
import {DeviceSummaryModel} from 'app/core/models/device-summary-model';
@Component({ @Component({
selector : 'example', selector : 'example',
@@ -29,7 +22,7 @@ import {DeviceSummaryModel} from 'app/core/models/device-summary-model';
}) })
export class DashboardComponent implements OnInit, AfterViewInit, OnDestroy export class DashboardComponent implements OnInit, AfterViewInit, OnDestroy
{ {
summaryData: { [key: string]: DeviceSummaryModel }; data: any;
hostGroups: { [hostId: string]: string[] } = {} hostGroups: { [hostId: string]: string[] } = {}
temperatureOptions: ApexOptions; temperatureOptions: ApexOptions;
tempDurationKey = 'forever' tempDurationKey = 'forever'
@@ -42,14 +35,11 @@ export class DashboardComponent implements OnInit, AfterViewInit, OnDestroy
/** /**
* Constructor * Constructor
* *
* @param {DashboardService} _dashboardService * @param {SmartService} _smartService
* @param {ScrutinyConfigService} _configService
* @param {MatDialog} dialog
* @param {Router} router
*/ */
constructor( constructor(
private _dashboardService: DashboardService, private _smartService: DashboardService,
private _configService: ScrutinyConfigService, private _configService: TreoConfigService,
public dialog: MatDialog, public dialog: MatDialog,
private router: Router, private router: Router,
) )
@@ -91,16 +81,16 @@ export class DashboardComponent implements OnInit, AfterViewInit, OnDestroy
}); });
// Get the data // Get the data
this._dashboardService.data$ this._smartService.data$
.pipe(takeUntil(this._unsubscribeAll)) .pipe(takeUntil(this._unsubscribeAll))
.subscribe((data) => { .subscribe((data) => {
// Store the data // Store the data
this.summaryData = data; this.data = data;
// generate group data. // generate group data.
for (const wwn in this.summaryData) { for(const wwn in this.data.data.summary){
const hostid = this.summaryData[wwn].device.host_id const hostid = this.data.data.summary[wwn].device.host_id
const hostDeviceList = this.hostGroups[hostid] || [] const hostDeviceList = this.hostGroups[hostid] || []
hostDeviceList.push(wwn) hostDeviceList.push(wwn)
this.hostGroups[hostid] = hostDeviceList this.hostGroups[hostid] = hostDeviceList
@@ -142,15 +132,15 @@ export class DashboardComponent implements OnInit, AfterViewInit, OnDestroy
private _deviceDataTemperatureSeries(): any[] { private _deviceDataTemperatureSeries(): any[] {
const deviceTemperatureSeries = [] const deviceTemperatureSeries = []
console.log('DEVICE DATA SUMMARY', this.summaryData) console.log('DEVICE DATA SUMMARY', this.data)
for (const wwn in this.summaryData) { for(const wwn in this.data.data.summary){
const deviceSummary = this.summaryData[wwn] const deviceSummary = this.data.data.summary[wwn]
if (!deviceSummary.temp_history){ if (!deviceSummary.temp_history){
continue continue
} }
const deviceName = DeviceTitlePipe.deviceTitleWithFallback(deviceSummary.device, this.config.dashboard_display) const deviceName = DeviceTitlePipe.deviceTitleWithFallback(deviceSummary.device, this.config.dashboardDisplay)
const deviceSeriesMetadata = { const deviceSeriesMetadata = {
name: deviceName, name: deviceName,
@@ -161,7 +151,7 @@ export class DashboardComponent implements OnInit, AfterViewInit, OnDestroy
const newDate = new Date(tempHistory.date); const newDate = new Date(tempHistory.date);
deviceSeriesMetadata.data.push({ deviceSeriesMetadata.data.push({
x: newDate, x: newDate,
y: TemperaturePipe.formatTemperature(tempHistory.temp, this.config.temperature_unit, false) y: TemperaturePipe.formatTemperature(tempHistory.temp, this.config.temperatureUnit, false)
}) })
} }
deviceTemperatureSeries.push(deviceSeriesMetadata) deviceTemperatureSeries.push(deviceSeriesMetadata)
@@ -212,7 +202,7 @@ export class DashboardComponent implements OnInit, AfterViewInit, OnDestroy
y : { y : {
formatter: (value) => { formatter: (value) => {
return TemperaturePipe.formatTemperature(value, this.config.temperature_unit, true) as string; return TemperaturePipe.formatTemperature(value, this.config.temperatureUnit, true) as string;
} }
} }
}, },
@@ -226,18 +216,18 @@ export class DashboardComponent implements OnInit, AfterViewInit, OnDestroy
// @ Public methods // @ Public methods
// ----------------------------------------------------------------------------------------------------- // -----------------------------------------------------------------------------------------------------
deviceSummariesForHostGroup(hostGroupWWNs: string[]): DeviceSummaryModel[] { deviceSummariesForHostGroup(hostGroupWWNs: string[]): any[] {
const deviceSummaries: DeviceSummaryModel[] = [] const deviceSummaries = []
for(const wwn of hostGroupWWNs){ for(const wwn of hostGroupWWNs){
if (this.summaryData[wwn]) { if(this.data.data.summary[wwn]){
deviceSummaries.push(this.summaryData[wwn]) deviceSummaries.push(this.data.data.summary[wwn])
} }
} }
return deviceSummaries return deviceSummaries
} }
openDialog(): void { openDialog(): void {
const dialogRef = this.dialog.open(DashboardSettingsComponent, {width: '600px',}); const dialogRef = this.dialog.open(DashboardSettingsComponent);
dialogRef.afterClosed().subscribe(result => { dialogRef.afterClosed().subscribe(result => {
console.log(`Dialog result: ${result}`); console.log(`Dialog result: ${result}`);
@@ -245,7 +235,7 @@ export class DashboardComponent implements OnInit, AfterViewInit, OnDestroy
} }
onDeviceDeleted(wwn: string): void { onDeviceDeleted(wwn: string): void {
delete this.summaryData[wwn] // remove the device from the summary list. delete this.data.data.summary[wwn] // remove the device from the summary list.
} }
/* /*
@@ -256,16 +246,16 @@ export class DashboardComponent implements OnInit, AfterViewInit, OnDestroy
DURATION_KEY_FOREVER = "forever" DURATION_KEY_FOREVER = "forever"
*/ */
changeSummaryTempDuration(durationKey: string): void { changeSummaryTempDuration(durationKey: string){
this.tempDurationKey = durationKey this.tempDurationKey = durationKey
this._dashboardService.getSummaryTempData(durationKey) this._smartService.getSummaryTempData(durationKey)
.subscribe((tempHistoryData) => { .subscribe((data) => {
// given a list of device temp history, override the data in the "summary" object. // given a list of device temp history, override the data in the "summary" object.
for (const wwn in this.summaryData) { for(const wwn in this.data.data.summary) {
// console.log(`Updating ${wwn}, length: ${this.data.data.summary[wwn].temp_history.length}`) // console.log(`Updating ${wwn}, length: ${this.data.data.summary[wwn].temp_history.length}`)
this.summaryData[wwn].temp_history = tempHistoryData[wwn] || [] this.data.data.summary[wwn].temp_history = data.data.temp_history[wwn] || []
} }
// Prepare the chart series data // Prepare the chart series data
@@ -2,12 +2,12 @@ import {Injectable} from '@angular/core';
import { ActivatedRouteSnapshot, Resolve, RouterStateSnapshot } from '@angular/router'; import { ActivatedRouteSnapshot, Resolve, RouterStateSnapshot } from '@angular/router';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { DashboardService } from 'app/modules/dashboard/dashboard.service'; import { DashboardService } from 'app/modules/dashboard/dashboard.service';
import {DeviceSummaryModel} from 'app/core/models/device-summary-model';
@Injectable({ @Injectable({
providedIn: 'root' providedIn: 'root'
}) })
export class DashboardResolver implements Resolve<any> { export class DashboardResolver implements Resolve<any>
{
/** /**
* Constructor * Constructor
* *
@@ -29,7 +29,8 @@ export class DashboardResolver implements Resolve<any> {
* @param route * @param route
* @param state * @param state
*/ */
resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<{ [p: string]: DeviceSummaryModel }> { resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<any>
{
return this._dashboardService.getSummaryData(); return this._dashboardService.getSummaryData();
} }
} }
@@ -1,44 +0,0 @@
import {HttpClient} from '@angular/common/http';
import {DashboardService} from './dashboard.service';
import {of} from 'rxjs';
import {summary} from 'app/data/mock/summary/data'
import {temp_history} from 'app/data/mock/summary/temp_history'
import {DeviceSummaryModel} from 'app/core/models/device-summary-model';
import {SmartTemperatureModel} from 'app/core/models/measurements/smart-temperature-model';
describe('DashboardService', () => {
let service: DashboardService;
let httpClientSpy: jasmine.SpyObj<HttpClient>;
beforeEach(() => {
httpClientSpy = jasmine.createSpyObj('HttpClient', ['get']);
service = new DashboardService(httpClientSpy);
});
it('should unwrap and return getSummaryData() (HttpClient called once)', (done: DoneFn) => {
httpClientSpy.get.and.returnValue(of(summary));
service.getSummaryData().subscribe(value => {
expect(value).toBe(summary.data.summary as { [key: string]: DeviceSummaryModel });
done();
});
expect(httpClientSpy.get.calls.count())
.withContext('one call')
.toBe(1);
});
it('should unwrap and return getSummaryTempData() (HttpClient called once)', (done: DoneFn) => {
// const expectedHeroes: any[] =
// [{ id: 1, name: 'A' }, { id: 2, name: 'B' }];
httpClientSpy.get.and.returnValue(of(temp_history));
service.getSummaryTempData('weekly').subscribe(value => {
expect(value).toBe(temp_history.data.temp_history as { [key: string]: SmartTemperatureModel[] });
done();
});
expect(httpClientSpy.get.calls.count())
.withContext('one call')
.toBe(1);
});
});
@@ -1,19 +1,16 @@
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http'; import { HttpClient } from '@angular/common/http';
import { BehaviorSubject, Observable } from 'rxjs'; import { BehaviorSubject, Observable } from 'rxjs';
import {map, tap} from 'rxjs/operators'; import { tap } from 'rxjs/operators';
import { getBasePath } from 'app/app.routing'; import { getBasePath } from 'app/app.routing';
import {DeviceSummaryResponseWrapper} from 'app/core/models/device-summary-response-wrapper';
import {DeviceSummaryModel} from 'app/core/models/device-summary-model';
import {SmartTemperatureModel} from 'app/core/models/measurements/smart-temperature-model';
import {DeviceSummaryTempResponseWrapper} from 'app/core/models/device-summary-temp-response-wrapper';
@Injectable({ @Injectable({
providedIn: 'root' providedIn: 'root'
}) })
export class DashboardService { export class DashboardService
{
// Observables // Observables
private _data: BehaviorSubject<{ [p: string]: DeviceSummaryModel }>; private _data: BehaviorSubject<any>;
/** /**
* Constructor * Constructor
@@ -35,7 +32,8 @@ export class DashboardService {
/** /**
* Getter for data * Getter for data
*/ */
get data$(): Observable<{ [p: string]: DeviceSummaryModel }> { get data$(): Observable<any>
{
return this._data.asObservable(); return this._data.asObservable();
} }
@@ -46,28 +44,22 @@ export class DashboardService {
/** /**
* Get data * Get data
*/ */
getSummaryData(): Observable<{ [key: string]: DeviceSummaryModel }> { getSummaryData(): Observable<any>
{
return this._httpClient.get(getBasePath() + '/api/summary').pipe( return this._httpClient.get(getBasePath() + '/api/summary').pipe(
map((response: DeviceSummaryResponseWrapper) => { tap((response: any) => {
// console.log("FILTERING=----", response.data.summary)
return response.data.summary
}),
tap((response: { [key: string]: DeviceSummaryModel }) => {
this._data.next(response); this._data.next(response);
}) })
); );
} }
getSummaryTempData(durationKey: string): Observable<{ [key: string]: SmartTemperatureModel[] }> { getSummaryTempData(durationKey: string): Observable<any>
{
const params = {} const params = {}
if(durationKey){ if(durationKey){
params['duration_key'] = durationKey params['duration_key'] = durationKey
} }
return this._httpClient.get(getBasePath() + '/api/summary/temp', {params: params}).pipe( return this._httpClient.get(getBasePath() + '/api/summary/temp', {params: params});
map((response: DeviceSummaryTempResponseWrapper) => {
return response.data.temp_history
})
);
} }
} }
@@ -4,7 +4,7 @@
<div class="flex items-center justify-between w-full my-4 px-4 xs:pr-0"> <div class="flex items-center justify-between w-full my-4 px-4 xs:pr-0">
<div class="mr-6"> <div class="mr-6">
<h2 class="m-0">Drive Details - {{device | deviceTitle:config.dashboard_display}} </h2> <h2 class="m-0">Drive Details - {{device | deviceTitle:config.dashboardDisplay}} </h2>
<div class="text-secondary tracking-tight">Dive into S.M.A.R.T data</div> <div class="text-secondary tracking-tight">Dive into S.M.A.R.T data</div>
</div> </div>
<!-- Action buttons --> <!-- Action buttons -->
@@ -17,7 +17,7 @@
<span class="ml-2">Export</span> <span class="ml-2">Export</span>
</button> </button>
<button class="ml-2 xs:hidden" <button class="ml-2 xs:hidden"
matTooltip="not yet implemented" (click)="openDialog()"
mat-stroked-button> mat-stroked-button>
<mat-icon class="icon-size-20 rotate-90 mirror" <mat-icon class="icon-size-20 rotate-90 mirror"
[svgIcon]="'tune'"></mat-icon> [svgIcon]="'tune'"></mat-icon>
@@ -56,13 +56,12 @@
<div *ngIf="device" class="my-2 col-span-2 lt-md:col-span-1"> <div *ngIf="device" class="my-2 col-span-2 lt-md:col-span-1">
<div> <div>
<span class="inline-flex items-center font-bold text-xs px-2 py-2px rounded-full tracking-wide uppercase" <span class="inline-flex items-center font-bold text-xs px-2 py-2px rounded-full tracking-wide uppercase"
[ngClass]="{'red-200': deviceStatusForModelWithThreshold(device, !!smart_results, config.metrics.status_threshold) == 'failed', [ngClass]="{'red-200': device?.device_status != 0,
'green-200': device?.device_status == 0}"> 'green-200': device?.device_status == 0}">
<span class="w-2 h-2 rounded-full mr-2" <span class="w-2 h-2 rounded-full mr-2"
[ngClass]="{'bg-red': device?.device_status != 0, [ngClass]="{'bg-red': device?.device_status != 0,
'bg-green': device?.device_status == 0}"></span> 'bg-green': device?.device_status == 0}"></span>
<span <span class="pr-2px leading-relaxed whitespace-no-wrap">{{device?.device_status | deviceStatus}}</span>
class="pr-2px leading-relaxed whitespace-no-wrap">{{device | deviceStatus:!!smart_results:config.metrics.status_threshold:true}}</span>
</span> </span>
</div> </div>
<div class="text-secondary text-md">Status</div> <div class="text-secondary text-md">Status</div>
@@ -107,7 +106,7 @@
<div class="text-secondary text-md">Firmware Version</div> <div class="text-secondary text-md">Firmware Version</div>
</div> </div>
<div class="my-2 col-span-2 lt-md:col-span-1"> <div class="my-2 col-span-2 lt-md:col-span-1">
<div>{{device?.capacity | fileSize:config.file_size_si_units}}</div> <div>{{device?.capacity | fileSize}}</div>
<div class="text-secondary text-md">Capacity</div> <div class="text-secondary text-md">Capacity</div>
</div> </div>
<div *ngIf="device?.rotational_speed" class="my-2 col-span-2 lt-md:col-span-1"> <div *ngIf="device?.rotational_speed" class="my-2 col-span-2 lt-md:col-span-1">
@@ -127,7 +126,7 @@
<div class="text-secondary text-md">Powered On</div> <div class="text-secondary text-md">Powered On</div>
</div> </div>
<div class="my-2 col-span-2 lt-md:col-span-1"> <div class="my-2 col-span-2 lt-md:col-span-1">
<div>{{smart_results[0]?.temp | temperature:config.temperature_unit:true}}</div> <div>{{smart_results[0]?.temp | temperature:config.temperatureUnit:true}}</div>
<div class="text-secondary text-md">Temperature</div> <div class="text-secondary text-md">Temperature</div>
</div> </div>
</div> </div>
@@ -0,0 +1,25 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { DetailComponent } from './detail.component';
describe('DetailComponent', () => {
let component: DetailComponent;
let fixture: ComponentFixture<DetailComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ DetailComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(DetailComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -1,22 +1,18 @@
import humanizeDuration from 'humanize-duration'; import {AfterViewInit, Component, OnDestroy, OnInit, ViewChild} from '@angular/core';
import {AfterViewInit, Component, Inject, LOCALE_ID, OnDestroy, OnInit, ViewChild} from '@angular/core';
import {ApexOptions} from 'ng-apexcharts'; import {ApexOptions} from 'ng-apexcharts';
import {AppConfig} from 'app/core/config/app.config'; import {MatTableDataSource} from '@angular/material/table';
import {MatSort} from '@angular/material/sort';
import {Subject} from 'rxjs';
import {DetailService} from './detail.service'; import {DetailService} from './detail.service';
import {takeUntil} from 'rxjs/operators';
import {DetailSettingsComponent} from 'app/layout/common/detail-settings/detail-settings.component'; import {DetailSettingsComponent} from 'app/layout/common/detail-settings/detail-settings.component';
import {MatDialog} from '@angular/material/dialog'; import {MatDialog} from '@angular/material/dialog';
import {MatSort} from '@angular/material/sort'; import humanizeDuration from 'humanize-duration';
import {MatTableDataSource} from '@angular/material/table'; import {TreoConfigService} from '@treo/services/config';
import {Subject} from 'rxjs'; import {AppConfig} from 'app/core/config/app.config';
import {ScrutinyConfigService} from 'app/core/config/scrutiny-config.service';
import {animate, state, style, transition, trigger} from '@angular/animations'; import {animate, state, style, transition, trigger} from '@angular/animations';
import {formatDate} from '@angular/common'; import {formatDate} from '@angular/common';
import {takeUntil} from 'rxjs/operators'; import { LOCALE_ID, Inject } from '@angular/core';
import {DeviceModel} from 'app/core/models/device-model';
import {SmartModel} from 'app/core/models/measurements/smart-model';
import {SmartAttributeModel} from 'app/core/models/measurements/smart-attribute-model';
import {AttributeMetadataModel} from 'app/core/models/thresholds/attribute-metadata-model';
import {DeviceStatusPipe} from 'app/shared/device-status.pipe';
// from Constants.go - these must match // from Constants.go - these must match
const AttributeStatusPassed = 0 const AttributeStatusPassed = 0
@@ -44,16 +40,15 @@ export class DetailComponent implements OnInit, AfterViewInit, OnDestroy {
* Constructor * Constructor
* *
* @param {DetailService} _detailService * @param {DetailService} _detailService
* @param {MatDialog} dialog
* @param {ScrutinyConfigService} _configService
* @param {string} locale
*/ */
constructor( constructor(
private _detailService: DetailService, private _detailService: DetailService,
public dialog: MatDialog, public dialog: MatDialog,
private _configService: ScrutinyConfigService, private _configService: TreoConfigService,
@Inject(LOCALE_ID) public locale: string @Inject(LOCALE_ID) public locale: string
) {
)
{
// Set the private defaults // Set the private defaults
this._unsubscribeAll = new Subject(); this._unsubscribeAll = new Subject();
@@ -70,15 +65,14 @@ export class DetailComponent implements OnInit, AfterViewInit, OnDestroy {
onlyCritical = true; onlyCritical = true;
// data: any; // data: any;
expandedAttribute: SmartAttributeModel | null; expandedAttribute: any | null;
metadata: { [p: string]: AttributeMetadataModel } | { [p: number]: AttributeMetadataModel }; metadata: any;
device: DeviceModel; device: any;
// tslint:disable-next-line:variable-name smart_results: any[];
smart_results: SmartModel[];
commonSparklineOptions: Partial<ApexOptions>; commonSparklineOptions: Partial<ApexOptions>;
smartAttributeDataSource: MatTableDataSource<SmartAttributeModel>; smartAttributeDataSource: MatTableDataSource<any>;
smartAttributeTableColumns: string[]; smartAttributeTableColumns: string[];
@ViewChild('smartAttributeTable', {read: MatSort}) @ViewChild('smartAttributeTable', {read: MatSort})
@@ -90,7 +84,6 @@ export class DetailComponent implements OnInit, AfterViewInit, OnDestroy {
readonly humanizeDuration = humanizeDuration; readonly humanizeDuration = humanizeDuration;
deviceStatusForModelWithThreshold = DeviceStatusPipe.deviceStatusForModelWithThreshold
// ----------------------------------------------------------------------------------------------------- // -----------------------------------------------------------------------------------------------------
// @ Lifecycle hooks // @ Lifecycle hooks
// ----------------------------------------------------------------------------------------------------- // -----------------------------------------------------------------------------------------------------
@@ -98,7 +91,8 @@ export class DetailComponent implements OnInit, AfterViewInit, OnDestroy {
/** /**
* On init * On init
*/ */
ngOnInit(): void { ngOnInit(): void
{
// Subscribe to config changes // Subscribe to config changes
this._configService.config$ this._configService.config$
.pipe(takeUntil(this._unsubscribeAll)) .pipe(takeUntil(this._unsubscribeAll))
@@ -110,13 +104,13 @@ export class DetailComponent implements OnInit, AfterViewInit, OnDestroy {
// Get the data // Get the data
this._detailService.data$ this._detailService.data$
.pipe(takeUntil(this._unsubscribeAll)) .pipe(takeUntil(this._unsubscribeAll))
.subscribe((respWrapper) => { .subscribe((data) => {
// Store the data // Store the data
// this.data = data; // this.data = data;
this.device = respWrapper.data.device; this.device = data.data.device;
this.smart_results = respWrapper.data.smart_results this.smart_results = data.data.smart_results
this.metadata = respWrapper.metadata; this.metadata = data.metadata;
// Store the table data // Store the table data
@@ -130,7 +124,8 @@ export class DetailComponent implements OnInit, AfterViewInit, OnDestroy {
/** /**
* After view init * After view init
*/ */
ngAfterViewInit(): void { ngAfterViewInit(): void
{
// Make the data source sortable // Make the data source sortable
this.smartAttributeDataSource.sort = this.smartAttributeTableMatSort; this.smartAttributeDataSource.sort = this.smartAttributeTableMatSort;
} }
@@ -138,7 +133,8 @@ export class DetailComponent implements OnInit, AfterViewInit, OnDestroy {
/** /**
* On destroy * On destroy
*/ */
ngOnDestroy(): void { ngOnDestroy(): void
{
// Unsubscribe from all subscriptions // Unsubscribe from all subscriptions
this._unsubscribeAll.next(); this._unsubscribeAll.next();
this._unsubscribeAll.complete(); this._unsubscribeAll.complete();
@@ -162,7 +158,6 @@ export class DetailComponent implements OnInit, AfterViewInit, OnDestroy {
return '' return ''
// tslint:enable:no-bitwise // tslint:enable:no-bitwise
} }
getAttributeScrutinyStatusName(attributeStatus: number): string { getAttributeScrutinyStatusName(attributeStatus: number): string {
// tslint:disable:no-bitwise // tslint:disable:no-bitwise
if ((attributeStatus & AttributeStatusFailedScrutiny) !== 0){ if ((attributeStatus & AttributeStatusFailedScrutiny) !== 0){
@@ -186,136 +181,134 @@ export class DetailComponent implements OnInit, AfterViewInit, OnDestroy {
} }
getAttributeName(attributeData: SmartAttributeModel): string { getAttributeName(attribute_data): string {
const attributeMetadata = this.metadata[attributeData.attribute_id] const attribute_metadata = this.metadata[attribute_data.attribute_id]
if (!attributeMetadata) { if(!attribute_metadata){
return 'Unknown Attribute Name' return 'Unknown Attribute Name'
} else { } else {
return attributeMetadata.display_name return attribute_metadata.display_name
} }
} }
getAttributeDescription(attribute_data){
getAttributeDescription(attributeData: SmartAttributeModel): string { const attribute_metadata = this.metadata[attribute_data.attribute_id]
const attributeMetadata = this.metadata[attributeData.attribute_id] if(!attribute_metadata){
if (!attributeMetadata) {
return 'Unknown' return 'Unknown'
} else { } else {
return attributeMetadata.description return attribute_metadata.description
}
return
}
getAttributeValue(attribute_data){
if(this.isAta()) {
const attribute_metadata = this.metadata[attribute_data.attribute_id]
if(!attribute_metadata){
return attribute_data.value
} else if (attribute_metadata.display_type == 'raw') {
return attribute_data.raw_value
} else if (attribute_metadata.display_type == 'transformed' && attribute_data.transformed_value) {
return attribute_data.transformed_value
} else {
return attribute_data.value
}
}
else{
return attribute_data.value
} }
} }
getAttributeValue(attributeData: SmartAttributeModel): number { getAttributeValueType(attribute_data){
if(this.isAta()) { if(this.isAta()) {
const attributeMetadata = this.metadata[attributeData.attribute_id] const attribute_metadata = this.metadata[attribute_data.attribute_id]
if (!attributeMetadata) { if(!attribute_metadata){
return attributeData.value
} else if (attributeMetadata.display_type === 'raw') {
return attributeData.raw_value
} else if (attributeMetadata.display_type === 'transformed' && attributeData.transformed_value) {
return attributeData.transformed_value
} else {
return attributeData.value
}
} else {
return attributeData.value
}
}
getAttributeValueType(attributeData: SmartAttributeModel): string {
if (this.isAta()) {
const attributeMetadata = this.metadata[attributeData.attribute_id]
if (!attributeMetadata) {
return '' return ''
} else { } else {
return attributeMetadata.display_type return attribute_metadata.display_type
} }
} else { } else {
return '' return ''
} }
} }
getAttributeIdeal(attributeData: SmartAttributeModel): string { getAttributeIdeal(attribute_data){
if(this.isAta()){ if(this.isAta()){
return this.metadata[attributeData.attribute_id]?.display_type === 'raw' ? this.metadata[attributeData.attribute_id]?.ideal : '' return this.metadata[attribute_data.attribute_id]?.display_type == 'raw' ? this.metadata[attribute_data.attribute_id]?.ideal : ''
} else { } else {
return this.metadata[attributeData.attribute_id]?.ideal return this.metadata[attribute_data.attribute_id]?.ideal
} }
} }
getAttributeWorst(attributeData: SmartAttributeModel): number | string { getAttributeWorst(attribute_data){
const attributeMetadata = this.metadata[attributeData.attribute_id] const attribute_metadata = this.metadata[attribute_data.attribute_id]
if (!attributeMetadata) { if(!attribute_metadata){
return attributeData.worst return attribute_data.worst
} else { } else {
return attributeMetadata?.display_type === 'normalized' ? attributeData.worst : '' return attribute_metadata?.display_type == 'normalized' ? attribute_data.worst : ''
} }
} }
getAttributeThreshold(attributeData: SmartAttributeModel): number | string { getAttributeThreshold(attribute_data){
if(this.isAta()){ if(this.isAta()){
const attributeMetadata = this.metadata[attributeData.attribute_id] const attribute_metadata = this.metadata[attribute_data.attribute_id]
if (!attributeMetadata || attributeMetadata.display_type === 'normalized') { if(!attribute_metadata || attribute_metadata.display_type == 'normalized'){
return attributeData.thresh return attribute_data.thresh
} else { } else {
// if(this.data.metadata[attribute_data.attribute_id].observed_thresholds){ // if(this.data.metadata[attribute_data.attribute_id].observed_thresholds){
// //
// } else { // } else {
// } // }
// return '' // return ''
return attributeData.thresh return attribute_data.thresh
} }
} else { } else {
return (attributeData.thresh === -1 ? '' : attributeData.thresh) return (attribute_data.thresh == -1 ? '' : attribute_data.thresh )
} }
} }
getAttributeCritical(attributeData: SmartAttributeModel): boolean { getAttributeCritical(attribute_data){
return this.metadata[attributeData.attribute_id]?.critical return this.metadata[attribute_data.attribute_id]?.critical
} }
getHiddenAttributes(){
getHiddenAttributes(): number { if (!this.smart_results || this.smart_results.length == 0) {
if (!this.smart_results || this.smart_results.length === 0) {
return 0 return 0
} }
let attributesLength = 0 let attributes_length = 0
const attributes = this.smart_results[0]?.attrs const attributes = this.smart_results[0]?.attrs
if (attributes) { if (attributes) {
attributesLength = Object.keys(attributes).length attributes_length = Object.keys(attributes).length
} }
return attributesLength - this.smartAttributeDataSource.data.length return attributes_length - this.smartAttributeDataSource.data.length
} }
isAta(): boolean { isAta(): boolean {
return this.device.device_protocol === 'ATA' return this.device.device_protocol == 'ATA'
} }
isScsi(): boolean { isScsi(): boolean {
return this.device.device_protocol === 'SCSI' return this.device.device_protocol == 'SCSI'
} }
isNvme(): boolean { isNvme(): boolean {
return this.device.device_protocol === 'NVMe' return this.device.device_protocol == 'NVMe'
} }
private _generateSmartAttributeTableDataSource(smartResults: SmartModel[]): SmartAttributeModel[] { private _generateSmartAttributeTableDataSource(smart_results){
const smartAttributeDataSource: SmartAttributeModel[] = []; const smartAttributeDataSource = [];
if (smartResults.length === 0) { if(smart_results.length == 0){
return smartAttributeDataSource return smartAttributeDataSource
} }
const latestSmartResult = smartResults[0]; const latest_smart_result = smart_results[0];
let attributes: { [p: string]: SmartAttributeModel } = {} let attributes = {}
if(this.isScsi()) { if(this.isScsi()) {
this.smartAttributeTableColumns = ['status', 'name', 'value', 'thresh', 'history']; this.smartAttributeTableColumns = ['status', 'name', 'value', 'thresh', 'history'];
attributes = latestSmartResult.attrs attributes = latest_smart_result.attrs
} else if(this.isNvme()){ } else if(this.isNvme()){
this.smartAttributeTableColumns = ['status', 'name', 'value', 'thresh', 'ideal', 'history']; this.smartAttributeTableColumns = ['status', 'name', 'value', 'thresh', 'ideal', 'history'];
attributes = latestSmartResult.attrs attributes = latest_smart_result.attrs
} else { } else {
// ATA // ATA
attributes = latestSmartResult.attrs attributes = latest_smart_result.attrs
this.smartAttributeTableColumns = ['status', 'id', 'name', 'value', 'thresh','ideal', 'failure', 'history']; this.smartAttributeTableColumns = ['status', 'id', 'name', 'value', 'thresh','ideal', 'failure', 'history'];
} }
@@ -327,14 +320,14 @@ export class DetailComponent implements OnInit, AfterViewInit, OnDestroy {
const attrHistory = [] const attrHistory = []
for (const smartResult of smartResults) { for (const smart_result of smart_results){
// attrHistory.push(this.getAttributeValue(smart_result.attrs[attrId])) // attrHistory.push(this.getAttributeValue(smart_result.attrs[attrId]))
const chartDatapoint = { const chartDatapoint = {
x: formatDate(smartResult.date, 'MMMM dd, yyyy - HH:mm', this.locale), x: formatDate(smart_result.date, 'MMMM dd, yyyy - HH:mm', this.locale),
y: this.getAttributeValue(smartResult.attrs[attrId]) y: this.getAttributeValue(smart_result.attrs[attrId])
} }
const attributeStatusName = this.getAttributeStatusName(smartResult.attrs[attrId].status) const attributeStatusName = this.getAttributeStatusName(smart_result.attrs[attrId].status)
if(attributeStatusName === 'failed') { if(attributeStatusName === 'failed') {
chartDatapoint['strokeColor'] = '#F05252' chartDatapoint['strokeColor'] = '#F05252'
chartDatapoint['fillColor'] = '#F05252' chartDatapoint['fillColor'] = '#F05252'
@@ -351,9 +344,7 @@ export class DetailComponent implements OnInit, AfterViewInit, OnDestroy {
attributes[attrId].chartData = [ attributes[attrId].chartData = [
{ {
name: 'chart-line-sparkline', name: 'chart-line-sparkline',
// attrHistory needs to be reversed, so the newest data is on the right data: attrHistory
// fixes #339
data: attrHistory.reverse()
} }
] ]
} }
@@ -371,7 +362,8 @@ export class DetailComponent implements OnInit, AfterViewInit, OnDestroy {
* *
* @private * @private
*/ */
private _prepareChartData(): void { private _prepareChartData(): void
{
// Account balance // Account balance
this.commonSparklineOptions = { this.commonSparklineOptions = {
@@ -400,7 +392,7 @@ export class DetailComponent implements OnInit, AfterViewInit, OnDestroy {
}, },
y: { y: {
title: { title: {
formatter: (seriesName) => { formatter: function(seriesName) {
return ''; return '';
} }
} }
@@ -425,21 +417,20 @@ export class DetailComponent implements OnInit, AfterViewInit, OnDestroy {
return config.theme return config.theme
} }
} }
// ----------------------------------------------------------------------------------------------------- // -----------------------------------------------------------------------------------------------------
// @ Public methods // @ Public methods
// ----------------------------------------------------------------------------------------------------- // -----------------------------------------------------------------------------------------------------
toHex(decimalNumb: number | string): string { toHex(decimalNumb){
return '0x' + Number(decimalNumb).toString(16).padStart(2, '0').toUpperCase() return '0x' + Number(decimalNumb).toString(16).padStart(2, '0').toUpperCase()
} }
toggleOnlyCritical(){
toggleOnlyCritical(): void {
this.onlyCritical = !this.onlyCritical this.onlyCritical = !this.onlyCritical
this.smartAttributeDataSource.data = this._generateSmartAttributeTableDataSource(this.smart_results); this.smartAttributeDataSource.data = this._generateSmartAttributeTableDataSource(this.smart_results);
} }
openDialog(): void { openDialog() {
const dialogRef = this.dialog.open(DetailSettingsComponent); const dialogRef = this.dialog.open(DetailSettingsComponent);
dialogRef.afterClosed().subscribe(result => { dialogRef.afterClosed().subscribe(result => {
@@ -453,7 +444,8 @@ export class DetailComponent implements OnInit, AfterViewInit, OnDestroy {
* @param index * @param index
* @param item * @param item
*/ */
trackByFn(index: number, item: any): any { trackByFn(index: number, item: any): any
{
return index; return index;
// return item.id || index; // return item.id || index;
} }
@@ -2,12 +2,12 @@ import {Injectable} from '@angular/core';
import { ActivatedRouteSnapshot, Resolve, RouterStateSnapshot } from '@angular/router'; import { ActivatedRouteSnapshot, Resolve, RouterStateSnapshot } from '@angular/router';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { DetailService } from 'app/modules/detail/detail.service'; import { DetailService } from 'app/modules/detail/detail.service';
import {DeviceDetailsResponseWrapper} from 'app/core/models/device-details-response-wrapper';
@Injectable({ @Injectable({
providedIn: 'root' providedIn: 'root'
}) })
export class DetailResolver implements Resolve<any> { export class DetailResolver implements Resolve<any>
{
/** /**
* Constructor * Constructor
* *
@@ -29,7 +29,8 @@ export class DetailResolver implements Resolve<any> {
* @param route * @param route
* @param state * @param state
*/ */
resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<DeviceDetailsResponseWrapper> { resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<any>
{
return this._detailService.getData(route.params.wwn); return this._detailService.getData(route.params.wwn);
} }
} }
@@ -1,28 +0,0 @@
import {HttpClient} from '@angular/common/http';
import {DetailService} from './detail.service';
import {of} from 'rxjs';
import {sda} from 'app/data/mock/device/details/sda'
import {DeviceDetailsResponseWrapper} from 'app/core/models/device-details-response-wrapper';
describe('DetailService', () => {
describe('#getData', () => {
let service: DetailService;
let httpClientSpy: jasmine.SpyObj<HttpClient>;
beforeEach(() => {
httpClientSpy = jasmine.createSpyObj('HttpClient', ['get']);
service = new DetailService(httpClientSpy);
});
it('should return getData() (HttpClient called once)', (done: DoneFn) => {
httpClientSpy.get.and.returnValue(of(sda));
service.getData('test').subscribe(value => {
expect(value).toBe(sda as DeviceDetailsResponseWrapper);
done();
});
expect(httpClientSpy.get.calls.count())
.withContext('one call')
.toBe(1);
});
})
});
@@ -3,14 +3,14 @@ import {HttpClient} from '@angular/common/http';
import { BehaviorSubject, Observable } from 'rxjs'; import { BehaviorSubject, Observable } from 'rxjs';
import { tap } from 'rxjs/operators'; import { tap } from 'rxjs/operators';
import { getBasePath } from 'app/app.routing'; import { getBasePath } from 'app/app.routing';
import {DeviceDetailsResponseWrapper} from 'app/core/models/device-details-response-wrapper';
@Injectable({ @Injectable({
providedIn: 'root' providedIn: 'root'
}) })
export class DetailService { export class DetailService
{
// Observables // Observables
private _data: BehaviorSubject<DeviceDetailsResponseWrapper>; private _data: BehaviorSubject<any>;
/** /**
* Constructor * Constructor
@@ -19,7 +19,8 @@ export class DetailService {
*/ */
constructor( constructor(
private _httpClient: HttpClient private _httpClient: HttpClient
) { )
{
// Set the private defaults // Set the private defaults
this._data = new BehaviorSubject(null); this._data = new BehaviorSubject(null);
} }
@@ -31,7 +32,8 @@ export class DetailService {
/** /**
* Getter for data * Getter for data
*/ */
get data$(): Observable<DeviceDetailsResponseWrapper> { get data$(): Observable<any>
{
return this._data.asObservable(); return this._data.asObservable();
} }
@@ -42,9 +44,10 @@ export class DetailService {
/** /**
* Get data * Get data
*/ */
getData(wwn): Observable<DeviceDetailsResponseWrapper> { getData(wwn): Observable<any>
{
return this._httpClient.get(getBasePath() + `/api/device/${wwn}/details`).pipe( return this._httpClient.get(getBasePath() + `/api/device/${wwn}/details`).pipe(
tap((response: DeviceDetailsResponseWrapper) => { tap((response: any) => {
this._data.next(response); this._data.next(response);
}) })
); );

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