Compare commits

..

78 Commits

Author SHA1 Message Date
packagrio-bot 0f935ceb48 (v0.4.7) Automated packaging of release by Packagr 2022-05-25 15:04:55 +00:00
Jason Kulatunga f844a435fd fix error message. 2022-05-25 07:55:15 -07:00
Jason Kulatunga 3a970e7a27 Merge pull request #262 from AnalogJ/beta
pre-v0.4.7 release
2022-05-25 07:50:21 -07:00
Jason Kulatunga 307c2bcdef fix error message.
Simpler GormMigrateOptions.
2022-05-25 07:39:56 -07:00
Jason Kulatunga d62928aaae adding documentation for script based notifications. 2022-05-24 15:15:37 -07:00
Jason Kulatunga d08a1e3ef6 ignore retention policy errors during migration.
- fixes #256
2022-05-24 14:26:40 -07:00
Jason Kulatunga 2292041f9f never drop tables. 2022-05-24 10:00:41 -07:00
Jason Kulatunga 75e4bf1d6e added a helpful comment that the database migration might take a looong time. 2022-05-24 09:47:09 -07:00
Jason Kulatunga 97add04276 make sure the migration step runs with transactions, so that we can debug easier.
- related #256
2022-05-24 09:07:38 -07:00
Jason Kulatunga 1423f55d78 remove Power Cycle Count failure attribute for ATA drives. Unrealistic for consumer users (BackBlaze data is datacenter focused).
- fixed #31
2022-05-23 10:19:12 -07:00
Jason Kulatunga 46d0b70399 disable NVMe Scrutiny failures for "Numb Error Log Entries" attribute. More analysis needed for NVMe drives & their critical attributes.
- fixes #187
- fixes #247
2022-05-23 09:50:15 -07:00
Jason Kulatunga 168ca802d1 add support for specifying scheme for influxdb endpoint url (http vs https).
fixes #258
2022-05-23 09:34:16 -07:00
Jason Kulatunga 8c07e91f39 grey out and mark thresholds as not yet implemented. 2022-05-23 09:23:22 -07:00
Jason Kulatunga 7979950c3b fixing device sort and display title.
fixes #194
2022-05-23 08:49:51 -07:00
Jason Kulatunga 9846ba13e0 adding support for device sort in UI. 2022-05-23 08:19:58 -07:00
Jason Kulatunga 83839f7faf adding group by hostId support in dashboard.
fixes #151
2022-05-20 22:33:59 -07:00
Jason Kulatunga 85fa3b1f8f moved device summary info panel into isolated component. 2022-05-20 21:55:40 -07:00
Jason Kulatunga 4190f9a633 remove filter not implemented message. 2022-05-20 20:59:29 -07:00
Jason Kulatunga 743ce27d2e adding comment. 2022-05-20 20:59:29 -07:00
Jason Kulatunga 399a2450ff make sure we can change the temperature duration key for the chart. 2022-05-20 20:59:29 -07:00
Jason Kulatunga 934f16f0a5 persist settings across sessions (in local storage). 2022-05-20 20:59:29 -07:00
Jason Kulatunga 0aeb13c181 support custom display of devices by UUID/ID/Label & Scrutiny Name. (Does not persist).
Related #225
2022-05-20 20:59:29 -07:00
Jason Kulatunga 5899bf2026 started working on Dashboard UI sorting and naming 2022-05-20 20:59:29 -07:00
Jason Kulatunga 3b137964fc make sure we include the host id in the temp history label. 2022-05-20 20:59:29 -07:00
Jason Kulatunga 1bfdd0043f added a way to retrieve raw udev data. Can be used to retrieve disk label, UUID and "disk/by-id/*" device info.
Storing it in the database during device registration.
2022-05-20 20:59:29 -07:00
Jason Kulatunga 999c12748c added a way to retrieve raw udev data. Can be used to retrieve disk label, UUID and "disk/by-id/*" device info.
Storing it in the database during device registration.
2022-05-20 20:59:29 -07:00
Jason Kulatunga 6f283fd736 Update README.md 2022-05-20 10:25:02 -07:00
packagrio-bot 65d31046a0 (v0.4.6) Automated packaging of release by Packagr 2022-05-20 17:02:35 +00:00
Jason Kulatunga 601d632ae4 update xgo version. 2022-05-20 09:52:24 -07:00
Jason Kulatunga 8466c5e750 upgrade to v2.9.0 for influxdb sdk -- this includes the SetupWithToken method. 2022-05-20 09:18:01 -07:00
Jason Kulatunga aa786c0db8 upgrade to go 1.17 2022-05-18 09:40:52 -07:00
Jason Kulatunga f3faee389b trying to fix the docker builds. 2022-05-18 09:30:37 -07:00
Jason Kulatunga 5ac0aa8f74 Forked InfluxDB SDK and added support for using pre-generated admin token during setup. This ensures we no longer need to persist the token during startup.
fixes #248
2022-05-18 09:14:05 -07:00
Jason Kulatunga a589d11d01 update influxdb host default to localhost. 2022-05-17 09:39:03 -07:00
packagrio-bot 1a05868381 (v0.4.5) Automated packaging of release by Packagr 2022-05-15 05:29:17 +00:00
Jason Kulatunga a35c3bae08 make 50-cron-config and entrypoint-collector.sh mirrors of each other. This is for ease of maintainability.
related #121
2022-05-14 22:04:46 -07:00
Jason Kulatunga 0c908786e0 Update TROUBLESHOOTING_INFLUXDB.md 2022-05-14 18:13:25 -07:00
Jason Kulatunga 2ba196d6a8 added instructions about upgrading from 0.3.x to 0.4.x 2022-05-13 16:30:07 -07:00
Jason Kulatunga 0e00999d79 added instructions about upgrading from 0.3.x to 0.4.x 2022-05-13 16:29:07 -07:00
Jason Kulatunga 5adceeb9a5 update with SMART vs Scrutiny details. 2022-05-12 21:58:36 -07:00
packagrio-bot b5920e35e3 (v0.4.4) Automated packaging of release by Packagr 2022-05-13 02:00:33 +00:00
Jason Kulatunga fb8f248366 add image for instructions. 2022-05-12 17:29:13 -07:00
Jason Kulatunga 7a931bd018 instructions for how to get your influxdb admin token + other troubleshooting tricks. 2022-05-12 16:00:04 -07:00
Jason Kulatunga a004f85145 fixing cron.
related #121
2022-05-12 15:24:51 -07:00
Jason Kulatunga eeb086c77f using export -p ratehr than printenv to export environmental variables (export -p correctly wraps envs in quotes) 2022-05-12 15:06:29 -07:00
Jason Kulatunga 54b195f851 updating ignore file for testing. 2022-05-12 14:09:20 -07:00
Jason Kulatunga 5dc79134b2 cron file consistent logging (still broken) 2022-05-12 13:20:54 -07:00
Jason Kulatunga f3fad47d9e cleanup example config files. added instructions for influxdb. 2022-05-12 10:33:54 -07:00
Jason Kulatunga 489534cb73 update manual instructions to include InfluxDB installation. 2022-05-12 10:30:36 -07:00
Jason Kulatunga e7801619cd added additional tests from #187.
Detected that the frontend was incorrectly classifying Scrutiny Failures as Warnings.

Fixed.
2022-05-12 10:04:06 -07:00
Jason Kulatunga 7b75b5f9bb update docker instructions. 2022-05-12 09:26:00 -07:00
Jason Kulatunga 0022d848d6 added example hub/spoke docker-compose file. 2022-05-12 09:26:00 -07:00
Jason Kulatunga d47c4ea99a added example hub/spoke docker-compose file. 2022-05-12 09:26:00 -07:00
Jason Kulatunga 62354f2ab8 created example omnibus docker-compose file. 2022-05-12 09:26:00 -07:00
Jason Kulatunga 3a0adb406f Merge pull request #243 from Zorlin/master
Add Ansible instructions
2022-05-12 09:21:36 -07:00
Jason Kulatunga 2a39421524 Merge pull request #245 from henfri/patch-1 2022-05-11 11:34:52 -07:00
henfri a7dc68822f Update docker-compose.yml
to fix https://github.com/AnalogJ/scrutiny/issues/241
2022-05-11 20:09:40 +02:00
Benjamin Arntzen 3ad87aecc6 Add Ansible instructions 2022-05-11 10:06:09 +08:00
Jason Kulatunga 2ab714f575 Update README.md 2022-05-10 15:33:53 -07:00
Jason Kulatunga 9e60fb8d73 schedule the collector upload before the webapp upload (webapp seems to fail with OOM error during compilation) 2022-05-10 08:49:52 -07:00
Jason Kulatunga a846522830 disable sponsor label. broken. 2022-05-10 08:42:35 -07:00
Jason Kulatunga 3d7d276236 enable caching on all docker builds. 2022-05-09 21:55:48 -07:00
Jason Kulatunga 2660af7ce3 build arm32v7 images for web and collector (cannot for omnibus because InfluxDB does not support it yet. See #236 ) 2022-05-09 21:09:53 -07:00
Jason Kulatunga f5af86fd46 using docker build cache for speedier builds 2022-05-09 20:37:15 -07:00
Jason Kulatunga 4bad2d7b03 upgrading github actions for docker builds. (hopefully faster). 2022-05-09 20:21:52 -07:00
Jason Kulatunga a79930916e only run CI on PR. 2022-05-09 19:58:52 -07:00
packagrio-bot 9ea283e8d2 (v0.4.3) Automated packaging of release by Packagr 2022-05-10 02:57:43 +00:00
Jason Kulatunga bd6d192006 fix packagr config. 2022-05-09 19:49:18 -07:00
Jason Kulatunga 39848eda0b provide info about freebsd binaries. 2022-05-09 19:28:42 -07:00
packagrio-bot 2f67d6f9ae (v0.4.2) Automated packaging of release by Packagr 2022-05-10 02:17:50 +00:00
Jason Kulatunga 30153f9656 adding mechanism to rebuild freebsd artifacts manually. 2022-05-09 18:42:51 -07:00
Jason Kulatunga 3150201348 adding mechanism to rebuild freebsd artifacts manually. 2022-05-09 18:38:46 -07:00
Jason Kulatunga bce6225e9a added list of supported architectures. 2022-05-09 18:28:08 -07:00
Jason Kulatunga 893774c557 trying to fix freebsd builds. 2022-05-09 18:20:47 -07:00
Jason Kulatunga 145996055a use locked versions of database models when doing migrations. 2022-05-09 17:06:02 -07:00
Jason Kulatunga 1cae5ea864 create a "beta" branch for testing docker images. 2022-05-09 16:44:06 -07:00
Jason Kulatunga 0fe6e74eb4 clarification to REVERSE_PROXY docs. 2022-05-09 16:41:25 -07:00
Jason Kulatunga eb4a738746 documentation & updates for configuring scrutiny behind a reverse proxy with path rewriting. 2022-05-09 16:37:05 -07:00
68 changed files with 3275 additions and 353 deletions
+2 -2
View File
@@ -1,12 +1,12 @@
name: CI
# This workflow is triggered on pushes & pull requests
on: [push, pull_request]
on: [pull_request]
jobs:
build:
name: Build
runs-on: ubuntu-latest
container: techknowlogick/xgo:go-1.13.x
container: techknowlogick/xgo:go-1.17.x
# Service containers to run with `build` (Required for end-to-end testing)
services:
+24 -19
View File
@@ -3,7 +3,7 @@ on:
schedule:
- cron: '36 12 * * *'
push:
branches: [ master, influxdb ]
branches: [ master, beta ]
# Publish semver tags as releases.
tags: [ 'v*.*.*' ]
@@ -22,14 +22,14 @@ jobs:
- name: Checkout repository
uses: actions/checkout@v2
- name: Set up QEMU
uses: docker/setup-qemu-action@v1
uses: docker/setup-qemu-action@v2
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1
uses: docker/setup-buildx-action@v2
# Login against a Docker registry except on PR
# https://github.com/docker/login-action
- name: Log into registry ${{ env.REGISTRY }}
if: github.event_name != 'pull_request'
uses: docker/login-action@28218f9b04b4f3f62068d7b6ce6ca5b26e35336c
uses: docker/login-action@v2
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
@@ -38,7 +38,7 @@ jobs:
# https://github.com/docker/metadata-action
- name: Extract Docker metadata
id: meta
uses: docker/metadata-action@98669ae865ea3cffbcbaa878cf57c20bbf1c6c38
uses: docker/metadata-action@v4
with:
tags: |
type=ref,enable=true,event=branch,suffix=-collector
@@ -48,14 +48,16 @@ jobs:
# Build and push Docker image with Buildx (don't push on PR)
# https://github.com/docker/build-push-action
- name: Build and push Docker image
uses: docker/build-push-action@ad44023a93711e3deb337508980b4b5e9bcdc5dc
uses: docker/build-push-action@v3
with:
platforms: linux/amd64,linux/arm64
platforms: linux/amd64,linux/arm64,linux/arm/v7
context: .
file: docker/Dockerfile.collector
push: ${{ github.event_name != 'pull_request' }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
web:
runs-on: ubuntu-latest
@@ -67,14 +69,14 @@ jobs:
- name: Checkout repository
uses: actions/checkout@v2
- name: Set up QEMU
uses: docker/setup-qemu-action@v1
uses: docker/setup-qemu-action@v2
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1
uses: docker/setup-buildx-action@v2
# Login against a Docker registry except on PR
# https://github.com/docker/login-action
- name: Log into registry ${{ env.REGISTRY }}
if: github.event_name != 'pull_request'
uses: docker/login-action@28218f9b04b4f3f62068d7b6ce6ca5b26e35336c
uses: docker/login-action@v2
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
@@ -83,7 +85,7 @@ jobs:
# https://github.com/docker/metadata-action
- name: Extract Docker metadata
id: meta
uses: docker/metadata-action@98669ae865ea3cffbcbaa878cf57c20bbf1c6c38
uses: docker/metadata-action@v4
with:
tags: |
type=ref,enable=true,event=branch,suffix=-web
@@ -93,15 +95,16 @@ jobs:
# Build and push Docker image with Buildx (don't push on PR)
# https://github.com/docker/build-push-action
- name: Build and push Docker image
uses: docker/build-push-action@ad44023a93711e3deb337508980b4b5e9bcdc5dc
uses: docker/build-push-action@v3
with:
platforms: linux/amd64,linux/arm64
platforms: linux/amd64,linux/arm64,linux/arm/v7
context: .
file: docker/Dockerfile.web
push: ${{ github.event_name != 'pull_request' }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
omnibus:
runs-on: ubuntu-latest
permissions:
@@ -112,14 +115,14 @@ jobs:
- name: Checkout repository
uses: actions/checkout@v2
- name: Set up QEMU
uses: docker/setup-qemu-action@v1
uses: docker/setup-qemu-action@v2
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1
uses: docker/setup-buildx-action@v2
# Login against a Docker registry except on PR
# https://github.com/docker/login-action
- name: Log into registry ${{ env.REGISTRY }}
if: github.event_name != 'pull_request'
uses: docker/login-action@28218f9b04b4f3f62068d7b6ce6ca5b26e35336c
uses: docker/login-action@v2
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
@@ -128,7 +131,7 @@ jobs:
# https://github.com/docker/metadata-action
- name: Extract Docker metadata
id: meta
uses: docker/metadata-action@98669ae865ea3cffbcbaa878cf57c20bbf1c6c38
uses: docker/metadata-action@v4
with:
tags: |
type=ref,enable=true,event=branch,suffix=-omnibus
@@ -138,7 +141,7 @@ jobs:
# Build and push Docker image with Buildx (don't push on PR)
# https://github.com/docker/build-push-action
- name: Build and push Docker image
uses: docker/build-push-action@ad44023a93711e3deb337508980b4b5e9bcdc5dc
uses: docker/build-push-action@v3
with:
platforms: linux/amd64,linux/arm64
context: .
@@ -146,3 +149,5 @@ jobs:
push: ${{ github.event_name != 'pull_request' }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
+19 -13
View File
@@ -5,11 +5,17 @@ on:
release:
# Only use the types keyword to narrow down the activity types that will trigger your workflow.
types: [published]
workflow_dispatch:
inputs:
tag_name:
description: 'tag to build artifacts for'
required: true
default: 'v0.0.0'
jobs:
release-freebsd:
name: Release FreeBSD
runs-on: macos-latest
runs-on: macos-10.15
env:
PROJECT_PATH: /go/src/github.com/analogj/scrutiny
GOPATH: /go
@@ -19,9 +25,9 @@ jobs:
- name: Checkout
uses: actions/checkout@v2
with:
ref: ${{github.event.release.tag_name}}
ref: ${{github.event.release.tag_name || github.event.inputs.tag_name }}
- name: Build Binaries
uses: vmactions/freebsd-vm@v0.1.3
uses: vmactions/freebsd-vm@v0.1.5
with:
envs: 'PROJECT_PATH GOPATH GOOS GOARCH'
usesh: true
@@ -53,6 +59,16 @@ jobs:
file "$GITHUB_WORKSPACE/dist/scrutiny-collector-metrics-${GOOS}-${GOARCH}" || true
ldd "$GITHUB_WORKSPACE/dist/scrutiny-collector-metrics-${GOOS}-${GOARCH}" || true
- name: Release Asset - Collector - freebsd-amd64
id: upload-release-asset2
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.SCRUTINY_GITHUB_TOKEN }}
with:
upload_url: ${{ github.event.release.upload_url }} # This pulls from the CREATE RELEASE step above, referencing it's ID to get its outputs object, which include a `upload_url`. See this blog post for more info: https://jasonet.co/posts/new-features-of-github-actions/#passing-data-to-future-steps
asset_path: './dist/scrutiny-collector-metrics-freebsd-amd64'
asset_name: scrutiny-collector-metrics-freebsd-amd64
asset_content_type: application/octet-stream
- name: Release Asset - Web - freebsd-amd64
id: upload-release-asset1
@@ -65,13 +81,3 @@ jobs:
asset_name: scrutiny-web-freebsd-amd64
asset_content_type: application/octet-stream
- name: Release Asset - Collector - freebsd-amd64
id: upload-release-asset2
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.SCRUTINY_GITHUB_TOKEN }}
with:
upload_url: ${{ github.event.release.upload_url }} # This pulls from the CREATE RELEASE step above, referencing it's ID to get its outputs object, which include a `upload_url`. See this blog post for more info: https://jasonet.co/posts/new-features-of-github-actions/#passing-data-to-future-steps
asset_path: './dist/scrutiny-collector-metrics-freebsd-amd64'
asset_name: scrutiny-collector-metrics-freebsd-amd64
asset_content_type: application/octet-stream
+1 -1
View File
@@ -16,7 +16,7 @@ jobs:
build:
name: Build
runs-on: ubuntu-latest
container: techknowlogick/xgo:go-1.13.x
container: techknowlogick/xgo:go-1.17.x
# Service containers to run with `build` (Required for end-to-end testing)
services:
influxdb:
+1
View File
@@ -8,6 +8,7 @@ jobs:
build:
name: is-sponsor-label
runs-on: ubuntu-latest
if: ${{ false }}
steps:
- uses: JasonEtco/is-sponsor-label-action@v1.2.0
env:
+2
View File
@@ -64,3 +64,5 @@ scrutiny-*.db
scrutiny_test.db
scrutiny.yaml
coverage.txt
/config
/influxdb
+25 -3
View File
@@ -70,8 +70,10 @@ Scrutiny uses `smartctl --scan` to detect devices/drives.
If you're using Docker, getting started is as simple as running the following command:
> See [docker/example.omnibus.docker-compose.yml](./docker/example.omnibus.docker-compose.yml) for a docker-compose file.
```bash
docker run -it --rm -p 8080:8080 \
docker run -it --rm -p 8080:8080 -p 8086:8086 \
-v `pwd`/scrutiny:/opt/scrutiny/config \
-v `pwd`/influxdb2:/opt/scrutiny/influxdb \
-v /run/udev:/run/udev:ro \
@@ -95,6 +97,8 @@ In addition to the Omnibus image (available under the `latest` tag) there are 2
- `ghcr.io/analogj/scrutiny:master-collector` - Contains the Scrutiny data collector, `smartctl` binary and cron-like scheduler. You can run one collector on each server.
- `ghcr.io/analogj/scrutiny:master-web` - Contains the Web UI, API and Database. Only one container necessary
> See [docker/example.hubspoke.docker-compose.yml](./docker/example.hubspoke.docker-compose.yml) for a docker-compose file.
```bash
docker run --rm -p 8086:8086 \
-v `pwd`/influxdb2:/var/lib/influxdb2 \
@@ -111,7 +115,7 @@ docker run --rm \
--cap-add SYS_RAWIO \
--device=/dev/sda \
--device=/dev/sdb \
-e SCRUTINY_API_ENDPOINT=http://SCRUTINY_WEB_IPADDRESS:8080 \
-e COLLECTOR_API_ENDPOINT=http://SCRUTINY_WEB_IPADDRESS:8080 \
--name scrutiny-collector \
ghcr.io/analogj/scrutiny:master-collector
```
@@ -174,7 +178,9 @@ Scrutiny supports sending SMART device failure notifications via the following s
- Telegram
- Tulip
Check the `notify.urls` section of [example.scrutiny.yml](example.scrutiny.yaml) for more information and documentation for service specific setup.
Check the `notify.urls` section of [example.scrutiny.yml](example.scrutiny.yaml) for examples.
For more information and troubleshooting, see the [TROUBLESHOOTING_NOTIFICATIONS.md](./docs/TROUBLESHOOTING_NOTIFICATIONS.md) file
### Testing Notifications
@@ -225,6 +231,22 @@ Or if you're not using docker, you can pass CLI arguments to the collector durin
scrutiny-collector-metrics run --debug --log-file /tmp/collector.log
```
# Supported Architectures
| Architecture Name | Binaries | Docker |
| --- | --- | --- |
| amd64 | :white_check_mark: | :white_check_mark: |
| arm-5 | :white_check_mark: | |
| arm-6 | :white_check_mark: | |
| arm-7 | :white_check_mark: | web/collector only. see [#236](https://github.com/AnalogJ/scrutiny/issues/236) |
| arm64 | :white_check_mark: | :white_check_mark: |
| freebsd | collector only. see [#238](https://github.com/AnalogJ/scrutiny/issues/238) | |
| macos-amd64 | | :white_check_mark: |
| macos-arm64 | | :white_check_mark: |
| windows-amd64 | :white_check_mark: | |
# Contributing
Please see the [CONTRIBUTING.md](CONTRIBUTING.md) for instructions for how to develop and contribute to the scrutiny codebase.
@@ -161,7 +161,8 @@ OPTIONS:
&cli.StringFlag{
Name: "api-endpoint",
Usage: "The api server endpoint",
EnvVars: []string{"SCRUTINY_API_ENDPOINT"},
EnvVars: []string{"COLLECTOR_API_ENDPOINT", "SCRUTINY_API_ENDPOINT"},
//SCRUTINY_API_ENDPOINT is deprecated, but kept for backwards compatibility
},
&cli.StringFlag{
@@ -114,7 +114,7 @@ OPTIONS:
Name: "api-endpoint",
Usage: "The api server endpoint",
Value: "http://localhost:8080",
EnvVars: []string{"SCRUTINY_API_ENDPOINT"},
EnvVars: []string{"COLLECTOR_API_ENDPOINT"},
},
&cli.StringFlag{
+1 -1
View File
@@ -13,7 +13,7 @@ func DevicePrefix() string {
func (d *Detect) Start() ([]models.Device, error) {
d.Shell = shell.Create()
// call the base/common functionality to get a list of devicess
// call the base/common functionality to get a list of devices
detectedDevices, err := d.SmartctlScan()
if err != nil {
return nil, err
+52
View File
@@ -1,9 +1,12 @@
package detect
import (
"fmt"
"github.com/analogj/scrutiny/collector/pkg/common/shell"
"github.com/analogj/scrutiny/collector/pkg/models"
"github.com/jaypipes/ghw"
"io/ioutil"
"path/filepath"
"strings"
)
@@ -22,6 +25,7 @@ func (d *Detect) Start() ([]models.Device, error) {
//inflate device info for detected devices.
for ndx, _ := range detectedDevices {
d.SmartCtlInfo(&detectedDevices[ndx]) //ignore errors.
populateUdevInfo(&detectedDevices[ndx]) //ignore errors.
}
return detectedDevices, nil
@@ -49,3 +53,51 @@ func (d *Detect) wwnFallback(detectedDevice *models.Device) {
//wwn must always be lowercase.
detectedDevice.WWN = strings.ToLower(detectedDevice.WWN)
}
// as discussed in
// - https://github.com/AnalogJ/scrutiny/issues/225
// - https://github.com/jaypipes/ghw/issues/59#issue-361915216
// udev exposes its data in a standardized way under /run/udev/data/....
func populateUdevInfo(detectedDevice *models.Device) error {
// Get device major:minor numbers
// `cat /sys/class/block/sda/dev`
devNo, err := ioutil.ReadFile(filepath.Join("/sys/class/block/", detectedDevice.DeviceName, "dev"))
if err != nil {
return err
}
// Look up block device in udev runtime database
// `cat /run/udev/data/b8:0`
udevID := "b" + strings.TrimSpace(string(devNo))
udevBytes, err := ioutil.ReadFile(filepath.Join("/run/udev/data/", udevID))
if err != nil {
return err
}
deviceMountPaths := []string{}
udevInfo := make(map[string]string)
for _, udevLine := range strings.Split(string(udevBytes), "\n") {
if strings.HasPrefix(udevLine, "E:") {
if s := strings.SplitN(udevLine[2:], "=", 2); len(s) == 2 {
udevInfo[s[0]] = s[1]
}
} else if strings.HasPrefix(udevLine, "S:") {
deviceMountPaths = append(deviceMountPaths, udevLine[2:])
}
}
//Set additional device information.
if deviceLabel, exists := udevInfo["ID_FS_LABEL"]; exists {
detectedDevice.DeviceLabel = deviceLabel
}
if deviceUUID, exists := udevInfo["ID_FS_UUID"]; exists {
detectedDevice.DeviceUUID = deviceUUID
}
if deviceSerialID, exists := udevInfo["ID_SERIAL"]; exists {
detectedDevice.DeviceSerialID = fmt.Sprintf("%s-%s", udevInfo["ID_BUS"], deviceSerialID)
}
return nil
}
+9 -2
View File
@@ -1,10 +1,13 @@
package models
type Device struct {
WWN string `json:"wwn"`
HostId string `json:"host_id"`
WWN string `json:"wwn"`
DeviceName string `json:"device_name"`
DeviceUUID string `json:"device_uuid"`
DeviceSerialID string `json:"device_serial_id"`
DeviceLabel string `json:"device_label"`
Manufacturer string `json:"manufacturer"`
ModelName string `json:"model_name"`
InterfaceType string `json:"interface_type"`
@@ -17,6 +20,10 @@ type Device struct {
SmartSupport bool `json:"smart_support"`
DeviceProtocol string `json:"device_protocol"` //protocol determines which smart attribute types are available (ATA, NVMe, SCSI)
DeviceType string `json:"device_type"` //device type is used for querying with -d/t flag, should only be used by collector.
// User provided metadata
Label string `json:"label"`
HostId string `json:"host_id"`
}
type DeviceWrapper struct {
+1 -1
View File
@@ -1,5 +1,5 @@
########
FROM golang:1.14.4-buster as backendbuild
FROM golang:1.17.10-buster as backendbuild
WORKDIR /go/src/github.com/analogj/scrutiny
+1 -1
View File
@@ -1,5 +1,5 @@
########
FROM golang:1.14.4-buster as backendbuild
FROM golang:1.17.10-buster as backendbuild
WORKDIR /go/src/github.com/analogj/scrutiny
+1 -1
View File
@@ -1,5 +1,5 @@
########
FROM golang:1.14.4-buster as backendbuild
FROM golang:1.17.10-buster as backendbuild
WORKDIR /go/src/github.com/analogj/scrutiny
+1 -1
View File
@@ -1,4 +1,4 @@
FROM techknowlogick/xgo:go-1.13.x
FROM techknowlogick/xgo:go-1.17.x
WORKDIR /go/src/github.com/analogj/scrutiny
+9 -4
View File
@@ -1,14 +1,19 @@
#!/bin/bash
# Cron runs in its own isolated environment (usually using only /etc/environment )
# So when the container starts up, we will do a dump of the runtime environment into a .env file that we
# will then source into the crontab file (/etc/cron.d/scrutiny.sh)
printenv | sed 's/^\(.*\)$/export \1/g' > /env.sh
# will then source into the crontab file (/etc/cron.d/scrutiny)
(set -o posix; export -p) > /env.sh
# adding ability to customize the cron schedule.
COLLECTOR_CRON_SCHEDULE=${COLLECTOR_CRON_SCHEDULE:-"0 0 * * *"}
# if the cron schedule has been overridden via env variable (eg docker-compose) we should make sure to strip quotes
[[ "${COLLECTOR_CRON_SCHEDULE}" == \"*\" || "${COLLECTOR_CRON_SCHEDULE}" == \'*\' ]] && COLLECTOR_CRON_SCHEDULE="${COLLECTOR_CRON_SCHEDULE:1:-1}"
# replace placeholder with correct value
sed -i 's|{COLLECTOR_CRON_SCHEDULE}|'"${COLLECTOR_CRON_SCHEDULE}"'|g' /etc/cron.d/scrutiny
# now that we have the env start cron in the foreground
echo "starting cron"
su -c "cron -l 8 -f" root
su -c "cron -f -L 15" root
@@ -0,0 +1,48 @@
version: '2.4'
services:
influxdb:
image: influxdb:2.2
ports:
- '8086:8086'
volumes:
- './influxdb:/var/lib/influxdb2'
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8086/health"]
interval: 5s
timeout: 10s
retries: 20
web:
image: 'ghcr.io/analogj/scrutiny:master-web'
ports:
- '8080:8080'
volumes:
- './config:/opt/scrutiny/config'
environment:
SCRUTINY_WEB_INFLUXDB_HOST: 'influxdb'
depends_on:
influxdb:
condition: service_healthy
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8080/api/health"]
interval: 5s
timeout: 10s
retries: 20
start_period: 10s
collector:
image: 'ghcr.io/analogj/scrutiny:master-collector'
cap_add:
- SYS_RAWIO
volumes:
- '/run/udev:/run/udev:ro'
environment:
COLLECTOR_API_ENDPOINT: 'http://web:8080'
depends_on:
web:
condition: service_healthy
devices:
- "/dev/sda"
- "/dev/sdb"
@@ -7,11 +7,12 @@ services:
cap_add:
- SYS_RAWIO
ports:
- "8080:8080"
- "8080:8080" # webapp
- "8086:8086" # influxDB admin
volumes:
- /run/udev:/run/udev:ro
- ./config:/opt/scrutiny/config
- ./influxdb:/var/lib/influxdb2
- ./influxdb:/opt/scrutiny/influxdb
devices:
- "/dev/sda"
- "/dev/sdb"
+17
View File
@@ -0,0 +1,17 @@
# Ansible Install
[Zorlin](https://github.com/Zorlin) has developed and now maintains [an Ansible playbook](https://github.com/Zorlin/scrutiny-playbook) which automates the steps involved in manually setting up Scrutiny.
Using it is simple:
* Grab a copy of the playbook
* Follow the directions in the playbook repository
* Run `ansible-playbook site.yml`
* Visit http://your-machine:8080 to see your new Scrutiny installation.
It will automatically pull metrics from machines once a day, at 1am.
You can see it in action below.
[![asciicast](https://asciinema.org/a/493531.svg)](https://asciinema.org/a/493531)
+1
View File
@@ -0,0 +1 @@
> See [docker/example.hubspoke.docker-compose.yml](./docker/example.hubspoke.docker-compose.yml) for a docker-compose file.
+19 -2
View File
@@ -2,12 +2,18 @@
While the easiest way to get started with [Scrutiny is using Docker](https://github.com/AnalogJ/scrutiny#docker),
it is possible to run it manually without much work. You can even mix and match, using Docker for one component and
a manual installation for the other.
a manual installation for the other. There's also [an installer](INSTALL_ANSIBLE.md) which automates this manual installation procedure.
Scrutiny is made up of two components: a collector and a webapp/api. Here's how each component can be deployed manually.
Scrutiny is made up of three components: an influxdb Database, a collector and a webapp/api. Here's how each component can be deployed manually.
> Note: the `/opt/scrutiny` directory is not hardcoded, you can use any directory name/path.
## InfluxDB
Please follow the official InfluxDB installation guide. Note, you'll need to install v2.2.0+.
https://docs.influxdata.com/influxdb/v2.2/install/
## Webapp/API
### Dependencies
@@ -45,6 +51,17 @@ web:
# The path to the Scrutiny frontend files (js, css, images) must be specified.
# We'll populate it with files in the next section
path: /opt/scrutiny/web
# if you're runnning influxdb on a different host (or using a cloud-provider) you'll need to update the host & port below.
# token, org, bucket are unnecessary for a new InfluxDB installation, as Scrutiny will automatically run the InfluxDB setup,
# and store the information in the config file. If you 're re-using an existing influxdb installation, you'll need to provide
# the `token`
influxdb:
host: 0.0.0.0
port: 8086
# token: 'my-token'
# org: 'my-org'
# bucket: 'bucket'
```
> Note: for a full list of available configuration options, please check the [example.scrutiny.yaml](https://github.com/AnalogJ/scrutiny/blob/master/example.scrutiny.yaml) file.
+13
View File
@@ -118,6 +118,19 @@ instead of the block device (`/dev/nvme0n1`). See [#209](https://github.com/Anal
### Volume Mount All Devices (`/dev`) - Privileged
## Scrutiny detects Failure but SMART Passed?
There's 2 different mechanisms that Scrutiny uses to detect failures.
The first is simple SMART failures. If SMART thinks an attribute is in a failed state, Scrutiny will display it as failed as well.
The second is using BackBlaze failure data: [https://backblaze.com/blog-smart-stats-2014-8.html](https://backblaze.com/blog-smart-stats-2014-8.html)
If Scrutiny detects that an attribute corresponds with a high rate of failure using BackBlaze's data, it will also mark that attribute (and disk) as failed (even though SMART may think the device is still healthy).
This can cause some confusion when comparing Scrutiny's dashboard against other SMART analysis tools.
If you hover over the "failed" label beside an attribute, Scrutiny will tell you if the failure was due to SMART or Scrutiny/BackBlaze data.
## Hub & Spoke model, with multiple Hosts.
When deploying Scrutiny in a hub & spoke model, it can be difficult to determine exactly which node a set of devices are associated with.
+68
View File
@@ -0,0 +1,68 @@
# InfluxDB Troubleshooting
## Installation
InfluxDB is a required dependency for Scrutiny v0.4.0+.
https://docs.influxdata.com/influxdb/v2.2/install/
## Persistence
To ensure that all data is correctly stored, you must also persist the InfluxDB database directory
- If you're using the Official Scrutiny Omnibus image (`ghcr.io/analogj/scrutiny:master-omnibus`), the path is `/opt/scrutiny/influxdb`
- If you're deploying in Hub/Spoke mode with the InfluxDB maintained image (`influxdb:2.2`), the path is `/var/lib/influxdb2`
If you attempt to restart Scrutiny but you forgot to persist the InfluxDB directory, you will get an error message like follows:
```
scrutiny | time="2022-05-12T22:54:12Z" level=info msg="Trying to connect to scrutiny sqlite db: /opt/scrutiny/config/scrutiny.db\n"
scrutiny | time="2022-05-12T22:54:12Z" level=info msg="Successfully connected to scrutiny sqlite db: /opt/scrutiny/config/scrutiny.db\n"
scrutiny | ts=2022-05-12T22:54:12.240791Z lvl=info msg=Unauthorized log_id=0aQcVlOW000 error="authorization not found"
scrutiny | panic: unauthorized: unauthorized access
```
Unfortunately this may mean that your database is lost, and the previous Scrutiny data is unavailable.
You should fix the docker-compose/docker run command that you're using to ensure that your database folder is persisted correctly,
then delete the `web.influxdb.token` field in your `scrutiny.yaml` file, and then restart Scrutiny.
## First Start
The web/api service will trigger an InfluxDB onboarding process automatically when it first starts. After that, it will store the newly generated influxdb api token in the Scrutiny config file.
If this Credential is not correctly stored in the scrutiny config file, Scrutiny will fail to start (with an authentication error)
```
scrutiny | time="2022-05-12T22:52:55Z" level=info msg="Successfully connected to scrutiny sqlite db: /opt/scrutiny/config/scrutiny.db\n"
scrutiny | ts=2022-05-12T22:52:55.235753Z lvl=error msg="failed to onboard user admin" log_id=0aQcRnc0000 handler=onboard error="onboarding has already been completed" took=0.038ms
scrutiny | ts=2022-05-12T22:52:55.235816Z lvl=error msg="api error encountered" log_id=0aQcRnc0000 error="onboarding has already been completed"
scrutiny | panic: conflict: onboarding has already been completed
```
You can fix this issue by authenticating to the InfluxDB admin portal (the default credentials are username: `admin`, password: `password12345`),
then retrieving the API token, and writing it to your `scrutiny.yaml` config file under the `web.influxdb.token` field:
![influx db admin token](./influxdb-admin-token.png)
## Upgrading from v0.3.x to v0.4.x
When upgrading from v0.3.x to v0.4.x, some users have noticed problems such as:
```
2022/05/13 14:38:05 Loading configuration file: /opt/scrutiny/config/scrutiny.yaml
time="2022-05-13T14:38:05Z" level=info msg="Trying to connect to scrutiny sqlite db:"
time="2022-05-13T14:38:05Z" level=info msg="Successfully connected to scrutiny sqlite db:"
panic: a username and password is required for a setup
```
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:
- Upgrading from the LSIO Scrutiny image to the Official Scrutiny image, without removing LSIO specific environmental 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.
- 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:
- 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
+25
View File
@@ -0,0 +1,25 @@
# Notifications
As documented in [example.scrutiny.yaml](https://github.com/AnalogJ/scrutiny/blob/master/example.scrutiny.yaml#L59-L75)
there are multiple ways to configure notifications for Scrutiny.
Under the hood we use a library called [Shoutrrr](https://github.com/containrrr/shoutrrr) to send our notifications, and you should use their documentation if you run into
any issues: https://containrrr.dev/shoutrrr/services/overview/
# Script Notifications
While the Shoutrrr library supports many popular providers for sending notifications Scrutiny also supports a "script" based
notification system, allowing you to execute a custom script whenever a notification needs to be sent.
Data is provided to this script using the following environmental variables:
```
SCRUTINY_SUBJECT - eg. "Scrutiny SMART error (%s) detected on device: %s"
SCRUTINY_DATE
SCRUTINY_FAILURE_TYPE - EmailTest, SmartFail, ScrutinyFail
SCRUTINY_DEVICE_NAME - eg. /dev/sda
SCRUTINY_DEVICE_TYPE - ATA/SCSI/NVMe
SCRUTINY_DEVICE_SERIAL - eg. WDDJ324KSO
SCRUTINY_MESSAGE - eg. "Scrutiny SMART error notification for device: %s\nFailure Type: %s\nDevice Name: %s\nDevice Serial: %s\nDevice Type: %s\nDate: %s"
```
+58
View File
@@ -0,0 +1,58 @@
# Reverse Proxy Support
Scrutiny is designed so that it can be used with a reverse proxy, leveraging `domain`, `port` or `path` based matching to correctly route to the Scrutiny service.
For simple `domain` and/or `port` based routing, this is easy.
If your domain:port pair is similar to `http://scrutiny.example.com` or `http://localhost:54321`, just update your reverse proxy configuration
to route traffic to the Scrutiny backend, which is listening on `0.0.0.0:8080` by default.
```yaml
# default config
web:
listen:
port: 8080
host: 0.0.0.0
```
However if you're using `path` based routing to differentiate your reverse proxy protected services, things become more complicated.
If you'd like to access Scrutiny using a path like: `http://example.com/scrutiny/`, then we need a way to configure Scrutiny so that it
understands `http://example.com/scrutiny/api/health` actually means `http://localhost:8080/api/health`.
Thankfully this can be done by changing **two** settings (both are required).
1. The webserver has a `web.listen.basepath` key
2. The collectors have a `api.endpoint` key.
## Webserver Configuration
When setting the `web.listen.basepath` key in the web config file, make sure the `basepath` key is prefixed with `/`.
```yaml
# customized webserver config
web:
listen:
port: 8080
host: 0.0.0.0
# if you're using a reverse proxy like apache/nginx, you can override this value to serve scrutiny on a subpath.
# eg. http://example.com/custombasepath/* vs http://example.com:8080
basepath: '/custombasepath'
```
## Collector Configuration
Here's how you can update the collector `api.endpoint` key:
```yaml
# customized collector config
api:
endpoint: 'http://localhost:8080/custombasepath'
```
# Environmental Variables.
You may also configure these values using the following environmental variables (both are required).
- `COLLECTOR_API_ENDPOINT=http://localhost:8080/custombasepath`
- `SCRUTINY_WEB_LISTEN_BASEPATH=/custombasepath`
Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

+14
View File
@@ -60,6 +60,10 @@ devices:
#
#api:
# endpoint: 'http://localhost:8080'
# endpoint: 'http://localhost:8080/custombasepath'
# if you need to use a custom base path (for a reverse proxy), you can add a suffix to the endpoint.
# See docs/TROUBLESHOOTING_REVERSE_PROXY.md for more info,
########################################################################################################################
# FEATURES COMING SOON
@@ -68,3 +72,13 @@ devices:
#
########################################################################################################################
#collect:
# metric:
# enable: true
# command: '-a -o on -S on'
# long:
# enable: false
# command: ''
# short:
# enable: false
# command: ''
+6 -17
View File
@@ -34,7 +34,13 @@ web:
# the location on the filesystem where scrutiny javascript + css is located
frontend:
path: /opt/scrutiny/web
# if you're running influxdb on a different host (or using a cloud-provider) you'll need to update the host & port below.
# token, org, bucket are unnecessary for a new InfluxDB installation, as Scrutiny will automatically run the InfluxDB setup,
# and store the information in the config file. If you 're re-using an existing influxdb installation, you'll need to provide
# the `token`
influxdb:
# scheme: 'http'
host: 0.0.0.0
port: 8086
# token: 'my-token'
@@ -75,12 +81,6 @@ log:
#
########################################################################################################################
#disks:
# include:
# # - /dev/sda
# exclude:
# # - /dev/sdb
#limits:
# ata:
# critical:
@@ -95,14 +95,3 @@ log:
# critical: true
# standard: true
#collect:
# metric:
# enable: true
# command: '-a -o on -S on'
# long:
# enable: false
# command: ''
# short:
# enable: false
# command: ''
+61 -9
View File
@@ -1,31 +1,83 @@
module github.com/analogj/scrutiny
go 1.13
go 1.17
require (
github.com/analogj/go-util v0.0.0-20190301173314-5295e364eb14
github.com/citilinkru/libudev v1.0.0 // indirect
github.com/containrrr/shoutrrr v0.4.4
github.com/fatih/color v1.10.0
github.com/gin-gonic/gin v1.6.3
github.com/go-gormigrate/gormigrate/v2 v2.0.0
github.com/golang/mock v1.4.3
github.com/google/uuid v1.2.0 // indirect
github.com/influxdata/influxdb-client-go/v2 v2.2.3
github.com/influxdata/influxdb-client-go/v2 v2.9.0
github.com/jaypipes/ghw v0.6.1
github.com/jinzhu/gorm v1.9.16
github.com/klauspost/compress v1.12.1 // indirect
github.com/kvz/logstreamer v0.0.0-20150507115422-a635b98146f0 // indirect
github.com/mattn/go-sqlite3 v1.14.4 // indirect
github.com/mitchellh/mapstructure v1.2.2
github.com/onsi/ginkgo v1.16.1 // indirect
github.com/sirupsen/logrus v1.4.2
github.com/spf13/viper v1.7.0
github.com/stretchr/testify v1.5.1
github.com/urfave/cli/v2 v2.2.0
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9
golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7 // indirect
gorm.io/driver/sqlite v1.1.3
gorm.io/gorm v1.20.2
)
require (
github.com/StackExchange/wmi v0.0.0-20190523213315-cbe66965904d // 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/deepmap/oapi-codegen v1.8.2 // indirect
github.com/fsnotify/fsnotify v1.4.9 // indirect
github.com/ghodss/yaml v1.0.0 // indirect
github.com/gin-contrib/sse v0.1.0 // indirect
github.com/go-ole/go-ole v1.2.4 // indirect
github.com/go-playground/locales v0.13.0 // indirect
github.com/go-playground/universal-translator v0.17.0 // indirect
github.com/go-playground/validator/v10 v10.2.0 // indirect
github.com/golang/protobuf v1.4.2 // indirect
github.com/google/uuid v1.2.0 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect
github.com/influxdata/line-protocol v0.0.0-20200327222509-2487e7298839 // indirect
github.com/jaypipes/pcidb v0.5.0 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.1 // indirect
github.com/json-iterator/go v1.1.9 // indirect
github.com/klauspost/compress v1.12.1 // indirect
github.com/konsorten/go-windows-terminal-sequences v1.0.2 // indirect
github.com/kvz/logstreamer v0.0.0-20150507115422-a635b98146f0 // indirect
github.com/leodido/go-urn v1.2.0 // indirect
github.com/magiconair/properties v1.8.1 // indirect
github.com/mattn/go-colorable v0.1.8 // indirect
github.com/mattn/go-isatty v0.0.12 // indirect
github.com/mattn/go-sqlite3 v1.14.4 // indirect
github.com/mitchellh/go-homedir v1.1.0 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.1 // indirect
github.com/nxadm/tail v1.4.8 // indirect
github.com/onsi/ginkgo v1.16.1 // indirect
github.com/pelletier/go-toml v1.7.0 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/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/cast v1.3.1 // indirect
github.com/spf13/jwalterweatherman v1.1.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/subosito/gotenv v1.2.0 // indirect
github.com/ugorji/go/codec v1.1.7 // indirect
golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad // indirect
golang.org/x/net v0.0.0-20210119194325-5f4716e94777 // indirect
golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7 // indirect
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1 // indirect
golang.org/x/text v0.3.5 // indirect
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
google.golang.org/protobuf v1.23.0 // indirect
gopkg.in/ini.v1 v1.55.0 // indirect
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect
gopkg.in/yaml.v2 v2.3.0 // indirect
gosrc.io/xmpp v0.5.1 // indirect
howett.net/plist v0.0.0-20181124034731-591f970eefbb // indirect
nhooyr.io/websocket v1.8.7 // indirect
)
+709
View File
@@ -0,0 +1,709 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU=
cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc=
cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0=
cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk=
cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
github.com/PuerkitoBio/goquery v1.5.1/go.mod h1:GsLWisAFVj4WgDibEWF4pvYnkVQBpKBKeU+7zCJoLcc=
github.com/StackExchange/wmi v0.0.0-20190523213315-cbe66965904d h1:G0m3OIz70MZUWq3EgK3CesDbo8upS2Vm9/P3FtgI+Jk=
github.com/StackExchange/wmi v0.0.0-20190523213315-cbe66965904d/go.mod h1:3eOhrUMpNV+6aFIbp5/iudMxNCF27Vw2OZgy4xEx0Fg=
github.com/agnivade/wasmbrowsertest v0.3.1/go.mod h1:zQt6ZTdl338xxRaMW395qccVE2eQm0SjC/SDz0mPWQI=
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/analogj/go-util v0.0.0-20190301173314-5295e364eb14 h1:wsrSjiqQtseStRIoLLxS4C5IEtXkazZVEPDHq8jW7r8=
github.com/analogj/go-util v0.0.0-20190301173314-5295e364eb14/go.mod h1:lJQVqFKMV5/oDGYR2bra2OljcF3CvolAoyDRyOA4k4E=
github.com/andybalholm/cascadia v1.1.0/go.mod h1:GsXiBklL0woXo1j/WYWtSYYC4ouU9PqHO0sqidkEA4Y=
github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=
github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84=
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
github.com/chromedp/cdproto v0.0.0-20190614062957-d6d2f92b486d/go.mod h1:S8mB5wY3vV+vRIzf39xDXsw3XKYewW9X6rW2aEmkrSw=
github.com/chromedp/cdproto v0.0.0-20190621002710-8cbd498dd7a0/go.mod h1:S8mB5wY3vV+vRIzf39xDXsw3XKYewW9X6rW2aEmkrSw=
github.com/chromedp/cdproto v0.0.0-20190812224334-39ef923dcb8d/go.mod h1:0YChpVzuLJC5CPr+x3xkHN6Z8KOSXjNbL7qV8Wc4GW0=
github.com/chromedp/cdproto v0.0.0-20190926234355-1b4886c6fad6/go.mod h1:0YChpVzuLJC5CPr+x3xkHN6Z8KOSXjNbL7qV8Wc4GW0=
github.com/chromedp/chromedp v0.3.1-0.20190619195644-fd957a4d2901/go.mod h1:mJdvfrVn594N9tfiPecUidF6W5jPRKHymqHfzbobPsM=
github.com/chromedp/chromedp v0.4.0/go.mod h1:DC3QUn4mJ24dwjcaGQLoZrhm4X/uPHZ6spDbS2uFhm4=
github.com/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/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ=
github.com/containrrr/shoutrrr v0.4.4 h1:vHZ4E/76pKVY+Jyn/qhBz3X540Bn8NI5ppPHK4PyILY=
github.com/containrrr/shoutrrr v0.4.4/go.mod h1:zqL2BvfC1W4FujrT4b3/ZCLxvD+uoeEpBL7rg9Dqpbg=
github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk=
github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
github.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/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/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
github.com/cpuguy83/go-md2man/v2 v2.0.0 h1:EoUDS0afbrsXAZ9YQ9jdu/mZ2sXgT1/2yyNng4PGlyM=
github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY=
github.com/cyberdelia/templates v0.0.0-20141128023046-ca7fffd4298c/go.mod h1:GyV+0YP4qX0UQ7r2MoYZ+AvYDp12OF5yg4q8rGnyNh4=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/deepmap/oapi-codegen v1.8.2 h1:SegyeYGcdi0jLLrpbCMoJxnUUn8GBXHsvr4rbzjuhfU=
github.com/deepmap/oapi-codegen v1.8.2/go.mod h1:YLgSKSDv/bZQB7N4ws6luhozi3cEdRktEqrX88CvjIw=
github.com/denisenkom/go-mssqldb v0.0.0-20191124224453-732737034ffd/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU=
github.com/denisenkom/go-mssqldb v0.0.0-20200428022330-06a60b6afbbc h1:VRRKCwnzqk8QCaRC4os14xoKDdbHqqlJtJA0oc1ZAjg=
github.com/denisenkom/go-mssqldb v0.0.0-20200428022330-06a60b6afbbc/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU=
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/edsrzf/mmap-go v1.0.0/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M=
github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5/go.mod h1:a2zkGnVExMxdzMo3M0Hi/3sEU+cWnZpSni0O6/Yb/P0=
github.com/fatih/color v1.6.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
github.com/fatih/color v1.10.0 h1:s36xzo75JdqLaaWoiEHk767eHiwo0598uUxyfiPkDsg=
github.com/fatih/color v1.10.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
github.com/getkin/kin-openapi v0.61.0/go.mod h1:7Yn5whZr5kJi6t+kShccXS8ae1APpYTW6yheSwk8Yi4=
github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
github.com/gin-gonic/gin v1.6.3 h1:ahKqKTFpO5KTPHxWZjEdPScmYaGtLo8Y4DMHoEsnp14=
github.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M=
github.com/go-chi/chi/v5 v5.0.0/go.mod h1:BBug9lr0cqtdAhsu6R4AAdvufI0/XBzAQSsUqJpoZOs=
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
github.com/go-gormigrate/gormigrate/v2 v2.0.0 h1:e2A3Uznk4viUC4UuemuVgsNnvYZyOA8B3awlYk3UioU=
github.com/go-gormigrate/gormigrate/v2 v2.0.0/go.mod h1:YuVJ+D/dNt4HWrThTBnjgZuRbt7AuwINeg4q52ZE3Jw=
github.com/go-interpreter/wagon v0.5.1-0.20190713202023-55a163980b6c/go.mod h1:5+b/MBYkclRZngKF5s6qrgWxSLgE9F5dFdO1hAueZLc=
github.com/go-interpreter/wagon v0.6.0/go.mod h1:5+b/MBYkclRZngKF5s6qrgWxSLgE9F5dFdO1hAueZLc=
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
github.com/go-ole/go-ole v1.2.4 h1:nNBDSCOigTSiarFpYE9J/KtEA1IOW4CNeqT9TQDqCxI=
github.com/go-ole/go-ole v1.2.4/go.mod h1:XCwSNxSkXRo4vlyPy93sltvi/qJq0jqQhjqQNIwKuxM=
github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk=
github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A=
github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q=
github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8=
github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no=
github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA=
github.com/go-playground/validator/v10 v10.2.0 h1:KgJ0snyC2R9VXYN2rneOtQcw5aHQB1Vv0sFl1UcHBOY=
github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI=
github.com/go-sql-driver/mysql v1.5.0 h1:ozyZYNQW3x3HtqT1jira07DN2PArx2v7/mN66gGcHOs=
github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=
github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee h1:s+21KNqlpePfkah2I+gwHF8xmJWRjooY+5248k6m4A0=
github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo=
github.com/gobwas/pool v0.2.0 h1:QEmUOlnSjWtnpRGHF3SauEiOsy82Cup83Vf2LcMlnc8=
github.com/gobwas/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
github.com/gobwas/ws v1.0.2 h1:CoAavW/wd/kulfZmSIBt6p24n4j7tHgNVCjsfHVNUbo=
github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM=
github.com/gofrs/uuid v3.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe h1:lXe2qZdvpiX5WZkZR4hgp4KJVfY3nMkvmwbVkpv1rVY=
github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
github.com/golang/mock v1.4.3 h1:GV+pQPG/EUUbkh47niozDcADz6go/dUwhVzdUQHIVRw=
github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/protobuf v1.2.0/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.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
github.com/golang/protobuf v1.4.2 h1:+Z5KGCizgyZCbGh1KZqA0fcLLkwbsjIzS4aV2v7wJX0=
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/golangci/lint-1 v0.0.0-20181222135242-d2cdd8c08219/go.mod h1:/X8TswGSh1pIozq4ZwCfxS0WA5JGXguxk94ar/4c87Y=
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/pprof v0.0.0-20190908185732-236ed259b199/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.1.5/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.2.0 h1:qJYtXnJRWmpe7m/3XlyhrsLrEURqHRM2kxzoxXqyUDs=
github.com/google/uuid v1.2.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.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc=
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q=
github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8=
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM=
github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk=
github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU=
github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU=
github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4=
github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64=
github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ=
github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I=
github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
github.com/influxdata/influxdb-client-go/v2 v2.9.0 h1:1Ejxpt+cpWkadefxd5xvVx7pFgFaafdNp1ItfHzKRW4=
github.com/influxdata/influxdb-client-go/v2 v2.9.0/go.mod h1:x7Jo5UHHl+w8wu8UnGiNobDDHygojXwJX4mx7rXGKMk=
github.com/influxdata/line-protocol v0.0.0-20200327222509-2487e7298839 h1:W9WBk7wlPfJLvMCdtV4zPulc4uCPrlywQOmbFOhgQNU=
github.com/influxdata/line-protocol v0.0.0-20200327222509-2487e7298839/go.mod h1:xaLFMmpvUxqXtVkUJfg9QmT88cDaCJ3ZKgdZ78oO8Qo=
github.com/jackc/chunkreader v1.0.0 h1:4s39bBR8ByfqH+DKm8rQA3E1LHZWB9XWcrz8fqaZbe0=
github.com/jackc/chunkreader v1.0.0/go.mod h1:RT6O25fNZIuasFJRyZ4R/Y2BbhasbmZXF9QQ7T3kePo=
github.com/jackc/chunkreader/v2 v2.0.0/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk=
github.com/jackc/chunkreader/v2 v2.0.1 h1:i+RDz65UE+mmpjTfyz0MoVTnzeYxroil2G82ki7MGG8=
github.com/jackc/chunkreader/v2 v2.0.1/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk=
github.com/jackc/pgconn v0.0.0-20190420214824-7e0022ef6ba3/go.mod h1:jkELnwuX+w9qN5YIfX0fl88Ehu4XC3keFuOJJk9pcnA=
github.com/jackc/pgconn v0.0.0-20190824142844-760dd75542eb/go.mod h1:lLjNuW/+OfW9/pnVKPazfWOgNfH2aPem8YQ7ilXGvJE=
github.com/jackc/pgconn v0.0.0-20190831204454-2fabfa3c18b7/go.mod h1:ZJKsE/KZfsUgOEh9hBm+xYTstcNHg7UPMVJqRfQxq4s=
github.com/jackc/pgconn v1.4.0/go.mod h1:Y2O3ZDF0q4mMacyWV3AstPJpeHXWGEetiFttmq5lahk=
github.com/jackc/pgconn v1.5.0/go.mod h1:QeD3lBfpTFe8WUnPZWN5KY/mB8FGMIYRdd8P8Jr0fAI=
github.com/jackc/pgconn v1.5.1-0.20200601181101-fa742c524853/go.mod h1:QeD3lBfpTFe8WUnPZWN5KY/mB8FGMIYRdd8P8Jr0fAI=
github.com/jackc/pgconn v1.6.4 h1:S7T6cx5o2OqmxdHaXLH1ZeD1SbI8jBznyYE9Ec0RCQ8=
github.com/jackc/pgconn v1.6.4/go.mod h1:w2pne1C2tZgP+TvjqLpOigGzNqjBgQW9dUw/4Chex78=
github.com/jackc/pgio v1.0.0 h1:g12B9UwVnzGhueNavwioyEEpAmqMe1E/BN9ES+8ovkE=
github.com/jackc/pgio v1.0.0/go.mod h1:oP+2QK2wFfUWgr+gxjoBH9KGBb31Eio69xUb0w5bYf8=
github.com/jackc/pgmock v0.0.0-20190831213851-13a1b77aafa2/go.mod h1:fGZlG77KXmcq05nJLRkk0+p82V8B8Dw8KN2/V9c/OAE=
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
github.com/jackc/pgproto3 v1.1.0 h1:FYYE4yRw+AgI8wXIinMlNjBbp/UitDJwfj5LqqewP1A=
github.com/jackc/pgproto3 v1.1.0/go.mod h1:eR5FA3leWg7p9aeAqi37XOTgTIbkABlvcPB3E5rlc78=
github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190420180111-c116219b62db/go.mod h1:bhq50y+xrl9n5mRYyCBFKkpRVTLYJVWeCc+mEAI3yXA=
github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190609003834-432c2951c711/go.mod h1:uH0AWtUmuShn0bcesswc4aBTWGvw0cAxIJp+6OB//Wg=
github.com/jackc/pgproto3/v2 v2.0.0-rc3/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM=
github.com/jackc/pgproto3/v2 v2.0.0-rc3.0.20190831210041-4c03ce451f29/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM=
github.com/jackc/pgproto3/v2 v2.0.1/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA=
github.com/jackc/pgproto3/v2 v2.0.2 h1:q1Hsy66zh4vuNsajBUF2PNqfAMMfxU5mk594lPE9vjY=
github.com/jackc/pgproto3/v2 v2.0.2/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA=
github.com/jackc/pgservicefile v0.0.0-20200307190119-3430c5407db8/go.mod h1:vsD4gTJCa9TptPL8sPkXrLZ+hDuNrZCnj29CQpr4X1E=
github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b h1:C8S2+VttkHFdOOCXJe+YGfa4vHYwlt4Zx+IVXQ97jYg=
github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b/go.mod h1:vsD4gTJCa9TptPL8sPkXrLZ+hDuNrZCnj29CQpr4X1E=
github.com/jackc/pgtype v0.0.0-20190421001408-4ed0de4755e0/go.mod h1:hdSHsc1V01CGwFsrv11mJRHWJ6aifDLfdV3aVjFF0zg=
github.com/jackc/pgtype v0.0.0-20190824184912-ab885b375b90/go.mod h1:KcahbBH1nCMSo2DXpzsoWOAfFkdEtEJpPbVLq8eE+mc=
github.com/jackc/pgtype v0.0.0-20190828014616-a8802b16cc59/go.mod h1:MWlu30kVJrUS8lot6TQqcg7mtthZ9T0EoIBFiJcmcyw=
github.com/jackc/pgtype v1.2.0/go.mod h1:5m2OfMh1wTK7x+Fk952IDmI4nw3nPrvtQdM0ZT4WpC0=
github.com/jackc/pgtype v1.3.1-0.20200510190516-8cd94a14c75a/go.mod h1:vaogEUkALtxZMCH411K+tKzNpwzCKU+AnPzBKZ+I+Po=
github.com/jackc/pgtype v1.3.1-0.20200606141011-f6355165a91c/go.mod h1:cvk9Bgu/VzJ9/lxTO5R5sf80p0DiucVtN7ZxvaC4GmQ=
github.com/jackc/pgtype v1.4.2 h1:t+6LWm5eWPLX1H5Se702JSBcirq6uWa4jiG4wV1rAWY=
github.com/jackc/pgtype v1.4.2/go.mod h1:JCULISAZBFGrHaOXIIFiyfzW5VY0GRitRr8NeJsrdig=
github.com/jackc/pgx/v4 v4.0.0-20190420224344-cc3461e65d96/go.mod h1:mdxmSJJuR08CZQyj1PVQBHy9XOp5p8/SHH6a0psbY9Y=
github.com/jackc/pgx/v4 v4.0.0-20190421002000-1b8f0016e912/go.mod h1:no/Y67Jkk/9WuGR0JG/JseM9irFbnEPbuWV2EELPNuM=
github.com/jackc/pgx/v4 v4.0.0-pre1.0.20190824185557-6972a5742186/go.mod h1:X+GQnOEnf1dqHGpw7JmHqHc1NxDoalibchSk9/RWuDc=
github.com/jackc/pgx/v4 v4.5.0/go.mod h1:EpAKPLdnTorwmPUUsqrPxy5fphV18j9q3wrfRXgo+kA=
github.com/jackc/pgx/v4 v4.6.1-0.20200510190926-94ba730bb1e9/go.mod h1:t3/cdRQl6fOLDxqtlyhe9UWgfIi9R8+8v8GKV5TRA/o=
github.com/jackc/pgx/v4 v4.6.1-0.20200606145419-4e5062306904/go.mod h1:ZDaNWkt9sW1JMiNn0kdYBaLelIhw7Pg4qd+Vk6tw7Hg=
github.com/jackc/pgx/v4 v4.8.1 h1:SUbCLP2pXvf/Sr/25KsuI4aTxiFYIvpfk4l6aTSdyCw=
github.com/jackc/pgx/v4 v4.8.1/go.mod h1:4HOLxrl8wToZJReD04/yB20GDwf4KBYETvlHciCnwW0=
github.com/jackc/puddle v0.0.0-20190413234325-e4ced69a3a2b/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
github.com/jackc/puddle v0.0.0-20190608224051-11cab39313c9/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
github.com/jackc/puddle v1.1.0/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
github.com/jackc/puddle v1.1.1/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
github.com/jarcoal/httpmock v1.0.4 h1:jp+dy/+nonJE4g4xbVtl9QdrUNbn6/3hDT5R4nDIZnA=
github.com/jarcoal/httpmock v1.0.4/go.mod h1:ATjnClrvW/3tijVmpL/va5Z3aAyGvqU3gCT8nX0Txik=
github.com/jaypipes/ghw v0.6.1 h1:Ewt3mdpiyhWotGyzg1ursV/6SnToGcG4215X6rR2af8=
github.com/jaypipes/ghw v0.6.1/go.mod h1:QOXppNRCLGYR1H+hu09FxZPqjNt09bqUZUnOL3Rcero=
github.com/jaypipes/pcidb v0.5.0 h1:4W5gZ+G7QxydevI8/MmmKdnIPJpURqJ2JNXTzfLxF5c=
github.com/jaypipes/pcidb v0.5.0/go.mod h1:L2RGk04sfRhp5wvHO0gfRAMoLY/F3PKv/nwJeVoho0o=
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
github.com/jinzhu/gorm v1.9.16 h1:+IyIjPEABKRpsu/F8OvDPy9fyQlgsg2luMV2ZIH5i5o=
github.com/jinzhu/gorm v1.9.16/go.mod h1:G3LB3wezTOWM2ITLzPxEXgSkOXAntiLHS7UdBefADcs=
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
github.com/jinzhu/now v1.0.1/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/jinzhu/now v1.1.1 h1:g39TucaRWyV3dwDO++eEc6qf8TVIQ/Da48WmqjZ3i7E=
github.com/jinzhu/now v1.1.1/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/joho/godotenv v1.3.0 h1:Zjp+RcGpHhGlrMbJzXTrZZPrWj+1vfm90La1wgB6Bhc=
github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg=
github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
github.com/json-iterator/go v1.1.9 h1:9yzud/Ht36ygwatGx56VwCZtlI/2AD15T1X2sjSuGns=
github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/klauspost/compress v1.10.3/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
github.com/klauspost/compress v1.11.7/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
github.com/klauspost/compress v1.12.1 h1:/+xsCsk06wE38cyiqOR/o7U2fSftcH72xD+BQXmja/g=
github.com/klauspost/compress v1.12.1/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8eO+e+Dq5Gzg=
github.com/knq/sysutil v0.0.0-20181215143952-f05b59f0f307/go.mod h1:BjPj+aVjl9FW/cCGiF3nGh5v+9Gd3VCgBQbod/GlMaQ=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/konsorten/go-windows-terminal-sequences v1.0.2 h1:DB17ag19krx9CFsz4o3enTrPXyIXCl+2iCXH/aMAp9s=
github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
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/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
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/kvz/logstreamer v0.0.0-20150507115422-a635b98146f0 h1:3tLzEnUizyN9YLWFTT9loC30lSBvh2y70LTDcZOTs1s=
github.com/kvz/logstreamer v0.0.0-20150507115422-a635b98146f0/go.mod h1:8/LTPeDLaklcUjgSQBHbhBF1ibKAFxzS5o+H7USfMSA=
github.com/labstack/echo/v4 v4.2.1/go.mod h1:AA49e0DZ8kk5jTOOCKNuPR6oTnBS0dYiM4FW1e6jwpg=
github.com/labstack/gommon v0.3.0/go.mod h1:MULnywXg0yavhxWKc+lOruYdAhDwPK9wf0OL7NoOu+k=
github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y=
github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=
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.1/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/lib/pq v1.3.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
github.com/magiconair/properties v1.8.1 h1:ZC2Vc7/ZFkGmsVC9KvOjumD+G5lXy2RtTKyzRKO2BQ4=
github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
github.com/mailru/easyjson v0.0.0-20190403194419-1ea4449da983/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/mailru/easyjson v0.0.0-20190620125010-da37f6c1e481/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/mailru/easyjson v0.7.0/go.mod h1:KAzv3t3aY1NaHWoQz1+4F1ccyAH66Jk7yos7ldAVICs=
github.com/matryer/moq v0.0.0-20190312154309-6cfb0558e1bd/go.mod h1:9ELz6aaclSIGnZBoaSLZ3NAl1VTufbOrXBPvtcy6WiQ=
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ=
github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-colorable v0.1.7/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-colorable v0.1.8 h1:c1ghPdyEDarC70ftn0y+A/Ee++9zz8ljHG1b13eJ0s8=
github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ=
github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-sqlite3 v1.14.0/go.mod h1:JIl7NbARA7phWnGvh0LKTyg7S9BA+6gx71ShQilpsus=
github.com/mattn/go-sqlite3 v1.14.3/go.mod h1:WVKg1VTActs4Qso6iwGbiFih2UIHo0ENGwNd0Lj+XmI=
github.com/mattn/go-sqlite3 v1.14.4 h1:4rQjbDxdu9fSgI/r3KN72G3c2goxknAqHHgPWWs8UlI=
github.com/mattn/go-sqlite3 v1.14.4/go.mod h1:WVKg1VTActs4Qso6iwGbiFih2UIHo0ENGwNd0Lj+XmI=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc=
github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI=
github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg=
github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY=
github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/mitchellh/mapstructure v1.2.2 h1:dxe5oCinTXiTIcfgmZecdCzPmAJKd46KsCWc35r0TV4=
github.com/mitchellh/mapstructure v1.2.2/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
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/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
github.com/nxadm/tail v1.4.6/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=
github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.8.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
github.com/onsi/ginkgo v1.14.2/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY=
github.com/onsi/ginkgo v1.16.1 h1:foqVmeWDD6yYpK+Yz3fHyNIxFYNxswxqNFjSKe+vI54=
github.com/onsi/ginkgo v1.16.1/go.mod h1:CObGmKUOKaSC0RjmoAK7tKyn4Azo5P2IWuoMnvwxz1E=
github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
github.com/onsi/gomega v1.10.1 h1:o0+MgICZLuZ7xjH7Vx6zS/zcu93/BEp1VwkIW1mEXCE=
github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
github.com/pelletier/go-toml v1.7.0 h1:7utD74fnzVc/cpcyy8sjrlFr5vYpypUixARcHIMIGuI=
github.com/pelletier/go-toml v1.7.0/go.mod h1:vwGMzjaWMwyfHwgIBhI2YUM4fB6nL6lVAvS1LBMMhTE=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso=
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=
github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ=
github.com/rs/zerolog v1.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU=
github.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThCjNc=
github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q=
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
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/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4=
github.com/shopspring/decimal v0.0.0-20200227202807-02e2044944cc/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo=
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
github.com/sirupsen/logrus v1.0.5/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc=
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=
github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4=
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s=
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM=
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
github.com/spf13/afero v1.2.2 h1:5jhuqJyZCZf2JRofRvN/nIFgIWNzPa3/Vz8mYylgbWc=
github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk=
github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
github.com/spf13/cast v1.3.1 h1:nFm6S0SMdyzrzcmThSipiEubIDy8WEXKNZ0UOgiRpng=
github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ=
github.com/spf13/cobra v0.0.7/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHNrgE=
github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk=
github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo=
github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
github.com/spf13/pflag v1.0.2/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE=
github.com/spf13/viper v1.6.3/go.mod h1:jUMtyi0/lB5yZH/FjyGAoH7IMNrIhlBf6pXZmbMDvzw=
github.com/spf13/viper v1.7.0 h1:xVKxvI7ouOI5I+U9s2eeiUfMaWBVoXA3AWskkrqK0VM=
github.com/spf13/viper v1.7.0/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
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/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/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs=
github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY=
github.com/urfave/cli/v2 v2.2.0 h1:JTTnM6wKzdA0Jqodd966MVj4vWbbquZykeX1sKbe2C4=
github.com/urfave/cli/v2 v2.2.0/go.mod h1:SE9GqnLQmjVa0iPEY0f1w3ygNIYcIJ0OKPMoW2caLfQ=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8=
github.com/valyala/fasttemplate v1.2.1/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q=
go.coder.com/go-tools v0.0.0-20190317003359-0c6a35b74a16/go.mod h1:iKV5yK9t+J5nG9O3uF6KYdPEz3dyfMyB15MN1rbQ8Qw=
go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU=
go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA=
go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
golang.org/x/crypto v0.0.0-20180426230345-b49d69b5da94/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190411191339-88737f569e3a/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE=
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190911031432-227b76d455e7/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191205180655-e7c4368fe9dd/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200323165209-0ec3e9974c59/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad h1:DN0cp81fZ3njFcrLCytUHRSUkqBjfTo4Tx9RJTWs0EY=
golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
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-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=
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/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/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-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181102091132-c10e9556a7bc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20210119194325-5f4716e94777 h1:003p0dJM77cxMSyCPFphvZf/Y5/NXf5fzg6ufd1/Oew=
golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/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-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9 h1:SQFwaSi55rU7vdNs9Yr0Z324VNlrF+0wMqRXT4St8ck=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/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-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190306220234-b354f8bf4d9e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/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-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190618155005-516e3c20635f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190712062909-fae7ac547cb7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190927073244-c990c680b611/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200826173525-f9321e4c35a6/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210113181707-4bcb84eeeb78/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7 h1:iGu644GcxtEcrInvDsQRCwJjtCIOlT2V7IRt6ah2Whw=
golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1 h1:v+OssWQX+hTHEmOBgwxdZxK4zHq3yOs8F9J7mk0PY8E=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.5 h1:i6eZZ+zk0SOf0xgBpEpPD18qWcJda6q1sxt3S0kzyUQ=
golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20201208040808-7e3f01d25324/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190425163242-31fd60d6bfdc/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20190823170909-c4a336ef6a2f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20190920225731-5eefd052ad72/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=
google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
google.golang.org/protobuf v1.23.0 h1:4MY060fB1DLGMB/7MBTLnwQUY6+F09GEiz6SsrNqyzM=
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
gopkg.in/airbrake/gobrake.v2 v2.0.9/go.mod h1:/h5ZAUhDkGaJfjzjKLSjv6zCL6O0LLBxU4K+aSYdM/U=
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
gopkg.in/gemnasium/logrus-airbrake-hook.v2 v2.1.2/go.mod h1:Xk6kEKp8OKb+X14hQBKWaSkCsqBpgog8nAV2xsGOxlo=
gopkg.in/inconshreveable/log15.v2 v2.0.0-20180818164646-67afb5ed74ec/go.mod h1:aPpfJ7XW+gOuirDoZ8gHhLh3kZ1B08FtV2bbmy7Jv3s=
gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/ini.v1 v1.55.0 h1:E8yzL5unfpW3M6fz/eB7Cb5MQAYSZ7GKo4Qth+N2sgQ=
gopkg.in/ini.v1 v1.55.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU=
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gorm.io/driver/mysql v1.0.1 h1:omJoilUzyrAp0xNoio88lGJCroGdIOen9hq2A/+3ifw=
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/go.mod h1:wtMFcOzmuA5QigNsgEIb7O5lhvH1tHAF1RbWmLWV4to=
gorm.io/driver/sqlite v1.1.1/go.mod h1:hm2olEcl8Tmsc6eZyxYSeznnsDaMqamBvEXLNtBg4cI=
gorm.io/driver/sqlite v1.1.3 h1:BYfdVuZB5He/u9dt4qDpZqiqDJ6KhPqs5QUqsr/Eeuc=
gorm.io/driver/sqlite v1.1.3/go.mod h1:AKDgRWk8lcSQSw+9kxCJnX/yySj8G3rdwYlU57cB45c=
gorm.io/driver/sqlserver v1.0.2 h1:FzxAlw0/7hntMzSiNfotpYCo9Lz8dqWQGdmCGqIiFGo=
gorm.io/driver/sqlserver v1.0.2/go.mod h1:gb0Y9QePGgqjzrVyTQUZeh9zkd5v0iz71cM1B4ZycEY=
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.1/go.mod h1:0HFTzE/SqkGTzK6TlDPPQbAYCluiVvhzoA1+aVyzenw=
gorm.io/gorm v1.20.2 h1:bZzSEnq7NDGsrd+n3evOOedDrY5oLM5QPlCjZJUK2ro=
gorm.io/gorm v1.20.2/go.mod h1:0HFTzE/SqkGTzK6TlDPPQbAYCluiVvhzoA1+aVyzenw=
gosrc.io/xmpp v0.5.1 h1:Rgrm5s2rt+npGggJH3HakQxQXR8ZZz3+QRzakRQqaq4=
gosrc.io/xmpp v0.5.1/go.mod h1:L3NFMqYOxyLz3JGmgFyWf7r9htE91zVGiK40oW4RwdY=
gotest.tools v2.1.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw=
gotest.tools/gotestsum v0.3.5/go.mod h1:Mnf3e5FUzXbkCfynWBGOwLssY7gTQgCHObK9tMpAriY=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
howett.net/plist v0.0.0-20181124034731-591f970eefbb h1:jhnBjNi9UFpfpl8YZhA9CrOqpnJdvzuiHsl/dnxl11M=
howett.net/plist v0.0.0-20181124034731-591f970eefbb/go.mod h1:vMygbs4qMhSZSc4lCUl2OEE+rDiIIJAIdR4m7MiMcm0=
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/go.mod h1:B70DZP8IakI65RVQ51MsWP/8jndNma26DVA/nFSCgW0=
nhooyr.io/websocket v1.8.7 h1:usjR2uOr/zjjkVMy0lW+PPohFok7PCow5sDjLgX4P4g=
nhooyr.io/websocket v1.8.7/go.mod h1:B70DZP8IakI65RVQ51MsWP/8jndNma26DVA/nFSCgW0=
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
+2 -14
View File
@@ -1,18 +1,6 @@
---
engine_enable_code_mutation: true
engine_cmd_compile:
- go build -ldflags '-w -extldflags "-static"' -o scrutiny webapp/backend/cmd/scrutiny/scrutiny.go
- 'GOOS=linux GOARCH=amd64 go build -ldflags "-X main.goos=linux -X main.goarch=amd64" -o scrutiny-web-linux-amd64 -tags "static" webapp/backend/cmd/scrutiny/scrutiny.go'
- 'chmod +x scrutiny-web-linux-amd64'
- 'GOOS=linux GOARCH=amd64 go build -ldflags "-X main.goos=linux -X main.goarch=amd64" -o scrutiny-collector-metrics-linux-amd64 -tags "static" collector/cmd/collector-metrics/collector-metrics.go'
- 'chmod +x scrutiny-collector-metrics-linux-amd64'
mgr_keep_lock_file: true
engine_version_metadata_path: 'webapp/backend/pkg/version/version.go'
engine_cmd_test: 'go test -v -tags "static" $(go list ./... | grep -v /vendor/)'
engine_golang_package_path: 'github.com/analogj/scrutiny'
scm_enable_branch_cleanup: true
engine_disable_lint: true
scm_release_assets:
- local_path: scrutiny-web-linux-amd64
artifact_name: scrutiny-web-linux-amd64
- local_path: scrutiny-collector-metrics-linux-amd64
artifact_name: scrutiny-collector-metrics-linux-amd64
-4
View File
@@ -1,4 +0,0 @@
#!/usr/bin/with-contenv bash
COLLECTOR_CRON_SCHEDULE=${COLLECTOR_CRON_SCHEDULE:-"0 0 * * *"}
sed -i 's|{COLLECTOR_CRON_SCHEDULE}|'"${COLLECTOR_CRON_SCHEDULE}"'|g' /etc/cron.d/scrutiny
+15
View File
@@ -0,0 +1,15 @@
#!/usr/bin/with-contenv bash
# Cron runs in its own isolated environment (usually using only /etc/environment )
# So when the container starts up, we will do a dump of the runtime environment into a .env file that we
# will then source into the crontab file (/etc/cron.d/scrutiny)
(set -o posix; export -p) > /env.sh
# adding ability to customize the cron schedule.
COLLECTOR_CRON_SCHEDULE=${COLLECTOR_CRON_SCHEDULE:-"0 0 * * *"}
# if the cron schedule has been overridden via env variable (eg docker-compose) we should make sure to strip quotes
[[ "${COLLECTOR_CRON_SCHEDULE}" == \"*\" || "${COLLECTOR_CRON_SCHEDULE}" == \'*\' ]] && COLLECTOR_CRON_SCHEDULE="${COLLECTOR_CRON_SCHEDULE:1:-1}"
# replace placeholder with correct value
sed -i 's|{COLLECTOR_CRON_SCHEDULE}|'"${COLLECTOR_CRON_SCHEDULE}"'|g' /etc/cron.d/scrutiny
+1 -1
View File
@@ -9,5 +9,5 @@ s6-svc -O /var/run/s6/services/collector-once
# wait until scrutiny is "Ready"
until $(curl --output /dev/null --silent --head --fail http://localhost:8080/api/health); do echo "scrutiny api not ready" && sleep 5; done
echo "starting scrutiny collector"
echo "starting scrutiny collector (run-once mode. subsequent calls will be triggered via cron service)"
/opt/scrutiny/bin/scrutiny-collector-metrics run
Regular → Executable
+1 -7
View File
@@ -1,10 +1,4 @@
#!/usr/bin/with-contenv bash
# Cron runs in its own isolated environment (usually using only /etc/environment )
# So when the container starts up, we will do a dump of the runtime environment into a .env file that we
# will then source into the crontab file (/etc/cron.d/scrutiny.sh)
printenv | sed 's/^\(.*\)$/export \1/g' > /env.sh
echo "starting cron"
cron -f
cron -f -L 15
+3 -1
View File
@@ -39,12 +39,14 @@ func (c *configuration) Init() error {
c.SetDefault("notify.urls", []string{})
c.SetDefault("web.influxdb.host", "0.0.0.0")
c.SetDefault("web.influxdb.scheme", "http")
c.SetDefault("web.influxdb.host", "localhost")
c.SetDefault("web.influxdb.port", "8086")
c.SetDefault("web.influxdb.org", "scrutiny")
c.SetDefault("web.influxdb.bucket", "metrics")
c.SetDefault("web.influxdb.init_username", "admin")
c.SetDefault("web.influxdb.init_password", "password12345")
c.SetDefault("web.influxdb.token", "scrutiny-default-admin-token")
c.SetDefault("web.influxdb.retention_policy", true)
//c.SetDefault("disks.include", []string{})
@@ -0,0 +1,37 @@
package m20220503120000
import (
"github.com/analogj/scrutiny/webapp/backend/pkg"
"time"
)
// Deprecated: m20220503120000.Device is deprecated, only used by db migrations
type Device struct {
//GORM attributes, see: http://gorm.io/docs/conventions.html
CreatedAt time.Time
UpdatedAt time.Time
DeletedAt *time.Time
WWN string `json:"wwn" gorm:"primary_key"`
DeviceName string `json:"device_name"`
Manufacturer string `json:"manufacturer"`
ModelName string `json:"model_name"`
InterfaceType string `json:"interface_type"`
InterfaceSpeed string `json:"interface_speed"`
SerialNumber string `json:"serial_number"`
Firmware string `json:"firmware"`
RotationSpeed int `json:"rotational_speed"`
Capacity int64 `json:"capacity"`
FormFactor string `json:"form_factor"`
SmartSupport bool `json:"smart_support"`
DeviceProtocol string `json:"device_protocol"` //protocol determines which smart attribute types are available (ATA, NVMe, SCSI)
DeviceType string `json:"device_type"` //device type is used for querying with -d/t flag, should only be used by collector.
// User provided metadata
Label string `json:"label"`
HostId string `json:"host_id"`
// Data set by Scrutiny
DeviceStatus pkg.DeviceStatus `json:"device_status"`
}
@@ -0,0 +1,41 @@
package m20220509170100
import (
"github.com/analogj/scrutiny/webapp/backend/pkg"
"time"
)
type Device struct {
//GORM attributes, see: http://gorm.io/docs/conventions.html
CreatedAt time.Time
UpdatedAt time.Time
DeletedAt *time.Time
WWN string `json:"wwn" gorm:"primary_key"`
DeviceName string `json:"device_name"`
DeviceUUID string `json:"device_uuid"`
DeviceSerialID string `json:"device_serial_id"`
DeviceLabel string `json:"device_label"`
Manufacturer string `json:"manufacturer"`
ModelName string `json:"model_name"`
InterfaceType string `json:"interface_type"`
InterfaceSpeed string `json:"interface_speed"`
SerialNumber string `json:"serial_number"`
Firmware string `json:"firmware"`
RotationSpeed int `json:"rotational_speed"`
Capacity int64 `json:"capacity"`
FormFactor string `json:"form_factor"`
SmartSupport bool `json:"smart_support"`
DeviceProtocol string `json:"device_protocol"` //protocol determines which smart attribute types are available (ATA, NVMe, SCSI)
DeviceType string `json:"device_type"` //device type is used for querying with -d/t flag, should only be used by collector.
// User provided metadata
Label string `json:"label"`
HostId string `json:"host_id"`
// Data set by Scrutiny
DeviceStatus pkg.DeviceStatus `json:"device_status"`
}
@@ -2,6 +2,7 @@ package database
import (
"context"
"encoding/json"
"fmt"
"github.com/analogj/scrutiny/webapp/backend/pkg/config"
"github.com/analogj/scrutiny/webapp/backend/pkg/models"
@@ -11,6 +12,9 @@ import (
"github.com/sirupsen/logrus"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
"io/ioutil"
"net/http"
"net/url"
"time"
)
@@ -75,36 +79,38 @@ func NewScrutinyRepository(appConfig config.Interface, globalLogger logrus.Field
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Create a new client using an InfluxDB server base URL and an authentication token
influxdbUrl := fmt.Sprintf("http://%s:%s", appConfig.GetString("web.influxdb.host"), appConfig.GetString("web.influxdb.port"))
influxdbUrl := fmt.Sprintf("%s://%s:%s", appConfig.GetString("web.influxdb.scheme"), appConfig.GetString("web.influxdb.host"), appConfig.GetString("web.influxdb.port"))
globalLogger.Debugf("InfluxDB url: %s", influxdbUrl)
client := influxdb2.NewClient(influxdbUrl, appConfig.GetString("web.influxdb.token"))
if !appConfig.IsSet("web.influxdb.token") {
globalLogger.Debugf("No influxdb token found, running first-time setup...")
//if !appConfig.IsSet("web.influxdb.token") {
globalLogger.Debugf("Determine Influxdb setup status...")
influxSetupComplete, err := InfluxSetupComplete(influxdbUrl)
if err != nil {
return nil, fmt.Errorf("failed to check influxdb setup status - %w", err)
}
if !influxSetupComplete {
globalLogger.Debugf("Influxdb un-initialized, running first-time setup...")
// if no token is provided, but we have a valid server, we're going to assume this is the first setup of our server.
// we will initialize with a predetermined username & password, that you should change.
// metrics bucket will have a retention period of 8 days (since it will be down-sampled once a week)
// in seconds (60seconds * 60minutes * 24hours * 15 days) = 1_296_000 (see EnsureBucket() function)
onboardingResponse, err := client.Setup(
_, err := client.SetupWithToken(
backgroundContext,
appConfig.GetString("web.influxdb.init_username"),
appConfig.GetString("web.influxdb.init_password"),
appConfig.GetString("web.influxdb.org"),
appConfig.GetString("web.influxdb.bucket"),
0)
0,
appConfig.GetString("web.influxdb.token"),
)
if err != nil {
return nil, err
}
appConfig.Set("web.influxdb.token", *onboardingResponse.Auth.Token)
// we should write the config file out here. Ignore failures.
err = appConfig.WriteConfig()
if err != nil {
globalLogger.Infof("ignoring error while writing influxdb info to config: %v", err)
}
}
// Use blocking write client for writes to desired bucket
@@ -176,6 +182,37 @@ func (sr *scrutinyRepository) Close() error {
return nil
}
func InfluxSetupComplete(influxEndpoint string) (bool, error) {
influxUri, err := url.Parse(influxEndpoint)
if err != nil {
return false, err
}
influxUri, err = influxUri.Parse("/api/v2/setup")
if err != nil {
return false, err
}
res, err := http.Get(influxUri.String())
if err != nil {
return false, err
}
body, err := ioutil.ReadAll(res.Body)
if err != nil {
return false, err
}
type SetupStatus struct {
Allowed bool `json:"allowed"`
}
var data SetupStatus
err = json.Unmarshal(body, &data)
if err != nil {
return false, err
}
return !data.Allowed, nil
}
func (sr *scrutinyRepository) EnsureBuckets(ctx context.Context, org *domain.Organization) error {
var mainBucketRetentionRule domain.RetentionRule
@@ -18,7 +18,7 @@ import (
func (sr *scrutinyRepository) RegisterDevice(ctx context.Context, dev models.Device) error {
if err := sr.gormClient.WithContext(ctx).Clauses(clause.OnConflict{
Columns: []clause.Column{{Name: "wwn"}},
DoUpdates: clause.AssignmentColumns([]string{"host_id", "device_name", "device_type"}),
DoUpdates: clause.AssignmentColumns([]string{"host_id", "device_name", "device_type", "device_uuid", "device_serial_id", "device_label"}),
}).Create(&dev).Error; err != nil {
return err
}
@@ -2,13 +2,18 @@ package database
import (
"context"
"errors"
"fmt"
"github.com/analogj/scrutiny/webapp/backend/pkg/database/migrations/m20201107210306"
"github.com/analogj/scrutiny/webapp/backend/pkg/database/migrations/m20220503120000"
"github.com/analogj/scrutiny/webapp/backend/pkg/database/migrations/m20220509170100"
"github.com/analogj/scrutiny/webapp/backend/pkg/models"
"github.com/analogj/scrutiny/webapp/backend/pkg/models/collector"
"github.com/analogj/scrutiny/webapp/backend/pkg/models/measurements"
"github.com/go-gormigrate/gormigrate/v2"
"github.com/influxdata/influxdb-client-go/v2/api/http"
_ "github.com/jinzhu/gorm/dialects/sqlite"
log "github.com/sirupsen/logrus"
"gorm.io/gorm"
"strconv"
"time"
@@ -21,9 +26,12 @@ import (
func (sr *scrutinyRepository) Migrate(ctx context.Context) error {
sr.logger.Infoln("Database migration starting")
sr.logger.Infoln("Database migration starting. Please wait, this process may take a long time....")
m := gormigrate.New(sr.gormClient, gormigrate.DefaultOptions, []*gormigrate.Migration{
gormMigrateOptions := gormigrate.DefaultOptions
gormMigrateOptions.UseTransaction = true
m := gormigrate.New(sr.gormClient, gormMigrateOptions, []*gormigrate.Migration{
{
ID: "20201107210306", // v0.3.13 (pre-influxdb schema). 9fac3c6308dc6cb6cd5bbc43a68cd93e8fb20b87
Migrate: func(tx *gorm.DB) error {
@@ -37,16 +45,6 @@ func (sr *scrutinyRepository) Migrate(ctx context.Context) error {
&m20201107210306.SmartNvmeAttribute{},
)
},
Rollback: func(tx *gorm.DB) error {
return tx.Migrator().DropTable(
&m20201107210306.Device{},
&m20201107210306.Smart{},
&m20201107210306.SmartAtaAttribute{},
&m20201107210306.SmartNvmeAttribute{},
&m20201107210306.SmartNvmeAttribute{},
"self_tests",
)
},
},
{
ID: "20220503113100", // backwards compatible - influxdb schema
@@ -136,7 +134,7 @@ func (sr *scrutinyRepository) Migrate(ctx context.Context) error {
smartTags,
smartFields,
postSmartResults.Date, ctx)
if err != nil {
if ignorePastRetentionPolicyError(err) != nil {
return err
}
@@ -146,7 +144,7 @@ func (sr *scrutinyRepository) Migrate(ctx context.Context) error {
tempTags,
tempFields,
postSmartResults.Date, ctx)
if err != nil {
if ignorePastRetentionPolicyError(err) != nil {
return err
}
}
@@ -165,7 +163,7 @@ func (sr *scrutinyRepository) Migrate(ctx context.Context) error {
smartFields,
postSmartResults.Date, ctx)
if err != nil {
if ignorePastRetentionPolicyError(err) != nil {
return err
}
@@ -175,7 +173,7 @@ func (sr *scrutinyRepository) Migrate(ctx context.Context) error {
tempTags,
tempFields,
postSmartResults.Date, ctx)
if err != nil {
if ignorePastRetentionPolicyError(err) != nil {
return err
}
}
@@ -193,7 +191,7 @@ func (sr *scrutinyRepository) Migrate(ctx context.Context) error {
smartTags,
smartFields,
postSmartResults.Date, ctx)
if err != nil {
if ignorePastRetentionPolicyError(err) != nil {
return err
}
@@ -203,7 +201,7 @@ func (sr *scrutinyRepository) Migrate(ctx context.Context) error {
tempTags,
tempFields,
postSmartResults.Date, ctx)
if err != nil {
if ignorePastRetentionPolicyError(err) != nil {
return err
}
}
@@ -220,7 +218,7 @@ func (sr *scrutinyRepository) Migrate(ctx context.Context) error {
smartTags,
smartFields,
postSmartResults.Date, ctx)
if err != nil {
if ignorePastRetentionPolicyError(err) != nil {
return err
}
@@ -230,7 +228,7 @@ func (sr *scrutinyRepository) Migrate(ctx context.Context) error {
tempTags,
tempFields,
postSmartResults.Date, ctx)
if err != nil {
if ignorePastRetentionPolicyError(err) != nil {
return err
}
}
@@ -256,20 +254,44 @@ func (sr *scrutinyRepository) Migrate(ctx context.Context) error {
return err
}
//migrate the device database to the final version
return tx.AutoMigrate(models.Device{})
//migrate the device database
return tx.AutoMigrate(m20220503120000.Device{})
},
},
{
ID: "m20220509170100", // addl udev device data
Migrate: func(tx *gorm.DB) error {
//migrate the device database.
// adding addl columns (device_label, device_uuid, device_serial_id)
return tx.AutoMigrate(m20220509170100.Device{})
},
},
})
if err := m.Migrate(); err != nil {
sr.logger.Errorf("Database migration failed with error: %w", err)
sr.logger.Errorf("Database migration 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("Database migration completed successfully")
return nil
}
// helpers
//When adding data to influxdb, an error may be returned if the data point is outside the range of the retention policy.
//This function will ignore retention policy errors, and allow the migration to continue.
func ignorePastRetentionPolicyError(err error) error {
var influxDbWriteError *http.Error
if errors.As(err, &influxDbWriteError) {
if influxDbWriteError.StatusCode == 422 {
log.Infoln("ignoring error: attempted to writePoint past retention period duration")
return nil
}
}
return err
}
// Deprecated
func m20201107210306_FromPreInfluxDBTempCreatePostInfluxDBTemp(preDevice m20201107210306.Device, preSmartResult m20201107210306.Smart) (error, measurements.SmartTemperature) {
//extract temperature data for every datapoint
+4
View File
@@ -21,6 +21,10 @@ type Device struct {
WWN string `json:"wwn" gorm:"primary_key"`
DeviceName string `json:"device_name"`
DeviceUUID string `json:"device_uuid"`
DeviceSerialID string `json:"device_serial_id"`
DeviceLabel string `json:"device_label"`
Manufacturer string `json:"manufacturer"`
ModelName string `json:"model_name"`
InterfaceType string `json:"interface_type"`
@@ -171,7 +171,7 @@ func (sm *Smart) ProcessNvmeSmartInfo(nvmeSmartHealthInformationLog collector.Nv
"power_on_hours": (&SmartNvmeAttribute{AttributeId: "power_on_hours", Value: nvmeSmartHealthInformationLog.PowerOnHours, Threshold: -1}).PopulateAttributeStatus(),
"unsafe_shutdowns": (&SmartNvmeAttribute{AttributeId: "unsafe_shutdowns", Value: nvmeSmartHealthInformationLog.UnsafeShutdowns, Threshold: -1}).PopulateAttributeStatus(),
"media_errors": (&SmartNvmeAttribute{AttributeId: "media_errors", Value: nvmeSmartHealthInformationLog.MediaErrors, Threshold: 0}).PopulateAttributeStatus(),
"num_err_log_entries": (&SmartNvmeAttribute{AttributeId: "num_err_log_entries", Value: nvmeSmartHealthInformationLog.NumErrLogEntries, Threshold: 0}).PopulateAttributeStatus(),
"num_err_log_entries": (&SmartNvmeAttribute{AttributeId: "num_err_log_entries", Value: nvmeSmartHealthInformationLog.NumErrLogEntries, Threshold: -1}).PopulateAttributeStatus(),
"warning_temp_time": (&SmartNvmeAttribute{AttributeId: "warning_temp_time", Value: nvmeSmartHealthInformationLog.WarningTempTime, Threshold: -1}).PopulateAttributeStatus(),
"critical_comp_time": (&SmartNvmeAttribute{AttributeId: "critical_comp_time", Value: nvmeSmartHealthInformationLog.CriticalCompTime, Threshold: -1}).PopulateAttributeStatus(),
}
@@ -93,6 +93,8 @@ func (sa *SmartAtaAttribute) PopulateAttributeStatus() *SmartAtaAttribute {
//this attribute has previously failed
sa.Status = pkg.SmartAttributeStatusFailed
sa.StatusReason = "Attribute is failing manufacturer SMART threshold"
//if the Smart Status is failed, we should exit early, no need to look at thresholds.
return sa
} else if strings.ToUpper(sa.WhenFailed) == pkg.SmartWhenFailedInThePast {
sa.Status = pkg.SmartAttributeStatusWarning
@@ -381,6 +381,70 @@ func TestFromCollectorSmartInfo_Fail_ScrutinySmart(t *testing.T) {
require.Equal(t, 17, len(smartMdl.Attributes))
}
func TestFromCollectorSmartInfo_Fail_ScrutinyNonCriticalFailed(t *testing.T) {
//setup
smartDataFile, err := os.Open("../testdata/smart-ata-failed-scrutiny.json")
require.NoError(t, err)
defer smartDataFile.Close()
var smartJson collector.SmartInfo
smartDataBytes, err := ioutil.ReadAll(smartDataFile)
require.NoError(t, err)
err = json.Unmarshal(smartDataBytes, &smartJson)
require.NoError(t, err)
//test
smartMdl := measurements.Smart{}
err = smartMdl.FromCollectorSmartInfo("WWN-test", smartJson)
//assert
require.NoError(t, err)
require.Equal(t, "WWN-test", smartMdl.DeviceWWN)
require.Equal(t, pkg.DeviceStatusFailedScrutiny, smartMdl.Status)
require.Equal(t, int64(pkg.SmartAttributeStatusFailed), smartMdl.Attributes["199"].GetStatus(),
"scrutiny should detect that %d failed (status: %d, %s)",
smartMdl.Attributes["199"].(*measurements.SmartAtaAttribute).AttributeId,
smartMdl.Attributes["199"].GetStatus(), smartMdl.Attributes["199"].(*measurements.SmartAtaAttribute).StatusReason,
)
require.Equal(t, 14, len(smartMdl.Attributes))
}
//TODO: Scrutiny Warn
//TODO: Smart + Scrutiny Warn
func TestFromCollectorSmartInfo_NVMe_Fail_Scrutiny(t *testing.T) {
//setup
smartDataFile, err := os.Open("../testdata/smart-nvme-failed.json")
require.NoError(t, err)
defer smartDataFile.Close()
var smartJson collector.SmartInfo
smartDataBytes, err := ioutil.ReadAll(smartDataFile)
require.NoError(t, err)
err = json.Unmarshal(smartDataBytes, &smartJson)
require.NoError(t, err)
//test
smartMdl := measurements.Smart{}
err = smartMdl.FromCollectorSmartInfo("WWN-test", smartJson)
//assert
require.NoError(t, err)
require.Equal(t, "WWN-test", smartMdl.DeviceWWN)
require.Equal(t, pkg.DeviceStatusFailedScrutiny, smartMdl.Status)
require.Equal(t, int64(pkg.SmartAttributeStatusFailed), smartMdl.Attributes["media_errors"].GetStatus(),
"scrutiny should detect that %s failed (status: %d, %s)",
smartMdl.Attributes["media_errors"].(*measurements.SmartNvmeAttribute).AttributeId,
smartMdl.Attributes["media_errors"].GetStatus(),
smartMdl.Attributes["media_errors"].(*measurements.SmartNvmeAttribute).StatusReason,
)
require.Equal(t, 16, len(smartMdl.Attributes))
}
func TestFromCollectorSmartInfo_Nvme(t *testing.T) {
//setup
smartDataFile, err := os.Open("../testdata/smart-nvme.json")
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,107 @@
{
"json_format_version": [
1,
0
],
"smartctl": {
"version": [
7,
0
],
"svn_revision": "4883",
"platform_info": "x86_64-linux-5.13.0-40-generic",
"build_info": "(local build)",
"argv": [
"smartctl",
"-x",
"-j",
"/dev/nvme0"
],
"exit_status": 0
},
"device": {
"name": "/dev/nvme0",
"info_name": "/dev/nvme0",
"type": "nvme",
"protocol": "NVMe"
},
"model_name": "Samsung SSD 970 EVO 500GB",
"serial_number": "S466NX0M776250H",
"firmware_version": "2B2QEXE7",
"nvme_pci_vendor": {
"id": 5197,
"subsystem_id": 5197
},
"nvme_ieee_oui_identifier": 9528,
"nvme_total_capacity": 500107862016,
"nvme_unallocated_capacity": 0,
"nvme_controller_id": 4,
"nvme_number_of_namespaces": 1,
"nvme_namespaces": [
{
"id": 1,
"size": {
"blocks": 976773168,
"bytes": 500107862016
},
"capacity": {
"blocks": 976773168,
"bytes": 500107862016
},
"utilization": {
"blocks": 327275384,
"bytes": 167564996608
},
"formatted_lba_size": 512,
"eui64": {
"oui": 9528,
"ext_id": 376106710327
}
}
],
"user_capacity": {
"blocks": 976773168,
"bytes": 500107862016
},
"logical_block_size": 512,
"local_time": {
"time_t": 1652220188,
"asctime": "Tue May 10 22:03:08 2022 UTC"
},
"smart_status": {
"passed": true,
"nvme": {
"value": 0
}
},
"nvme_smart_health_information_log": {
"critical_warning": 0,
"temperature": 35,
"available_spare": 99,
"available_spare_threshold": 10,
"percentage_used": 3,
"data_units_read": 17176794,
"data_units_written": 65602088,
"host_reads": 118020838,
"host_writes": 874050000,
"controller_busy_time": 7601,
"power_cycles": 25,
"power_on_hours": 12798,
"unsafe_shutdowns": 10,
"media_errors": 7,
"num_err_log_entries": 62,
"warning_temp_time": 0,
"critical_comp_time": 0,
"temperature_sensors": [
35,
39
]
},
"temperature": {
"current": 35
},
"power_cycle_count": 25,
"power_on_time": {
"hours": 12798
}
}
@@ -445,50 +445,6 @@ var AtaMetadata = map[int]AtaAttributeMetadata{
Ideal: ObservedThresholdIdealLow,
Critical: false,
Description: "This attribute indicates the count of full hard disk power on/off cycles.",
ObservedThresholds: []ObservedThreshold{
{
Low: 0,
High: 13,
AnnualFailureRate: 0.019835987118930823,
ErrorInterval: []float64{0.016560870164523494, 0.023569242386797896},
},
{
Low: 13,
High: 26,
AnnualFailureRate: 0.038210930067894826,
ErrorInterval: []float64{0.03353859179329295, 0.0433520775718649},
},
{
Low: 26,
High: 39,
AnnualFailureRate: 0.11053528307302571,
ErrorInterval: []float64{0.09671061589521368, 0.1257816678419765},
},
{
Low: 39,
High: 52,
AnnualFailureRate: 0.16831189443375036,
ErrorInterval: []float64{0.1440976510675928, 0.19543066007594895},
},
{
Low: 52,
High: 65,
AnnualFailureRate: 0.20630344262550107,
ErrorInterval: []float64{0.1693965932069108, 0.2488633537247856},
},
{
Low: 65,
High: 78,
AnnualFailureRate: 0.1030972634140512,
ErrorInterval: []float64{0.06734655535304743, 0.15106137807407605},
},
{
Low: 78,
High: 91,
AnnualFailureRate: 0.12354840389522469,
ErrorInterval: []float64{0.06578432170016109, 0.21127153335749593},
},
},
},
13: {
ID: 13,
+1 -1
View File
@@ -2,4 +2,4 @@ package version
// VERSION is the app-global version string, which will be replaced with a
// new value during packaging
const VERSION = "0.4.1"
const VERSION = "0.4.7"
+10 -1
View File
@@ -93,6 +93,7 @@ func (suite *ServerTestSuite) TestHealthRoute() {
fakeConfig.EXPECT().GetString("web.src.frontend.path").Return(parentPath).AnyTimes()
fakeConfig.EXPECT().GetString("web.listen.basepath").Return(suite.Basepath).AnyTimes()
fakeConfig.EXPECT().GetString("web.influxdb.scheme").Return("http").AnyTimes()
fakeConfig.EXPECT().GetString("web.influxdb.port").Return("8086").AnyTimes()
fakeConfig.EXPECT().IsSet("web.influxdb.token").Return(true).AnyTimes()
fakeConfig.EXPECT().GetString("web.influxdb.token").Return("my-super-secret-auth-token").AnyTimes()
@@ -132,6 +133,7 @@ func (suite *ServerTestSuite) TestRegisterDevicesRoute() {
fakeConfig.EXPECT().GetString("web.database.location").Return(path.Join(parentPath, "scrutiny_test.db")).AnyTimes()
fakeConfig.EXPECT().GetString("web.src.frontend.path").Return(parentPath).AnyTimes()
fakeConfig.EXPECT().GetString("web.listen.basepath").Return(suite.Basepath).AnyTimes()
fakeConfig.EXPECT().GetString("web.influxdb.scheme").Return("http").AnyTimes()
fakeConfig.EXPECT().GetString("web.influxdb.port").Return("8086").AnyTimes()
fakeConfig.EXPECT().IsSet("web.influxdb.token").Return(true).AnyTimes()
fakeConfig.EXPECT().GetString("web.influxdb.token").Return("my-super-secret-auth-token").AnyTimes()
@@ -171,6 +173,7 @@ func (suite *ServerTestSuite) TestUploadDeviceMetricsRoute() {
fakeConfig.EXPECT().GetString("web.database.location").AnyTimes().Return(path.Join(parentPath, "scrutiny_test.db"))
fakeConfig.EXPECT().GetString("web.src.frontend.path").AnyTimes().Return(parentPath)
fakeConfig.EXPECT().GetString("web.listen.basepath").Return(suite.Basepath).AnyTimes()
fakeConfig.EXPECT().GetString("web.influxdb.scheme").Return("http").AnyTimes()
fakeConfig.EXPECT().GetString("web.influxdb.port").Return("8086").AnyTimes()
fakeConfig.EXPECT().IsSet("web.influxdb.token").Return(true).AnyTimes()
fakeConfig.EXPECT().GetString("web.influxdb.token").Return("my-super-secret-auth-token").AnyTimes()
@@ -219,6 +222,7 @@ func (suite *ServerTestSuite) TestPopulateMultiple() {
fakeConfig.EXPECT().GetString("web.database.location").AnyTimes().Return(path.Join(parentPath, "scrutiny_test.db"))
fakeConfig.EXPECT().GetString("web.src.frontend.path").AnyTimes().Return(parentPath)
fakeConfig.EXPECT().GetString("web.listen.basepath").Return(suite.Basepath).AnyTimes()
fakeConfig.EXPECT().GetString("web.influxdb.scheme").Return("http").AnyTimes()
fakeConfig.EXPECT().GetString("web.influxdb.port").Return("8086").AnyTimes()
fakeConfig.EXPECT().IsSet("web.influxdb.token").Return(true).AnyTimes()
fakeConfig.EXPECT().GetString("web.influxdb.token").Return("my-super-secret-auth-token").AnyTimes()
@@ -314,6 +318,7 @@ func (suite *ServerTestSuite) TestSendTestNotificationRoute_WebhookFailure() {
fakeConfig.EXPECT().GetString("web.database.location").AnyTimes().Return(path.Join(parentPath, "scrutiny_test.db"))
fakeConfig.EXPECT().GetString("web.src.frontend.path").AnyTimes().Return(parentPath)
fakeConfig.EXPECT().GetString("web.listen.basepath").Return(suite.Basepath).AnyTimes()
fakeConfig.EXPECT().GetString("web.influxdb.scheme").Return("http").AnyTimes()
fakeConfig.EXPECT().GetString("web.influxdb.port").Return("8086").AnyTimes()
fakeConfig.EXPECT().IsSet("web.influxdb.token").Return(true).AnyTimes()
fakeConfig.EXPECT().GetString("web.influxdb.token").Return("my-super-secret-auth-token").AnyTimes()
@@ -352,6 +357,7 @@ func (suite *ServerTestSuite) TestSendTestNotificationRoute_ScriptFailure() {
fakeConfig.EXPECT().GetString("web.database.location").AnyTimes().Return(path.Join(parentPath, "scrutiny_test.db"))
fakeConfig.EXPECT().GetString("web.src.frontend.path").AnyTimes().Return(parentPath)
fakeConfig.EXPECT().GetString("web.listen.basepath").Return(suite.Basepath).AnyTimes()
fakeConfig.EXPECT().GetString("web.influxdb.scheme").Return("http").AnyTimes()
fakeConfig.EXPECT().GetString("web.influxdb.port").Return("8086").AnyTimes()
fakeConfig.EXPECT().IsSet("web.influxdb.token").Return(true).AnyTimes()
fakeConfig.EXPECT().GetString("web.influxdb.token").Return("my-super-secret-auth-token").AnyTimes()
@@ -390,6 +396,7 @@ func (suite *ServerTestSuite) TestSendTestNotificationRoute_ScriptSuccess() {
fakeConfig.EXPECT().GetString("web.database.location").AnyTimes().Return(path.Join(parentPath, "scrutiny_test.db"))
fakeConfig.EXPECT().GetString("web.src.frontend.path").AnyTimes().Return(parentPath)
fakeConfig.EXPECT().GetString("web.listen.basepath").Return(suite.Basepath).AnyTimes()
fakeConfig.EXPECT().GetString("web.influxdb.scheme").Return("http").AnyTimes()
fakeConfig.EXPECT().GetString("web.influxdb.port").Return("8086").AnyTimes()
fakeConfig.EXPECT().IsSet("web.influxdb.token").Return(true).AnyTimes()
fakeConfig.EXPECT().GetString("web.influxdb.token").Return("my-super-secret-auth-token").AnyTimes()
@@ -428,6 +435,7 @@ func (suite *ServerTestSuite) TestSendTestNotificationRoute_ShoutrrrFailure() {
fakeConfig.EXPECT().GetString("web.database.location").AnyTimes().Return(path.Join(parentPath, "scrutiny_test.db"))
fakeConfig.EXPECT().GetString("web.src.frontend.path").AnyTimes().Return(parentPath)
fakeConfig.EXPECT().GetString("web.listen.basepath").Return(suite.Basepath).AnyTimes()
fakeConfig.EXPECT().GetString("web.influxdb.scheme").Return("http").AnyTimes()
fakeConfig.EXPECT().GetString("web.influxdb.port").Return("8086").AnyTimes()
fakeConfig.EXPECT().IsSet("web.influxdb.token").Return(true).AnyTimes()
fakeConfig.EXPECT().GetString("web.influxdb.token").Return("my-super-secret-auth-token").AnyTimes()
@@ -465,6 +473,7 @@ func (suite *ServerTestSuite) TestGetDevicesSummaryRoute_Nvme() {
fakeConfig.EXPECT().GetString("web.database.location").AnyTimes().Return(path.Join(parentPath, "scrutiny_test.db"))
fakeConfig.EXPECT().GetString("web.src.frontend.path").AnyTimes().Return(parentPath)
fakeConfig.EXPECT().GetString("web.listen.basepath").Return(suite.Basepath).AnyTimes()
fakeConfig.EXPECT().GetString("web.influxdb.scheme").Return("http").AnyTimes()
fakeConfig.EXPECT().GetString("web.influxdb.port").Return("8086").AnyTimes()
fakeConfig.EXPECT().IsSet("web.influxdb.token").Return(true).AnyTimes()
fakeConfig.EXPECT().GetString("web.influxdb.token").Return("my-super-secret-auth-token").AnyTimes()
@@ -509,5 +518,5 @@ func (suite *ServerTestSuite) TestGetDevicesSummaryRoute_Nvme() {
//assert
require.Equal(suite.T(), "a4c8e8ed-11a0-4c97-9bba-306440f1b944", deviceSummary.Data.Summary["a4c8e8ed-11a0-4c97-9bba-306440f1b944"].Device.WWN)
require.Equal(suite.T(), pkg.DeviceStatusFailedScrutiny, deviceSummary.Data.Summary["a4c8e8ed-11a0-4c97-9bba-306440f1b944"].Device.DeviceStatus)
require.Equal(suite.T(), pkg.DeviceStatusPassed, deviceSummary.Data.Summary["a4c8e8ed-11a0-4c97-9bba-306440f1b944"].Device.DeviceStatus)
}
@@ -3,6 +3,8 @@ import { BehaviorSubject, Observable } from 'rxjs';
import * as _ from 'lodash';
import { TREO_APP_CONFIG } from '@treo/services/config/config.constants';
const SCRUTINY_CONFIG_LOCAL_STORAGE_KEY = 'scrutiny';
@Injectable({
providedIn: 'root'
})
@@ -10,14 +12,22 @@ export class TreoConfigService
{
// Private
private _config: BehaviorSubject<any>;
/**
* Constructor
*/
constructor(@Inject(TREO_APP_CONFIG) config: any)
constructor(@Inject(TREO_APP_CONFIG) defaultConfig: any)
{
let currentScrutinyConfig = defaultConfig
let localConfigStr = localStorage.getItem(SCRUTINY_CONFIG_LOCAL_STORAGE_KEY)
if(localConfigStr){
//check localstorage for a value
let localConfig = JSON.parse(localConfigStr)
currentScrutinyConfig = localConfig
}
// Set the private defaults
this._config = new BehaviorSubject(config);
this._config = new BehaviorSubject(currentScrutinyConfig);
}
// -----------------------------------------------------------------------------------------------------
@@ -27,15 +37,20 @@ export class TreoConfigService
/**
* Setter and getter for config
*/
//Setter
set config(value: any)
{
// Merge the new config over to the current config
const config = _.merge({}, this._config.getValue(), value);
//Store the config in localstorage
localStorage.setItem(SCRUTINY_CONFIG_LOCAL_STORAGE_KEY, JSON.stringify(config));
// Execute the observable
this._config.next(config);
}
//Getter
get config$(): Observable<any>
{
return this._config.asObservable();
@@ -11,6 +11,11 @@ export interface AppConfig
{
theme: Theme;
layout: Layout;
// Dashboard options
dashboardDisplay: string;
dashboardSort: string;
}
/**
@@ -23,6 +28,9 @@ export interface AppConfig
*/
export const appConfig: AppConfig = {
theme : "light",
layout: "material"
layout: "material",
dashboardDisplay: "name",
dashboardSort: "status",
};
@@ -11,6 +11,9 @@ export const summary = {
"DeletedAt": null,
"wwn": "0x5000c500673e6b5f",
"device_name": "sdg",
"device_label": "14TB-WD-DRIVE2",
"device_uuid": "",
"device_serial_id": "ata-ST6000DX000-1H217Z-Z4DXXXXX",
"manufacturer": "ATA",
"model_name": "ST6000DX000-1H217Z",
"interface_type": "SCSI",
@@ -35,6 +38,9 @@ export const summary = {
"DeletedAt": null,
"wwn": "0x5000cca252c859cc",
"device_name": "sdd",
"device_label": "14TB-WD-DRIVE1",
"device_uuid": "806cf4bc-d160-4d96-8ee9-3ab7cf2a2e1f",
"device_serial_id": "ata-WDC_WD80EFAX-68LHPN0-7SGLXXXXX",
"manufacturer": "ATA",
"model_name": "WDC_WD80EFAX-68LHPN0",
"interface_type": "SCSI",
@@ -68,6 +74,9 @@ export const summary = {
"DeletedAt": null,
"wwn": "0x5000cca264eb01d7",
"device_name": "sdb",
"device_label": "14TB-WD-DRIVE5",
"device_uuid": "8125ec6d-a7e4-4950-ac84-72d6a4d67128",
"device_serial_id": "ata-WDC_WD140EDFZ-11A0VA0-9RK1XXXXX",
"manufacturer": "ATA",
"model_name": "WDC_WD140EDFZ-11A0VA0",
"interface_type": "SCSI",
@@ -101,6 +110,9 @@ export const summary = {
"DeletedAt": null,
"wwn": "0x5000cca264ebc248",
"device_name": "sde",
"device_label": "14TB-WD-DRIVE3",
"device_uuid": "9eb60cde-d6d0-4172-b520-b241a6a5477f",
"device_serial_id": "ata-WDC_WD140EDFZ-11A0VA0-9RK3XXXXX",
"manufacturer": "ATA",
"model_name": "WDC_WD140EDFZ-11A0VA0",
"interface_type": "SCSI",
@@ -125,6 +137,9 @@ export const summary = {
"DeletedAt": null,
"wwn": "0x5000cca264ec3183",
"device_name": "sdc",
"device_label": "14TB-WD-DRIVE6",
"device_uuid": "e1378723-7861-49b9-8e01-0bd063f0ecdd",
"device_serial_id": "ata-WDC_WD140EDFZ-11A0VA0-9RK4XXXXX",
"manufacturer": "ATA",
"model_name": "WDC_WD140EDFZ-11A0VA0",
"interface_type": "SCSI",
@@ -138,7 +153,7 @@ export const summary = {
"device_protocol": "",
"device_type": "",
"label": "",
"host_id": "",
"host_id": "custom host id",
"device_status": 1
},
"smart": {
@@ -542,6 +557,9 @@ export const summary = {
"DeletedAt": null,
"wwn": "0x50014ee20b2a72a9",
"device_name": "sdf",
"device_label": "8.0TB-WD-4",
"device_uuid": "fc684dcc-aa2f-44f3-a958-d302dc7dd46d",
"device_serial_id": "ata-WDC_WD60EFRX-68MYMN1-WXL1HXXXXX",
"manufacturer": "ATA",
"model_name": "WDC_WD60EFRX-68MYMN1",
"interface_type": "SCSI",
@@ -566,6 +584,9 @@ export const summary = {
"DeletedAt": null,
"wwn": "0x5002538e40a22954",
"device_name": "sda",
"device_label": "",
"device_uuid": "",
"device_serial_id": "ata-Samsung_SSD_860_EVO_500GB-S3YZNB0KBXXXXXX",
"manufacturer": "ATA",
"model_name": "Samsung_SSD_860_EVO_500GB",
"interface_type": "SCSI",
@@ -0,0 +1,61 @@
<div [ngClass]="{ 'border-green': deviceSummary.device.device_status == 0 && deviceSummary.smart,
'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">
<div class="absolute bottom-0 right-0 w-24 h-24 -m-6">
<mat-icon class="icon-size-96 opacity-12 text-green"
*ngIf="deviceSummary.device.device_status == 0 && deviceSummary.smart"
[svgIcon]="'heroicons_outline:check-circle'"></mat-icon>
<mat-icon class="icon-size-96 opacity-12 text-red"
*ngIf="deviceSummary.device.device_status != 0"
[svgIcon]="'heroicons_outline:exclamation-circle'"></mat-icon>
<mat-icon class="icon-size-96 opacity-12 text-yellow"
*ngIf="!deviceSummary.smart"
[svgIcon]="'heroicons_outline:question-mark-circle'"></mat-icon>
</div>
<div class="flex items-center">
<div class="flex flex-col">
<a [routerLink]="'/device/'+ deviceSummary.device.wwn"
class="font-bold text-md text-secondary uppercase tracking-wider">{{deviceTitle(deviceSummary.device)}}</a>
<div [ngClass]="classDeviceLastUpdatedOn(deviceSummary)" class="font-medium text-sm" *ngIf="deviceSummary.smart">
Last Updated on {{deviceSummary.smart.collector_date | date:'MMMM dd, yyyy - HH:mm' }}
</div>
</div>
<div class="ml-auto" *ngIf="deviceSummary.device">
<button mat-icon-button
[matMenuTriggerFor]="previousStatementMenu">
<mat-icon [svgIcon]="'more_vert'"></mat-icon>
</button>
<mat-menu #previousStatementMenu="matMenu">
<a mat-menu-item [routerLink]="'/device/'+ deviceSummary.device.wwn">
<span class="flex items-center">
<mat-icon class="icon-size-20 mr-3"
[svgIcon]="'payment'"></mat-icon>
<span>View Details</span>
</span>
</a>
</mat-menu>
</div>
</div>
<div class="flex flex-row flex-wrap mt-4 -mx-6">
<div class="flex flex-col mx-6 my-3 xs:w-full">
<div class="font-semibold text-xs text-hint uppercase tracking-wider leading-none">Status</div>
<div class="mt-2 font-medium text-3xl leading-none" *ngIf="deviceSummary.smart?.collector_date; else unknownStatus">{{ deviceStatusString(deviceSummary.device.device_status) | titlecase}}</div>
<ng-template #unknownStatus><div class="mt-2 font-medium text-3xl leading-none">No Data</div></ng-template>
</div>
<div class="flex flex-col mx-6 my-3 xs:w-full">
<div class="font-semibold text-xs text-hint uppercase tracking-wider leading-none">Temperature</div>
<div class="mt-2 font-medium text-3xl leading-none" *ngIf="deviceSummary.smart?.collector_date; else unknownTemp">{{ deviceSummary.smart?.temp }}°C</div>
<ng-template #unknownTemp><div class="mt-2 font-medium text-3xl leading-none">--</div></ng-template>
</div>
<div class="flex flex-col mx-6 my-3 xs:w-full">
<div class="font-semibold text-xs text-hint uppercase tracking-wider leading-none">Capacity</div>
<div class="mt-2 font-medium text-3xl leading-none">{{ deviceSummary.device.capacity | fileSize}}</div>
</div>
<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="mt-2 font-medium text-3xl leading-none" *ngIf="deviceSummary.smart?.power_on_hours; else unknownPoweredOn">{{ humanizeDuration(deviceSummary.smart?.power_on_hours * 60 * 60 * 1000, { round: true, largest: 1, units: ['y', 'd', 'h'] }) }}</div>
<ng-template #unknownPoweredOn><div class="mt-2 font-medium text-3xl leading-none">--</div></ng-template>
</div>
</div>
</div>
@@ -0,0 +1,25 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { DashboardDeviceComponent } from './dashboard-device.component';
describe('DashboardDeviceComponent', () => {
let component: DashboardDeviceComponent;
let fixture: ComponentFixture<DashboardDeviceComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ DashboardDeviceComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(DashboardDeviceComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -0,0 +1,115 @@
import {Component, Input, OnInit} from '@angular/core';
import * as moment from "moment";
import {takeUntil} from "rxjs/operators";
import {AppConfig} from "app/core/config/app.config";
import {TreoConfigService} from "@treo/services/config";
import {Subject} from "rxjs";
import humanizeDuration from 'humanize-duration'
@Component({
selector: 'app-dashboard-device',
templateUrl: './dashboard-device.component.html',
styleUrls: ['./dashboard-device.component.scss']
})
export class DashboardDeviceComponent implements OnInit {
@Input() deviceSummary: any;
@Input() deviceWWN: string;
config: AppConfig;
private _unsubscribeAll: Subject<any>;
constructor(
private _configService: TreoConfigService,
) {
// Set the private defaults
this._unsubscribeAll = new Subject();
}
ngOnInit(): void {
// Subscribe to config changes
this._configService.config$
.pipe(takeUntil(this._unsubscribeAll))
.subscribe((config: AppConfig) => {
this.config = config;
});
}
// -----------------------------------------------------------------------------------------------------
// @ Public methods
// -----------------------------------------------------------------------------------------------------
classDeviceLastUpdatedOn(deviceSummary){
if (deviceSummary.device.device_status !== 0) {
return 'text-red' // if the device has failed, always highlight in red
} else if(deviceSummary.device.device_status === 0 && deviceSummary.smart){
if(moment().subtract(14, 'd').isBefore(deviceSummary.smart.collector_date)){
// this device was updated in the last 2 weeks.
return 'text-green'
} else if(moment().subtract(1, 'm').isBefore(deviceSummary.smart.collector_date)){
// this device was updated in the last month
return 'text-yellow'
} else{
// last updated more than a month ago.
return 'text-red'
}
} else {
return ''
}
}
deviceTitle(disk){
console.log(`Displaying Device ${disk.wwn} with: ${this.config.dashboardDisplay}`)
let titleParts = []
if (disk.host_id) titleParts.push(disk.host_id)
//add device identifier (fallback to generated device name)
titleParts.push(deviceDisplayTitle(disk, this.config.dashboardDisplay) || deviceDisplayTitle(disk, 'name'))
return titleParts.join(' - ')
}
deviceStatusString(deviceStatus){
if(deviceStatus == 0){
return "passed"
} else {
return "failed"
}
}
readonly humanizeDuration = humanizeDuration;
}
export function deviceDisplayTitle(disk, titleType: string){
let titleParts = []
switch(titleType){
case 'name':
titleParts.push(`/dev/${disk.device_name}`)
if (disk.device_type && disk.device_type != 'scsi' && disk.device_type != 'ata'){
titleParts.push(disk.device_type)
}
titleParts.push(disk.model_name)
break;
case 'serial_id':
if(!disk.device_serial_id) return ''
titleParts.push(`/by-id/${disk.device_serial_id}`)
break;
case 'uuid':
if(!disk.device_uuid) return ''
titleParts.push(`/by-uuid/${disk.device_uuid}`)
break;
case 'label':
if(disk.label){
titleParts.push(disk.label)
} else if(disk.device_label){
titleParts.push(`/by-label/${disk.device_label}`)
}
break;
}
return titleParts.join(' - ')
}
@@ -0,0 +1,52 @@
import { NgModule } from '@angular/core';
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 { MatSelectModule } from '@angular/material/select';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatIconModule } from '@angular/material/icon';
import { MatInputModule } from '@angular/material/input';
import { SharedModule } from 'app/shared/shared.module';
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 {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 {DashboardSettingsModule} from "../dashboard-settings/dashboard-settings.module";
@NgModule({
declarations: [
DashboardDeviceComponent
],
imports : [
RouterModule.forChild([]),
RouterModule.forChild(dashboardRoutes),
MatButtonModule,
MatDividerModule,
MatTooltipModule,
MatIconModule,
MatMenuModule,
MatProgressBarModule,
MatSortModule,
MatTableModule,
NgApexchartsModule,
SharedModule,
],
exports : [
DashboardDeviceComponent,
],
providers : []
})
export class DashboardDeviceModule
{
}
@@ -1,14 +1,23 @@
<h2 mat-dialog-title>Scrutiny Settings</h2>
<mat-dialog-content class="mat-typography">
<form class="flex flex-col p-8 pb-0 overflow-hidden">
<div class="flex flex-col gt-xs:flex-row">
<mat-form-field class="flex-auto gt-xs:pr-3">
<div class="flex flex-col p-8 pb-0 overflow-hidden">
<div class="flex flex-col mt-5 gt-md:flex-row">
<mat-form-field class="flex-auto gt-xs:pr-3 gt-md:pr-3">
<mat-label>Display Title</mat-label>
<mat-select [(ngModel)]="dashboardDisplay">
<mat-option value="name">Name</mat-option>
<mat-option value="serial_id">Serial ID</mat-option>
<mat-option value="uuid">UUID</mat-option>
<mat-option value="label">Label</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field class="flex-auto gt-xs:pr-3 gt-md:pl-3">
<mat-label>Sort By</mat-label>
<mat-select [value]="'status'">
<mat-option value="status">Status</mat-option>
<mat-option value="name" disabled>Name</mat-option>
<mat-option value="label" disabled>Label</mat-option>
<mat-select [(ngModel)]="dashboardSort">
<mat-option value="status">Status</mat-option>
<mat-option value="title">Title</mat-option>
</mat-select>
</mat-form-field>
</div>
@@ -17,61 +26,61 @@
<mat-tab-group mat-align-tabs="start">
<mat-tab label="Ata">
<div class="flex flex-col mt-5 gt-md:flex-row">
<div matTooltip="not yet implemented" class="gray-200 flex flex-col mt-5 gt-md:flex-row">
<mat-form-field class="flex-auto gt-md:pr-3">
<mat-label>Critical Error Threshold</mat-label>
<input matInput [value]="'10%'">
<input disabled matInput [value]="'10%'">
</mat-form-field>
<mat-form-field class="flex-auto gt-md:pl-3">
<mat-label>Critical Warning Threshold</mat-label>
<input matInput>
<input disabled matInput>
</mat-form-field>
</div>
<div class="flex flex-col gt-md:flex-row">
<div matTooltip="not yet implemented" class="gray-200 flex flex-col gt-md:flex-row">
<mat-form-field class="flex-auto gt-md:pr-3">
<mat-label>Error Threshold</mat-label>
<input matInput [value]="'20%'">
<input disabled matInput [value]="'20%'">
</mat-form-field>
<mat-form-field class="flex-auto gt-md:pl-3">
<mat-label>Warning Threshold</mat-label>
<input matInput [value]="'10%'">
<input disabled matInput [value]="'10%'">
</mat-form-field>
</div>
</mat-tab>
<mat-tab label="NVMe">
<div class="flex flex-col mt-5 gt-md:flex-row">
<div matTooltip="not yet implemented" class="gray-200 flex flex-col mt-5 gt-md:flex-row">
<mat-form-field class="flex-auto gt-md:pr-3">
<mat-label>Critical Error Threshold</mat-label>
<input matInput [value]="'enabled'">
<input disabled matInput [value]="'enabled'">
</mat-form-field>
<mat-form-field class="flex-auto gt-md:pl-3">
<mat-label>Critical Warning Threshold</mat-label>
<input matInput>
<input disabled matInput>
</mat-form-field>
</div>
</mat-tab>
<mat-tab label="SCSI">
<div class="flex flex-col mt-5 gt-md:flex-row">
<div matTooltip="not yet implemented" class="gray-200 flex flex-col mt-5 gt-md:flex-row">
<mat-form-field class="flex-auto gt-md:pr-3">
<mat-label>Critical Error Threshold</mat-label>
<input matInput [value]="'enabled'">
<input disabled matInput [value]="'enabled'">
</mat-form-field>
<mat-form-field class="flex-auto gt-md:pl-3">
<mat-label>Critical Warning Threshold</mat-label>
<input matInput>
<input disabled matInput>
</mat-form-field>
</div>
</mat-tab>
</mat-tab-group>
</div>
</form>
</div>
</mat-dialog-content>
<mat-dialog-actions align="end">
<button mat-button mat-dialog-close>Cancel</button>
<button mat-button matTooltip="not yet implemented" [mat-dialog-close]="true" cdkFocusInitial>Save</button>
<button mat-button mat-dialog-close (click)="saveSettings()" cdkFocusInitial>Save</button>
</mat-dialog-actions>
@@ -1,4 +1,8 @@
import { Component, OnInit } from '@angular/core';
import {AppConfig} from 'app/core/config/app.config';
import { TreoConfigService } from '@treo/services/config';
import {Subject} from "rxjs";
import {takeUntil} from "rxjs/operators";
@Component({
selector: 'app-dashboard-settings',
@@ -7,10 +11,41 @@ import { Component, OnInit } from '@angular/core';
})
export class DashboardSettingsComponent implements OnInit {
constructor() { }
dashboardDisplay: string;
dashboardSort: string;
// Private
private _unsubscribeAll: Subject<any>;
constructor(
private _configService: TreoConfigService,
) {
// Set the private defaults
this._unsubscribeAll = new Subject();
}
ngOnInit(): void {
// Subscribe to config changes
this._configService.config$
.pipe(takeUntil(this._unsubscribeAll))
.subscribe((config: AppConfig) => {
// Store the config
this.dashboardDisplay = config.dashboardDisplay;
this.dashboardSort = config.dashboardSort;
});
}
saveSettings(): void {
var newSettings = {
dashboardDisplay: this.dashboardDisplay,
dashboardSort: this.dashboardSort
}
this._configService.config = newSettings
console.log(`Saved Settings: ${JSON.stringify(newSettings)}`)
}
formatLabel(value: number) {
return value;
}
@@ -47,71 +47,15 @@
</div>
</div>
<div class="flex flex-wrap w-full">
<div *ngFor="let summary of data.data.summary | keyvalue" class="flex gt-sm:w-1/2 min-w-80 p-4">
<div [ngClass]="{ 'border-green': summary.value.device.device_status == 0 && summary.value.smart,
'border-red': summary.value.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">
<div class="absolute bottom-0 right-0 w-24 h-24 -m-6">
<mat-icon class="icon-size-96 opacity-12 text-green"
*ngIf="summary.value.device.device_status == 0 && summary.value.smart"
[svgIcon]="'heroicons_outline:check-circle'"></mat-icon>
<mat-icon class="icon-size-96 opacity-12 text-red"
*ngIf="summary.value.device.device_status != 0"
[svgIcon]="'heroicons_outline:exclamation-circle'"></mat-icon>
<mat-icon class="icon-size-96 opacity-12 text-yellow"
*ngIf="!summary.value.smart"
[svgIcon]="'heroicons_outline:question-mark-circle'"></mat-icon>
</div>
<div class="flex items-center">
<div class="flex flex-col">
<a [routerLink]="'/device/'+ summary.value.device.wwn"
class="font-bold text-md text-secondary uppercase tracking-wider">{{deviceTitle(summary.value.device)}}</a>
<div [ngClass]="classDeviceLastUpdatedOn(summary.value)" class="font-medium text-sm" *ngIf="summary.value.smart">
Last Updated on {{summary.value.smart.collector_date | date:'MMMM dd, yyyy - HH:mm' }}
</div>
</div>
<div class="ml-auto" *ngIf="summary.value.device">
<button mat-icon-button
[matMenuTriggerFor]="previousStatementMenu">
<mat-icon [svgIcon]="'more_vert'"></mat-icon>
</button>
<mat-menu #previousStatementMenu="matMenu">
<a mat-menu-item [routerLink]="'/device/'+ summary.value.device.wwn">
<span class="flex items-center">
<mat-icon class="icon-size-20 mr-3"
[svgIcon]="'payment'"></mat-icon>
<span>View Details</span>
</span>
</a>
</mat-menu>
</div>
</div>
<div class="flex flex-row flex-wrap mt-4 -mx-6">
<div class="flex flex-col mx-6 my-3 xs:w-full">
<div class="font-semibold text-xs text-hint uppercase tracking-wider leading-none">Status</div>
<div class="mt-2 font-medium text-3xl leading-none" *ngIf="summary.value.smart?.collector_date; else unknownStatus">{{ deviceStatusString(summary.value.device.device_status) | titlecase}}</div>
<ng-template #unknownStatus><div class="mt-2 font-medium text-3xl leading-none">No Data</div></ng-template>
</div>
<div class="flex flex-col mx-6 my-3 xs:w-full">
<div class="font-semibold text-xs text-hint uppercase tracking-wider leading-none">Temperature</div>
<div class="mt-2 font-medium text-3xl leading-none" *ngIf="summary.value.smart?.collector_date; else unknownTemp">{{ summary.value.smart?.temp }}°C</div>
<ng-template #unknownTemp><div class="mt-2 font-medium text-3xl leading-none">--</div></ng-template>
</div>
<div class="flex flex-col mx-6 my-3 xs:w-full">
<div class="font-semibold text-xs text-hint uppercase tracking-wider leading-none">Capacity</div>
<div class="mt-2 font-medium text-3xl leading-none">{{ summary.value.device.capacity | fileSize}}</div>
</div>
<div class="flex flex-col mx-6 my-3 xs:w-full">
<div class="font-semibold text-xs text-hint uppercase tracking-wider leading-none">Powered On</div>
<div class="mt-2 font-medium text-3xl leading-none" *ngIf="summary.value.smart?.power_on_hours; else unknownPoweredOn">{{ humanizeDuration(summary.value.smart?.power_on_hours * 60 * 60 * 1000, { round: true, largest: 1, units: ['y', 'd', 'h'] }) }}</div>
<ng-template #unknownPoweredOn><div class="mt-2 font-medium text-3xl leading-none">--</div></ng-template>
</div>
</div>
</div>
<div class="flex flex-wrap w-full" *ngFor="let hostId of hostGroups | keyvalue">
<h3 class="ml-4" *ngIf="hostId.key">{{hostId.key}}</h3>
<div class="flex flex-wrap w-full">
<app-dashboard-device 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>
</div>
</div>
<!-- Drive Temperatures -->
<div class="flex flex-auto w-full min-w-80 h-90 p-4">
<div class="flex flex-col flex-auto bg-card shadow-md rounded overflow-hidden">
@@ -123,22 +67,22 @@
</div>
<div>
<button class="h-8 min-h-8 px-2"
matTooltip="not yet implemented"
mat-button
[matMenuTriggerFor]="tempRangeMenu">
<span class="font-medium text-sm text-hint">1 week</span>
<span class="font-medium text-sm text-hint">{{tempDurationKey}}</span>
</button>
<mat-menu #tempRangeMenu="matMenu">
<button mat-menu-item>1 month</button>
<button mat-menu-item>12 months</button>
<button mat-menu-item>all time</button>
<button (click)="changeSummaryTempDuration('forever')" mat-menu-item>forever</button>
<button (click)="changeSummaryTempDuration('year')" mat-menu-item>year</button>
<button (click)="changeSummaryTempDuration('month')" mat-menu-item>month</button>
<button (click)="changeSummaryTempDuration('week')" mat-menu-item>week</button>
</mat-menu>
</div>
</div>
</div>
<div class="flex flex-col flex-auto">
<apx-chart *ngIf="temperatureOptions" class="flex-auto w-full h-full"
<apx-chart #tempChart *ngIf="temperatureOptions" class="flex-auto w-full h-full"
[chart]="temperatureOptions.chart"
[colors]="temperatureOptions.colors"
[fill]="temperatureOptions.fill"
@@ -3,12 +3,14 @@ import { MatSort } from '@angular/material/sort';
import { MatTableDataSource } from '@angular/material/table';
import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
import { ApexOptions } from 'ng-apexcharts';
import {ApexOptions, ChartComponent} from 'ng-apexcharts';
import { DashboardService } from 'app/modules/dashboard/dashboard.service';
import * as moment from "moment";
import {MatDialog} from '@angular/material/dialog';
import { DashboardSettingsComponent } from 'app/layout/common/dashboard-settings/dashboard-settings.component';
import humanizeDuration from 'humanize-duration'
import {deviceDisplayTitle} from "app/layout/common/dashboard-device/dashboard-device.component";
import {AppConfig} from "app/core/config/app.config";
import {TreoConfigService} from "@treo/services/config";
import {Router} from "@angular/router";
@Component({
selector : 'example',
@@ -20,10 +22,14 @@ import humanizeDuration from 'humanize-duration'
export class DashboardComponent implements OnInit, AfterViewInit, OnDestroy
{
data: any;
hostGroups: { [hostId: string]: string[] } = {}
temperatureOptions: ApexOptions;
tempDurationKey: string = "forever"
config: AppConfig;
// Private
private _unsubscribeAll: Subject<any>;
@ViewChild("tempChart", { static: false }) tempChart: ChartComponent;
/**
* Constructor
@@ -32,7 +38,9 @@ export class DashboardComponent implements OnInit, AfterViewInit, OnDestroy
*/
constructor(
private _smartService: DashboardService,
public dialog: MatDialog
private _configService: TreoConfigService,
public dialog: MatDialog,
private router: Router,
)
{
// Set the private defaults
@@ -49,6 +57,28 @@ export class DashboardComponent implements OnInit, AfterViewInit, OnDestroy
*/
ngOnInit(): void
{
// Subscribe to config changes
this._configService.config$
.pipe(takeUntil(this._unsubscribeAll))
.subscribe((config: AppConfig) => {
//check if the old config and the new config do not match.
let oldConfig = JSON.stringify(this.config)
let newConfig = JSON.stringify(config)
if(oldConfig != newConfig){
console.log(`Configuration updated: ${newConfig} vs ${oldConfig}`)
// Store the config
this.config = config;
if(oldConfig){
console.log("reloading component...")
this.refreshComponent()
}
}
});
// Get the data
this._smartService.data$
.pipe(takeUntil(this._unsubscribeAll))
@@ -57,6 +87,15 @@ export class DashboardComponent implements OnInit, AfterViewInit, OnDestroy
// Store the data
this.data = data;
//generate group data.
for(let wwn in this.data.data.summary){
let hostid = this.data.data.summary[wwn].device.host_id
let hostDeviceList = this.hostGroups[hostid] || []
hostDeviceList.push(wwn)
this.hostGroups[hostid] = hostDeviceList
}
console.log(this.hostGroups)
// Prepare the chart data
this._prepareChartData();
});
@@ -81,6 +120,14 @@ export class DashboardComponent implements OnInit, AfterViewInit, OnDestroy
// -----------------------------------------------------------------------------------------------------
// @ Private methods
// -----------------------------------------------------------------------------------------------------
private refreshComponent(){
let currentUrl = this.router.url;
this.router.routeReuseStrategy.shouldReuseRoute = () => false;
this.router.onSameUrlNavigation = 'reload';
this.router.navigate([currentUrl]);
}
private _deviceDataTemperatureSeries() {
var deviceTemperatureSeries = []
@@ -91,8 +138,11 @@ export class DashboardComponent implements OnInit, AfterViewInit, OnDestroy
if (!deviceSummary.temp_history){
continue
}
let deviceName = this.deviceTitle(deviceSummary.device)
var deviceSeriesMetadata = {
name: `/dev/${deviceSummary.device.device_name}`,
name: deviceName,
data: []
}
@@ -164,6 +214,26 @@ export class DashboardComponent implements OnInit, AfterViewInit, OnDestroy
// @ Public methods
// -----------------------------------------------------------------------------------------------------
deviceTitle(disk){
console.log(`Displaying Device ${disk.wwn} with: ${this.config.dashboardDisplay}`)
let titleParts = []
if (disk.host_id) titleParts.push(disk.host_id)
//add device identifier (fallback to generated device name)
titleParts.push(deviceDisplayTitle(disk, this.config.dashboardDisplay) || deviceDisplayTitle(disk, 'name'))
return titleParts.join(' - ')
}
deviceSummariesForHostGroup(hostGroupWWNs: string[]) {
let deviceSummaries = []
for(let wwn of hostGroupWWNs){
deviceSummaries.push(this.data.data.summary[wwn])
}
return deviceSummaries
}
openDialog() {
const dialogRef = this.dialog.open(DashboardSettingsComponent);
@@ -172,48 +242,29 @@ export class DashboardComponent implements OnInit, AfterViewInit, OnDestroy
});
}
deviceTitle(disk){
let title = []
/*
if (disk.host_id) title.push(disk.host_id)
DURATION_KEY_WEEK = "week"
DURATION_KEY_MONTH = "month"
DURATION_KEY_YEAR = "year"
DURATION_KEY_FOREVER = "forever"
*/
title.push(`/dev/${disk.device_name}`)
changeSummaryTempDuration(durationKey: string){
this.tempDurationKey = durationKey
if (disk.device_type && disk.device_type != 'scsi' && disk.device_type != 'ata'){
title.push(disk.device_type)
}
this._smartService.getSummaryTempData(durationKey)
.subscribe((data) => {
title.push(disk.model_name)
// given a list of device temp history, override the data in the "summary" object.
for(const wwn in this.data.data.summary) {
// console.log(`Updating ${wwn}, length: ${this.data.data.summary[wwn].temp_history.length}`)
this.data.data.summary[wwn].temp_history = data.data.temp_history[wwn] || []
}
return title.join(' - ')
}
deviceStatusString(deviceStatus){
if(deviceStatus == 0){
return "passed"
} else {
return "failed"
}
}
classDeviceLastUpdatedOn(deviceSummary){
if (deviceSummary.device.device_status !== 0) {
return 'text-red' // if the device has failed, always highlight in red
} else if(deviceSummary.device.device_status === 0 && deviceSummary.smart){
if(moment().subtract(14, 'd').isBefore(deviceSummary.smart.collector_date)){
// this device was updated in the last 2 weeks.
return 'text-green'
} else if(moment().subtract(1, 'm').isBefore(deviceSummary.smart.collector_date)){
// this device was updated in the last month
return 'text-yellow'
} else{
// last updated more than a month ago.
return 'text-red'
}
} else {
return ''
}
// Prepare the chart series data
this.tempChart.updateSeries(this._deviceDataTemperatureSeries())
});
}
/**
@@ -227,6 +278,4 @@ export class DashboardComponent implements OnInit, AfterViewInit, OnDestroy
return item.id || index;
}
readonly humanizeDuration = humanizeDuration;
}
@@ -13,12 +13,13 @@ import { MatTableModule } from '@angular/material/table';
import { NgApexchartsModule } from 'ng-apexcharts';
import { MatTooltipModule } from '@angular/material/tooltip'
import { DashboardSettingsModule } from "app/layout/common/dashboard-settings/dashboard-settings.module";
import { DashboardDeviceModule } from "app/layout/common/dashboard-device/dashboard-device.module";
@NgModule({
declarations: [
DashboardComponent
],
imports : [
imports: [
RouterModule.forChild(dashboardRoutes),
MatButtonModule,
MatDividerModule,
@@ -30,7 +31,8 @@ import { DashboardSettingsModule } from "app/layout/common/dashboard-settings/da
MatTableModule,
NgApexchartsModule,
SharedModule,
DashboardSettingsModule
DashboardSettingsModule,
DashboardDeviceModule
]
})
export class DashboardModule
@@ -31,6 +31,6 @@ export class DashboardResolver implements Resolve<any>
*/
resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<any>
{
return this._dashboardService.getData();
return this._dashboardService.getSummaryData();
}
}
@@ -44,7 +44,7 @@ export class DashboardService
/**
* Get data
*/
getData(): Observable<any>
getSummaryData(): Observable<any>
{
return this._httpClient.get(getBasePath() + '/api/summary').pipe(
tap((response: any) => {
@@ -52,4 +52,14 @@ export class DashboardService
})
);
}
getSummaryTempData(durationKey: string): Observable<any>
{
let params = {}
if(durationKey){
params["duration_key"] = durationKey
}
return this._httpClient.get(getBasePath() + '/api/summary/temp', {params: params});
}
}
@@ -111,9 +111,9 @@ export class DetailComponent implements OnInit, AfterViewInit, OnDestroy {
if(attribute_status == 0){
return "passed"
} else if (attribute_status == 1){
return "warn"
} else if (attribute_status == 2){
return "failed"
} else if (attribute_status == 2){
return "warn"
}
return
}
@@ -1,33 +1,60 @@
import { Pipe, PipeTransform } from '@angular/core';
import {deviceDisplayTitle} from "app/layout/common/dashboard-device/dashboard-device.component";
@Pipe({
name: 'deviceSort'
})
export class DeviceSortPipe implements PipeTransform {
numericalStatus(device): number {
if(!device.smart_results[0]){
return 0
} else if (device.smart_results[0].smart_status == 'passed'){
return 1
} else {
return -1
statusCompareFn(a: any, b: any) {
function deviceStatus(deviceSummary): number {
if(!deviceSummary.smart){
return 0
} else if (deviceSummary.device.device_status == 0){
return 1
} else {
return deviceSummary.device.device_status * -1 // will return range from -1, -2, -3
}
}
let left = deviceStatus(a)
let right = deviceStatus(b)
return left - right;
}
titleCompareFn(dashboardDisplay: string) {
return function (a: any, b: any){
let _dashboardDisplay = dashboardDisplay
let left = deviceDisplayTitle(a.device, _dashboardDisplay) || deviceDisplayTitle(a.device, 'name')
let right = deviceDisplayTitle(b.device, _dashboardDisplay) || deviceDisplayTitle(b.device, 'name')
if( left < right )
return -1;
if( left > right )
return 1;
return 0;
}
}
transform(devices: Array<unknown>, ...args: unknown[]): Array<unknown> {
transform(deviceSummaries: Array<unknown>, sortBy = 'status', dashboardDisplay = "name"): Array<unknown> {
let compareFn = undefined
switch (sortBy) {
case 'status':
compareFn = this.statusCompareFn
break;
case 'title':
compareFn = this.titleCompareFn(dashboardDisplay)
break;
}
//failed, unknown/empty, passed
devices.sort((a: any, b: any) => {
deviceSummaries.sort(compareFn);
let left = this.numericalStatus(a)
let right = this.numericalStatus(b)
return left - right;
});
return devices;
return deviceSummaries;
}
}