Compare commits
130 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2e4ba44952 | |||
| 4192ac719e | |||
| 539c94595f | |||
| de21e611a3 | |||
| dea362361e | |||
| 7b77519f49 | |||
| 94df7e1ec3 | |||
| babd8d3089 | |||
| 52ef28f091 | |||
| 80d72f8a1b | |||
| 2e8f4a0581 | |||
| 7fd2e2b050 | |||
| 8c65166a90 | |||
| 733b49c2c4 | |||
| 711b5c40b1 | |||
| f94e616d8d | |||
| 007857afd5 | |||
| e4bbe8c035 | |||
| cb5226f6e4 | |||
| c69770d1dd | |||
| e07a53046f | |||
| 19a0b8c2ac | |||
| 97f73703b1 | |||
| 4fcd11f497 | |||
| 7f1023fa9b | |||
| d49497da80 | |||
| d8c359bd8a | |||
| ad4b117f6e | |||
| 22d2f9847c | |||
| 1c7f299b98 | |||
| 602fdce0ee | |||
| 61fde6a2ca | |||
| d843bcc258 | |||
| f2856e0f26 | |||
| 58ef1aa311 | |||
| 6a6570b8e3 | |||
| 29a0860caa | |||
| fb760a9f6d | |||
| c9f13f4398 | |||
| dd03a8cf63 | |||
| 0a6ade4da9 | |||
| 5c8c11d78b | |||
| 00502cc565 | |||
| 0febe3fda5 | |||
| fcd4bb4561 | |||
| 89f763e65d | |||
| 075eb94fa2 | |||
| e9cf8a9180 | |||
| 64ad353628 | |||
| 5518865bc6 | |||
| 50321d897a | |||
| e18a7e9ce0 | |||
| 536b590080 | |||
| 098ce0673a | |||
| 2677796322 | |||
| 5cc7fb30ed | |||
| 222b8103d6 | |||
| 727d5b0ace | |||
| d7b45e5f01 | |||
| 578a262d90 | |||
| c6e11f88b4 | |||
| b795331efb | |||
| f1e5bd3ed4 | |||
| d8d56f77f9 | |||
| 26b221532e | |||
| 15d3206f6f | |||
| 59e2e928a8 | |||
| 51f59e4fcd | |||
| f823127825 | |||
| d41d535ab7 | |||
| 9a4a8de341 | |||
| 2d6f60abaa | |||
| d201f798fb | |||
| a1b0108503 | |||
| f0275d2349 | |||
| 9dafde8a43 | |||
| fa8f86ab7b | |||
| 41c9daa939 | |||
| 83186ba36e | |||
| 3205e3d022 | |||
| 3f272b36d4 | |||
| b238579fe6 | |||
| ce2f990eb1 | |||
| b11b8732aa | |||
| 5cd441da7b | |||
| 2e768fb491 | |||
| e8755ff617 | |||
| e41ee47371 | |||
| 7a68a68e76 | |||
| 94594db20a | |||
| 7e672e8b8e | |||
| 54e2cacb00 | |||
| c0f1dfdb0b | |||
| 29bc79996b | |||
| 99af2b8b16 | |||
| dd0c3e6fba | |||
| 5b2746f389 | |||
| e9c1de9664 | |||
| 6ca4bd4912 | |||
| c34ee85e48 | |||
| 91e8eb1def | |||
| a01b8fe083 | |||
| 550fb542d4 | |||
| 7841063783 | |||
| 8e05b2e2f8 | |||
| 64e1c93d16 | |||
| b227054b52 | |||
| 66bd6f99c5 | |||
| c6579864b8 | |||
| 2361c329e2 | |||
| 5ea149d878 | |||
| 30bd18f816 | |||
| 0f0efac866 | |||
| 04563c0d0d | |||
| 9316eccabe | |||
| b71d6660a6 | |||
| 0e2fec4e93 | |||
| ff171282cc | |||
| ea8fe208d0 | |||
| 9ae9c387cc | |||
| 772b4f6528 | |||
| 4a16ca0d5a | |||
| 316ce856f7 | |||
| 6e0321f488 | |||
| 338d2ae04e | |||
| 4419f7f429 | |||
| 797a6b0429 | |||
| d0b545dfb7 | |||
| b0bff53bbd | |||
| b4adf3d88d |
@@ -1,85 +0,0 @@
|
||||
name: CI
|
||||
# This workflow is triggered on pushes & pull requests
|
||||
on: [pull_request]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build
|
||||
runs-on: ubuntu-latest
|
||||
container: techknowlogick/xgo:go-1.17.x
|
||||
|
||||
# Service containers to run with `build` (Required for end-to-end testing)
|
||||
services:
|
||||
influxdb:
|
||||
image: influxdb:2.2
|
||||
env:
|
||||
DOCKER_INFLUXDB_INIT_MODE: setup
|
||||
DOCKER_INFLUXDB_INIT_USERNAME: admin
|
||||
DOCKER_INFLUXDB_INIT_PASSWORD: password12345
|
||||
DOCKER_INFLUXDB_INIT_ORG: scrutiny
|
||||
DOCKER_INFLUXDB_INIT_BUCKET: metrics
|
||||
DOCKER_INFLUXDB_INIT_ADMIN_TOKEN: my-super-secret-auth-token
|
||||
ports:
|
||||
- 8086:8086
|
||||
env:
|
||||
PROJECT_PATH: /go/src/github.com/analogj/scrutiny
|
||||
CGO_ENABLED: 1
|
||||
steps:
|
||||
- name: Git
|
||||
run: |
|
||||
apt-get update && apt-get install -y software-properties-common
|
||||
add-apt-repository ppa:git-core/ppa && apt-get update && apt-get install -y git
|
||||
git --version
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
- name: Test
|
||||
run: |
|
||||
mkdir -p $(dirname "$PROJECT_PATH")
|
||||
cp -a $GITHUB_WORKSPACE $PROJECT_PATH
|
||||
cd $PROJECT_PATH
|
||||
|
||||
go mod vendor
|
||||
go test -race -coverprofile=coverage.txt -covermode=atomic -v -tags "static" $(go list ./... | grep -v /vendor/)
|
||||
- name: Generate coverage report
|
||||
uses: codecov/codecov-action@v2
|
||||
with:
|
||||
files: ${{ env.PROJECT_PATH }}/coverage.txt
|
||||
flags: unittests
|
||||
fail_ci_if_error: true
|
||||
verbose: true
|
||||
- name: Build Binaries
|
||||
run: |
|
||||
|
||||
cd $PROJECT_PATH
|
||||
make all
|
||||
|
||||
- name: Archive
|
||||
uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: binaries.zip
|
||||
path: |
|
||||
/build/scrutiny-web-linux-amd64
|
||||
/build/scrutiny-collector-metrics-linux-amd64
|
||||
/build/scrutiny-web-linux-arm64
|
||||
/build/scrutiny-collector-metrics-linux-arm64
|
||||
/build/scrutiny-web-linux-arm-5
|
||||
/build/scrutiny-collector-metrics-linux-arm-5
|
||||
/build/scrutiny-web-linux-arm-6
|
||||
/build/scrutiny-collector-metrics-linux-arm-6
|
||||
/build/scrutiny-web-linux-arm-7
|
||||
/build/scrutiny-collector-metrics-linux-arm-7
|
||||
/build/scrutiny-web-windows-4.0-amd64.exe
|
||||
/build/scrutiny-collector-metrics-windows-4.0-amd64.exe
|
||||
# /build/scrutiny-web-darwin-arm64
|
||||
# /build/scrutiny-collector-metrics-darwin-arm64
|
||||
# /build/scrutiny-web-darwin-amd64
|
||||
# /build/scrutiny-collector-metrics-darwin-amd64
|
||||
# /build/scrutiny-web-freebsd-amd64
|
||||
# /build/scrutiny-collector-metrics-freebsd-amd64
|
||||
- uses: codecov/codecov-action@v2
|
||||
with:
|
||||
file: ${{ env.PROJECT_PATH }}/coverage.txt
|
||||
flags: unittests
|
||||
fail_ci_if_error: false
|
||||
|
||||
|
||||
@@ -0,0 +1,114 @@
|
||||
name: CI
|
||||
# This workflow is triggered on pushes & pull requests
|
||||
on: [pull_request]
|
||||
|
||||
jobs:
|
||||
test-frontend:
|
||||
name: Test Frontend
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
- name: Test Frontend
|
||||
run: |
|
||||
make binary-frontend-test-coverage
|
||||
- name: Upload coverage
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: coverage
|
||||
path: ${{ github.workspace }}/webapp/frontend/coverage/lcov.info
|
||||
retention-days: 1
|
||||
test-backend:
|
||||
name: Test Backend
|
||||
runs-on: ubuntu-latest
|
||||
container: ghcr.io/packagrio/packagr:latest-golang
|
||||
# Service containers to run with `build` (Required for end-to-end testing)
|
||||
services:
|
||||
influxdb:
|
||||
image: influxdb:2.2
|
||||
env:
|
||||
DOCKER_INFLUXDB_INIT_MODE: setup
|
||||
DOCKER_INFLUXDB_INIT_USERNAME: admin
|
||||
DOCKER_INFLUXDB_INIT_PASSWORD: password12345
|
||||
DOCKER_INFLUXDB_INIT_ORG: scrutiny
|
||||
DOCKER_INFLUXDB_INIT_BUCKET: metrics
|
||||
DOCKER_INFLUXDB_INIT_ADMIN_TOKEN: my-super-secret-auth-token
|
||||
ports:
|
||||
- 8086:8086
|
||||
env:
|
||||
STATIC: true
|
||||
steps:
|
||||
- name: Git
|
||||
run: |
|
||||
apt-get update && apt-get install -y software-properties-common
|
||||
add-apt-repository ppa:git-core/ppa && apt-get update && apt-get install -y git
|
||||
git --version
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
- name: Test Backend
|
||||
run: |
|
||||
make binary-clean binary-test-coverage
|
||||
- name: Upload coverage
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: coverage
|
||||
path: ${{ github.workspace }}/coverage.txt
|
||||
retention-days: 1
|
||||
test-coverage:
|
||||
name: Test Coverage Upload
|
||||
needs:
|
||||
- test-backend
|
||||
- test-frontend
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
- name: Download coverage reports
|
||||
uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: coverage
|
||||
- name: Upload coverage reports
|
||||
uses: codecov/codecov-action@v2
|
||||
with:
|
||||
files: ${{ github.workspace }}/coverage.txt,${{ github.workspace }}/lcov.info
|
||||
flags: unittests
|
||||
fail_ci_if_error: true
|
||||
verbose: true
|
||||
|
||||
build:
|
||||
name: Build ${{ matrix.cfg.goos }}/${{ matrix.cfg.goarch }}
|
||||
runs-on: ${{ matrix.cfg.on }}
|
||||
env:
|
||||
GOOS: ${{ matrix.cfg.goos }}
|
||||
GOARCH: ${{ matrix.cfg.goarch }}
|
||||
GOARM: ${{ matrix.cfg.goarm }}
|
||||
STATIC: true
|
||||
strategy:
|
||||
matrix:
|
||||
cfg:
|
||||
- { on: ubuntu-latest, goos: linux, goarch: amd64 }
|
||||
- { on: ubuntu-latest, goos: linux, goarch: arm, goarm: 5 }
|
||||
- { on: ubuntu-latest, goos: linux, goarch: arm, goarm: 6 }
|
||||
- { on: ubuntu-latest, goos: linux, goarch: arm, goarm: 7 }
|
||||
- { on: ubuntu-latest, goos: linux, goarch: arm64 }
|
||||
- { on: macos-latest, goos: darwin, goarch: amd64 }
|
||||
- { on: macos-latest, goos: darwin, goarch: arm64 }
|
||||
- { on: macos-latest, goos: freebsd, goarch: amd64 }
|
||||
- { on: windows-latest, goos: windows, goarch: amd64 }
|
||||
- { on: windows-latest, goos: windows, goarch: arm64 }
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
- uses: actions/setup-go@v3
|
||||
with:
|
||||
go-version: '^1.20.1'
|
||||
- name: Build Binaries
|
||||
run: |
|
||||
make binary-clean binary-all
|
||||
- name: Archive
|
||||
uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: binaries.zip
|
||||
path: |
|
||||
scrutiny-web-*
|
||||
scrutiny-collector-metrics-*
|
||||
@@ -74,15 +74,13 @@ jobs:
|
||||
uses: actions/checkout@v2
|
||||
- name: "Populate frontend version information"
|
||||
run: "cd webapp/frontend && ./git.version.sh"
|
||||
- name: "Generate frontend"
|
||||
uses: addnab/docker-run-action@v3
|
||||
- name: "Install Node"
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
image: node:lts
|
||||
options: -v ${{ github.workspace }}:/work
|
||||
run: |
|
||||
cd /work
|
||||
make frontend && echo "print contents of /work/dist" && ls -alt /work/dist
|
||||
|
||||
node-version: 16
|
||||
- name: "Generate frontend"
|
||||
run: |
|
||||
make binary-frontend && echo "print contents of ./dist" && ls -alt ./dist
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v2
|
||||
with:
|
||||
@@ -134,16 +132,13 @@ jobs:
|
||||
uses: actions/checkout@v2
|
||||
- name: "Populate frontend version information"
|
||||
run: "cd webapp/frontend && ./git.version.sh"
|
||||
- name: "Generate frontend & version information"
|
||||
uses: addnab/docker-run-action@v3
|
||||
- name: "Install Node"
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
image: node:lts
|
||||
options: -v ${{ github.workspace }}:/work
|
||||
run: |
|
||||
cd /work
|
||||
make frontend && echo "print contents of /work/dist" && ls -alt /work/dist
|
||||
|
||||
|
||||
node-version: 16
|
||||
- name: "Generate frontend"
|
||||
run: |
|
||||
make binary-frontend && echo "print contents of ./dist" && ls -alt ./dist
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v2
|
||||
with:
|
||||
@@ -181,4 +176,4 @@ jobs:
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
# cache-from: type=gha
|
||||
# cache-to: type=gha,mode=max
|
||||
# cache-to: type=gha,mode=max
|
||||
|
||||
@@ -19,16 +19,13 @@ jobs:
|
||||
uses: actions/checkout@v2
|
||||
- name: "Populate frontend version information"
|
||||
run: "cd webapp/frontend && ./git.version.sh"
|
||||
- name: "Generate frontend & version information"
|
||||
uses: addnab/docker-run-action@v3
|
||||
- name: "Install Node"
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
image: node:lts
|
||||
options: -v ${{ github.workspace }}:/work
|
||||
run: |
|
||||
cd /work
|
||||
make frontend && echo "print contents of /work/dist" && ls -alt /work/dist
|
||||
|
||||
|
||||
node-version: 16
|
||||
- name: "Generate frontend"
|
||||
run: |
|
||||
make binary-frontend && echo "print contents of ./dist" && ls -alt ./dist
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v2
|
||||
with:
|
||||
|
||||
@@ -1,83 +0,0 @@
|
||||
# compiles FreeBSD artifacts and attaches them to build
|
||||
name: Release FreeBSD
|
||||
|
||||
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-10.15
|
||||
env:
|
||||
PROJECT_PATH: /go/src/github.com/analogj/scrutiny
|
||||
GOPATH: /go
|
||||
GOOS: freebsd
|
||||
GOARCH: amd64
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
ref: ${{github.event.release.tag_name || github.event.inputs.tag_name }}
|
||||
- name: Build Binaries
|
||||
uses: vmactions/freebsd-vm@v0.1.5
|
||||
with:
|
||||
envs: 'PROJECT_PATH GOPATH GOOS GOARCH'
|
||||
usesh: true
|
||||
#TODO: lock go version using https://www.jeremymorgan.com/tutorials/golang/how-to-install-go-freebsd/
|
||||
prepare: pkg install -y curl go gmake
|
||||
run: |
|
||||
pwd
|
||||
ls -lah
|
||||
whoami
|
||||
freebsd-version
|
||||
|
||||
mkdir -p $(dirname "$PROJECT_PATH")
|
||||
cp -R $GITHUB_WORKSPACE $PROJECT_PATH
|
||||
cd $PROJECT_PATH
|
||||
|
||||
mkdir -p $GITHUB_WORKSPACE/dist
|
||||
|
||||
echo "building web binary (OS = ${GOOS}, ARCH = ${GOARCH})"
|
||||
go build -ldflags "-extldflags=-static -X main.goos=${GOOS} -X main.goarch=${GOARCH}" -o $GITHUB_WORKSPACE/dist/scrutiny-web-${GOOS}-${GOARCH} -tags "static netgo sqlite_omit_load_extension" webapp/backend/cmd/scrutiny/scrutiny.go
|
||||
|
||||
chmod +x "$GITHUB_WORKSPACE/dist/scrutiny-web-${GOOS}-${GOARCH}"
|
||||
file "$GITHUB_WORKSPACE/dist/scrutiny-web-${GOOS}-${GOARCH}" || true
|
||||
ldd "$GITHUB_WORKSPACE/dist/scrutiny-web-${GOOS}-${GOARCH}" || true
|
||||
|
||||
echo "building collector binary (OS = ${GOOS}, ARCH = ${GOARCH})"
|
||||
go build -ldflags "-extldflags=-static -X main.goos=${GOOS} -X main.goarch=${GOARCH}" -o $GITHUB_WORKSPACE/dist/scrutiny-collector-metrics-${GOOS}-${GOARCH} -tags "static netgo" collector/cmd/collector-metrics/collector-metrics.go
|
||||
|
||||
chmod +x "$GITHUB_WORKSPACE/dist/scrutiny-collector-metrics-${GOOS}-${GOARCH}"
|
||||
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
|
||||
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-web-freebsd-amd64'
|
||||
asset_name: scrutiny-web-freebsd-amd64
|
||||
asset_content_type: application/octet-stream
|
||||
|
||||
@@ -19,11 +19,8 @@ jobs:
|
||||
run: "cd webapp/frontend && ./git.version.sh"
|
||||
- name: Build Frontend
|
||||
run: |
|
||||
cd webapp/frontend
|
||||
npm install -g @angular/cli@9.1.4
|
||||
npm install
|
||||
mkdir -p dist
|
||||
npm run build:prod -- --output-path=dist
|
||||
apt-get update && apt-get install -y make
|
||||
make binary-frontend
|
||||
tar -czf scrutiny-web-frontend.tar.gz dist
|
||||
- name: Upload Frontend Asset
|
||||
id: upload-release-asset3
|
||||
@@ -32,6 +29,6 @@ jobs:
|
||||
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: './webapp/frontend/scrutiny-web-frontend.tar.gz'
|
||||
asset_path: './scrutiny-web-frontend.tar.gz'
|
||||
asset_name: scrutiny-web-frontend.tar.gz
|
||||
asset_content_type: application/gzip
|
||||
|
||||
@@ -13,10 +13,10 @@ on:
|
||||
default: 'webapp/backend/pkg/version/version.go'
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build
|
||||
release:
|
||||
name: Create Release Commit
|
||||
runs-on: ubuntu-latest
|
||||
container: techknowlogick/xgo:go-1.17.x
|
||||
container: ghcr.io/packagrio/packagr:latest-golang
|
||||
# Service containers to run with `build` (Required for end-to-end testing)
|
||||
services:
|
||||
influxdb:
|
||||
@@ -31,8 +31,7 @@ jobs:
|
||||
ports:
|
||||
- 8086:8086
|
||||
env:
|
||||
PROJECT_PATH: /go/src/github.com/analogj/scrutiny
|
||||
CGO_ENABLED: 1
|
||||
STATIC: true
|
||||
steps:
|
||||
- name: Git
|
||||
run: |
|
||||
@@ -53,34 +52,80 @@ jobs:
|
||||
GITHUB_TOKEN: ${{ secrets.SCRUTINY_GITHUB_TOKEN }} # Leave this line unchanged
|
||||
- name: Test
|
||||
run: |
|
||||
mkdir -p $(dirname "$PROJECT_PATH")
|
||||
cp -a $GITHUB_WORKSPACE $PROJECT_PATH
|
||||
cd $PROJECT_PATH
|
||||
|
||||
go mod vendor
|
||||
go test -v -tags "static" $(go list ./... | grep -v /vendor/)
|
||||
|
||||
- name: Build Binaries
|
||||
run: |
|
||||
|
||||
cd $PROJECT_PATH
|
||||
make all
|
||||
|
||||
# restore modified dir to GH workspace.
|
||||
cp -arf $PROJECT_PATH/. $GITHUB_WORKSPACE/
|
||||
|
||||
# copy all the build artifacts to the GH workspace
|
||||
cp -arf /build/. $GITHUB_WORKSPACE/
|
||||
|
||||
- name: Commit Changes
|
||||
make binary-clean binary-test-coverage
|
||||
- name: Commit Changes Locally
|
||||
id: commit
|
||||
uses: packagrio/action-releasr-go@master
|
||||
env:
|
||||
# This is necessary in order to push a commit to the repo
|
||||
GITHUB_TOKEN: ${{ secrets.SCRUTINY_GITHUB_TOKEN }} # Leave this line unchanged
|
||||
with:
|
||||
version_metadata_path: ${{ github.event.inputs.version_metadata_path }}
|
||||
- name: Publish Release
|
||||
- name: Upload workspace
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: workspace
|
||||
path: ${{ github.workspace }}/**/*
|
||||
retention-days: 1
|
||||
|
||||
build:
|
||||
name: Build ${{ matrix.cfg.goos }}/${{ matrix.cfg.goarch }}${{ matrix.cfg.goarm }}
|
||||
needs: release
|
||||
runs-on: ${{ matrix.cfg.on }}
|
||||
env:
|
||||
GOOS: ${{ matrix.cfg.goos }}
|
||||
GOARCH: ${{ matrix.cfg.goarch }}
|
||||
GOARM: ${{ matrix.cfg.goarm }}
|
||||
STATIC: true
|
||||
strategy:
|
||||
matrix:
|
||||
cfg:
|
||||
- { on: ubuntu-latest, goos: linux, goarch: amd64 }
|
||||
- { on: ubuntu-latest, goos: linux, goarch: arm, goarm: 5 }
|
||||
- { on: ubuntu-latest, goos: linux, goarch: arm, goarm: 6 }
|
||||
- { on: ubuntu-latest, goos: linux, goarch: arm, goarm: 7 }
|
||||
- { on: ubuntu-latest, goos: linux, goarch: arm64 }
|
||||
- { on: macos-latest, goos: darwin, goarch: amd64 }
|
||||
- { on: macos-latest, goos: darwin, goarch: arm64 }
|
||||
- { on: macos-latest, goos: freebsd, goarch: amd64 }
|
||||
- { on: windows-latest, goos: windows, goarch: amd64 }
|
||||
- { on: windows-latest, goos: windows, goarch: arm64 }
|
||||
steps:
|
||||
- name: Download workspace
|
||||
uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: workspace
|
||||
- uses: actions/setup-go@v3
|
||||
with:
|
||||
go-version: '1.20.1' # The Go version to download (if necessary) and use.
|
||||
- name: Build Binaries
|
||||
run: |
|
||||
make binary-clean binary-all
|
||||
- name: Archive
|
||||
uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: binaries.zip
|
||||
path: |
|
||||
scrutiny-web-*
|
||||
scrutiny-collector-metrics-*
|
||||
|
||||
release-publish:
|
||||
name: Publish Release
|
||||
needs: build
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Download workspace
|
||||
uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: workspace
|
||||
- name: Download binaries
|
||||
uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: binaries.zip
|
||||
- name: List
|
||||
shell: bash
|
||||
run: |
|
||||
ls -alt
|
||||
- name: Publish Release & Assets
|
||||
id: publish
|
||||
uses: packagrio/action-publishr-go@master
|
||||
env:
|
||||
@@ -89,15 +134,23 @@ jobs:
|
||||
with:
|
||||
version_metadata_path: ${{ github.event.inputs.version_metadata_path }}
|
||||
upload_assets:
|
||||
scrutiny-web-linux-amd64
|
||||
scrutiny-collector-metrics-darwin-amd64
|
||||
scrutiny-collector-metrics-darwin-arm64
|
||||
scrutiny-collector-metrics-freebsd-amd64
|
||||
scrutiny-collector-metrics-linux-amd64
|
||||
scrutiny-web-linux-arm64
|
||||
scrutiny-collector-metrics-linux-arm64
|
||||
scrutiny-web-linux-arm-5
|
||||
scrutiny-collector-metrics-linux-arm-5
|
||||
scrutiny-web-linux-arm-6
|
||||
scrutiny-collector-metrics-linux-arm-6
|
||||
scrutiny-web-linux-arm-7
|
||||
scrutiny-collector-metrics-linux-arm-7
|
||||
scrutiny-web-windows-4.0-amd64.exe
|
||||
scrutiny-collector-metrics-windows-4.0-amd64.exe
|
||||
scrutiny-collector-metrics-linux-arm64
|
||||
scrutiny-collector-metrics-windows-amd64.exe
|
||||
scrutiny-collector-metrics-windows-arm64.exe
|
||||
scrutiny-web-darwin-amd64
|
||||
scrutiny-web-darwin-arm64
|
||||
scrutiny-web-freebsd-amd64
|
||||
scrutiny-web-linux-amd64
|
||||
scrutiny-web-linux-arm-5
|
||||
scrutiny-web-linux-arm-6
|
||||
scrutiny-web-linux-arm-7
|
||||
scrutiny-web-linux-arm64
|
||||
scrutiny-web-windows-amd64.exe
|
||||
scrutiny-web-windows-arm64.exe
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
name: Cleanup Artifacts
|
||||
|
||||
on:
|
||||
schedule:
|
||||
# Every day at 1am
|
||||
- cron: '0 1 * * *'
|
||||
|
||||
jobs:
|
||||
remove-old-artifacts:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
|
||||
steps:
|
||||
- name: Remove old artifacts
|
||||
uses: c-hive/gha-remove-artifacts@v1
|
||||
with:
|
||||
age: '1 day'
|
||||
skip-tags: true
|
||||
skip-recent: 5
|
||||
+13
-12
@@ -5,17 +5,18 @@ The Scrutiny repository is a [monorepo](https://en.wikipedia.org/wiki/Monorepo)
|
||||
- Scrutiny Frontend Angular SPA
|
||||
- S.M.A.R.T Collector
|
||||
|
||||
Depending on the functionality you are adding, you may need to setup a development environment for 1 or more projects.
|
||||
Depending on the functionality you are adding, you may need to setup a development environment for 1 or more projects.
|
||||
|
||||
# Modifying the Scrutiny Backend Server (API)
|
||||
|
||||
1. install the [Go runtime](https://go.dev/doc/install) (v1.17+)
|
||||
2. download the `scrutiny-web-frontend.tar.gz` for the [latest release](https://github.com/AnalogJ/scrutiny/releases/latest). Extract to a folder named `dist`
|
||||
1. install the [Go runtime](https://go.dev/doc/install) (v1.20+)
|
||||
2. download the `scrutiny-web-frontend.tar.gz` for
|
||||
the [latest release](https://github.com/AnalogJ/scrutiny/releases/latest). Extract to a folder named `dist`
|
||||
3. create a `scrutiny.yaml` config file
|
||||
```yaml
|
||||
# config file for local development. store as scrutiny.yaml
|
||||
version: 1
|
||||
|
||||
|
||||
web:
|
||||
listen:
|
||||
port: 8080
|
||||
@@ -28,13 +29,13 @@ Depending on the functionality you are adding, you may need to setup a developme
|
||||
path: ./dist
|
||||
influxdb:
|
||||
retention_policy: false
|
||||
|
||||
|
||||
log:
|
||||
file: 'web.log' #absolute or relative paths allowed, eg. web.log
|
||||
level: DEBUG
|
||||
|
||||
```
|
||||
4. start a InfluxDB docker container.
|
||||
4. start a InfluxDB docker container.
|
||||
```bash
|
||||
docker run -p 8086:8086 --rm influxdb:2.2
|
||||
```
|
||||
@@ -54,21 +55,21 @@ The frontend is written in Angular. If you're working on the frontend and can us
|
||||
```bash
|
||||
cd webapp/frontend
|
||||
npm install
|
||||
npm run start -- --deploy-url="/web/" --base-href="/web/" --port 4200
|
||||
npm run start -- --serve-path="/web/" --port 4200
|
||||
```
|
||||
3. open your browser and visit [http://localhost:4200/web](http://localhost:4200/web)
|
||||
|
||||
# Modifying both Scrutiny Backend and Frontend Applications
|
||||
If you're developing a feature that requires changes to the backend and the frontend, or a frontend feature that requires real data,
|
||||
If you're developing a feature that requires changes to the backend and the frontend, or a frontend feature that requires real data,
|
||||
you'll need to follow the steps below:
|
||||
|
||||
1. install the [Go runtime](https://go.dev/doc/install) (v1.17+)
|
||||
1. install the [Go runtime](https://go.dev/doc/install) (v1.20+)
|
||||
2. install [NodeJS](https://nodejs.org/en/download/)
|
||||
3. create a `scrutiny.yaml` config file
|
||||
```yaml
|
||||
# config file for local development. store as scrutiny.yaml
|
||||
version: 1
|
||||
|
||||
|
||||
web:
|
||||
listen:
|
||||
port: 8080
|
||||
@@ -81,7 +82,7 @@ you'll need to follow the steps below:
|
||||
path: ./dist
|
||||
influxdb:
|
||||
retention_policy: false
|
||||
|
||||
|
||||
log:
|
||||
file: 'web.log' #absolute or relative paths allowed, eg. web.log
|
||||
level: DEBUG
|
||||
@@ -184,4 +185,4 @@ docker run -p 8086:8086 -d --rm \
|
||||
influxdb:2.2
|
||||
go test ./...
|
||||
|
||||
```
|
||||
```
|
||||
|
||||
@@ -1,66 +1,133 @@
|
||||
export CGO_ENABLED = 1
|
||||
.ONESHELL: # Applies to every targets in the file! .ONESHELL instructs make to invoke a single instance of the shell and provide it with the entire recipe, regardless of how many lines it contains.
|
||||
.SHELLFLAGS = -ec
|
||||
|
||||
########################################################################################################################
|
||||
# Global Env Settings
|
||||
########################################################################################################################
|
||||
|
||||
GO_WORKSPACE ?= /go/src/github.com/analogj/scrutiny
|
||||
|
||||
BINARY=\
|
||||
linux/amd64 \
|
||||
linux/arm-5 \
|
||||
linux/arm-6 \
|
||||
linux/arm-7 \
|
||||
linux/arm64 \
|
||||
COLLECTOR_BINARY_NAME = scrutiny-collector-metrics
|
||||
WEB_BINARY_NAME = scrutiny-web
|
||||
LD_FLAGS =
|
||||
|
||||
.ONESHELL: # Applies to every targets in the file! .ONESHELL instructs make to invoke a single instance of the shell and provide it with the entire recipe, regardless of how many lines it contains.
|
||||
.PHONY: all $(BINARY)
|
||||
all: $(BINARY) windows/amd64
|
||||
STATIC_TAGS =
|
||||
# enable multiarch docker image builds
|
||||
DOCKER_TARGETARCH_BUILD_ARG =
|
||||
ifdef TARGETARCH
|
||||
DOCKER_TARGETARCH_BUILD_ARG := $(DOCKER_TARGETARCH_BUILD_ARG) --build-arg TARGETARCH=$(TARGETARCH)
|
||||
endif
|
||||
|
||||
$(BINARY): OS = $(word 1,$(subst /, ,$*))
|
||||
$(BINARY): ARCH = $(word 2,$(subst /, ,$*))
|
||||
$(BINARY): build/scrutiny-web-%:
|
||||
@echo "building web binary (OS = $(OS), ARCH = $(ARCH))"
|
||||
xgo -v --targets="$(OS)/$(ARCH)" -ldflags "-extldflags=-static -X main.goos=$(OS) -X main.goarch=$(ARCH)" -out scrutiny-web -tags "static netgo sqlite_omit_load_extension" ${GO_WORKSPACE}/webapp/backend/cmd/scrutiny/
|
||||
# enable to build static binaries.
|
||||
ifdef STATIC
|
||||
export CGO_ENABLED = 0
|
||||
LD_FLAGS := $(LD_FLAGS) -extldflags=-static
|
||||
STATIC_TAGS := $(STATIC_TAGS) -tags "static netgo"
|
||||
endif
|
||||
ifdef GOOS
|
||||
COLLECTOR_BINARY_NAME := $(COLLECTOR_BINARY_NAME)-$(GOOS)
|
||||
WEB_BINARY_NAME := $(WEB_BINARY_NAME)-$(GOOS)
|
||||
LD_FLAGS := $(LD_FLAGS) -X main.goos=$(GOOS)
|
||||
endif
|
||||
ifdef GOARCH
|
||||
COLLECTOR_BINARY_NAME := $(COLLECTOR_BINARY_NAME)-$(GOARCH)
|
||||
WEB_BINARY_NAME := $(WEB_BINARY_NAME)-$(GOARCH)
|
||||
LD_FLAGS := $(LD_FLAGS) -X main.goarch=$(GOARCH)
|
||||
endif
|
||||
ifdef GOARM
|
||||
COLLECTOR_BINARY_NAME := $(COLLECTOR_BINARY_NAME)-$(GOARM)
|
||||
WEB_BINARY_NAME := $(WEB_BINARY_NAME)-$(GOARM)
|
||||
endif
|
||||
ifeq ($(OS),Windows_NT)
|
||||
COLLECTOR_BINARY_NAME := $(COLLECTOR_BINARY_NAME).exe
|
||||
WEB_BINARY_NAME := $(WEB_BINARY_NAME).exe
|
||||
endif
|
||||
|
||||
chmod +x "/build/scrutiny-web-$(OS)-$(ARCH)"
|
||||
file "/build/scrutiny-web-$(OS)-$(ARCH)" || true
|
||||
ldd "/build/scrutiny-web-$(OS)-$(ARCH)" || true
|
||||
########################################################################################################################
|
||||
# Binary
|
||||
########################################################################################################################
|
||||
.PHONY: all
|
||||
all: binary-all
|
||||
|
||||
@echo "building collector binary (OS = $(OS), ARCH = $(ARCH))"
|
||||
xgo -v --targets="$(OS)/$(ARCH)" -ldflags "-extldflags=-static -X main.goos=$(OS) -X main.goarch=$(ARCH)" -out scrutiny-collector-metrics -tags "static netgo" ${GO_WORKSPACE}/collector/cmd/collector-metrics/
|
||||
|
||||
chmod +x "/build/scrutiny-collector-metrics-$(OS)-$(ARCH)"
|
||||
file "/build/scrutiny-collector-metrics-$(OS)-$(ARCH)" || true
|
||||
ldd "/build/scrutiny-collector-metrics-$(OS)-$(ARCH)" || true
|
||||
|
||||
windows/amd64: export OS = windows
|
||||
windows/amd64: export ARCH = amd64
|
||||
windows/amd64:
|
||||
@echo "building web binary (OS = $(OS), ARCH = $(ARCH))"
|
||||
xgo -v --targets="$(OS)/$(ARCH)" -ldflags "-extldflags=-static -X main.goos=$(OS) -X main.goarch=$(ARCH)" -out scrutiny-web -tags "static netgo sqlite_omit_load_extension" ${GO_WORKSPACE}/webapp/backend/cmd/scrutiny/
|
||||
|
||||
@echo "building collector binary (OS = $(OS), ARCH = $(ARCH))"
|
||||
xgo -v --targets="$(OS)/$(ARCH)" -ldflags "-extldflags=-static -X main.goos=$(OS) -X main.goarch=$(ARCH)" -out scrutiny-collector-metrics -tags "static netgo" ${GO_WORKSPACE}/collector/cmd/collector-metrics/
|
||||
.PHONY: binary-all
|
||||
binary-all: binary-collector binary-web
|
||||
@echo "built binary-collector and binary-web targets"
|
||||
|
||||
|
||||
docker-collector:
|
||||
@echo "building collector docker image"
|
||||
docker build --build-arg TARGETARCH=amd64 -f docker/Dockerfile.collector -t analogj/scrutiny-dev:collector .
|
||||
.PHONY: binary-clean
|
||||
binary-clean:
|
||||
go clean
|
||||
|
||||
docker-web:
|
||||
@echo "building web docker image"
|
||||
docker build --build-arg TARGETARCH=amd64 -f docker/Dockerfile.web -t analogj/scrutiny-dev:web .
|
||||
.PHONY: binary-dep
|
||||
binary-dep:
|
||||
go mod vendor
|
||||
|
||||
docker-omnibus:
|
||||
@echo "building omnibus docker image"
|
||||
docker build --build-arg TARGETARCH=amd64 -f docker/Dockerfile -t analogj/scrutiny-dev:omnibus .
|
||||
.PHONY: binary-test
|
||||
binary-test: binary-dep
|
||||
go test -v $(STATIC_TAGS) ./...
|
||||
|
||||
.PHONY: binary-test-coverage
|
||||
binary-test-coverage: binary-dep
|
||||
go test -coverprofile=coverage.txt -covermode=atomic -v $(STATIC_TAGS) ./...
|
||||
|
||||
.PHONY: binary-collector
|
||||
binary-collector: binary-dep
|
||||
go build -ldflags "$(LD_FLAGS)" -o $(COLLECTOR_BINARY_NAME) $(STATIC_TAGS) ./collector/cmd/collector-metrics/
|
||||
ifneq ($(OS),Windows_NT)
|
||||
chmod +x $(COLLECTOR_BINARY_NAME)
|
||||
file $(COLLECTOR_BINARY_NAME) || true
|
||||
ldd $(COLLECTOR_BINARY_NAME) || true
|
||||
./$(COLLECTOR_BINARY_NAME) || true
|
||||
endif
|
||||
|
||||
.PHONY: binary-web
|
||||
binary-web: binary-dep
|
||||
go build -ldflags "$(LD_FLAGS)" -o $(WEB_BINARY_NAME) $(STATIC_TAGS) ./webapp/backend/cmd/scrutiny/
|
||||
ifneq ($(OS),Windows_NT)
|
||||
chmod +x $(WEB_BINARY_NAME)
|
||||
file $(WEB_BINARY_NAME) || true
|
||||
ldd $(WEB_BINARY_NAME) || true
|
||||
./$(WEB_BINARY_NAME) || true
|
||||
endif
|
||||
|
||||
########################################################################################################################
|
||||
# Binary
|
||||
########################################################################################################################
|
||||
|
||||
.PHONY: binary-frontend
|
||||
# reduce logging, disable angular-cli analytics for ci environment
|
||||
frontend: export NPM_CONFIG_LOGLEVEL = warn
|
||||
frontend: export NG_CLI_ANALYTICS = false
|
||||
frontend:
|
||||
binary-frontend: export NPM_CONFIG_LOGLEVEL = warn
|
||||
binary-frontend: export NG_CLI_ANALYTICS = false
|
||||
binary-frontend:
|
||||
cd webapp/frontend
|
||||
npm install -g @angular/cli@9.1.4
|
||||
mkdir -p $(CURDIR)/dist
|
||||
npm ci
|
||||
npm run build:prod -- --output-path=$(CURDIR)/dist
|
||||
|
||||
# clean:
|
||||
# rm scrutiny-collector-metrics-* scrutiny-web-*
|
||||
.PHONY: binary-frontend-test-coverage
|
||||
# reduce logging, disable angular-cli analytics for ci environment
|
||||
binary-frontend-test-coverage:
|
||||
cd webapp/frontend
|
||||
npm ci
|
||||
npx ng test --watch=false --browsers=ChromeHeadless --code-coverage
|
||||
|
||||
########################################################################################################################
|
||||
# Docker
|
||||
# NOTE: these docker make targets are only used for local development (not used by Github Actions/CI)
|
||||
# NOTE: docker-web and docker-omnibus require `make binary-frontend` or frontend.tar.gz content in /dist before executing.
|
||||
########################################################################################################################
|
||||
.PHONY: docker-collector
|
||||
docker-collector:
|
||||
@echo "building collector docker image"
|
||||
docker build $(DOCKER_TARGETARCH_BUILD_ARG) -f docker/Dockerfile.collector -t analogj/scrutiny-dev:collector .
|
||||
|
||||
.PHONY: docker-web
|
||||
docker-web:
|
||||
@echo "building web docker image"
|
||||
docker build $(DOCKER_TARGETARCH_BUILD_ARG) -f docker/Dockerfile.web -t analogj/scrutiny-dev:web .
|
||||
|
||||
.PHONY: docker-omnibus
|
||||
docker-omnibus:
|
||||
@echo "building omnibus docker image"
|
||||
docker build $(DOCKER_TARGETARCH_BUILD_ARG) -f docker/Dockerfile -t analogj/scrutiny-dev:omnibus .
|
||||
|
||||
@@ -46,7 +46,7 @@ Scrutiny is a simple but focused application, with a couple of core features:
|
||||
- Customized thresholds using real world failure rates
|
||||
- Temperature tracking
|
||||
- Provided as an all-in-one Docker image (but can be installed manually)
|
||||
- Future Configurable Alerting/Notifications via Webhooks
|
||||
- Configurable Alerting/Notifications via Webhooks
|
||||
- (Future) Hard Drive performance testing & tracking
|
||||
|
||||
# Getting Started
|
||||
@@ -91,10 +91,14 @@ docker run -it --rm -p 8080:8080 -p 8086:8086 \
|
||||
|
||||
### Hub/Spoke Deployment
|
||||
|
||||
In addition to the Omnibus image (available under the `latest` tag) there are 2 other Docker images available:
|
||||
In addition to the Omnibus image (available under the `latest` tag) you can deploy in Hub/Spoke mode, which requires 3
|
||||
other Docker images:
|
||||
|
||||
- `ghcr.io/analogj/scrutiny:master-collector` - Contains the Scrutiny data collector, `smartctl` binary and cron-like scheduler. You can run one collector on each server.
|
||||
- `ghcr.io/analogj/scrutiny:master-web` - Contains the Web UI, API and Database. Only one container necessary
|
||||
- `ghcr.io/analogj/scrutiny:master-collector` - Contains the Scrutiny data collector, `smartctl` binary and cron-like
|
||||
scheduler. You can run one collector on each server.
|
||||
- `ghcr.io/analogj/scrutiny:master-web` - Contains the Web UI and API. Only one container necessary
|
||||
- `influxdb:2.2` - InfluxDB image, used by the Web container to persist SMART data. Only one container necessary
|
||||
See [docs/TROUBLESHOOTING_INFLUXDB.md](./docs/TROUBLESHOOTING_INFLUXDB.md)
|
||||
|
||||
> See [docker/example.hubspoke.docker-compose.yml](./docker/example.hubspoke.docker-compose.yml) for a docker-compose file.
|
||||
|
||||
@@ -232,18 +236,18 @@ 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: | |
|
||||
| linux-amd64 | :white_check_mark: | :white_check_mark: |
|
||||
| linux-arm-5 | :white_check_mark: | |
|
||||
| linux-arm-6 | :white_check_mark: | |
|
||||
| linux-arm-7 | :white_check_mark: | web/collector only. see [#236](https://github.com/AnalogJ/scrutiny/issues/236) |
|
||||
| linux-arm64 | :white_check_mark: | :white_check_mark: |
|
||||
| freebsd-amd64 | :white_check_mark: | |
|
||||
| macos-amd64 | :white_check_mark: | :white_check_mark: |
|
||||
| macos-arm64 | :white_check_mark: | :white_check_mark: |
|
||||
| windows-amd64 | :white_check_mark: | WIP, see [#15](https://github.com/AnalogJ/scrutiny/issues/15) |
|
||||
| windows-arm64 | :white_check_mark: | |
|
||||
|
||||
|
||||
# Contributing
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/analogj/scrutiny/collector/pkg/collector"
|
||||
"github.com/analogj/scrutiny/collector/pkg/config"
|
||||
@@ -120,26 +121,16 @@ OPTIONS:
|
||||
config.Set("api.endpoint", apiEndpoint)
|
||||
}
|
||||
|
||||
collectorLogger := logrus.WithFields(logrus.Fields{
|
||||
"type": "metrics",
|
||||
})
|
||||
|
||||
if level, err := logrus.ParseLevel(config.GetString("log.level")); err == nil {
|
||||
logrus.SetLevel(level)
|
||||
} else {
|
||||
logrus.SetLevel(logrus.InfoLevel)
|
||||
}
|
||||
|
||||
if config.IsSet("log.file") && len(config.GetString("log.file")) > 0 {
|
||||
logFile, err := os.OpenFile(config.GetString("log.file"), os.O_CREATE|os.O_WRONLY, 0644)
|
||||
if err != nil {
|
||||
logrus.Errorf("Failed to open log file %s for output: %s", config.GetString("log.file"), err)
|
||||
return err
|
||||
}
|
||||
collectorLogger, logFile, err := CreateLogger(config)
|
||||
if logFile != nil {
|
||||
defer logFile.Close()
|
||||
logrus.SetOutput(io.MultiWriter(os.Stderr, logFile))
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
settingsData, err := json.MarshalIndent(config.AllSettings(), "", "\t")
|
||||
collectorLogger.Debug(string(settingsData), err)
|
||||
metricCollector, err := collector.CreateMetricsCollector(
|
||||
config,
|
||||
collectorLogger,
|
||||
@@ -192,5 +183,28 @@ OPTIONS:
|
||||
if err != nil {
|
||||
log.Fatal(color.HiRedString("ERROR: %v", err))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func CreateLogger(appConfig config.Interface) (*logrus.Entry, *os.File, error) {
|
||||
logger := logrus.WithFields(logrus.Fields{
|
||||
"type": "metrics",
|
||||
})
|
||||
|
||||
if level, err := logrus.ParseLevel(appConfig.GetString("log.level")); err == nil {
|
||||
logger.Logger.SetLevel(level)
|
||||
} else {
|
||||
logger.Logger.SetLevel(logrus.InfoLevel)
|
||||
}
|
||||
|
||||
var logFile *os.File
|
||||
var err error
|
||||
if appConfig.IsSet("log.file") && len(appConfig.GetString("log.file")) > 0 {
|
||||
logFile, err = os.OpenFile(appConfig.GetString("log.file"), os.O_CREATE|os.O_WRONLY, 0644)
|
||||
if err != nil {
|
||||
logger.Logger.Errorf("Failed to open log file %s for output: %s", appConfig.GetString("log.file"), err)
|
||||
return nil, logFile, err
|
||||
}
|
||||
logger.Logger.SetOutput(io.MultiWriter(os.Stderr, logFile))
|
||||
}
|
||||
return logger, logFile, nil
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
var httpClient = &http.Client{Timeout: 10 * time.Second}
|
||||
var httpClient = &http.Client{Timeout: 60 * time.Second}
|
||||
|
||||
type BaseCollector struct {
|
||||
logger *logrus.Entry
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"github.com/analogj/scrutiny/collector/pkg/detect"
|
||||
"github.com/analogj/scrutiny/collector/pkg/errors"
|
||||
"github.com/analogj/scrutiny/collector/pkg/models"
|
||||
"github.com/samber/lo"
|
||||
"github.com/sirupsen/logrus"
|
||||
"net/url"
|
||||
"os"
|
||||
@@ -56,11 +57,16 @@ func (mc *MetricsCollector) Run() error {
|
||||
Logger: mc.logger,
|
||||
Config: mc.config,
|
||||
}
|
||||
detectedStorageDevices, err := deviceDetector.Start()
|
||||
rawDetectedStorageDevices, err := deviceDetector.Start()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
//filter any device with empty wwn (they are invalid)
|
||||
detectedStorageDevices := lo.Filter[models.Device](rawDetectedStorageDevices, func(dev models.Device, _ int) bool {
|
||||
return len(dev.WWN) > 0
|
||||
})
|
||||
|
||||
mc.logger.Infoln("Sending detected devices to API, for filtering & validation")
|
||||
jsonObj, _ := json.Marshal(detectedStorageDevices)
|
||||
mc.logger.Debugf("Detected devices: %v", string(jsonObj))
|
||||
|
||||
@@ -36,6 +36,25 @@ func TestConfiguration_GetScanOverrides_Simple(t *testing.T) {
|
||||
require.Equal(t, []models.ScanOverride{{Device: "/dev/sda", DeviceType: []string{"sat"}, Ignore: false}}, scanOverrides)
|
||||
}
|
||||
|
||||
// fixes #418
|
||||
func TestConfiguration_GetScanOverrides_DeviceTypeComma(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
//setup
|
||||
testConfig, _ := config.Create()
|
||||
|
||||
//test
|
||||
err := testConfig.ReadConfig(path.Join("testdata", "device_type_comma.yaml"))
|
||||
require.NoError(t, err, "should correctly load simple device config")
|
||||
scanOverrides := testConfig.GetDeviceOverrides()
|
||||
|
||||
//assert
|
||||
require.Equal(t, []models.ScanOverride{
|
||||
{Device: "/dev/sda", DeviceType: []string{"sat", "auto"}, Ignore: false},
|
||||
{Device: "/dev/sdb", DeviceType: []string{"sat,auto"}, Ignore: false},
|
||||
}, scanOverrides)
|
||||
}
|
||||
|
||||
func TestConfiguration_GetScanOverrides_Ignore(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
version: 1
|
||||
devices:
|
||||
# the scrutiny config parser will detect `sat,auto` as two separate items in a list. If you want to use `-d sat,auto` you must
|
||||
# set 'sat,auto' in a list (see eg. /dev/sbd)
|
||||
- device: /dev/sda
|
||||
type: 'sat,auto'
|
||||
- device: /dev/sdb
|
||||
type:
|
||||
- sat,auto
|
||||
+8
-8
@@ -1,14 +1,16 @@
|
||||
########################################################################################################################
|
||||
# Omnibus Image
|
||||
# NOTE: this image requires the `make binary-frontend` target to have been run before `docker build` The `dist` directory must exist.
|
||||
########################################################################################################################
|
||||
|
||||
|
||||
########
|
||||
FROM golang:1.17-bullseye as backendbuild
|
||||
FROM golang:1.20-bullseye as backendbuild
|
||||
|
||||
WORKDIR /go/src/github.com/analogj/scrutiny
|
||||
|
||||
COPY . /go/src/github.com/analogj/scrutiny
|
||||
RUN make binary-clean binary-all WEB_BINARY_NAME=scrutiny
|
||||
|
||||
RUN go mod vendor && \
|
||||
go build -o scrutiny webapp/backend/cmd/scrutiny/scrutiny.go && \
|
||||
go build -o scrutiny-collector-selftest collector/cmd/collector-selftest/collector-selftest.go && \
|
||||
go build -o scrutiny-collector-metrics collector/cmd/collector-metrics/collector-metrics.go
|
||||
|
||||
########
|
||||
FROM debian:bullseye-slim as runtime
|
||||
@@ -34,11 +36,9 @@ COPY /rootfs /
|
||||
|
||||
COPY /rootfs/etc/cron.d/scrutiny /etc/cron.d/scrutiny
|
||||
COPY --from=backendbuild /go/src/github.com/analogj/scrutiny/scrutiny /opt/scrutiny/bin/
|
||||
COPY --from=backendbuild /go/src/github.com/analogj/scrutiny/scrutiny-collector-selftest /opt/scrutiny/bin/
|
||||
COPY --from=backendbuild /go/src/github.com/analogj/scrutiny/scrutiny-collector-metrics /opt/scrutiny/bin/
|
||||
COPY dist /opt/scrutiny/web
|
||||
RUN chmod +x /opt/scrutiny/bin/scrutiny && \
|
||||
chmod +x /opt/scrutiny/bin/scrutiny-collector-selftest && \
|
||||
chmod +x /opt/scrutiny/bin/scrutiny-collector-metrics && \
|
||||
chmod 0644 /etc/cron.d/scrutiny && \
|
||||
rm -f /etc/cron.daily/* && \
|
||||
|
||||
@@ -1,27 +1,28 @@
|
||||
########################################################################################################################
|
||||
# Collector Image
|
||||
########################################################################################################################
|
||||
|
||||
|
||||
########
|
||||
FROM golang:1.17-bullseye as backendbuild
|
||||
FROM golang:1.20-bullseye as backendbuild
|
||||
|
||||
WORKDIR /go/src/github.com/analogj/scrutiny
|
||||
|
||||
COPY . /go/src/github.com/analogj/scrutiny
|
||||
|
||||
RUN go mod vendor && \
|
||||
go build -ldflags '-w -extldflags "-static"' -o scrutiny-collector-selftest collector/cmd/collector-selftest/collector-selftest.go && \
|
||||
go build -ldflags '-w -extldflags "-static"' -o scrutiny-collector-metrics collector/cmd/collector-metrics/collector-metrics.go
|
||||
RUN make binary-clean binary-collector
|
||||
|
||||
########
|
||||
FROM debian:bullseye-slim as runtime
|
||||
WORKDIR /scrutiny
|
||||
WORKDIR /opt/scrutiny
|
||||
ENV PATH="/opt/scrutiny/bin:${PATH}"
|
||||
|
||||
RUN apt-get update && apt-get install -y cron smartmontools ca-certificates tzdata && update-ca-certificates
|
||||
|
||||
COPY /docker/entrypoint-collector.sh /entrypoint-collector.sh
|
||||
COPY /rootfs/etc/cron.d/scrutiny /etc/cron.d/scrutiny
|
||||
COPY --from=backendbuild /go/src/github.com/analogj/scrutiny/scrutiny-collector-selftest /opt/scrutiny/bin/
|
||||
COPY --from=backendbuild /go/src/github.com/analogj/scrutiny/scrutiny-collector-metrics /opt/scrutiny/bin/
|
||||
RUN chmod +x /opt/scrutiny/bin/scrutiny-collector-selftest && \
|
||||
chmod +x /opt/scrutiny/bin/scrutiny-collector-metrics && \
|
||||
RUN chmod +x /opt/scrutiny/bin/scrutiny-collector-metrics && \
|
||||
chmod +x /entrypoint-collector.sh && \
|
||||
chmod 0644 /etc/cron.d/scrutiny && \
|
||||
rm -f /etc/cron.daily/apt /etc/cron.daily/dpkg /etc/cron.daily/passwd
|
||||
|
||||
@@ -1,12 +1,18 @@
|
||||
########################################################################################################################
|
||||
# Web Image
|
||||
# NOTE: this image requires the `make binary-frontend` target to have been run before `docker build` The `dist` directory must exist.
|
||||
########################################################################################################################
|
||||
|
||||
|
||||
########
|
||||
FROM golang:1.17-bullseye as backendbuild
|
||||
FROM golang:1.20-bullseye as backendbuild
|
||||
|
||||
WORKDIR /go/src/github.com/analogj/scrutiny
|
||||
|
||||
COPY . /go/src/github.com/analogj/scrutiny
|
||||
|
||||
RUN go mod vendor && \
|
||||
go build -o scrutiny webapp/backend/cmd/scrutiny/scrutiny.go
|
||||
RUN make binary-clean binary-all WEB_BINARY_NAME=scrutiny
|
||||
|
||||
|
||||
########
|
||||
FROM debian:bullseye-slim as runtime
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
FROM techknowlogick/xgo:go-1.17.x
|
||||
|
||||
WORKDIR /go/src/github.com/analogj/scrutiny
|
||||
|
||||
COPY . /go/src/github.com/analogj/scrutiny
|
||||
|
||||
RUN make all
|
||||
Vendored
-18
@@ -1,18 +0,0 @@
|
||||
# This vagrant file is only used for local development & testing.
|
||||
|
||||
Vagrant.configure("2") do |config|
|
||||
config.vm.guest = :freebsd
|
||||
config.vm.synced_folder ".", "/vagrant", id: "vagrant-root", disabled: true
|
||||
config.vm.box = "freebsd/FreeBSD-11.0-CURRENT"
|
||||
config.ssh.shell = "sh"
|
||||
config.vm.base_mac = "080027D14C66"
|
||||
|
||||
config.vm.provider :virtualbox do |vb|
|
||||
vb.customize ["modifyvm", :id, "--memory", "1024"]
|
||||
vb.customize ["modifyvm", :id, "--cpus", "1"]
|
||||
vb.customize ["modifyvm", :id, "--hwvirtex", "on"]
|
||||
vb.customize ["modifyvm", :id, "--audio", "none"]
|
||||
vb.customize ["modifyvm", :id, "--nictype1", "virtio"]
|
||||
vb.customize ["modifyvm", :id, "--nictype2", "virtio"]
|
||||
end
|
||||
end
|
||||
@@ -7,6 +7,8 @@
|
||||
|
||||
# adding ability to customize the cron schedule.
|
||||
COLLECTOR_CRON_SCHEDULE=${COLLECTOR_CRON_SCHEDULE:-"0 0 * * *"}
|
||||
COLLECTOR_RUN_STARTUP=${COLLECTOR_RUN_STARTUP:-"false"}
|
||||
COLLECTOR_RUN_STARTUP_SLEEP=${COLLECTOR_RUN_STARTUP_SLEEP:-"1"}
|
||||
|
||||
# if the cron schedule has been overridden via env variable (eg docker-compose) we should make sure to strip quotes
|
||||
[[ "${COLLECTOR_CRON_SCHEDULE}" == \"*\" || "${COLLECTOR_CRON_SCHEDULE}" == \'*\' ]] && COLLECTOR_CRON_SCHEDULE="${COLLECTOR_CRON_SCHEDULE:1:-1}"
|
||||
@@ -14,6 +16,13 @@ COLLECTOR_CRON_SCHEDULE=${COLLECTOR_CRON_SCHEDULE:-"0 0 * * *"}
|
||||
# replace placeholder with correct value
|
||||
sed -i 's|{COLLECTOR_CRON_SCHEDULE}|'"${COLLECTOR_CRON_SCHEDULE}"'|g' /etc/cron.d/scrutiny
|
||||
|
||||
if [[ "${COLLECTOR_RUN_STARTUP}" == "true" ]]; then
|
||||
sleep ${COLLECTOR_RUN_STARTUP_SLEEP}
|
||||
echo "starting scrutiny collector (run-once mode. subsequent calls will be triggered via cron service)"
|
||||
/opt/scrutiny/bin/scrutiny-collector-metrics run
|
||||
fi
|
||||
|
||||
|
||||
# now that we have the env start cron in the foreground
|
||||
echo "starting cron"
|
||||
su -c "cron -f -L 15" root
|
||||
|
||||
+181
-1
@@ -1 +1,181 @@
|
||||
> See [docker/example.hubspoke.docker-compose.yml](https://github.com/AnalogJ/scrutiny/blob/master/docker/example.hubspoke.docker-compose.yml) for a docker-compose file.
|
||||
>
|
||||
See [docker/example.hubspoke.docker-compose.yml](https://github.com/AnalogJ/scrutiny/blob/master/docker/example.hubspoke.docker-compose.yml)
|
||||
for a docker-compose file.
|
||||
|
||||
> The following guide was contributed by @TinJoy59 in #417
|
||||
> It describes how to deploy the Scrutiny in Hub/Spoke mode, where the Hub is running in Docker, and the Spokes (
|
||||
> collectors) are running as binaries.
|
||||
> He's using Proxmox & Synology in his guide, however this should be applicable for almost anyone
|
||||
|
||||
# S.M.A.R.T. Monitoring with Scrutiny across machines
|
||||
|
||||

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

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

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

|
||||
|
||||
Setting up a remote Spoke in Docker requires you to split
|
||||
the [official Hub-Spoke layout docker-compose.yml](https://github.com/AnalogJ/scrutiny/blob/master/docker/example.hubspoke.docker-compose.yml)
|
||||
. In the following docker-compose you need to provide the `${API_ENDPOINT}`, in my case `http://192.168.0.100:8080`.
|
||||
Also all drives that you wish to monitor need to be presented to the container under `devices`.
|
||||
|
||||
The image handles the periodic scanning of the drives.
|
||||
|
||||
```yaml
|
||||
version: "3.4"
|
||||
|
||||
services:
|
||||
|
||||
collector:
|
||||
image: 'ghcr.io/analogj/scrutiny:master-collector'
|
||||
cap_add:
|
||||
- SYS_RAWIO
|
||||
volumes:
|
||||
- '/run/udev:/run/udev:ro'
|
||||
environment:
|
||||
COLLECTOR_API_ENDPOINT: ${API_ENDPOINT}
|
||||
devices:
|
||||
- "/dev/sda"
|
||||
- "/dev/sdb"
|
||||
```
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
# Manual Windows Install
|
||||
|
||||
This guide is specifically for people who are on a Windows machine using [WSL](https://learn.microsoft.com/en-us/windows/wsl/about) with Docker.
|
||||
|
||||
Scrutiny is made up of three components: an influxdb Database, a collector and a webapp/api. Docker will be used for
|
||||
the influxdb and webapp/API, the collector component will be facilitated by [Windows Task Scheduler](https://learn.microsoft.com/en-us/windows/win32/taskschd/task-scheduler-start-page).
|
||||
|
||||
> **NOTE:** If you are **NOT** using WSL with docker, then the easiest way to get started with [Scrutiny is the omnibus Docker image](https://github.com/AnalogJ/scrutiny#docker).
|
||||
|
||||
## InfluxDB and Webapp/API (Docker)
|
||||
|
||||
1. Copy the [example.hubspoke.docker-compose.yml](https://github.com/AnalogJ/scrutiny/blob/master/docker/example.hubspoke.docker-compose.yml)
|
||||
file and delete the collector section near the bottom of the file.
|
||||
2. Run `docker-compose up -d` to verify that the DB and webapp are working correctly and once its completed, your webapp
|
||||
should be up and running but the dashboard will be empty (default location is `localhost:8080`)
|
||||
|
||||
## Collector (Windows Task Scheduler)
|
||||
|
||||
1. Download the latest `scrutiny-collector-metrics-windows-amd64.exe` from the [releases page](https://github.com/AnalogJ/scrutiny/releases) (under assets)
|
||||
2. On your windows host, open [Windows Task Scheduler](https://www.wikihow.com/Open-Task-Scheduler-in-Windows-10) as **Administrator**
|
||||
1. In the **Start Menu** (Windows key), type `Task Scheduler` and then right click `Run as Administrator` to open
|
||||
3. On the status bar (under the `action` tab), click `Create Task...`
|
||||
4. A new window should open with the `General` Tab open, enter relevant information into the `Name` and `Description` fields
|
||||
1. Under **Security Options** check:
|
||||
1. **Run whether user is logged on or not**
|
||||
2. **Run with highest privileges**
|
||||
5. Next, click the `Triggers` tab and then click `New...` (bottom left-hand side of the window)
|
||||
6. Here you can set how often you want this task to run, example settings are the following:
|
||||
1. **Settings:**
|
||||
1. `Daily`, start at `TODAYS_DATE` `12:00:00 AM`, Recur every `1` days,
|
||||
2. **Advanced Settings:**
|
||||
1. Repeat Task every: `1 hour` for a duration of `Indefinitely`
|
||||
2. Stop task if it runs longer than: `30 minutes`
|
||||
3. Click Ok when satisfied with your schedule
|
||||
> **NOTE:** The above settings will trigger the task **every day at midnight** and then **run every hour after that** (modify as needed)
|
||||
7. Next, click the `Actions` tab and then click `New...` (bottom left-hand side of the window)
|
||||
1. **Action Settings:**
|
||||
1. In the **Program/Script** field, put: `scrutiny-collector-metrics-windows-amd64.exe`
|
||||
2. In the **Add arguments (optional)** field, put: `run --api-endpoint "http://localhost:8080" --config collector.yaml`
|
||||
> **NOTE:**
|
||||
> * Make sure that you put the correct port number (as specified in the docker-compose file) for the webapp (default is `8080`)
|
||||
> * The `--config` param is optional and is not needed if you just want to use the default collector config, see
|
||||
[example.collector.yaml](https://github.com/AnalogJ/scrutiny/blob/master/example.collector.yaml) for more info on the collector config.
|
||||
3. In the **Start in (optional)** field, put: FOLDER_PATH_TO_YOUR `scrutiny-collector-metrics-windows-amd64.exe` file
|
||||
> **NOTE:** Must be exact and do not include `scrutiny-collector-metrics-windows-amd64.exe` in the path
|
||||
4. Click Ok when finished
|
||||
8. Next, click the `Conditions` tab and make sure that everything is unchecked (unless you want to specify otherwise)
|
||||
9. Next, click the `Settings` tab and check everything except for the last checkbox
|
||||
1. **Examples for the following settings:**
|
||||
1. If the task fails, restart every: `5 minutes`
|
||||
2. Attempt restart up to: `3` times
|
||||
3. Stop the task if it runs longer than `1 hour`
|
||||
10. Next, once satisfied with everything, click Ok
|
||||
11. Then, find your newly created task (by its name) in the scheduler task list and then manually run it (right click it and then click `Run`)
|
||||
12. Finally, refresh your dashboard after a minute or two and your drive information should have populated the webapp dashboard.
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -91,9 +91,13 @@ wget https://raw.githubusercontent.com/smartmontools/smartmontools/master/smartm
|
||||
```
|
||||
#!/bin/bash
|
||||
|
||||
/volume1/\@Entware/scrutiny/bin/scrutiny-collector-metrics-linux-arm64 run --config /volume1/\@Entware/scrutiny/config/collector.yaml
|
||||
/volume1/\@Entware/scrutiny/bin/scrutiny-collector-metrics-linux-arm64 run --config /volume1/\@Entware/scrutiny/conf/collector.yaml
|
||||
```
|
||||
|
||||
**Make `run_collect.sh` executable**
|
||||
|
||||
`chmod +x /volume1/\@Entware/scrutiny/bin/run_collect.sh`
|
||||
|
||||
## Set up Synology to run a scheduled task.
|
||||
|
||||
Log in to DSM and do the following:
|
||||
@@ -131,4 +135,4 @@ Frequency: <Your desired frequency>
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
If you have any issues with your devices being detected, or incorrect data, please take a look at [TROUBLESHOOTING_DEVICE_COLLECTOR.md](./TROUBLESHOOTING_DEVICE_COLLECTOR.md)
|
||||
If you have any issues with your devices being detected, or incorrect data, please take a look at [TROUBLESHOOTING_DEVICE_COLLECTOR.md](./TROUBLESHOOTING_DEVICE_COLLECTOR.md)
|
||||
|
||||
@@ -1,17 +1,21 @@
|
||||
# Officially Supported NAS OS's
|
||||
# Officially Supported NAS/OS's
|
||||
|
||||
These are the officially supported NAS OS's (with documentation and setup guides).
|
||||
Once a guide is created (in `docs/guides/`) it will be linked here.
|
||||
These are the officially supported NAS OS's (with documentation and setup guides). Once a guide is created (
|
||||
in `docs/guides/` or elsewhere) it will be linked here.
|
||||
|
||||
- [ ] freenas/truenas
|
||||
- [x] [freenas/truenas](https://blog.stefandroid.com/2022/01/14/smart-scrutiny.html)
|
||||
- [x] [unraid](./INSTALL_UNRAID.md)
|
||||
- [ ] ESXI
|
||||
- [ ] Proxmox
|
||||
- [x] Synology(./INSTALL_SYNOLOGY_COLLECTOR.md)
|
||||
- [x] Synology
|
||||
- [Hub/Spoke Deployment - Collector](./INSTALL_SYNOLOGY_COLLECTOR.md)
|
||||
- [Omnibus Deployment](https://drfrankenstein.co.uk/2022/07/28/scrutiny-in-docker-on-a-synology-nas)
|
||||
- [ ] OMV
|
||||
- [ ] Amahi
|
||||
- [ ] Running in a LXC container
|
||||
- [x] [PFSense](./INSTALL_UNRAID.md)
|
||||
- [ ] QNAP
|
||||
- [ ] RockStor
|
||||
|
||||
- [x] [PFSense](./INSTALL_PFSENSE.md)
|
||||
- [x] QNAP
|
||||
- [x] [RockStor](https://rockstor.com/docs/interface/docker-based-rock-ons/scrutiny.html)
|
||||
- [ ] Solaris/OmniOS CE Support
|
||||
- [ ] Kubernetes
|
||||
- [x] [Windows](./INSTALL_MANUAL_WINDOWS.md)
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
# Testers
|
||||
|
||||
Scrutiny supports many operating systems, CPU architectures and runtime environments. Unfortunately that makes it incredibly
|
||||
difficult to test.
|
||||
Thankfully the following users have been gracious enough to test/validate Scrutiny works on their system.
|
||||
|
||||
> NOTE: If you're interested in volunteering to test Scrutiny beta builds on your system, please [open an issue](https://github.com/AnalogJ/scrutiny/issues).
|
||||
|
||||
| Architecture Name | Binaries | Docker |
|
||||
| --- | --- | --- |
|
||||
| linux-amd64 | -- | @feroxy @rshxyz |
|
||||
| linux-arm-5 | -- | |
|
||||
| linux-arm-6 | -- | |
|
||||
| linux-arm-7 | @Zorlin | @martini1992 |
|
||||
| linux-arm64 | @SiM22 @Zorlin | @ViRb3 @agneevX @benamajin |
|
||||
| freebsd-amd64 | @BadCo-NZ @varunsridharan @martadinata666 @KenwoodFox @FingerlessGlov3s | |
|
||||
| macos-amd64 | -- | -- |
|
||||
| macos-arm64 | -- | -- |
|
||||
| windows-amd64 | @gabrielv33 | -- |
|
||||
| windows-arm64 | -- | -- |
|
||||
@@ -19,6 +19,25 @@ Scrutiny stores and references the devices by their `WWN` which is globally uniq
|
||||
As such, passing devices to the Scrutiny collector container using `/dev/disk/by-id/`, `/dev/disk/by-label/`, `/dev/disk/by-path/` and `/dev/disk/by-uuid/`
|
||||
paths are unnecessary, unless you'd like to ensure the docker run command never needs to change.
|
||||
|
||||
#### Force /dev/disk/by-id paths
|
||||
|
||||
Since Scrutiny uses WWN under the hood, it really doesn't care about `/dev/sd*` vs `/dev/disk/by-id/`. The problem is the interaction between docker and smartmontools when using `--device /dev/disk/by-id` paths.
|
||||
|
||||
Basically Scrutiny offloads all device detection to smartmontools, which doesn't seem to detect devices that have been passed into the docker container using `/dev/disk/by-id` paths.
|
||||
|
||||
If you must use "static" device references, you can map the host device id/uuid/wwn references to device names within the container:
|
||||
|
||||
```
|
||||
# --device=<Host Device>:<Container Device Mapping>
|
||||
|
||||
docker run ....
|
||||
--device=/dev/disk/by-id/wwn-0x5000xxxxx:/dev/sda
|
||||
--device=/dev/disk/by-id/wwn-0x5001xxxxx:/dev/sdb
|
||||
--device=/dev/disk/by-id/wwn-0x5003xxxxx:/dev/sdc
|
||||
...
|
||||
```
|
||||
|
||||
|
||||
|
||||
## Device Detection By Smartctl
|
||||
|
||||
@@ -61,7 +80,8 @@ using a collector config file. See [example.collector.yaml](/example.collector.y
|
||||
|
||||
> NOTE: If you use docker, you **must** pass though the RAID virtual disk to the container using `--device` (see below)
|
||||
>
|
||||
> This device may be in `/dev/*` or `/dev/bus/*`.
|
||||
> This device may be in `/dev/*` or `/dev/bus/*`.
|
||||
> If you do not see a virtual device file `/dev/bus/*` you may need to use the `--privileged` flag. See [#366 for more info](https://github.com/AnalogJ/scrutiny/issues/366#issuecomment-1253196407)
|
||||
>
|
||||
> If you're unsure, run `smartctl --scan` on your host, and pass all listed devices to the container.
|
||||
|
||||
@@ -92,7 +112,7 @@ devices:
|
||||
type:
|
||||
- aacraid,0,0,0
|
||||
- aacraid,0,0,1
|
||||
|
||||
|
||||
# HPE Smart Array example: https://github.com/AnalogJ/scrutiny/issues/213
|
||||
- device: /dev/sda
|
||||
type:
|
||||
@@ -100,11 +120,14 @@ devices:
|
||||
- 'cciss,1'
|
||||
```
|
||||
|
||||
>
|
||||
|
||||
### NVMe Drives
|
||||
As mentioned in the [README.md](/README.md), NVMe devices require both `--cap-add SYS_RAWIO` and `--cap-add SYS_ADMIN`
|
||||
|
||||
As mentioned in the [README.md](/README.md), NVMe devices require both `--cap-add SYS_RAWIO` and `--cap-add SYS_ADMIN`
|
||||
to allow smartctl permission to query your NVMe device SMART data [#26](https://github.com/AnalogJ/scrutiny/issues/26)
|
||||
|
||||
When attaching NVMe devices using `--device=/dev/nvme..`, make sure to provide the device controller (`/dev/nvme0`)
|
||||
When attaching NVMe devices using `--device=/dev/nvme..`, make sure to provide the device controller (`/dev/nvme0`)
|
||||
instead of the block device (`/dev/nvme0n1`). See [#209](https://github.com/AnalogJ/scrutiny/issues/209).
|
||||
|
||||
> The character device /dev/nvme0 is the NVME device controller, and block devices like /dev/nvme0n1 are the NVME storage namespaces: the devices you use for actual storage, which will behave essentially as disks.
|
||||
@@ -113,15 +136,29 @@ instead of the block device (`/dev/nvme0n1`). See [#209](https://github.com/Anal
|
||||
|
||||
### ATA
|
||||
|
||||
### USB Devices
|
||||
|
||||
The following information is extracted from [#266](https://github.com/AnalogJ/scrutiny/issues/266)
|
||||
|
||||
External HDDs support two modes of operation usb-storage (old, slower, stable) and uas (new, faster, sometimes unstable)
|
||||
. On some external HDDs, uas mode does not properly pass through SMART information, or even causes hardware issues, so
|
||||
it has been disabled by the kernel. No amount of smartctl parameters will fix this, as it is being rejected by the
|
||||
kernel. This is especially true with Seagate HDDs. One solution is to force these devices into usb-storage mode, which
|
||||
will incur some performance penalty, but may work well enough for you. More info:
|
||||
|
||||
- https://smartmontools.org/wiki/Supported_USB-Devices
|
||||
- https://smartmontools.org/wiki/SAT-with-UAS-Linux
|
||||
- https://forums.raspberrypi.com/viewtopic.php?t=245931
|
||||
|
||||
### Exit Codes
|
||||
|
||||
If you see an error message similar to `smartctl returned an error code (2) while processing /dev/sda`, this means that
|
||||
`smartctl` (not Scrutiny) exited with an error code. Scrutiny will attempt to print a helpful error message to help you debug,
|
||||
but you can look at the table (and associated links) below to debug `smartctl`.
|
||||
`smartctl` (not Scrutiny) exited with an error code. Scrutiny will attempt to print a helpful error message to help you
|
||||
debug, but you can look at the table (and associated links) below to debug `smartctl`.
|
||||
|
||||
> smartctl Return Values
|
||||
> The return values of smartctl are defined by a bitmask. If all is well with the disk, the return value (exit status) of
|
||||
> smartctl is 0 (all bits turned off). If a problem occurs, or an error, potential error, or fault is detected, then
|
||||
> The return values of smartctl are defined by a bitmask. If all is well with the disk, the return value (exit status) of
|
||||
> smartctl is 0 (all bits turned off). If a problem occurs, or an error, potential error, or fault is detected, then
|
||||
> a non-zero status is returned. In this case, the eight different bits in the return value have the following meanings
|
||||
> for ATA disks; some of these values may also be returned for SCSI disks.
|
||||
>
|
||||
@@ -238,10 +275,12 @@ to disable Scrutiny analysis for them. Both are non-critical, and have low-corre
|
||||
If this is effecting your drives, you'll need to do the following:
|
||||
|
||||
1. Upgrade to v0.4.13+
|
||||
2. Reset your drive status using the SQLite script in [#device-failed-but-smart--scrutiny-passed](https://github.com/AnalogJ/scrutiny/blob/master/docs/TROUBLESHOOTING_DEVICE_COLLECTOR.md#device-failed-but-smart--scrutiny-passed)
|
||||
2. Reset your drive status using the SQLite script
|
||||
in [#device-failed-but-smart--scrutiny-passed](https://github.com/AnalogJ/scrutiny/blob/master/docs/TROUBLESHOOTING_DEVICE_COLLECTOR.md#device-failed-but-smart--scrutiny-passed)
|
||||
3. Wait for (or manually start) the collector.
|
||||
|
||||
If you'd like to learn more about how the Seagate Ironwolf SMART attributes work under the hood, and how they differ from
|
||||
If you'd like to learn more about how the Seagate Ironwolf SMART attributes work under the hood, and how they differ
|
||||
from
|
||||
other drives, please read the following:
|
||||
|
||||
- http://www.users.on.net/~fzabkar/HDD/Seagate_SER_RRER_HEC.html
|
||||
@@ -249,10 +288,23 @@ other drives, please read the following:
|
||||
|
||||
## Hub & Spoke model, with multiple Hosts.
|
||||
|
||||
When deploying Scrutiny in a hub & spoke model, it can be difficult to determine exactly which node a set of devices are associated with.
|
||||
Thankfully the collector has a special `--host-id` flag (or `COLLECTOR_HOST_ID` env variable) that can be used to associate devices with a friendly host name.
|
||||

|
||||
|
||||
See the [docs/INSTALL_HUB_SPOKE.md](/docs/INSTALL_HUB_SPOKE.md) guide for more information.
|
||||
When deploying Scrutiny in a hub & spoke model, it can be difficult to determine exactly which node a set of devices are
|
||||
associated with.
|
||||
Thankfully the collector has a special `--host-id` flag (or `COLLECTOR_HOST_ID` env variable) that can be used to
|
||||
associate devices with a friendly host name.
|
||||
|
||||
The host-id is passed from the collector to the web-api when SMART device data is uploaded. There's 3 ways you can set
|
||||
the host-id:
|
||||
|
||||
- using the collector config
|
||||
file: [master/example.collector.yaml#L19-L22](https://github.com/AnalogJ/scrutiny/blob/master/example.collector.yaml?rgh-link-date=2022-05-25T15%3A08%3A56Z#L19-L22)
|
||||
- using the `--host-id` collector CLI
|
||||
argument: [master/collector/cmd/collector-metrics/collector-metrics.go#L180-L185](https://github.com/AnalogJ/scrutiny/blob/master/collector/cmd/collector-metrics/collector-metrics.go?rgh-link-date=2022-05-25T15%3A08%3A56Z#L180-L185)
|
||||
- using the `COLLECTOR_HOST_ID` environmental variable.
|
||||
|
||||
See the [docs/INSTALL_HUB_SPOKE.md](/docs/INSTALL_HUB_SPOKE.md) guide for more information.
|
||||
|
||||
## Collector DEBUG mode
|
||||
|
||||
@@ -268,3 +320,20 @@ Or if you're not using docker, you can pass CLI arguments to the collector durin
|
||||
```bash
|
||||
scrutiny-collector-metrics run --debug --log-file /tmp/collector.log
|
||||
```
|
||||
|
||||
## Collector trigger on startup
|
||||
|
||||
When the `omnibus` docker image starts up, it will automatically trigger the collector, which will populate the Scrutiny
|
||||
Webui with your disks.
|
||||
This is not the case when running the collector docker image in **hub/spoke** mode, as the collector and webui are
|
||||
running in different containers (and potentially different host machines), so
|
||||
the web container may not be ready for incoming connections. By default the container will only run the collector at the
|
||||
time specified in the cron schedule.
|
||||
|
||||
You can force the collector to run on startup using the following env variables:
|
||||
|
||||
- `-e COLLECTOR_RUN_STARTUP=true` - forces the collector to run on startup (cron will be started after the collector
|
||||
completes)
|
||||
- `-e COLLECTOR_RUN_STARTUP_SLEEP=10` - if `COLLECTOR_RUN_STARTUP` is enabled, you can use this env variable to
|
||||
configure the delay before the collector is run (default: `1` second). Used to ensure the web container has started
|
||||
successfully.
|
||||
|
||||
@@ -1,7 +1,19 @@
|
||||
# InfluxDB Troubleshooting
|
||||
|
||||
## Installation
|
||||
InfluxDB is a required dependency for Scrutiny v0.4.0+.
|
||||
## Why??
|
||||
|
||||
Scrutiny has many features, but the relevant one to this conversation is the "S.M.A.R.T metric tracking for historical
|
||||
trends". Basically Scrutiny not only shows you the current SMART values, but how they've changed over weeks, months (or
|
||||
even years).
|
||||
|
||||
To efficiently handle that data at scale (and to make my life easier as a developer) I decided to add InfluxDB as a
|
||||
dependency. It's a dedicated timeseries database, as opposed to the general purpose sqlite DB I used before. I also did
|
||||
a bunch of testing and analysis before I made the change. With InfluxDB the memory footprint for Scrutiny (at idle) is ~
|
||||
100mb, which is still fairly reasonable.
|
||||
|
||||
## Installation
|
||||
|
||||
InfluxDB is a required dependency for Scrutiny v0.4.0+.
|
||||
|
||||
https://docs.influxdata.com/influxdb/v2.2/install/
|
||||
|
||||
@@ -66,12 +78,349 @@ panic: failed to check influxdb setup status - parse "://:": missing protocol sc
|
||||
As discussed in [#248](https://github.com/AnalogJ/scrutiny/issues/248) and [#234](https://github.com/AnalogJ/scrutiny/issues/234),
|
||||
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`)
|
||||
- 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.
|
||||
- Change your volume mappings to `/opt/scrutiny` from `/scrutiny`
|
||||
- Updated versions of the [LSIO Scrutiny images are broken](https://github.com/linuxserver/docker-scrutiny/issues/22),
|
||||
as they have not installed InfluxDB which is a required dependency of Scrutiny v0.4.x
|
||||
- You can revert to an earlier version of the LSIO image (`lscr.io/linuxserver/scrutiny:060ac7b8-ls34`), or just
|
||||
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
|
||||
|
||||
## Bring your own InfluxDB
|
||||
|
||||
> WARNING: Most users should not follow these steps. This is ONLY for users who have an EXISTING InfluxDB installation which contains data from multiple services.
|
||||
> The Scrutiny Docker omnibus image includes an empty InfluxDB instance which it can configure.
|
||||
> If you're deploying manually or via Hub/Spoke, you can just follow the installation instructions, Scrutiny knows how
|
||||
> to run the first-time setup automatically.
|
||||
|
||||
The goal here is to create an InfluxDB API key with minimal permissions for use by Scrutiny.
|
||||
|
||||
- Create Scrutiny buckets (`metrics`, `metrics_weekly`, `metrics_monthly`, `metrics_yearly`) with placeholder config
|
||||
- Create Downsampling tasks (`tsk-weekly-aggr`, `tsk-monthly-aggr`, `tsk-yearly-aggr`) with placeholder script.
|
||||
- Create API token with restricted scope
|
||||
- NOTE: Placeholder bucket & task configuration will be replaced automatically by Scrutiny during startup
|
||||
|
||||
The placeholder buckets and tasks need to be created before the API token can be created, as the resource ID's need to
|
||||
exist for the scope restriction to work.
|
||||
|
||||
Scopes:
|
||||
|
||||
- `orgs`: read - required for scrutiny to find it's configured org_id
|
||||
- `tasks`: scrutiny specific read/write access - Scrutiny only needs access to the downsampling tasks you created above
|
||||
- `buckets`: scrutiny specific read/write access - Scrutiny only needs access to the buckets you created above
|
||||
|
||||
### Setup Environmental Variables
|
||||
|
||||
```bash
|
||||
# replace the following values with correct values for your InfluxDB installation
|
||||
export INFLUXDB_ADMIN_TOKEN=pCqRq7xxxxxx-FZgNLfstIs0w==
|
||||
export INFLUXDB_ORG_ID=b2495xxxxx
|
||||
export INFLUXDB_HOSTNAME=http://localhost:8086
|
||||
|
||||
# if you want to change the bucket name prefix below, you'll also need to update the setting in the scrutiny.yaml config file.
|
||||
export INFLUXDB_SCRUTINY_BUCKET_BASENAME=metrics
|
||||
```
|
||||
|
||||
### Create placeholder buckets
|
||||
|
||||
<details>
|
||||
<summary>Click to expand!</summary>
|
||||
|
||||
```bash
|
||||
curl -sS -X POST ${INFLUXDB_HOSTNAME}/api/v2/buckets \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "Authorization: Token ${INFLUXDB_ADMIN_TOKEN}" \
|
||||
--data-binary @- << EOF
|
||||
{
|
||||
"name": "${INFLUXDB_SCRUTINY_BUCKET_BASENAME}",
|
||||
"orgID": "${INFLUXDB_ORG_ID}",
|
||||
"retentionRules": []
|
||||
}
|
||||
EOF
|
||||
|
||||
curl -sS -X POST ${INFLUXDB_HOSTNAME}/api/v2/buckets \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "Authorization: Token ${INFLUXDB_ADMIN_TOKEN}" \
|
||||
--data-binary @- << EOF
|
||||
{
|
||||
"name": "${INFLUXDB_SCRUTINY_BUCKET_BASENAME}_weekly",
|
||||
"orgID": "${INFLUXDB_ORG_ID}",
|
||||
"retentionRules": []
|
||||
}
|
||||
EOF
|
||||
|
||||
curl -sS -X POST ${INFLUXDB_HOSTNAME}/api/v2/buckets \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "Authorization: Token ${INFLUXDB_ADMIN_TOKEN}" \
|
||||
--data-binary @- << EOF
|
||||
{
|
||||
"name": "${INFLUXDB_SCRUTINY_BUCKET_BASENAME}_monthly",
|
||||
"orgID": "${INFLUXDB_ORG_ID}",
|
||||
"retentionRules": []
|
||||
}
|
||||
EOF
|
||||
|
||||
curl -sS -X POST ${INFLUXDB_HOSTNAME}/api/v2/buckets \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "Authorization: Token ${INFLUXDB_ADMIN_TOKEN}" \
|
||||
--data-binary @- << EOF
|
||||
{
|
||||
"name": "${INFLUXDB_SCRUTINY_BUCKET_BASENAME}_yearly",
|
||||
"orgID": "${INFLUXDB_ORG_ID}",
|
||||
"retentionRules": []
|
||||
}
|
||||
EOF
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
### Create placeholder tasks
|
||||
|
||||
<details>
|
||||
<summary>Click to expand!</summary>
|
||||
|
||||
```bash
|
||||
curl -sS -X POST ${INFLUXDB_HOSTNAME}/api/v2/tasks \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "Authorization: Token ${INFLUXDB_ADMIN_TOKEN}" \
|
||||
--data-binary @- << EOF
|
||||
{
|
||||
"orgID": "${INFLUXDB_ORG_ID}",
|
||||
"flux": "option task = {name: \"tsk-weekly-aggr\", every: 1y} \nyield now()"
|
||||
}
|
||||
EOF
|
||||
|
||||
curl -sS -X POST ${INFLUXDB_HOSTNAME}/api/v2/tasks \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "Authorization: Token ${INFLUXDB_ADMIN_TOKEN}" \
|
||||
--data-binary @- << EOF
|
||||
{
|
||||
"orgID": "${INFLUXDB_ORG_ID}",
|
||||
"flux": "option task = {name: \"tsk-monthly-aggr\", every: 1y} \nyield now()"
|
||||
}
|
||||
EOF
|
||||
|
||||
curl -sS -X POST ${INFLUXDB_HOSTNAME}/api/v2/tasks \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "Authorization: Token ${INFLUXDB_ADMIN_TOKEN}" \
|
||||
--data-binary @- << EOF
|
||||
{
|
||||
"orgID": "${INFLUXDB_ORG_ID}",
|
||||
"flux": "option task = {name: \"tsk-yearly-aggr\", every: 1y} \nyield now()"
|
||||
}
|
||||
EOF
|
||||
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
### Create InfluxDB API Token
|
||||
|
||||
<details>
|
||||
<summary>Click to expand!</summary>
|
||||
|
||||
```bash
|
||||
# replace these values with placeholder bucket and task ids from your InfluxDB installation.
|
||||
export INFLUXDB_SCRUTINY_BASE_BUCKET_ID=1e0709xxxx
|
||||
export INFLUXDB_SCRUTINY_WEEKLY_BUCKET_ID=1af03dexxxxx
|
||||
export INFLUXDB_SCRUTINY_MONTHLY_BUCKET_ID=b3c59c7xxxxx
|
||||
export INFLUXDB_SCRUTINY_YEARLY_BUCKET_ID=f381d8cxxxxx
|
||||
|
||||
export INFLUXDB_SCRUTINY_WEEKLY_TASK_ID=09a64ecxxxxx
|
||||
export INFLUXDB_SCRUTINY_MONTHLY_TASK_ID=09a64xxxxx
|
||||
export INFLUXDB_SCRUTINY_YEARLY_TASK_ID=09a64ecxxxxx
|
||||
|
||||
|
||||
curl -sS -X POST ${INFLUXDB_HOSTNAME}/api/v2/authorizations \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "Authorization: Token ${INFLUXDB_ADMIN_TOKEN}" \
|
||||
--data-binary @- << EOF
|
||||
{
|
||||
"description": "scrutiny - restricted scope token",
|
||||
"orgID": "${INFLUXDB_ORG_ID}",
|
||||
"permissions": [
|
||||
{
|
||||
"action": "read",
|
||||
"resource": {
|
||||
"type": "orgs"
|
||||
}
|
||||
},
|
||||
{
|
||||
"action": "read",
|
||||
"resource": {
|
||||
"type": "tasks"
|
||||
}
|
||||
},
|
||||
{
|
||||
"action": "write",
|
||||
"resource": {
|
||||
"type": "tasks",
|
||||
"id": "${INFLUXDB_SCRUTINY_WEEKLY_TASK_ID}",
|
||||
"orgID": "${INFLUXDB_ORG_ID}"
|
||||
}
|
||||
},
|
||||
{
|
||||
"action": "write",
|
||||
"resource": {
|
||||
"type": "tasks",
|
||||
"id": "${INFLUXDB_SCRUTINY_MONTHLY_TASK_ID}",
|
||||
"orgID": "${INFLUXDB_ORG_ID}"
|
||||
}
|
||||
},
|
||||
{
|
||||
"action": "write",
|
||||
"resource": {
|
||||
"type": "tasks",
|
||||
"id": "${INFLUXDB_SCRUTINY_YEARLY_TASK_ID}",
|
||||
"orgID": "${INFLUXDB_ORG_ID}"
|
||||
}
|
||||
},
|
||||
{
|
||||
"action": "read",
|
||||
"resource": {
|
||||
"type": "buckets",
|
||||
"id": "${INFLUXDB_SCRUTINY_BASE_BUCKET_ID}",
|
||||
"orgID": "${INFLUXDB_ORG_ID}"
|
||||
}
|
||||
},
|
||||
{
|
||||
"action": "write",
|
||||
"resource": {
|
||||
"type": "buckets",
|
||||
"id": "${INFLUXDB_SCRUTINY_BASE_BUCKET_ID}",
|
||||
"orgID": "${INFLUXDB_ORG_ID}"
|
||||
}
|
||||
},
|
||||
{
|
||||
"action": "read",
|
||||
"resource": {
|
||||
"type": "buckets",
|
||||
"id": "${INFLUXDB_SCRUTINY_WEEKLY_BUCKET_ID}",
|
||||
"orgID": "${INFLUXDB_ORG_ID}"
|
||||
}
|
||||
},
|
||||
{
|
||||
"action": "write",
|
||||
"resource": {
|
||||
"type": "buckets",
|
||||
"id": "${INFLUXDB_SCRUTINY_WEEKLY_BUCKET_ID}",
|
||||
"orgID": "${INFLUXDB_ORG_ID}"
|
||||
}
|
||||
},
|
||||
{
|
||||
"action": "read",
|
||||
"resource": {
|
||||
"type": "buckets",
|
||||
"id": "${INFLUXDB_SCRUTINY_MONTHLY_BUCKET_ID}",
|
||||
"orgID": "${INFLUXDB_ORG_ID}"
|
||||
}
|
||||
},
|
||||
{
|
||||
"action": "write",
|
||||
"resource": {
|
||||
"type": "buckets",
|
||||
"id": "${INFLUXDB_SCRUTINY_MONTHLY_BUCKET_ID}",
|
||||
"orgID": "${INFLUXDB_ORG_ID}"
|
||||
}
|
||||
},
|
||||
{
|
||||
"action": "read",
|
||||
"resource": {
|
||||
"type": "buckets",
|
||||
"id": "${INFLUXDB_SCRUTINY_YEARLY_BUCKET_ID}",
|
||||
"orgID": "${INFLUXDB_ORG_ID}"
|
||||
}
|
||||
},
|
||||
{
|
||||
"action": "write",
|
||||
"resource": {
|
||||
"type": "buckets",
|
||||
"id": "${INFLUXDB_SCRUTINY_YEARLY_BUCKET_ID}",
|
||||
"orgID": "${INFLUXDB_ORG_ID}"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
EOF
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
### Save InfluxDB API Token
|
||||
|
||||
After running the Curl command above, you'll see a JSON response that looks like the following:
|
||||
|
||||
```json
|
||||
{
|
||||
"token": "ksVU2t5SkQwYkvIxxxxxxxYt2xUt0uRKSbSF1Po0UQ==",
|
||||
"status": "active",
|
||||
"description": "scrutiny - restricted scope token",
|
||||
"orgID": "b2495586xxxx",
|
||||
"org": "my-org",
|
||||
"user": "admin",
|
||||
"permissions": [
|
||||
{
|
||||
"action": "read",
|
||||
"resource": {
|
||||
"type": "orgs"
|
||||
}
|
||||
},
|
||||
{
|
||||
"action": "read",
|
||||
"resource": {
|
||||
"type": "tasks"
|
||||
}
|
||||
},
|
||||
{
|
||||
"action": "write",
|
||||
"resource": {
|
||||
"type": "tasks",
|
||||
"id": "09a64exxxxx",
|
||||
"orgID": "b24955860xxxxx",
|
||||
"org": "my-org"
|
||||
}
|
||||
},
|
||||
...
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
You must copy the token field from the JSON response, and save it in your `scrutiny.yaml` config file. After that's
|
||||
done, you can start the Scrutiny server
|
||||
|
||||
## Customize InfluxDB Admin Username & Password
|
||||
|
||||
The full set of InfluxDB configuration options are available
|
||||
in [code](https://github.com/AnalogJ/scrutiny/blob/master/webapp/backend/pkg/config/config.go?rgh-link-date=2023-01-19T16%3A23%3A40Z#L49-L51)
|
||||
.
|
||||
|
||||
During first startup Scrutiny will connect to the unprotected InfluxDB server, start the setup process (via API) using a
|
||||
username and password of `admin`:`password12345` and then create an API token of `scrutiny-default-admin-token`.
|
||||
|
||||
After that's complete, it will use the api token for all subsequent communication with InfluxDB.
|
||||
|
||||
You can configure the values for the Admin username, password and token using the config file, or env variables:
|
||||
|
||||
#### Config File Example
|
||||
|
||||
```yaml
|
||||
web:
|
||||
influxdb:
|
||||
token: 'my-custom-token'
|
||||
init_username: 'my-custom-username'
|
||||
init_password: 'my-custom-password'
|
||||
```
|
||||
|
||||
#### Environmental Variables Example
|
||||
|
||||
`SCRUTINY_WEB_INFLUXDB_TOKEN` , `SCRUTINY_WEB_INFLUXDB_INIT_USERNAME` and `SCRUTINY_WEB_INFLUXDB_INIT_PASSWORD`
|
||||
|
||||
It's safe to change the InfluxDB Admin username/password after setup has completed, only the API token is used for
|
||||
subsequent communication with InfluxDB.
|
||||
|
||||
@@ -21,5 +21,11 @@ SCRUTINY_DEVICE_NAME - eg. /dev/sda
|
||||
SCRUTINY_DEVICE_TYPE - ATA/SCSI/NVMe
|
||||
SCRUTINY_DEVICE_SERIAL - eg. WDDJ324KSO
|
||||
SCRUTINY_MESSAGE - eg. "Scrutiny SMART error notification for device: %s\nFailure Type: %s\nDevice Name: %s\nDevice Serial: %s\nDevice Type: %s\nDate: %s"
|
||||
SCRUTINY_HOST_ID - (optional) eg. "my-custom-host-id"
|
||||
```
|
||||
|
||||
# Testing Notifications
|
||||
You can test that your notifications are configured correctly by posting an empty payload to the notifications health check API.
|
||||
```
|
||||
curl -X POST http://localhost:8080/api/health/notify
|
||||
```
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
# Operating systems without udev
|
||||
|
||||
Some operating systems do not come with `udev` out of the box, for example Alpine Linux. In these instances you will not be able to bind `/run/udev` to the container for sharing device metadata. Some operating systems offer `udev` as a package that can be installed separately, or an alternative (such as `eudev` in the case of Alpine Linux) that provides the same functionality.
|
||||
|
||||
To install `eudev` in Alpine Linux (run as root):
|
||||
|
||||
```
|
||||
apk add eudev
|
||||
setup-udev
|
||||
```
|
||||
|
||||
Once your `udev` implementation is installed, create `/run/udev` with the following command:
|
||||
|
||||
```
|
||||
udevadm trigger
|
||||
```
|
||||
|
||||
On Alpine Linux, this also has the benefit of creating symlinks to device serial numbers in `/dev/disk/by-id`.
|
||||
+70
-44
@@ -1,62 +1,88 @@
|
||||
|
||||
// SQLite Table(s)
|
||||
Table device {
|
||||
created_at timestamp
|
||||
|
||||
wwn varchar [pk]
|
||||
Table Device {
|
||||
//GORM attributes, see: http://gorm.io/docs/conventions.html
|
||||
CreatedAt time
|
||||
UpdatedAt time
|
||||
DeletedAt time
|
||||
|
||||
//user provided
|
||||
label varchar
|
||||
host_id varchar
|
||||
WWN string
|
||||
|
||||
// smartctl provided
|
||||
device_name varchar
|
||||
manufacturer varchar
|
||||
model_name varchar
|
||||
interface_type varchar
|
||||
interface_speed varchar
|
||||
serial_number varchar
|
||||
firmware varchar
|
||||
rotational_speed varchar
|
||||
capacity varchar
|
||||
form_factor varchar
|
||||
smart_support varchar
|
||||
device_protocol varchar
|
||||
device_type varchar
|
||||
DeviceName string
|
||||
DeviceUUID string
|
||||
DeviceSerialID string
|
||||
DeviceLabel string
|
||||
|
||||
Manufacturer string
|
||||
ModelName string
|
||||
InterfaceType string
|
||||
InterfaceSpeed string
|
||||
SerialNumber string
|
||||
Firmware string
|
||||
RotationSpeed int
|
||||
Capacity int64
|
||||
FormFactor string
|
||||
SmartSupport bool
|
||||
DeviceProtocol string//protocol determines which smart attribute types are available (ATA, NVMe, SCSI)
|
||||
DeviceType string//device type is used for querying with -d/t flag, should only be used by collector.
|
||||
|
||||
// User provided metadata
|
||||
Label string
|
||||
HostId string
|
||||
|
||||
// Data set by Scrutiny
|
||||
DeviceStatus enum
|
||||
}
|
||||
|
||||
Table Setting {
|
||||
//GORM attributes, see: http://gorm.io/docs/conventions.html
|
||||
|
||||
SettingKeyName string
|
||||
SettingKeyDescription string
|
||||
SettingDataType string
|
||||
|
||||
SettingValueNumeric int64
|
||||
SettingValueString string
|
||||
}
|
||||
|
||||
|
||||
// InfluxDB Tables
|
||||
Table device_temperature {
|
||||
//timestamp
|
||||
created_at timestamp
|
||||
|
||||
//tags (indexed & queryable)
|
||||
device_wwn varchar [pk]
|
||||
|
||||
//fields
|
||||
temp bigint
|
||||
}
|
||||
Table SmartTemperature {
|
||||
Date time
|
||||
DeviceWWN string //(tag)
|
||||
Temp int64
|
||||
}
|
||||
|
||||
|
||||
Table smart_ata_results {
|
||||
//timestamp
|
||||
created_at timestamp
|
||||
Table Smart {
|
||||
Date time
|
||||
DeviceWWN string //(tag)
|
||||
DeviceProtocol string
|
||||
|
||||
//tags (indexed & queryable)
|
||||
device_wwn varchar [pk]
|
||||
smart_status varchar
|
||||
scrutiny_status varchar
|
||||
//Metrics (fields)
|
||||
Temp int64
|
||||
PowerOnHours int64
|
||||
PowerCycleCount int64
|
||||
|
||||
//Smart Status
|
||||
Status enum
|
||||
|
||||
|
||||
//fields
|
||||
temp bigint
|
||||
power_on_hours bigint
|
||||
power_cycle_count bigint
|
||||
|
||||
//SMART Attributes (fields)
|
||||
Attr_ID_AttributeId int
|
||||
Attr_ID_Value int64
|
||||
Attr_ID_Threshold int64
|
||||
Attr_ID_Worst int64
|
||||
Attr_ID_RawValue int64
|
||||
Attr_ID_RawString string
|
||||
Attr_ID_WhenFailed string
|
||||
//Generated data
|
||||
Attr_ID_TransformedValue int64
|
||||
Attr_ID_Status enum
|
||||
Attr_ID_StatusReason string
|
||||
Attr_ID_FailureRate float64
|
||||
|
||||
}
|
||||
|
||||
Ref: device.wwn < smart_ata_results.device_wwn
|
||||
Ref: Device.WWN < Smart.DeviceWWN
|
||||
Ref: Device.WWN < SmartTemperature.DeviceWWN
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 78 KiB |
@@ -31,6 +31,10 @@ devices:
|
||||
# - device: /dev/sda
|
||||
# type: 'sat'
|
||||
#
|
||||
# # example for using `-d sat,auto`, notice the square brackets (workaround for #418)
|
||||
# - device: /dev/sda
|
||||
# type: ['sat,auto']
|
||||
#
|
||||
# # example to show how to ignore a specific disk/device.
|
||||
# - device: /dev/sda
|
||||
# ignore: true
|
||||
@@ -87,9 +91,6 @@ devices:
|
||||
########################################################################################################################
|
||||
|
||||
#collect:
|
||||
# metric:
|
||||
# enable: true
|
||||
# command: '-a -o on -S on'
|
||||
# long:
|
||||
# enable: false
|
||||
# command: ''
|
||||
|
||||
@@ -47,6 +47,11 @@ web:
|
||||
# org: 'my-org'
|
||||
# bucket: 'bucket'
|
||||
retention_policy: true
|
||||
# if you wish to disable TLS certificate verification,
|
||||
# when using self-signed certificates for example,
|
||||
# then uncomment the lines below and set `insecure_skip_verify: true`
|
||||
# tls:
|
||||
# insecure_skip_verify: false
|
||||
|
||||
log:
|
||||
file: '' #absolute or relative paths allowed, eg. web.log
|
||||
@@ -73,8 +78,6 @@ log:
|
||||
# - "join://shoutrrr:api-key@join/?devices=device1[,device2, ...][&icon=icon][&title=title]"
|
||||
# - "script:///file/path/on/disk"
|
||||
# - "https://www.example.com/path"
|
||||
# filter_attributes: 'all' # options: 'all' or 'critical'
|
||||
# level: 'fail' # options: 'fail', 'fail_scrutiny', 'fail_smart'
|
||||
|
||||
########################################################################################################################
|
||||
# FEATURES COMING SOON
|
||||
|
||||
@@ -1,66 +1,62 @@
|
||||
module github.com/analogj/scrutiny
|
||||
|
||||
go 1.17
|
||||
go 1.20
|
||||
|
||||
require (
|
||||
github.com/analogj/go-util v0.0.0-20190301173314-5295e364eb14
|
||||
github.com/containrrr/shoutrrr v0.4.4
|
||||
github.com/containrrr/shoutrrr v0.6.1
|
||||
github.com/fatih/color v1.10.0
|
||||
github.com/gin-gonic/gin v1.6.3
|
||||
github.com/glebarez/sqlite v1.4.5
|
||||
github.com/go-gormigrate/gormigrate/v2 v2.0.0
|
||||
github.com/golang/mock v1.4.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/mitchellh/mapstructure v1.2.2
|
||||
github.com/samber/lo v1.25.0
|
||||
github.com/sirupsen/logrus v1.4.2
|
||||
github.com/spf13/viper v1.7.0
|
||||
github.com/stretchr/testify v1.5.1
|
||||
github.com/stretchr/testify v1.7.1
|
||||
github.com/urfave/cli/v2 v2.2.0
|
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9
|
||||
gorm.io/driver/sqlite v1.1.3
|
||||
gorm.io/gorm v1.20.2
|
||||
gorm.io/gorm v1.23.5
|
||||
)
|
||||
|
||||
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/cpuguy83/go-md2man/v2 v2.0.1 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/deepmap/oapi-codegen v1.8.2 // indirect
|
||||
github.com/fsnotify/fsnotify v1.4.9 // indirect
|
||||
github.com/ghodss/yaml v1.0.0 // indirect
|
||||
github.com/gin-contrib/sse v0.1.0 // indirect
|
||||
github.com/glebarez/go-sqlite v1.17.2 // indirect
|
||||
github.com/go-ole/go-ole v1.2.4 // indirect
|
||||
github.com/go-playground/locales v0.13.0 // indirect
|
||||
github.com/go-playground/universal-translator v0.17.0 // indirect
|
||||
github.com/go-playground/validator/v10 v10.2.0 // indirect
|
||||
github.com/golang/protobuf v1.4.2 // indirect
|
||||
github.com/google/uuid v1.2.0 // indirect
|
||||
github.com/google/uuid v1.3.0 // indirect
|
||||
github.com/hashicorp/hcl v1.0.0 // indirect
|
||||
github.com/influxdata/line-protocol v0.0.0-20200327222509-2487e7298839 // indirect
|
||||
github.com/jaypipes/pcidb v0.5.0 // indirect
|
||||
github.com/jinzhu/inflection v1.0.0 // indirect
|
||||
github.com/jinzhu/now v1.1.1 // indirect
|
||||
github.com/jinzhu/now v1.1.4 // 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/kvz/logstreamer v0.0.0-20201023134116-02d20f4338f5 // 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/mattn/go-isatty v0.0.14 // 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/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0 // indirect
|
||||
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
||||
github.com/spf13/afero v1.2.2 // indirect
|
||||
github.com/spf13/cast v1.3.1 // indirect
|
||||
github.com/spf13/jwalterweatherman v1.1.0 // indirect
|
||||
@@ -68,16 +64,18 @@ require (
|
||||
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/exp v0.0.0-20220303212507-bbda1eaf7a17 // indirect
|
||||
golang.org/x/net v0.0.0-20210119194325-5f4716e94777 // indirect
|
||||
golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7 // indirect
|
||||
golang.org/x/sys v0.0.0-20220405052023-b1e9470b6e64 // indirect
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1 // indirect
|
||||
golang.org/x/text v0.3.5 // indirect
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
|
||||
google.golang.org/protobuf v1.23.0 // indirect
|
||||
gopkg.in/ini.v1 v1.55.0 // indirect
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect
|
||||
gopkg.in/yaml.v2 v2.3.0 // indirect
|
||||
gosrc.io/xmpp v0.5.1 // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect
|
||||
howett.net/plist v0.0.0-20181124034731-591f970eefbb // indirect
|
||||
nhooyr.io/websocket v1.8.7 // indirect
|
||||
modernc.org/libc v1.16.8 // indirect
|
||||
modernc.org/mathutil v1.4.1 // indirect
|
||||
modernc.org/memory v1.1.1 // indirect
|
||||
modernc.org/sqlite v1.17.2 // indirect
|
||||
)
|
||||
|
||||
@@ -18,7 +18,6 @@ github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAE
|
||||
github.com/PuerkitoBio/goquery v1.5.1/go.mod h1:GsLWisAFVj4WgDibEWF4pvYnkVQBpKBKeU+7zCJoLcc=
|
||||
github.com/StackExchange/wmi v0.0.0-20190523213315-cbe66965904d h1:G0m3OIz70MZUWq3EgK3CesDbo8upS2Vm9/P3FtgI+Jk=
|
||||
github.com/StackExchange/wmi v0.0.0-20190523213315-cbe66965904d/go.mod h1:3eOhrUMpNV+6aFIbp5/iudMxNCF27Vw2OZgy4xEx0Fg=
|
||||
github.com/agnivade/wasmbrowsertest v0.3.1/go.mod h1:zQt6ZTdl338xxRaMW395qccVE2eQm0SjC/SDz0mPWQI=
|
||||
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
||||
github.com/analogj/go-util v0.0.0-20190301173314-5295e364eb14 h1:wsrSjiqQtseStRIoLLxS4C5IEtXkazZVEPDHq8jW7r8=
|
||||
@@ -33,20 +32,11 @@ github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+Ce
|
||||
github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
|
||||
github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84=
|
||||
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
|
||||
github.com/chromedp/cdproto v0.0.0-20190614062957-d6d2f92b486d/go.mod h1:S8mB5wY3vV+vRIzf39xDXsw3XKYewW9X6rW2aEmkrSw=
|
||||
github.com/chromedp/cdproto v0.0.0-20190621002710-8cbd498dd7a0/go.mod h1:S8mB5wY3vV+vRIzf39xDXsw3XKYewW9X6rW2aEmkrSw=
|
||||
github.com/chromedp/cdproto v0.0.0-20190812224334-39ef923dcb8d/go.mod h1:0YChpVzuLJC5CPr+x3xkHN6Z8KOSXjNbL7qV8Wc4GW0=
|
||||
github.com/chromedp/cdproto v0.0.0-20190926234355-1b4886c6fad6/go.mod h1:0YChpVzuLJC5CPr+x3xkHN6Z8KOSXjNbL7qV8Wc4GW0=
|
||||
github.com/chromedp/chromedp v0.3.1-0.20190619195644-fd957a4d2901/go.mod h1:mJdvfrVn594N9tfiPecUidF6W5jPRKHymqHfzbobPsM=
|
||||
github.com/chromedp/chromedp v0.4.0/go.mod h1:DC3QUn4mJ24dwjcaGQLoZrhm4X/uPHZ6spDbS2uFhm4=
|
||||
github.com/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/containrrr/shoutrrr v0.6.1 h1:6ih7jA6mo3t6C97MZbd3SxL/kRizOE3bI9CpBQZ6wzg=
|
||||
github.com/containrrr/shoutrrr v0.6.1/go.mod h1:ye9jGX5YzMnJ76waaNVWlJ4luhMEyt1EWU5unYTQSb0=
|
||||
github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk=
|
||||
github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
|
||||
github.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
|
||||
github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
|
||||
github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
|
||||
@@ -54,8 +44,8 @@ github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7
|
||||
github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
|
||||
github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.0 h1:EoUDS0afbrsXAZ9YQ9jdu/mZ2sXgT1/2yyNng4PGlyM=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.1 h1:r/myEWzV9lfsM1tFLgDyu0atFtJ1fXn261LKYj/3DxU=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.1/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||
github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY=
|
||||
github.com/cyberdelia/templates v0.0.0-20141128023046-ca7fffd4298c/go.mod h1:GyV+0YP4qX0UQ7r2MoYZ+AvYDp12OF5yg4q8rGnyNh4=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
@@ -63,14 +53,11 @@ 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/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
|
||||
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=
|
||||
@@ -84,12 +71,14 @@ 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/glebarez/go-sqlite v1.17.2 h1:gyTyFr2RFFQd2gp6fOOdfnTvUn99zwvVOrQFHA4S+DY=
|
||||
github.com/glebarez/go-sqlite v1.17.2/go.mod h1:lakPjzvnJ6uSIARV+5dPALDuSLL3879PlzHFMEpbceM=
|
||||
github.com/glebarez/sqlite v1.4.5 h1:oaJupO4X9iTn4sXRvP5Vs15BNvKh9dx5AQfciKlDvV4=
|
||||
github.com/glebarez/sqlite v1.4.5/go.mod h1:6D+bB+DdXlEC4mO+pUFJWixVcnrHTIAJ9U6Ynnn4Lxk=
|
||||
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=
|
||||
@@ -108,13 +97,6 @@ github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GO
|
||||
github.com/go-sql-driver/mysql v1.5.0 h1:ozyZYNQW3x3HtqT1jira07DN2PArx2v7/mN66gGcHOs=
|
||||
github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
|
||||
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
||||
github.com/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=
|
||||
@@ -131,7 +113,6 @@ github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5y
|
||||
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
|
||||
github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=
|
||||
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
|
||||
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
|
||||
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
|
||||
@@ -139,33 +120,28 @@ github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:W
|
||||
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/go-cmp v0.5.3 h1:x95R7cp+rSeeqAMI2knLtQ0DKlaBhv2NrtrOvafPHRo=
|
||||
github.com/google/go-cmp v0.5.3/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/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
|
||||
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
|
||||
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=
|
||||
@@ -254,13 +230,11 @@ github.com/jaypipes/ghw v0.6.1/go.mod h1:QOXppNRCLGYR1H+hu09FxZPqjNt09bqUZUnOL3R
|
||||
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/jinzhu/now v1.1.4 h1:tHnRBy1i5F2Dh8BAFxqFzxKqqvezXrL2OW1TnX+Mlas=
|
||||
github.com/jinzhu/now v1.1.4/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=
|
||||
@@ -271,42 +245,32 @@ github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1
|
||||
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/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
|
||||
github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
|
||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
github.com/klauspost/compress v1.10.3/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
|
||||
github.com/klauspost/compress v1.11.7/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/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kvz/logstreamer v0.0.0-20201023134116-02d20f4338f5 h1:dkCjlgGN81ahDFtM9R1x16gFGTa7ZvgZfdtAfM9lWOs=
|
||||
github.com/kvz/logstreamer v0.0.0-20201023134116-02d20f4338f5/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=
|
||||
@@ -320,12 +284,12 @@ github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hd
|
||||
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-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y=
|
||||
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
|
||||
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/mattn/go-sqlite3 v1.14.12 h1:TJ1bhYJPV44phC+IMu1u2K/i5RriLTPe+yc68XDJ1Z0=
|
||||
github.com/mattn/go-sqlite3 v1.14.12/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
|
||||
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=
|
||||
@@ -346,18 +310,15 @@ github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lN
|
||||
github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI=
|
||||
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
|
||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
|
||||
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
|
||||
github.com/nxadm/tail v1.4.6 h1:11TGpSHY7Esh/i/qnq02Jo5oVrI1Gue8Slbq0ujPZFQ=
|
||||
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 h1:8mVmC9kjFFmA8H4pKMUhcblgifdkOIXPvbhN1T36q1M=
|
||||
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=
|
||||
@@ -381,21 +342,24 @@ github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y8
|
||||
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/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0 h1:OdAsTTz6OkFY5QxjkYwrChwuRruF69c169dPK26NUlk=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||
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/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
|
||||
github.com/samber/lo v1.25.0 h1:H8F6cB0RotRdgcRCivTByAQePaYhGMdOTJIj2QFS2I0=
|
||||
github.com/samber/lo v1.25.0/go.mod h1:2I7tgIv8Q1SG2xEIkRq0F2i2zgxVpnyPOP0d3Gj2r+A=
|
||||
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=
|
||||
@@ -413,16 +377,14 @@ github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkU
|
||||
github.com/spf13/cast v1.3.1 h1:nFm6S0SMdyzrzcmThSipiEubIDy8WEXKNZ0UOgiRpng=
|
||||
github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
|
||||
github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ=
|
||||
github.com/spf13/cobra v0.0.7/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHNrgE=
|
||||
github.com/spf13/cobra v1.4.0/go.mod h1:Wo4iy3BUC+X2Fybo0PDqwJIv3dNRiZLHQymsfxlB84g=
|
||||
github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
|
||||
github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk=
|
||||
github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo=
|
||||
github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
|
||||
github.com/spf13/pflag v1.0.2/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
|
||||
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
|
||||
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
||||
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE=
|
||||
github.com/spf13/viper v1.6.3/go.mod h1:jUMtyi0/lB5yZH/FjyGAoH7IMNrIhlBf6pXZmbMDvzw=
|
||||
github.com/spf13/viper v1.7.0 h1:xVKxvI7ouOI5I+U9s2eeiUfMaWBVoXA3AWskkrqK0VM=
|
||||
github.com/spf13/viper v1.7.0/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg=
|
||||
@@ -432,14 +394,13 @@ github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoH
|
||||
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/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s=
|
||||
github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
|
||||
github.com/thoas/go-funk v0.9.1 h1:O549iLZqPpTUQ10ykd26sZhzD+rmR5pWhuElrhbC20M=
|
||||
github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
|
||||
github.com/twitchyliquid64/golang-asm v0.0.0-20190126203739-365674df15fc/go.mod h1:NoCfSFWosfqMqmmD7hApkirIK9ozpHjxRnRxs1l413A=
|
||||
github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc=
|
||||
github.com/ugorji/go v1.1.7 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=
|
||||
@@ -452,7 +413,6 @@ github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q
|
||||
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
|
||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q=
|
||||
go.coder.com/go-tools v0.0.0-20190317003359-0c6a35b74a16/go.mod h1:iKV5yK9t+J5nG9O3uF6KYdPEz3dyfMyB15MN1rbQ8Qw=
|
||||
go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
|
||||
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
|
||||
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
|
||||
@@ -464,7 +424,6 @@ go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKY
|
||||
go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA=
|
||||
go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
|
||||
go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
|
||||
golang.org/x/crypto v0.0.0-20180426230345-b49d69b5da94/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
@@ -475,7 +434,6 @@ golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8U
|
||||
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=
|
||||
@@ -486,6 +444,8 @@ golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL
|
||||
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
|
||||
golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek=
|
||||
golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=
|
||||
golang.org/x/exp v0.0.0-20220303212507-bbda1eaf7a17 h1:3MTrJm4PyNL9NBqvYDSj3DHl46qQakyfqfWo4jgfaEM=
|
||||
golang.org/x/exp v0.0.0-20220303212507-bbda1eaf7a17/go.mod h1:lgLbSvA5ygNOMpwM/9anMpWVlVJ7Z+cHWq/eFuinpGE=
|
||||
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
|
||||
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
@@ -505,7 +465,6 @@ golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73r
|
||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20181102091132-c10e9556a7bc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
@@ -535,7 +494,6 @@ golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJ
|
||||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9 h1:SQFwaSi55rU7vdNs9Yr0Z324VNlrF+0wMqRXT4St8ck=
|
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
@@ -547,7 +505,6 @@ golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5h
|
||||
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190306220234-b354f8bf4d9e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
@@ -555,13 +512,10 @@ golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7w
|
||||
golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190618155005-516e3c20635f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190712062909-fae7ac547cb7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190927073244-c990c680b611/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
@@ -572,11 +526,12 @@ golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7w
|
||||
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/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220405052023-b1e9470b6e64 h1:D1v9ucDTYBtbz5vNuBbAhIMAGhQhJ6Ym5ah3maMVNX4=
|
||||
golang.org/x/sys v0.0.0-20220405052023-b1e9470b6e64/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
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=
|
||||
@@ -589,7 +544,6 @@ golang.org/x/text v0.3.5 h1:i6eZZ+zk0SOf0xgBpEpPD18qWcJda6q1sxt3S0kzyUQ=
|
||||
golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20201208040808-7e3f01d25324/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
@@ -609,14 +563,13 @@ golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgw
|
||||
golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20190823170909-c4a336ef6a2f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20190920225731-5eefd052ad72/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
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/tools v0.0.0-20201124115921-2c860bdd6e78/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=
|
||||
@@ -653,14 +606,12 @@ google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miE
|
||||
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
|
||||
google.golang.org/protobuf v1.23.0 h1:4MY060fB1DLGMB/7MBTLnwQUY6+F09GEiz6SsrNqyzM=
|
||||
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
gopkg.in/airbrake/gobrake.v2 v2.0.9/go.mod h1:/h5ZAUhDkGaJfjzjKLSjv6zCL6O0LLBxU4K+aSYdM/U=
|
||||
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU=
|
||||
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
||||
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
|
||||
gopkg.in/gemnasium/logrus-airbrake-hook.v2 v2.1.2/go.mod h1:Xk6kEKp8OKb+X14hQBKWaSkCsqBpgog8nAV2xsGOxlo=
|
||||
gopkg.in/inconshreveable/log15.v2 v2.0.0-20180818164646-67afb5ed74ec/go.mod h1:aPpfJ7XW+gOuirDoZ8gHhLh3kZ1B08FtV2bbmy7Jv3s=
|
||||
gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||
gopkg.in/ini.v1 v1.55.0 h1:E8yzL5unfpW3M6fz/eB7Cb5MQAYSZ7GKo4Qth+N2sgQ=
|
||||
@@ -673,37 +624,56 @@ gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU=
|
||||
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo=
|
||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
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 h1:qtWqNAEUyi7gYSUAJXeiAMz0lUOdakZF5ia9Fqnp5G4=
|
||||
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=
|
||||
gorm.io/gorm v1.23.5 h1:TnlF26wScKSvknUC/Rn8t0NLLM22fypYBlvj1+aH6dM=
|
||||
gorm.io/gorm v1.23.5/go.mod h1:l2lP/RyAtc1ynaTjFksBde/O8v9oOGIApu2/xRitmZk=
|
||||
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=
|
||||
lukechampine.com/uint128 v1.1.1/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk=
|
||||
modernc.org/cc/v3 v3.36.0/go.mod h1:NFUHyPn4ekoC/JHeZFfZurN6ixxawE1BnVonP/oahEI=
|
||||
modernc.org/ccgo/v3 v3.0.0-20220428102840-41399a37e894/go.mod h1:eI31LL8EwEBKPpNpA4bU1/i+sKOwOrQy8D87zWUcRZc=
|
||||
modernc.org/ccgo/v3 v3.0.0-20220430103911-bc99d88307be/go.mod h1:bwdAnOoaIt8Ax9YdWGjxWsdkPcZyRPHqrOvJxaKAKGw=
|
||||
modernc.org/ccgo/v3 v3.16.4/go.mod h1:tGtX0gE9Jn7hdZFeU88slbTh1UtCYKusWOoCJuvkWsQ=
|
||||
modernc.org/ccgo/v3 v3.16.6/go.mod h1:tGtX0gE9Jn7hdZFeU88slbTh1UtCYKusWOoCJuvkWsQ=
|
||||
modernc.org/ccorpus v1.11.6/go.mod h1:2gEUTrWqdpH2pXsmTM1ZkjeSrUWDpjMu2T6m29L/ErQ=
|
||||
modernc.org/httpfs v1.0.6/go.mod h1:7dosgurJGp0sPaRanU53W4xZYKh14wfzX420oZADeHM=
|
||||
modernc.org/libc v0.0.0-20220428101251-2d5f3daf273b/go.mod h1:p7Mg4+koNjc8jkqwcoFBJx7tXkpj00G77X7A72jXPXA=
|
||||
modernc.org/libc v1.16.0/go.mod h1:N4LD6DBE9cf+Dzf9buBlzVJndKr/iJHG97vGLHYnb5A=
|
||||
modernc.org/libc v1.16.1/go.mod h1:JjJE0eu4yeK7tab2n4S1w8tlWd9MxXLRzheaRnAKymU=
|
||||
modernc.org/libc v1.16.7/go.mod h1:hYIV5VZczAmGZAnG15Vdngn5HSF5cSkbvfz2B7GRuVU=
|
||||
modernc.org/libc v1.16.8 h1:Ux98PaOMvolgoFX/YwusFOHBnanXdGRmWgI8ciI2z4o=
|
||||
modernc.org/libc v1.16.8/go.mod h1:hYIV5VZczAmGZAnG15Vdngn5HSF5cSkbvfz2B7GRuVU=
|
||||
modernc.org/mathutil v1.2.2/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E=
|
||||
modernc.org/mathutil v1.4.1 h1:ij3fYGe8zBF4Vu+g0oT7mB06r8sqGWKuJu1yXeR4by8=
|
||||
modernc.org/mathutil v1.4.1/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E=
|
||||
modernc.org/memory v1.1.1 h1:bDOL0DIDLQv7bWhP3gMvIrnoFw+Eo6F7a2QK9HPDiFU=
|
||||
modernc.org/memory v1.1.1/go.mod h1:/0wo5ibyrQiaoUoH7f9D8dnglAmILJ5/cxZlRECf+Nw=
|
||||
modernc.org/opt v0.1.1/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0=
|
||||
modernc.org/sqlite v1.17.2 h1:TjmF36Wi5QcPYqRoAacV1cAyJ7xB/CD0ExpVUEMebnw=
|
||||
modernc.org/sqlite v1.17.2/go.mod h1:GOQmuiXd6pTTes1Fi2s9apiCcD/wbKQtBZ0Nw6/etjM=
|
||||
modernc.org/strutil v1.1.1/go.mod h1:DE+MQQ/hjKBZS2zNInV5hhcipt5rLPWkmpbGeW5mmdw=
|
||||
modernc.org/tcl v1.13.1/go.mod h1:XOLfOwzhkljL4itZkK6T72ckMgvj0BDsnKNdZVUOecw=
|
||||
modernc.org/token v1.0.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
|
||||
modernc.org/z v1.5.1/go.mod h1:eWFB510QWW5Th9YGZT81s+LwvaAs3Q2yr4sP0rmLkv8=
|
||||
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=
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/analogj/scrutiny/webapp/backend/pkg/config"
|
||||
"github.com/analogj/scrutiny/webapp/backend/pkg/errors"
|
||||
"github.com/analogj/scrutiny/webapp/backend/pkg/version"
|
||||
"github.com/analogj/scrutiny/webapp/backend/pkg/web"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/sirupsen/logrus"
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
@@ -31,6 +34,7 @@ func main() {
|
||||
if _, ok := err.(errors.ConfigFileMissingError); ok { // Handle errors reading the config file
|
||||
//ignore "could not find config file"
|
||||
} else if err != nil {
|
||||
log.Print(color.HiRedString("CONFIG ERROR: %v", err))
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
@@ -107,7 +111,18 @@ OPTIONS:
|
||||
config.Set("log.file", c.String("log-file"))
|
||||
}
|
||||
|
||||
webServer := web.AppEngine{Config: config}
|
||||
webLogger, logFile, err := CreateLogger(config)
|
||||
if logFile != nil {
|
||||
defer logFile.Close()
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
settingsData, err := json.Marshal(config.AllSettings())
|
||||
webLogger.Debug(string(settingsData), err)
|
||||
|
||||
webServer := web.AppEngine{Config: config, Logger: webLogger}
|
||||
|
||||
return webServer.Start()
|
||||
},
|
||||
@@ -140,3 +155,27 @@ OPTIONS:
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func CreateLogger(appConfig config.Interface) (*logrus.Entry, *os.File, error) {
|
||||
logger := logrus.WithFields(logrus.Fields{
|
||||
"type": "web",
|
||||
})
|
||||
//set default log level
|
||||
if level, err := logrus.ParseLevel(appConfig.GetString("log.level")); err == nil {
|
||||
logger.Logger.SetLevel(level)
|
||||
} else {
|
||||
logger.Logger.SetLevel(logrus.InfoLevel)
|
||||
}
|
||||
|
||||
var logFile *os.File
|
||||
var err error
|
||||
if appConfig.IsSet("log.file") && len(appConfig.GetString("log.file")) > 0 {
|
||||
logFile, err = os.OpenFile(appConfig.GetString("log.file"), os.O_CREATE|os.O_WRONLY, 0644)
|
||||
if err != nil {
|
||||
logger.Logger.Errorf("Failed to open log file %s for output: %s", appConfig.GetString("log.file"), err)
|
||||
return nil, logFile, err
|
||||
}
|
||||
logger.Logger.SetOutput(io.MultiWriter(os.Stderr, logFile))
|
||||
}
|
||||
return logger, logFile, nil
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@ package config
|
||||
|
||||
import (
|
||||
"github.com/analogj/go-util/utils"
|
||||
"github.com/analogj/scrutiny/webapp/backend/pkg"
|
||||
"github.com/analogj/scrutiny/webapp/backend/pkg/errors"
|
||||
"github.com/spf13/viper"
|
||||
"log"
|
||||
@@ -10,6 +9,8 @@ import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
const DB_USER_SETTINGS_SUBKEY = "user"
|
||||
|
||||
// When initializing this class the following methods must be called:
|
||||
// Config.New
|
||||
// Config.Init
|
||||
@@ -39,8 +40,6 @@ func (c *configuration) Init() error {
|
||||
c.SetDefault("log.file", "")
|
||||
|
||||
c.SetDefault("notify.urls", []string{})
|
||||
c.SetDefault("notify.filter_attributes", pkg.NotifyFilterAttributesAll)
|
||||
c.SetDefault("notify.level", pkg.NotifyLevelFail)
|
||||
|
||||
c.SetDefault("web.influxdb.scheme", "http")
|
||||
c.SetDefault("web.influxdb.host", "localhost")
|
||||
@@ -50,22 +49,12 @@ func (c *configuration) Init() error {
|
||||
c.SetDefault("web.influxdb.init_username", "admin")
|
||||
c.SetDefault("web.influxdb.init_password", "password12345")
|
||||
c.SetDefault("web.influxdb.token", "scrutiny-default-admin-token")
|
||||
c.SetDefault("web.influxdb.tls.insecure_skip_verify", false)
|
||||
c.SetDefault("web.influxdb.retention_policy", true)
|
||||
|
||||
//c.SetDefault("disks.include", []string{})
|
||||
//c.SetDefault("disks.exclude", []string{})
|
||||
|
||||
//c.SetDefault("notify.metric.script", "/opt/scrutiny/config/notify-metrics.sh")
|
||||
//c.SetDefault("notify.long.script", "/opt/scrutiny/config/notify-long-test.sh")
|
||||
//c.SetDefault("notify.short.script", "/opt/scrutiny/config/notify-short-test.sh")
|
||||
|
||||
//c.SetDefault("collect.metric.enable", true)
|
||||
//c.SetDefault("collect.metric.command", "-a -o on -S on")
|
||||
//c.SetDefault("collect.long.enable", true)
|
||||
//c.SetDefault("collect.long.command", "-a -o on -S on")
|
||||
//c.SetDefault("collect.short.enable", true)
|
||||
//c.SetDefault("collect.short.command", "-a -o on -S on")
|
||||
|
||||
//if you want to load a non-standard location system config file (~/drawbridge.yml), use ReadConfig
|
||||
c.SetConfigType("yaml")
|
||||
//c.SetConfigName("drawbridge")
|
||||
@@ -77,7 +66,18 @@ func (c *configuration) Init() error {
|
||||
c.AutomaticEnv()
|
||||
|
||||
//CLI options will be added via the `Set()` function
|
||||
return nil
|
||||
return c.ValidateConfig()
|
||||
}
|
||||
|
||||
func (c *configuration) SubKeys(key string) []string {
|
||||
return c.Sub(key).AllKeys()
|
||||
}
|
||||
|
||||
func (c *configuration) Sub(key string) Interface {
|
||||
config := configuration{
|
||||
Viper: c.Viper.Sub(key),
|
||||
}
|
||||
return &config
|
||||
}
|
||||
|
||||
func (c *configuration) ReadConfig(configFilePath string) error {
|
||||
@@ -120,24 +120,18 @@ func (c *configuration) ReadConfig(configFilePath string) error {
|
||||
// This function ensures that the merged config works correctly.
|
||||
func (c *configuration) ValidateConfig() error {
|
||||
|
||||
////deserialize Questions
|
||||
//questionsMap := map[string]Question{}
|
||||
//err := c.UnmarshalKey("questions", &questionsMap)
|
||||
//
|
||||
//if err != nil {
|
||||
// log.Printf("questions could not be deserialized correctly. %v", err)
|
||||
// return err
|
||||
//}
|
||||
//
|
||||
//for _, v := range questionsMap {
|
||||
//
|
||||
// typeContent, ok := v.Schema["type"].(string)
|
||||
// if !ok || len(typeContent) == 0 {
|
||||
// return errors.QuestionSyntaxError("`type` is required for questions")
|
||||
// }
|
||||
//}
|
||||
//
|
||||
//
|
||||
//the following keys are deprecated, and no longer supported
|
||||
/*
|
||||
- notify.filter_attributes (replaced by metrics.status.filter_attributes SETTING)
|
||||
- notify.level (replaced by metrics.notify.level and metrics.status.threshold SETTING)
|
||||
*/
|
||||
//TODO add docs and upgrade doc.
|
||||
if c.IsSet("notify.filter_attributes") {
|
||||
return errors.ConfigValidationError("`notify.filter_attributes` configuration option is deprecated. Replaced by option in Dashboard Settings page")
|
||||
}
|
||||
if c.IsSet("notify.level") {
|
||||
return errors.ConfigValidationError("`notify.level` configuration option is deprecated. Replaced by option in Dashboard Settings page")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"github.com/spf13/viper"
|
||||
"github.com/stretchr/testify/require"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func Test_MergeConfigMap(t *testing.T) {
|
||||
//setup
|
||||
testConfig := configuration{
|
||||
Viper: viper.New(),
|
||||
}
|
||||
testConfig.Set("user.dashboard_display", "hello")
|
||||
testConfig.SetDefault("user.layout", "hello")
|
||||
|
||||
mergeSettings := map[string]interface{}{
|
||||
"user": map[string]interface{}{
|
||||
"dashboard_display": "dashboard_display",
|
||||
"layout": "layout",
|
||||
},
|
||||
}
|
||||
//test
|
||||
err := testConfig.MergeConfigMap(mergeSettings)
|
||||
|
||||
//verify
|
||||
require.NoError(t, err)
|
||||
|
||||
// if using Set, the MergeConfigMap functionality will not override
|
||||
// if using SetDefault, the MergeConfigMap will override correctly
|
||||
require.Equal(t, "hello", testConfig.GetString("user.dashboard_display"))
|
||||
require.Equal(t, "layout", testConfig.GetString("user.layout"))
|
||||
|
||||
}
|
||||
@@ -12,12 +12,17 @@ type Interface interface {
|
||||
WriteConfig() error
|
||||
Set(key string, value interface{})
|
||||
SetDefault(key string, value interface{})
|
||||
MergeConfigMap(cfg map[string]interface{}) error
|
||||
|
||||
Sub(key string) Interface
|
||||
AllSettings() map[string]interface{}
|
||||
AllKeys() []string
|
||||
SubKeys(key string) []string
|
||||
IsSet(key string) bool
|
||||
Get(key string) interface{}
|
||||
GetBool(key string) bool
|
||||
GetInt(key string) int
|
||||
GetInt64(key string) int64
|
||||
GetString(key string) string
|
||||
GetStringSlice(key string) []string
|
||||
UnmarshalKey(key string, rawVal interface{}, decoderOpts ...viper.DecoderConfigOption) error
|
||||
|
||||
@@ -7,6 +7,7 @@ package mock_config
|
||||
import (
|
||||
reflect "reflect"
|
||||
|
||||
config "github.com/analogj/scrutiny/webapp/backend/pkg/config"
|
||||
gomock "github.com/golang/mock/gomock"
|
||||
viper "github.com/spf13/viper"
|
||||
)
|
||||
@@ -34,6 +35,20 @@ func (m *MockInterface) EXPECT() *MockInterfaceMockRecorder {
|
||||
return m.recorder
|
||||
}
|
||||
|
||||
// AllKeys mocks base method.
|
||||
func (m *MockInterface) AllKeys() []string {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "AllKeys")
|
||||
ret0, _ := ret[0].([]string)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// AllKeys indicates an expected call of AllKeys.
|
||||
func (mr *MockInterfaceMockRecorder) AllKeys() *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AllKeys", reflect.TypeOf((*MockInterface)(nil).AllKeys))
|
||||
}
|
||||
|
||||
// AllSettings mocks base method.
|
||||
func (m *MockInterface) AllSettings() map[string]interface{} {
|
||||
m.ctrl.T.Helper()
|
||||
@@ -90,6 +105,20 @@ func (mr *MockInterfaceMockRecorder) GetInt(key interface{}) *gomock.Call {
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetInt", reflect.TypeOf((*MockInterface)(nil).GetInt), key)
|
||||
}
|
||||
|
||||
// GetInt64 mocks base method.
|
||||
func (m *MockInterface) GetInt64(key string) int64 {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "GetInt64", key)
|
||||
ret0, _ := ret[0].(int64)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// GetInt64 indicates an expected call of GetInt64.
|
||||
func (mr *MockInterfaceMockRecorder) GetInt64(key interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetInt64", reflect.TypeOf((*MockInterface)(nil).GetInt64), key)
|
||||
}
|
||||
|
||||
// GetString mocks base method.
|
||||
func (m *MockInterface) GetString(key string) string {
|
||||
m.ctrl.T.Helper()
|
||||
@@ -146,6 +175,20 @@ func (mr *MockInterfaceMockRecorder) IsSet(key interface{}) *gomock.Call {
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IsSet", reflect.TypeOf((*MockInterface)(nil).IsSet), key)
|
||||
}
|
||||
|
||||
// MergeConfigMap mocks base method.
|
||||
func (m *MockInterface) MergeConfigMap(cfg map[string]interface{}) error {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "MergeConfigMap", cfg)
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// MergeConfigMap indicates an expected call of MergeConfigMap.
|
||||
func (mr *MockInterfaceMockRecorder) MergeConfigMap(cfg interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "MergeConfigMap", reflect.TypeOf((*MockInterface)(nil).MergeConfigMap), cfg)
|
||||
}
|
||||
|
||||
// ReadConfig mocks base method.
|
||||
func (m *MockInterface) ReadConfig(configFilePath string) error {
|
||||
m.ctrl.T.Helper()
|
||||
@@ -184,6 +227,34 @@ func (mr *MockInterfaceMockRecorder) SetDefault(key, value interface{}) *gomock.
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetDefault", reflect.TypeOf((*MockInterface)(nil).SetDefault), key, value)
|
||||
}
|
||||
|
||||
// Sub mocks base method.
|
||||
func (m *MockInterface) Sub(key string) config.Interface {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "Sub", key)
|
||||
ret0, _ := ret[0].(config.Interface)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// Sub indicates an expected call of Sub.
|
||||
func (mr *MockInterfaceMockRecorder) Sub(key interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Sub", reflect.TypeOf((*MockInterface)(nil).Sub), key)
|
||||
}
|
||||
|
||||
// SubKeys mocks base method.
|
||||
func (m *MockInterface) SubKeys(key string) []string {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "SubKeys", key)
|
||||
ret0, _ := ret[0].([]string)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// SubKeys indicates an expected call of SubKeys.
|
||||
func (mr *MockInterfaceMockRecorder) SubKeys(key interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SubKeys", reflect.TypeOf((*MockInterface)(nil).SubKeys), key)
|
||||
}
|
||||
|
||||
// UnmarshalKey mocks base method.
|
||||
func (m *MockInterface) UnmarshalKey(key string, rawVal interface{}, decoderOpts ...viper.DecoderConfigOption) error {
|
||||
m.ctrl.T.Helper()
|
||||
|
||||
@@ -4,17 +4,11 @@ const DeviceProtocolAta = "ATA"
|
||||
const DeviceProtocolScsi = "SCSI"
|
||||
const DeviceProtocolNvme = "NVMe"
|
||||
|
||||
const NotifyFilterAttributesAll = "all"
|
||||
const NotifyFilterAttributesCritical = "critical"
|
||||
|
||||
const NotifyLevelFail = "fail"
|
||||
const NotifyLevelFailScrutiny = "fail_scrutiny"
|
||||
const NotifyLevelFailSmart = "fail_smart"
|
||||
|
||||
//go:generate stringer -type=AttributeStatus
|
||||
// AttributeStatus bitwise flag, 1,2,4,8,16,32,etc
|
||||
type AttributeStatus uint8
|
||||
|
||||
const (
|
||||
// AttributeStatusPassed binary, 1,2,4,8,16,32,etc
|
||||
AttributeStatusPassed AttributeStatus = 0
|
||||
AttributeStatusFailedSmart AttributeStatus = 1
|
||||
AttributeStatusWarningScrutiny AttributeStatus = 2
|
||||
@@ -29,10 +23,11 @@ func AttributeStatusClear(b, flag AttributeStatus) AttributeStatus { return b &
|
||||
func AttributeStatusToggle(b, flag AttributeStatus) AttributeStatus { return b ^ flag }
|
||||
func AttributeStatusHas(b, flag AttributeStatus) bool { return b&flag != 0 }
|
||||
|
||||
//go:generate stringer -type=DeviceStatus
|
||||
// DeviceStatus bitwise flag, 1,2,4,8,16,32,etc
|
||||
type DeviceStatus uint8
|
||||
|
||||
const (
|
||||
// DeviceStatusPassed binary, 1,2,4,8,16,32,etc
|
||||
DeviceStatusPassed DeviceStatus = 0
|
||||
DeviceStatusFailedSmart DeviceStatus = 1
|
||||
DeviceStatusFailedScrutiny DeviceStatus = 2
|
||||
@@ -42,3 +37,29 @@ func DeviceStatusSet(b, flag DeviceStatus) DeviceStatus { return b | flag }
|
||||
func DeviceStatusClear(b, flag DeviceStatus) DeviceStatus { return b &^ flag }
|
||||
func DeviceStatusToggle(b, flag DeviceStatus) DeviceStatus { return b ^ flag }
|
||||
func DeviceStatusHas(b, flag DeviceStatus) bool { return b&flag != 0 }
|
||||
|
||||
// Metrics Specific Filtering & Threshold Constants
|
||||
type MetricsNotifyLevel int64
|
||||
|
||||
const (
|
||||
MetricsNotifyLevelWarn MetricsNotifyLevel = 1
|
||||
MetricsNotifyLevelFail MetricsNotifyLevel = 2
|
||||
)
|
||||
|
||||
type MetricsStatusFilterAttributes int64
|
||||
|
||||
const (
|
||||
MetricsStatusFilterAttributesAll MetricsStatusFilterAttributes = 0
|
||||
MetricsStatusFilterAttributesCritical MetricsStatusFilterAttributes = 1
|
||||
)
|
||||
|
||||
// MetricsStatusThreshold bitwise flag, 1,2,4,8,16,32,etc
|
||||
type MetricsStatusThreshold int64
|
||||
|
||||
const (
|
||||
MetricsStatusThresholdSmart MetricsStatusThreshold = 1
|
||||
MetricsStatusThresholdScrutiny MetricsStatusThreshold = 2
|
||||
|
||||
//shortcut
|
||||
MetricsStatusThresholdBoth MetricsStatusThreshold = 3
|
||||
)
|
||||
|
||||
@@ -10,9 +10,7 @@ import (
|
||||
|
||||
type DeviceRepo interface {
|
||||
Close() error
|
||||
|
||||
//GetSettings()
|
||||
//SaveSetting()
|
||||
HealthCheck(ctx context.Context) error
|
||||
|
||||
RegisterDevice(ctx context.Context, dev models.Device) error
|
||||
GetDevices(ctx context.Context) ([]models.Device, error)
|
||||
@@ -28,4 +26,7 @@ type DeviceRepo interface {
|
||||
|
||||
GetSummary(ctx context.Context) (map[string]*models.DeviceSummary, error)
|
||||
GetSmartTemperatureHistory(ctx context.Context, durationKey string) (map[string][]measurements.SmartTemperature, error)
|
||||
|
||||
LoadSettings(ctx context.Context) (*models.Settings, error)
|
||||
SaveSettings(ctx context.Context, settings models.Settings) error
|
||||
}
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
package m20220716214900
|
||||
|
||||
import (
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type Setting struct {
|
||||
//GORM attributes, see: http://gorm.io/docs/conventions.html
|
||||
gorm.Model
|
||||
|
||||
SettingKeyName string `json:"setting_key_name"`
|
||||
SettingKeyDescription string `json:"setting_key_description"`
|
||||
SettingDataType string `json:"setting_data_type"`
|
||||
|
||||
SettingValueNumeric int `json:"setting_value_numeric"`
|
||||
SettingValueString string `json:"setting_value_string"`
|
||||
SettingValueBool bool `json:"setting_value_bool"`
|
||||
}
|
||||
@@ -2,15 +2,16 @@ package database
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/analogj/scrutiny/webapp/backend/pkg/config"
|
||||
"github.com/analogj/scrutiny/webapp/backend/pkg/models"
|
||||
"github.com/glebarez/sqlite"
|
||||
influxdb2 "github.com/influxdata/influxdb-client-go/v2"
|
||||
"github.com/influxdata/influxdb-client-go/v2/api"
|
||||
"github.com/influxdata/influxdb-client-go/v2/domain"
|
||||
"github.com/sirupsen/logrus"
|
||||
"gorm.io/driver/sqlite"
|
||||
"gorm.io/gorm"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
@@ -62,7 +63,20 @@ func NewScrutinyRepository(appConfig config.Interface, globalLogger logrus.Field
|
||||
// Gorm/SQLite setup
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
globalLogger.Infof("Trying to connect to scrutiny sqlite db: %s\n", appConfig.GetString("web.database.location"))
|
||||
database, err := gorm.Open(sqlite.Open(appConfig.GetString("web.database.location")), &gorm.Config{
|
||||
|
||||
// When a transaction cannot lock the database, because it is already locked by another one,
|
||||
// SQLite by default throws an error: database is locked. This behavior is usually not appropriate when
|
||||
// concurrent access is needed, typically when multiple processes write to the same database.
|
||||
// PRAGMA busy_timeout lets you set a timeout or a handler for these events. When setting a timeout,
|
||||
// SQLite will try the transaction multiple times within this timeout.
|
||||
// fixes #341
|
||||
// https://rsqlite.r-dbi.org/reference/sqlitesetbusyhandler
|
||||
// retrying for 30000 milliseconds, 30seconds - this would be unreasonable for a distributed multi-tenant application,
|
||||
// but should be fine for local usage.
|
||||
pragmaStr := sqlitePragmaString(map[string]string{
|
||||
"busy_timeout": "30000",
|
||||
})
|
||||
database, err := gorm.Open(sqlite.Open(appConfig.GetString("web.database.location")+pragmaStr), &gorm.Config{
|
||||
//TODO: figure out how to log database queries again.
|
||||
//Logger: logger
|
||||
DisableForeignKeyConstraintWhenMigrating: true,
|
||||
@@ -82,11 +96,20 @@ func NewScrutinyRepository(appConfig config.Interface, globalLogger logrus.Field
|
||||
influxdbUrl := fmt.Sprintf("%s://%s:%s", appConfig.GetString("web.influxdb.scheme"), appConfig.GetString("web.influxdb.host"), appConfig.GetString("web.influxdb.port"))
|
||||
globalLogger.Debugf("InfluxDB url: %s", influxdbUrl)
|
||||
|
||||
client := influxdb2.NewClient(influxdbUrl, appConfig.GetString("web.influxdb.token"))
|
||||
tlsConfig := &tls.Config{
|
||||
InsecureSkipVerify: appConfig.GetBool("web.influxdb.tls.insecure_skip_verify"),
|
||||
}
|
||||
globalLogger.Infof("InfluxDB certificate verification: %t\n", !tlsConfig.InsecureSkipVerify)
|
||||
|
||||
client := influxdb2.NewClientWithOptions(
|
||||
influxdbUrl,
|
||||
appConfig.GetString("web.influxdb.token"),
|
||||
influxdb2.DefaultOptions().SetTLSConfig(tlsConfig),
|
||||
)
|
||||
|
||||
//if !appConfig.IsSet("web.influxdb.token") {
|
||||
globalLogger.Debugf("Determine Influxdb setup status...")
|
||||
influxSetupComplete, err := InfluxSetupComplete(influxdbUrl)
|
||||
influxSetupComplete, err := InfluxSetupComplete(influxdbUrl, tlsConfig)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to check influxdb setup status - %w", err)
|
||||
}
|
||||
@@ -182,7 +205,30 @@ func (sr *scrutinyRepository) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func InfluxSetupComplete(influxEndpoint string) (bool, error) {
|
||||
func (sr *scrutinyRepository) HealthCheck(ctx context.Context) error {
|
||||
//check influxdb
|
||||
status, err := sr.influxClient.Health(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("influxdb healthcheck failed: %w", err)
|
||||
}
|
||||
if status.Status != "pass" {
|
||||
return fmt.Errorf("influxdb healthcheckf failed: status=%s", status.Status)
|
||||
}
|
||||
|
||||
//check sqlite db.
|
||||
database, err := sr.gormClient.DB()
|
||||
if err != nil {
|
||||
return fmt.Errorf("sqlite healthcheck failed: %w", err)
|
||||
}
|
||||
err = database.Ping()
|
||||
if err != nil {
|
||||
return fmt.Errorf("sqlite healthcheck failed during ping: %w", err)
|
||||
}
|
||||
return nil
|
||||
|
||||
}
|
||||
|
||||
func InfluxSetupComplete(influxEndpoint string, tlsConfig *tls.Config) (bool, error) {
|
||||
influxUri, err := url.Parse(influxEndpoint)
|
||||
if err != nil {
|
||||
return false, err
|
||||
@@ -192,7 +238,8 @@ func InfluxSetupComplete(influxEndpoint string) (bool, error) {
|
||||
return false, err
|
||||
}
|
||||
|
||||
res, err := http.Get(influxUri.String())
|
||||
client := &http.Client{Transport: &http.Transport{TLSClientConfig: tlsConfig}}
|
||||
res, err := client.Get(influxUri.String())
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
@@ -242,21 +289,29 @@ func (sr *scrutinyRepository) EnsureBuckets(ctx context.Context, org *domain.Org
|
||||
|
||||
//create buckets (used for downsampling)
|
||||
weeklyBucket := fmt.Sprintf("%s_weekly", sr.appConfig.GetString("web.influxdb.bucket"))
|
||||
if _, foundErr := sr.influxClient.BucketsAPI().FindBucketByName(ctx, weeklyBucket); foundErr != nil {
|
||||
if foundWeeklyBucket, foundErr := sr.influxClient.BucketsAPI().FindBucketByName(ctx, weeklyBucket); foundErr != nil {
|
||||
// metrics_weekly bucket will have a retention period of 8+1 weeks (since it will be down-sampled once a month)
|
||||
_, err := sr.influxClient.BucketsAPI().CreateBucketWithName(ctx, org, weeklyBucket, weeklyBucketRetentionRule)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else if sr.appConfig.GetBool("web.influxdb.retention_policy") {
|
||||
//correctly set the retention period for the bucket (may not be able to do it during setup/creation)
|
||||
foundWeeklyBucket.RetentionRules = domain.RetentionRules{weeklyBucketRetentionRule}
|
||||
sr.influxClient.BucketsAPI().UpdateBucket(ctx, foundWeeklyBucket)
|
||||
}
|
||||
|
||||
monthlyBucket := fmt.Sprintf("%s_monthly", sr.appConfig.GetString("web.influxdb.bucket"))
|
||||
if _, foundErr := sr.influxClient.BucketsAPI().FindBucketByName(ctx, monthlyBucket); foundErr != nil {
|
||||
if foundMonthlyBucket, foundErr := sr.influxClient.BucketsAPI().FindBucketByName(ctx, monthlyBucket); foundErr != nil {
|
||||
// metrics_monthly bucket will have a retention period of 24+1 months (since it will be down-sampled once a year)
|
||||
_, err := sr.influxClient.BucketsAPI().CreateBucketWithName(ctx, org, monthlyBucket, monthlyBucketRetentionRule)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else if sr.appConfig.GetBool("web.influxdb.retention_policy") {
|
||||
//correctly set the retention period for the bucket (may not be able to do it during setup/creation)
|
||||
foundMonthlyBucket.RetentionRules = domain.RetentionRules{monthlyBucketRetentionRule}
|
||||
sr.influxClient.BucketsAPI().UpdateBucket(ctx, foundMonthlyBucket)
|
||||
}
|
||||
|
||||
yearlyBucket := fmt.Sprintf("%s_yearly", sr.appConfig.GetString("web.influxdb.bucket"))
|
||||
@@ -442,3 +497,16 @@ func (sr *scrutinyRepository) lookupNestedDurationKeys(durationKey string) []str
|
||||
}
|
||||
return []string{DURATION_KEY_WEEK}
|
||||
}
|
||||
|
||||
func sqlitePragmaString(pragmas map[string]string) string {
|
||||
q := url.Values{}
|
||||
for key, val := range pragmas {
|
||||
q.Add("_pragma", key+"="+val)
|
||||
}
|
||||
|
||||
queryStr := q.Encode()
|
||||
if len(queryStr) > 0 {
|
||||
return "?" + queryStr
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
@@ -4,15 +4,17 @@ import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/analogj/scrutiny/webapp/backend/pkg"
|
||||
"github.com/analogj/scrutiny/webapp/backend/pkg/database/migrations/m20201107210306"
|
||||
"github.com/analogj/scrutiny/webapp/backend/pkg/database/migrations/m20220503120000"
|
||||
"github.com/analogj/scrutiny/webapp/backend/pkg/database/migrations/m20220509170100"
|
||||
"github.com/analogj/scrutiny/webapp/backend/pkg/database/migrations/m20220716214900"
|
||||
"github.com/analogj/scrutiny/webapp/backend/pkg/models"
|
||||
"github.com/analogj/scrutiny/webapp/backend/pkg/models/collector"
|
||||
"github.com/analogj/scrutiny/webapp/backend/pkg/models/measurements"
|
||||
_ "github.com/glebarez/sqlite"
|
||||
"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"
|
||||
@@ -267,6 +269,106 @@ func (sr *scrutinyRepository) Migrate(ctx context.Context) error {
|
||||
return tx.AutoMigrate(m20220509170100.Device{})
|
||||
},
|
||||
},
|
||||
{
|
||||
ID: "m20220709181300",
|
||||
Migrate: func(tx *gorm.DB) error {
|
||||
|
||||
// delete devices with empty `wwn` field (they are impossible to delete manually), and are invalid.
|
||||
return tx.Where("wwn = ?", "").Delete(&models.Device{}).Error
|
||||
},
|
||||
},
|
||||
{
|
||||
ID: "m20220716214900", // add settings table.
|
||||
Migrate: func(tx *gorm.DB) error {
|
||||
|
||||
// adding the settings table.
|
||||
err := tx.AutoMigrate(m20220716214900.Setting{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
//add defaults.
|
||||
|
||||
var defaultSettings = []m20220716214900.Setting{
|
||||
{
|
||||
SettingKeyName: "theme",
|
||||
SettingKeyDescription: "Frontend theme ('light' | 'dark' | 'system')",
|
||||
SettingDataType: "string",
|
||||
SettingValueString: "system", // options: 'light' | 'dark' | 'system'
|
||||
},
|
||||
{
|
||||
SettingKeyName: "layout",
|
||||
SettingKeyDescription: "Frontend layout ('material')",
|
||||
SettingDataType: "string",
|
||||
SettingValueString: "material",
|
||||
},
|
||||
{
|
||||
SettingKeyName: "dashboard_display",
|
||||
SettingKeyDescription: "Frontend device display title ('name' | 'serial_id' | 'uuid' | 'label')",
|
||||
SettingDataType: "string",
|
||||
SettingValueString: "name",
|
||||
},
|
||||
{
|
||||
SettingKeyName: "dashboard_sort",
|
||||
SettingKeyDescription: "Frontend device sort by ('status' | 'title' | 'age')",
|
||||
SettingDataType: "string",
|
||||
SettingValueString: "status",
|
||||
},
|
||||
{
|
||||
SettingKeyName: "temperature_unit",
|
||||
SettingKeyDescription: "Frontend temperature unit ('celsius' | 'fahrenheit')",
|
||||
SettingDataType: "string",
|
||||
SettingValueString: "celsius",
|
||||
},
|
||||
{
|
||||
SettingKeyName: "file_size_si_units",
|
||||
SettingKeyDescription: "File size in SI units (true | false)",
|
||||
SettingDataType: "bool",
|
||||
SettingValueBool: false,
|
||||
},
|
||||
{
|
||||
SettingKeyName: "line_stroke",
|
||||
SettingKeyDescription: "Temperature chart line stroke ('smooth' | 'straight' | 'stepline')",
|
||||
SettingDataType: "string",
|
||||
SettingValueString: "smooth",
|
||||
},
|
||||
|
||||
{
|
||||
SettingKeyName: "metrics.notify_level",
|
||||
SettingKeyDescription: "Determines which device status will cause a notification (fail or warn)",
|
||||
SettingDataType: "numeric",
|
||||
SettingValueNumeric: int(pkg.MetricsNotifyLevelFail), // options: 'fail' or 'warn'
|
||||
},
|
||||
{
|
||||
SettingKeyName: "metrics.status_filter_attributes",
|
||||
SettingKeyDescription: "Determines which attributes should impact device status",
|
||||
SettingDataType: "numeric",
|
||||
SettingValueNumeric: int(pkg.MetricsStatusFilterAttributesAll), // options: 'all' or 'critical'
|
||||
},
|
||||
{
|
||||
SettingKeyName: "metrics.status_threshold",
|
||||
SettingKeyDescription: "Determines which threshold should impact device status",
|
||||
SettingDataType: "numeric",
|
||||
SettingValueNumeric: int(pkg.MetricsStatusThresholdBoth), // options: 'scrutiny', 'smart', 'both'
|
||||
},
|
||||
}
|
||||
return tx.Create(&defaultSettings).Error
|
||||
},
|
||||
},
|
||||
{
|
||||
ID: "m20221115214900", // add line_stroke setting.
|
||||
Migrate: func(tx *gorm.DB) error {
|
||||
//add line_stroke setting default.
|
||||
var defaultSettings = []m20220716214900.Setting{
|
||||
{
|
||||
SettingKeyName: "line_stroke",
|
||||
SettingKeyDescription: "Temperature chart line stroke ('smooth' | 'straight' | 'stepline')",
|
||||
SettingDataType: "string",
|
||||
SettingValueString: "smooth",
|
||||
},
|
||||
}
|
||||
return tx.Create(&defaultSettings).Error
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
if err := m.Migrate(); err != nil {
|
||||
@@ -274,6 +376,30 @@ func (sr *scrutinyRepository) Migrate(ctx context.Context) error {
|
||||
return err
|
||||
}
|
||||
sr.logger.Infoln("Database migration completed successfully")
|
||||
|
||||
//these migrations cannot be done within a transaction, so they are done as a separate group, with `UseTransaction = false`
|
||||
sr.logger.Infoln("SQLite global configuration migrations starting. Please wait....")
|
||||
globalMigrateOptions := gormigrate.DefaultOptions
|
||||
globalMigrateOptions.UseTransaction = false
|
||||
gm := gormigrate.New(sr.gormClient, globalMigrateOptions, []*gormigrate.Migration{
|
||||
{
|
||||
ID: "g20220802211500",
|
||||
Migrate: func(tx *gorm.DB) error {
|
||||
//shrink the Database (maybe necessary after 20220503113100)
|
||||
if err := tx.Exec("VACUUM;").Error; err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
if err := gm.Migrate(); err != nil {
|
||||
sr.logger.Errorf("SQLite global configuration migrations failed with error. \n Please open a github issue at https://github.com/AnalogJ/scrutiny and attach a copy of your scrutiny.db file. \n %v", err)
|
||||
return err
|
||||
}
|
||||
sr.logger.Infoln("SQLite global configuration migrations completed successfully")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
package database
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/analogj/scrutiny/webapp/backend/pkg/config"
|
||||
"github.com/analogj/scrutiny/webapp/backend/pkg/models"
|
||||
"github.com/mitchellh/mapstructure"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// LoadSettings will retrieve settings from the database, store them in the AppConfig object, and return a Settings struct
|
||||
func (sr *scrutinyRepository) LoadSettings(ctx context.Context) (*models.Settings, error) {
|
||||
settingsEntries := []models.SettingEntry{}
|
||||
if err := sr.gormClient.WithContext(ctx).Find(&settingsEntries).Error; err != nil {
|
||||
return nil, fmt.Errorf("Could not get settings from DB: %v", err)
|
||||
}
|
||||
|
||||
// store retrieved settings in the AppConfig obj
|
||||
for _, settingsEntry := range settingsEntries {
|
||||
configKey := fmt.Sprintf("%s.%s", config.DB_USER_SETTINGS_SUBKEY, settingsEntry.SettingKeyName)
|
||||
|
||||
if settingsEntry.SettingDataType == "numeric" {
|
||||
sr.appConfig.SetDefault(configKey, settingsEntry.SettingValueNumeric)
|
||||
} else if settingsEntry.SettingDataType == "string" {
|
||||
sr.appConfig.SetDefault(configKey, settingsEntry.SettingValueString)
|
||||
} else if settingsEntry.SettingDataType == "bool" {
|
||||
sr.appConfig.SetDefault(configKey, settingsEntry.SettingValueBool)
|
||||
}
|
||||
}
|
||||
|
||||
// unmarshal the dbsetting object data to a settings object.
|
||||
var settings models.Settings
|
||||
err := sr.appConfig.UnmarshalKey(config.DB_USER_SETTINGS_SUBKEY, &settings)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &settings, nil
|
||||
}
|
||||
|
||||
// testing
|
||||
// curl -d '{"metrics": { "notify_level": 5, "status_filter_attributes": 5, "status_threshold": 5 }}' -H "Content-Type: application/json" -X POST http://localhost:9090/api/settings
|
||||
// SaveSettings will update settings in AppConfig object, then save the settings to the database.
|
||||
func (sr *scrutinyRepository) SaveSettings(ctx context.Context, settings models.Settings) error {
|
||||
//save the entries to the appconfig
|
||||
settingsMap := &map[string]interface{}{}
|
||||
err := mapstructure.Decode(settings, &settingsMap)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
settingsWrapperMap := map[string]interface{}{}
|
||||
settingsWrapperMap[config.DB_USER_SETTINGS_SUBKEY] = *settingsMap
|
||||
err = sr.appConfig.MergeConfigMap(settingsWrapperMap)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
sr.logger.Debugf("after merge settings: %v", sr.appConfig.AllSettings())
|
||||
//retrieve current settings from the database
|
||||
settingsEntries := []models.SettingEntry{}
|
||||
if err := sr.gormClient.WithContext(ctx).Find(&settingsEntries).Error; err != nil {
|
||||
return fmt.Errorf("Could not get settings from DB: %v", err)
|
||||
}
|
||||
|
||||
//update settingsEntries
|
||||
for ndx, settingsEntry := range settingsEntries {
|
||||
configKey := fmt.Sprintf("%s.%s", config.DB_USER_SETTINGS_SUBKEY, strings.ToLower(settingsEntry.SettingKeyName))
|
||||
|
||||
if settingsEntry.SettingDataType == "numeric" {
|
||||
settingsEntries[ndx].SettingValueNumeric = sr.appConfig.GetInt(configKey)
|
||||
} else if settingsEntry.SettingDataType == "string" {
|
||||
settingsEntries[ndx].SettingValueString = sr.appConfig.GetString(configKey)
|
||||
} else if settingsEntry.SettingDataType == "bool" {
|
||||
settingsEntries[ndx].SettingValueBool = sr.appConfig.GetBool(configKey)
|
||||
}
|
||||
|
||||
// store in database.
|
||||
//TODO: this should be `sr.gormClient.Updates(&settingsEntries).Error`
|
||||
err := sr.gormClient.Model(&models.SettingEntry{}).Where([]uint{settingsEntry.ID}).Select("setting_value_numeric", "setting_value_string", "setting_value_bool").Updates(settingsEntries[ndx]).Error
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -11,35 +11,71 @@ import (
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
func (sr *scrutinyRepository) EnsureTasks(ctx context.Context, orgID string) error {
|
||||
weeklyTaskName := "tsk-weekly-aggr"
|
||||
weeklyTaskScript := sr.DownsampleScript("weekly", weeklyTaskName, "0 1 * * 0")
|
||||
if found, findErr := sr.influxTaskApi.FindTasks(ctx, &api.TaskFilter{Name: weeklyTaskName}); findErr == nil && len(found) == 0 {
|
||||
//weekly on Sunday at 1:00am
|
||||
_, err := sr.influxTaskApi.CreateTaskWithCron(ctx, weeklyTaskName, sr.DownsampleScript("weekly"), "0 1 * * 0", orgID)
|
||||
_, err := sr.influxTaskApi.CreateTaskByFlux(ctx, weeklyTaskScript, orgID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else if len(found) == 1 {
|
||||
//check if we should update
|
||||
task := &found[0]
|
||||
if weeklyTaskScript != task.Flux {
|
||||
sr.logger.Infoln("updating weekly task script")
|
||||
task.Flux = weeklyTaskScript
|
||||
_, err := sr.influxTaskApi.UpdateTask(ctx, task)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
monthlyTaskName := "tsk-monthly-aggr"
|
||||
monthlyTaskScript := sr.DownsampleScript("monthly", monthlyTaskName, "30 1 1 * *")
|
||||
if found, findErr := sr.influxTaskApi.FindTasks(ctx, &api.TaskFilter{Name: monthlyTaskName}); findErr == nil && len(found) == 0 {
|
||||
//monthly on first day of the month at 1:30am
|
||||
_, err := sr.influxTaskApi.CreateTaskWithCron(ctx, monthlyTaskName, sr.DownsampleScript("monthly"), "30 1 1 * *", orgID)
|
||||
_, err := sr.influxTaskApi.CreateTaskByFlux(ctx, monthlyTaskScript, orgID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else if len(found) == 1 {
|
||||
//check if we should update
|
||||
task := &found[0]
|
||||
if monthlyTaskScript != task.Flux {
|
||||
sr.logger.Infoln("updating monthly task script")
|
||||
task.Flux = monthlyTaskScript
|
||||
_, err := sr.influxTaskApi.UpdateTask(ctx, task)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
yearlyTaskName := "tsk-yearly-aggr"
|
||||
yearlyTaskScript := sr.DownsampleScript("yearly", yearlyTaskName, "0 2 1 1 *")
|
||||
if found, findErr := sr.influxTaskApi.FindTasks(ctx, &api.TaskFilter{Name: yearlyTaskName}); findErr == nil && len(found) == 0 {
|
||||
//yearly on the first day of the year at 2:00am
|
||||
_, err := sr.influxTaskApi.CreateTaskWithCron(ctx, yearlyTaskName, sr.DownsampleScript("yearly"), "0 2 1 1 *", orgID)
|
||||
_, err := sr.influxTaskApi.CreateTaskByFlux(ctx, yearlyTaskScript, orgID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else if len(found) == 1 {
|
||||
//check if we should update
|
||||
task := &found[0]
|
||||
if yearlyTaskScript != task.Flux {
|
||||
sr.logger.Infoln("updating yearly task script")
|
||||
task.Flux = yearlyTaskScript
|
||||
_, err := sr.influxTaskApi.UpdateTask(ctx, task)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (sr *scrutinyRepository) DownsampleScript(aggregationType string) string {
|
||||
func (sr *scrutinyRepository) DownsampleScript(aggregationType string, name string, cron string) string {
|
||||
var sourceBucket string // the source of the data
|
||||
var destBucket string // the destination for the aggregated data
|
||||
var rangeStart string
|
||||
@@ -88,30 +124,37 @@ func (sr *scrutinyRepository) DownsampleScript(aggregationType string) string {
|
||||
*/
|
||||
|
||||
return fmt.Sprintf(`
|
||||
sourceBucket = "%s"
|
||||
rangeStart = %s
|
||||
rangeEnd = %s
|
||||
aggWindow = %s
|
||||
destBucket = "%s"
|
||||
destOrg = "%s"
|
||||
option task = {
|
||||
name: "%s",
|
||||
cron: "%s",
|
||||
}
|
||||
|
||||
from(bucket: sourceBucket)
|
||||
|> range(start: rangeStart, stop: rangeEnd)
|
||||
|> filter(fn: (r) => r["_measurement"] == "smart" )
|
||||
|> group(columns: ["device_wwn", "_field"])
|
||||
|> aggregateWindow(every: aggWindow, fn: last, createEmpty: false)
|
||||
|> to(bucket: destBucket, org: destOrg)
|
||||
sourceBucket = "%s"
|
||||
rangeStart = %s
|
||||
rangeEnd = %s
|
||||
aggWindow = %s
|
||||
destBucket = "%s"
|
||||
destOrg = "%s"
|
||||
|
||||
temp_data = from(bucket: sourceBucket)
|
||||
|> range(start: rangeStart, stop: rangeEnd)
|
||||
|> filter(fn: (r) => r["_measurement"] == "temp")
|
||||
|> group(columns: ["device_wwn"])
|
||||
|> toInt()
|
||||
from(bucket: sourceBucket)
|
||||
|> range(start: rangeStart, stop: rangeEnd)
|
||||
|> filter(fn: (r) => r["_measurement"] == "smart" )
|
||||
|> group(columns: ["device_wwn", "_field"])
|
||||
|> aggregateWindow(every: aggWindow, fn: last, createEmpty: false)
|
||||
|> to(bucket: destBucket, org: destOrg)
|
||||
|
||||
temp_data
|
||||
|> aggregateWindow(fn: mean, every: aggWindow, createEmpty: false)
|
||||
|> to(bucket: destBucket, org: destOrg)
|
||||
from(bucket: sourceBucket)
|
||||
|> range(start: rangeStart, stop: rangeEnd)
|
||||
|> filter(fn: (r) => r["_measurement"] == "temp")
|
||||
|> group(columns: ["device_wwn"])
|
||||
|> toInt()
|
||||
|> aggregateWindow(fn: mean, every: aggWindow, createEmpty: false)
|
||||
|> set(key: "_measurement", value: "temp")
|
||||
|> set(key: "_field", value: "temp")
|
||||
|> to(bucket: destBucket, org: destOrg)
|
||||
`,
|
||||
name,
|
||||
cron,
|
||||
sourceBucket,
|
||||
rangeStart,
|
||||
rangeEnd,
|
||||
|
||||
@@ -0,0 +1,164 @@
|
||||
package database
|
||||
|
||||
import (
|
||||
mock_config "github.com/analogj/scrutiny/webapp/backend/pkg/config/mock"
|
||||
"github.com/golang/mock/gomock"
|
||||
"github.com/stretchr/testify/require"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func Test_DownsampleScript_Weekly(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
//setup
|
||||
mockCtrl := gomock.NewController(t)
|
||||
defer mockCtrl.Finish()
|
||||
fakeConfig := mock_config.NewMockInterface(mockCtrl)
|
||||
fakeConfig.EXPECT().GetString("web.influxdb.bucket").Return("metrics").AnyTimes()
|
||||
fakeConfig.EXPECT().GetString("web.influxdb.org").Return("scrutiny").AnyTimes()
|
||||
|
||||
deviceRepo := scrutinyRepository{
|
||||
appConfig: fakeConfig,
|
||||
}
|
||||
|
||||
aggregationType := "weekly"
|
||||
|
||||
//test
|
||||
influxDbScript := deviceRepo.DownsampleScript(aggregationType, "tsk-weekly-aggr", "0 1 * * 0")
|
||||
|
||||
//assert
|
||||
require.Equal(t, `
|
||||
option task = {
|
||||
name: "tsk-weekly-aggr",
|
||||
cron: "0 1 * * 0",
|
||||
}
|
||||
|
||||
sourceBucket = "metrics"
|
||||
rangeStart = -2w
|
||||
rangeEnd = -1w
|
||||
aggWindow = 1w
|
||||
destBucket = "metrics_weekly"
|
||||
destOrg = "scrutiny"
|
||||
|
||||
from(bucket: sourceBucket)
|
||||
|> range(start: rangeStart, stop: rangeEnd)
|
||||
|> filter(fn: (r) => r["_measurement"] == "smart" )
|
||||
|> group(columns: ["device_wwn", "_field"])
|
||||
|> aggregateWindow(every: aggWindow, fn: last, createEmpty: false)
|
||||
|> to(bucket: destBucket, org: destOrg)
|
||||
|
||||
from(bucket: sourceBucket)
|
||||
|> range(start: rangeStart, stop: rangeEnd)
|
||||
|> filter(fn: (r) => r["_measurement"] == "temp")
|
||||
|> group(columns: ["device_wwn"])
|
||||
|> toInt()
|
||||
|> aggregateWindow(fn: mean, every: aggWindow, createEmpty: false)
|
||||
|> set(key: "_measurement", value: "temp")
|
||||
|> set(key: "_field", value: "temp")
|
||||
|> to(bucket: destBucket, org: destOrg)
|
||||
`, influxDbScript)
|
||||
}
|
||||
|
||||
func Test_DownsampleScript_Monthly(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
//setup
|
||||
mockCtrl := gomock.NewController(t)
|
||||
defer mockCtrl.Finish()
|
||||
fakeConfig := mock_config.NewMockInterface(mockCtrl)
|
||||
fakeConfig.EXPECT().GetString("web.influxdb.bucket").Return("metrics").AnyTimes()
|
||||
fakeConfig.EXPECT().GetString("web.influxdb.org").Return("scrutiny").AnyTimes()
|
||||
|
||||
deviceRepo := scrutinyRepository{
|
||||
appConfig: fakeConfig,
|
||||
}
|
||||
|
||||
aggregationType := "monthly"
|
||||
|
||||
//test
|
||||
influxDbScript := deviceRepo.DownsampleScript(aggregationType, "tsk-monthly-aggr", "30 1 1 * *")
|
||||
|
||||
//assert
|
||||
require.Equal(t, `
|
||||
option task = {
|
||||
name: "tsk-monthly-aggr",
|
||||
cron: "30 1 1 * *",
|
||||
}
|
||||
|
||||
sourceBucket = "metrics_weekly"
|
||||
rangeStart = -2mo
|
||||
rangeEnd = -1mo
|
||||
aggWindow = 1mo
|
||||
destBucket = "metrics_monthly"
|
||||
destOrg = "scrutiny"
|
||||
|
||||
from(bucket: sourceBucket)
|
||||
|> range(start: rangeStart, stop: rangeEnd)
|
||||
|> filter(fn: (r) => r["_measurement"] == "smart" )
|
||||
|> group(columns: ["device_wwn", "_field"])
|
||||
|> aggregateWindow(every: aggWindow, fn: last, createEmpty: false)
|
||||
|> to(bucket: destBucket, org: destOrg)
|
||||
|
||||
from(bucket: sourceBucket)
|
||||
|> range(start: rangeStart, stop: rangeEnd)
|
||||
|> filter(fn: (r) => r["_measurement"] == "temp")
|
||||
|> group(columns: ["device_wwn"])
|
||||
|> toInt()
|
||||
|> aggregateWindow(fn: mean, every: aggWindow, createEmpty: false)
|
||||
|> set(key: "_measurement", value: "temp")
|
||||
|> set(key: "_field", value: "temp")
|
||||
|> to(bucket: destBucket, org: destOrg)
|
||||
`, influxDbScript)
|
||||
}
|
||||
|
||||
func Test_DownsampleScript_Yearly(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
//setup
|
||||
mockCtrl := gomock.NewController(t)
|
||||
defer mockCtrl.Finish()
|
||||
fakeConfig := mock_config.NewMockInterface(mockCtrl)
|
||||
fakeConfig.EXPECT().GetString("web.influxdb.bucket").Return("metrics").AnyTimes()
|
||||
fakeConfig.EXPECT().GetString("web.influxdb.org").Return("scrutiny").AnyTimes()
|
||||
|
||||
deviceRepo := scrutinyRepository{
|
||||
appConfig: fakeConfig,
|
||||
}
|
||||
|
||||
aggregationType := "yearly"
|
||||
|
||||
//test
|
||||
influxDbScript := deviceRepo.DownsampleScript(aggregationType, "tsk-yearly-aggr", "0 2 1 1 *")
|
||||
|
||||
//assert
|
||||
require.Equal(t, `
|
||||
option task = {
|
||||
name: "tsk-yearly-aggr",
|
||||
cron: "0 2 1 1 *",
|
||||
}
|
||||
|
||||
sourceBucket = "metrics_monthly"
|
||||
rangeStart = -2y
|
||||
rangeEnd = -1y
|
||||
aggWindow = 1y
|
||||
destBucket = "metrics_yearly"
|
||||
destOrg = "scrutiny"
|
||||
|
||||
from(bucket: sourceBucket)
|
||||
|> range(start: rangeStart, stop: rangeEnd)
|
||||
|> filter(fn: (r) => r["_measurement"] == "smart" )
|
||||
|> group(columns: ["device_wwn", "_field"])
|
||||
|> aggregateWindow(every: aggWindow, fn: last, createEmpty: false)
|
||||
|> to(bucket: destBucket, org: destOrg)
|
||||
|
||||
from(bucket: sourceBucket)
|
||||
|> range(start: rangeStart, stop: rangeEnd)
|
||||
|> filter(fn: (r) => r["_measurement"] == "temp")
|
||||
|> group(columns: ["device_wwn"])
|
||||
|> toInt()
|
||||
|> aggregateWindow(fn: mean, every: aggWindow, createEmpty: false)
|
||||
|> set(key: "_measurement", value: "temp")
|
||||
|> set(key: "_field", value: "temp")
|
||||
|> to(bucket: destBucket, org: destOrg)
|
||||
`, influxDbScript)
|
||||
}
|
||||
@@ -17,6 +17,10 @@ func (sr *scrutinyRepository) SaveSmartTemperature(ctx context.Context, wwn stri
|
||||
if len(collectorSmartData.AtaSctTemperatureHistory.Table) > 0 {
|
||||
|
||||
for ndx, temp := range collectorSmartData.AtaSctTemperatureHistory.Table {
|
||||
//temp value may be null, we must skip/ignore them. See #393
|
||||
if temp == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
minutesOffset := collectorSmartData.AtaSctTemperatureHistory.LoggingIntervalMinutes * int64(ndx) * 60
|
||||
smartTemp := measurements.SmartTemperature{
|
||||
|
||||
@@ -0,0 +1,185 @@
|
||||
package database
|
||||
|
||||
import (
|
||||
mock_config "github.com/analogj/scrutiny/webapp/backend/pkg/config/mock"
|
||||
"github.com/golang/mock/gomock"
|
||||
"github.com/stretchr/testify/require"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func Test_aggregateTempQuery_Week(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
//setup
|
||||
mockCtrl := gomock.NewController(t)
|
||||
defer mockCtrl.Finish()
|
||||
fakeConfig := mock_config.NewMockInterface(mockCtrl)
|
||||
fakeConfig.EXPECT().GetString("web.influxdb.bucket").Return("metrics").AnyTimes()
|
||||
fakeConfig.EXPECT().GetString("web.influxdb.org").Return("scrutiny").AnyTimes()
|
||||
|
||||
deviceRepo := scrutinyRepository{
|
||||
appConfig: fakeConfig,
|
||||
}
|
||||
|
||||
aggregationType := DURATION_KEY_WEEK
|
||||
|
||||
//test
|
||||
influxDbScript := deviceRepo.aggregateTempQuery(aggregationType)
|
||||
|
||||
//assert
|
||||
require.Equal(t, `import "influxdata/influxdb/schema"
|
||||
weekData = from(bucket: "metrics")
|
||||
|> range(start: -1w, stop: now())
|
||||
|> filter(fn: (r) => r["_measurement"] == "temp" )
|
||||
|> aggregateWindow(every: 1h, fn: mean, createEmpty: false)
|
||||
|> group(columns: ["device_wwn"])
|
||||
|> toInt()
|
||||
|
||||
weekData
|
||||
|> schema.fieldsAsCols()
|
||||
|> yield()`, influxDbScript)
|
||||
}
|
||||
|
||||
func Test_aggregateTempQuery_Month(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
//setup
|
||||
mockCtrl := gomock.NewController(t)
|
||||
defer mockCtrl.Finish()
|
||||
fakeConfig := mock_config.NewMockInterface(mockCtrl)
|
||||
fakeConfig.EXPECT().GetString("web.influxdb.bucket").Return("metrics").AnyTimes()
|
||||
fakeConfig.EXPECT().GetString("web.influxdb.org").Return("scrutiny").AnyTimes()
|
||||
|
||||
deviceRepo := scrutinyRepository{
|
||||
appConfig: fakeConfig,
|
||||
}
|
||||
|
||||
aggregationType := DURATION_KEY_MONTH
|
||||
|
||||
//test
|
||||
influxDbScript := deviceRepo.aggregateTempQuery(aggregationType)
|
||||
|
||||
//assert
|
||||
require.Equal(t, `import "influxdata/influxdb/schema"
|
||||
weekData = from(bucket: "metrics")
|
||||
|> range(start: -1w, stop: now())
|
||||
|> filter(fn: (r) => r["_measurement"] == "temp" )
|
||||
|> aggregateWindow(every: 1h, fn: mean, createEmpty: false)
|
||||
|> group(columns: ["device_wwn"])
|
||||
|> toInt()
|
||||
|
||||
monthData = from(bucket: "metrics_weekly")
|
||||
|> range(start: -1mo, stop: -1w)
|
||||
|> filter(fn: (r) => r["_measurement"] == "temp" )
|
||||
|> aggregateWindow(every: 1h, fn: mean, createEmpty: false)
|
||||
|> group(columns: ["device_wwn"])
|
||||
|> toInt()
|
||||
|
||||
union(tables: [weekData, monthData])
|
||||
|> group(columns: ["device_wwn"])
|
||||
|> sort(columns: ["_time"], desc: false)
|
||||
|> schema.fieldsAsCols()`, influxDbScript)
|
||||
}
|
||||
|
||||
func Test_aggregateTempQuery_Year(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
//setup
|
||||
mockCtrl := gomock.NewController(t)
|
||||
defer mockCtrl.Finish()
|
||||
fakeConfig := mock_config.NewMockInterface(mockCtrl)
|
||||
fakeConfig.EXPECT().GetString("web.influxdb.bucket").Return("metrics").AnyTimes()
|
||||
fakeConfig.EXPECT().GetString("web.influxdb.org").Return("scrutiny").AnyTimes()
|
||||
|
||||
deviceRepo := scrutinyRepository{
|
||||
appConfig: fakeConfig,
|
||||
}
|
||||
|
||||
aggregationType := DURATION_KEY_YEAR
|
||||
|
||||
//test
|
||||
influxDbScript := deviceRepo.aggregateTempQuery(aggregationType)
|
||||
|
||||
//assert
|
||||
require.Equal(t, `import "influxdata/influxdb/schema"
|
||||
weekData = from(bucket: "metrics")
|
||||
|> range(start: -1w, stop: now())
|
||||
|> filter(fn: (r) => r["_measurement"] == "temp" )
|
||||
|> aggregateWindow(every: 1h, fn: mean, createEmpty: false)
|
||||
|> group(columns: ["device_wwn"])
|
||||
|> toInt()
|
||||
|
||||
monthData = from(bucket: "metrics_weekly")
|
||||
|> range(start: -1mo, stop: -1w)
|
||||
|> filter(fn: (r) => r["_measurement"] == "temp" )
|
||||
|> aggregateWindow(every: 1h, fn: mean, createEmpty: false)
|
||||
|> group(columns: ["device_wwn"])
|
||||
|> toInt()
|
||||
|
||||
yearData = from(bucket: "metrics_monthly")
|
||||
|> range(start: -1y, stop: -1mo)
|
||||
|> filter(fn: (r) => r["_measurement"] == "temp" )
|
||||
|> aggregateWindow(every: 1h, fn: mean, createEmpty: false)
|
||||
|> group(columns: ["device_wwn"])
|
||||
|> toInt()
|
||||
|
||||
union(tables: [weekData, monthData, yearData])
|
||||
|> group(columns: ["device_wwn"])
|
||||
|> sort(columns: ["_time"], desc: false)
|
||||
|> schema.fieldsAsCols()`, influxDbScript)
|
||||
}
|
||||
|
||||
func Test_aggregateTempQuery_Forever(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
//setup
|
||||
mockCtrl := gomock.NewController(t)
|
||||
defer mockCtrl.Finish()
|
||||
fakeConfig := mock_config.NewMockInterface(mockCtrl)
|
||||
fakeConfig.EXPECT().GetString("web.influxdb.bucket").Return("metrics").AnyTimes()
|
||||
fakeConfig.EXPECT().GetString("web.influxdb.org").Return("scrutiny").AnyTimes()
|
||||
|
||||
deviceRepo := scrutinyRepository{
|
||||
appConfig: fakeConfig,
|
||||
}
|
||||
|
||||
aggregationType := DURATION_KEY_FOREVER
|
||||
|
||||
//test
|
||||
influxDbScript := deviceRepo.aggregateTempQuery(aggregationType)
|
||||
|
||||
//assert
|
||||
require.Equal(t, `import "influxdata/influxdb/schema"
|
||||
weekData = from(bucket: "metrics")
|
||||
|> range(start: -1w, stop: now())
|
||||
|> filter(fn: (r) => r["_measurement"] == "temp" )
|
||||
|> aggregateWindow(every: 1h, fn: mean, createEmpty: false)
|
||||
|> group(columns: ["device_wwn"])
|
||||
|> toInt()
|
||||
|
||||
monthData = from(bucket: "metrics_weekly")
|
||||
|> range(start: -1mo, stop: -1w)
|
||||
|> filter(fn: (r) => r["_measurement"] == "temp" )
|
||||
|> aggregateWindow(every: 1h, fn: mean, createEmpty: false)
|
||||
|> group(columns: ["device_wwn"])
|
||||
|> toInt()
|
||||
|
||||
yearData = from(bucket: "metrics_monthly")
|
||||
|> range(start: -1y, stop: -1mo)
|
||||
|> filter(fn: (r) => r["_measurement"] == "temp" )
|
||||
|> aggregateWindow(every: 1h, fn: mean, createEmpty: false)
|
||||
|> group(columns: ["device_wwn"])
|
||||
|> toInt()
|
||||
|
||||
foreverData = from(bucket: "metrics_yearly")
|
||||
|> range(start: -10y, stop: -1y)
|
||||
|> filter(fn: (r) => r["_measurement"] == "temp" )
|
||||
|> aggregateWindow(every: 1h, fn: mean, createEmpty: false)
|
||||
|> group(columns: ["device_wwn"])
|
||||
|> toInt()
|
||||
|
||||
union(tables: [weekData, monthData, yearData, foreverData])
|
||||
|> group(columns: ["device_wwn"])
|
||||
|> sort(columns: ["_time"], desc: false)
|
||||
|> schema.fieldsAsCols()`, influxDbScript)
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
package models
|
||||
|
||||
// Temperature Format
|
||||
// Date Format
|
||||
// Device History window
|
||||
@@ -0,0 +1,23 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// SettingEntry matches a setting row in the database
|
||||
type SettingEntry struct {
|
||||
//GORM attributes, see: http://gorm.io/docs/conventions.html
|
||||
gorm.Model
|
||||
|
||||
SettingKeyName string `json:"setting_key_name" gorm:"unique;not null"`
|
||||
SettingKeyDescription string `json:"setting_key_description"`
|
||||
SettingDataType string `json:"setting_data_type"`
|
||||
|
||||
SettingValueNumeric int `json:"setting_value_numeric"`
|
||||
SettingValueString string `json:"setting_value_string"`
|
||||
SettingValueBool bool `json:"setting_value_bool"`
|
||||
}
|
||||
|
||||
func (s SettingEntry) TableName() string {
|
||||
return "settings"
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package models
|
||||
|
||||
// Settings is made up of parsed SettingEntry objects retrieved from the database
|
||||
//type Settings struct {
|
||||
// MetricsNotifyLevel pkg.MetricsNotifyLevel `json:"metrics.notify.level" mapstructure:"metrics.notify.level"`
|
||||
// MetricsStatusFilterAttributes pkg.MetricsStatusFilterAttributes `json:"metrics.status.filter_attributes" mapstructure:"metrics.status.filter_attributes"`
|
||||
// MetricsStatusThreshold pkg.MetricsStatusThreshold `json:"metrics.status.threshold" mapstructure:"metrics.status.threshold"`
|
||||
//}
|
||||
|
||||
type Settings struct {
|
||||
Theme string `json:"theme" mapstructure:"theme"`
|
||||
Layout string `json:"layout" mapstructure:"layout"`
|
||||
DashboardDisplay string `json:"dashboard_display" mapstructure:"dashboard_display"`
|
||||
DashboardSort string `json:"dashboard_sort" mapstructure:"dashboard_sort"`
|
||||
TemperatureUnit string `json:"temperature_unit" mapstructure:"temperature_unit"`
|
||||
FileSizeSIUnits bool `json:"file_size_si_units" mapstructure:"file_size_si_units"`
|
||||
LineStroke string `json:"line_stroke" mapstructure:"line_stroke"`
|
||||
|
||||
Metrics struct {
|
||||
NotifyLevel int `json:"notify_level" mapstructure:"notify_level"`
|
||||
StatusFilterAttributes int `json:"status_filter_attributes" mapstructure:"status_filter_attributes"`
|
||||
StatusThreshold int `json:"status_threshold" mapstructure:"status_threshold"`
|
||||
} `json:"metrics" mapstructure:"metrics"`
|
||||
}
|
||||
@@ -29,20 +29,22 @@ const NotifyFailureTypeSmartFailure = "SmartFailure"
|
||||
const NotifyFailureTypeScrutinyFailure = "ScrutinyFailure"
|
||||
|
||||
// ShouldNotify check if the error Message should be filtered (level mismatch or filtered_attributes)
|
||||
func ShouldNotify(device models.Device, smartAttrs measurements.Smart, notifyLevel string, notifyFilterAttributes string) bool {
|
||||
func ShouldNotify(device models.Device, smartAttrs measurements.Smart, statusThreshold pkg.MetricsStatusThreshold, statusFilterAttributes pkg.MetricsStatusFilterAttributes) bool {
|
||||
// 1. check if the device is healthy
|
||||
if device.DeviceStatus == pkg.DeviceStatusPassed {
|
||||
return false
|
||||
}
|
||||
|
||||
//TODO: cannot check for warning notifyLevel yet.
|
||||
|
||||
// setup constants for comparison
|
||||
var requiredDeviceStatus pkg.DeviceStatus
|
||||
var requiredAttrStatus pkg.AttributeStatus
|
||||
if notifyLevel == pkg.NotifyLevelFail {
|
||||
if statusThreshold == pkg.MetricsStatusThresholdBoth {
|
||||
// either scrutiny or smart failures should trigger an email
|
||||
requiredDeviceStatus = pkg.DeviceStatusSet(pkg.DeviceStatusFailedSmart, pkg.DeviceStatusFailedScrutiny)
|
||||
requiredAttrStatus = pkg.AttributeStatusSet(pkg.AttributeStatusFailedSmart, pkg.AttributeStatusFailedScrutiny)
|
||||
} else if notifyLevel == pkg.NotifyLevelFailSmart {
|
||||
} else if statusThreshold == pkg.MetricsStatusThresholdSmart {
|
||||
//only smart failures
|
||||
requiredDeviceStatus = pkg.DeviceStatusFailedSmart
|
||||
requiredAttrStatus = pkg.AttributeStatusFailedSmart
|
||||
@@ -53,9 +55,9 @@ func ShouldNotify(device models.Device, smartAttrs measurements.Smart, notifyLev
|
||||
|
||||
// 2. check if the attributes that are failing should be filtered (non-critical)
|
||||
// 3. for any unfiltered attribute, store the failure reason (Smart or Scrutiny)
|
||||
if notifyFilterAttributes == pkg.NotifyFilterAttributesCritical {
|
||||
if statusFilterAttributes == pkg.MetricsStatusFilterAttributesCritical {
|
||||
hasFailingCriticalAttr := false
|
||||
var statusFailingCrtiticalAttr pkg.AttributeStatus
|
||||
var statusFailingCriticalAttr pkg.AttributeStatus
|
||||
|
||||
for attrId, attrData := range smartAttrs.Attributes {
|
||||
//find failing attribute
|
||||
@@ -64,7 +66,7 @@ func ShouldNotify(device models.Device, smartAttrs measurements.Smart, notifyLev
|
||||
}
|
||||
|
||||
// merge the status's of all critical attributes
|
||||
statusFailingCrtiticalAttr = pkg.AttributeStatusSet(statusFailingCrtiticalAttr, attrData.GetStatus())
|
||||
statusFailingCriticalAttr = pkg.AttributeStatusSet(statusFailingCriticalAttr, attrData.GetStatus())
|
||||
|
||||
//found a failing attribute, see if its critical
|
||||
if device.IsScsi() && thresholds.ScsiMetadata[attrId].Critical {
|
||||
@@ -89,7 +91,7 @@ func ShouldNotify(device models.Device, smartAttrs measurements.Smart, notifyLev
|
||||
return false
|
||||
} else {
|
||||
// check if any of the critical attributes have a status that we're looking for
|
||||
return pkg.AttributeStatusHas(statusFailingCrtiticalAttr, requiredAttrStatus)
|
||||
return pkg.AttributeStatusHas(statusFailingCriticalAttr, requiredAttrStatus)
|
||||
}
|
||||
|
||||
} else {
|
||||
@@ -99,12 +101,13 @@ func ShouldNotify(device models.Device, smartAttrs measurements.Smart, notifyLev
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: include host and/or user label for device.
|
||||
// TODO: include user label for device.
|
||||
type Payload struct {
|
||||
DeviceType string `json:"device_type"` //ATA/SCSI/NVMe
|
||||
DeviceName string `json:"device_name"` //dev/sda
|
||||
DeviceSerial string `json:"device_serial"` //WDDJ324KSO
|
||||
Test bool `json:"test"` // false
|
||||
HostId string `json:"host_id,omitempty"` //host id (optional)
|
||||
DeviceType string `json:"device_type"` //ATA/SCSI/NVMe
|
||||
DeviceName string `json:"device_name"` //dev/sda
|
||||
DeviceSerial string `json:"device_serial"` //WDDJ324KSO
|
||||
Test bool `json:"test"` // false
|
||||
|
||||
//private, populated during init (marked as Public for JSON serialization)
|
||||
Date string `json:"date"` //populated by Send function.
|
||||
@@ -113,8 +116,9 @@ type Payload struct {
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
func NewPayload(device models.Device, test bool) Payload {
|
||||
func NewPayload(device models.Device, test bool, currentTime ...time.Time) Payload {
|
||||
payload := Payload{
|
||||
HostId: strings.TrimSpace(device.HostId),
|
||||
DeviceType: device.DeviceType,
|
||||
DeviceName: device.DeviceName,
|
||||
DeviceSerial: device.SerialNumber,
|
||||
@@ -122,7 +126,13 @@ func NewPayload(device models.Device, test bool) Payload {
|
||||
}
|
||||
|
||||
//validate that the Payload is populated
|
||||
sendDate := time.Now()
|
||||
var sendDate time.Time
|
||||
if currentTime != nil && len(currentTime) > 0 {
|
||||
sendDate = currentTime[0]
|
||||
} else {
|
||||
sendDate = time.Now()
|
||||
}
|
||||
|
||||
payload.Date = sendDate.Format(time.RFC3339)
|
||||
payload.FailureType = payload.GenerateFailureType(device.DeviceStatus)
|
||||
payload.Subject = payload.GenerateSubject()
|
||||
@@ -146,25 +156,39 @@ func (p *Payload) GenerateFailureType(deviceStatus pkg.DeviceStatus) string {
|
||||
|
||||
func (p *Payload) GenerateSubject() string {
|
||||
//generate a detailed failure message
|
||||
return fmt.Sprintf("Scrutiny SMART error (%s) detected on device: %s", p.FailureType, p.DeviceName)
|
||||
var subject string
|
||||
if len(p.HostId) > 0 {
|
||||
subject = fmt.Sprintf("Scrutiny SMART error (%s) detected on [host]device: [%s]%s", p.FailureType, p.HostId, p.DeviceName)
|
||||
} else {
|
||||
subject = fmt.Sprintf("Scrutiny SMART error (%s) detected on device: %s", p.FailureType, p.DeviceName)
|
||||
}
|
||||
return subject
|
||||
}
|
||||
|
||||
func (p *Payload) GenerateMessage() string {
|
||||
//generate a detailed failure message
|
||||
message := fmt.Sprintf(
|
||||
`Scrutiny SMART error notification for device: %s
|
||||
Failure Type: %s
|
||||
Device Name: %s
|
||||
Device Serial: %s
|
||||
Device Type: %s
|
||||
|
||||
Date: %s`, p.DeviceName, p.FailureType, p.DeviceName, p.DeviceSerial, p.DeviceType, p.Date)
|
||||
messageParts := []string{}
|
||||
|
||||
if p.Test {
|
||||
message = "TEST NOTIFICATION:\n" + message
|
||||
messageParts = append(messageParts, fmt.Sprintf("Scrutiny SMART error notification for device: %s", p.DeviceName))
|
||||
if len(p.HostId) > 0 {
|
||||
messageParts = append(messageParts, fmt.Sprintf("Host Id: %s", p.HostId))
|
||||
}
|
||||
|
||||
return message
|
||||
messageParts = append(messageParts,
|
||||
fmt.Sprintf("Failure Type: %s", p.FailureType),
|
||||
fmt.Sprintf("Device Name: %s", p.DeviceName),
|
||||
fmt.Sprintf("Device Serial: %s", p.DeviceSerial),
|
||||
fmt.Sprintf("Device Type: %s", p.DeviceType),
|
||||
"",
|
||||
fmt.Sprintf("Date: %s", p.Date),
|
||||
)
|
||||
|
||||
if p.Test {
|
||||
messageParts = append([]string{"TEST NOTIFICATION:"}, messageParts...)
|
||||
}
|
||||
|
||||
return strings.Join(messageParts, "\n")
|
||||
}
|
||||
|
||||
func New(logger logrus.FieldLogger, appconfig config.Interface, device models.Device, test bool) Notify {
|
||||
@@ -285,6 +309,9 @@ func (n *Notify) SendScriptNotification(scriptUrl string) error {
|
||||
copyEnv = append(copyEnv, fmt.Sprintf("SCRUTINY_DEVICE_TYPE=%s", n.Payload.DeviceType))
|
||||
copyEnv = append(copyEnv, fmt.Sprintf("SCRUTINY_DEVICE_SERIAL=%s", n.Payload.DeviceSerial))
|
||||
copyEnv = append(copyEnv, fmt.Sprintf("SCRUTINY_MESSAGE=%s", n.Payload.Message))
|
||||
if len(n.Payload.HostId) > 0 {
|
||||
copyEnv = append(copyEnv, fmt.Sprintf("SCRUTINY_HOST_ID=%s", n.Payload.HostId))
|
||||
}
|
||||
err := utils.CmdExec(scriptPath, []string{}, "", copyEnv, "")
|
||||
if err != nil {
|
||||
n.Logger.Errorf("An error occurred while executing script %s: %v", scriptPath, err)
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
package notify
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/analogj/scrutiny/webapp/backend/pkg"
|
||||
"github.com/analogj/scrutiny/webapp/backend/pkg/models"
|
||||
"github.com/analogj/scrutiny/webapp/backend/pkg/models/measurements"
|
||||
"github.com/stretchr/testify/require"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestShouldNotify_MustSkipPassingDevices(t *testing.T) {
|
||||
@@ -15,56 +17,56 @@ func TestShouldNotify_MustSkipPassingDevices(t *testing.T) {
|
||||
DeviceStatus: pkg.DeviceStatusPassed,
|
||||
}
|
||||
smartAttrs := measurements.Smart{}
|
||||
notifyLevel := pkg.NotifyLevelFail
|
||||
notifyFilterAttributes := pkg.NotifyFilterAttributesAll
|
||||
statusThreshold := pkg.MetricsStatusThresholdBoth
|
||||
notifyFilterAttributes := pkg.MetricsStatusFilterAttributesAll
|
||||
|
||||
//assert
|
||||
require.False(t, ShouldNotify(device, smartAttrs, notifyLevel, notifyFilterAttributes))
|
||||
require.False(t, ShouldNotify(device, smartAttrs, statusThreshold, notifyFilterAttributes))
|
||||
}
|
||||
|
||||
func TestShouldNotify_NotifyLevelFail_FailingSmartDevice(t *testing.T) {
|
||||
func TestShouldNotify_MetricsStatusThresholdBoth_FailingSmartDevice(t *testing.T) {
|
||||
t.Parallel()
|
||||
//setup
|
||||
device := models.Device{
|
||||
DeviceStatus: pkg.DeviceStatusFailedSmart,
|
||||
}
|
||||
smartAttrs := measurements.Smart{}
|
||||
notifyLevel := pkg.NotifyLevelFail
|
||||
notifyFilterAttributes := pkg.NotifyFilterAttributesAll
|
||||
statusThreshold := pkg.MetricsStatusThresholdBoth
|
||||
notifyFilterAttributes := pkg.MetricsStatusFilterAttributesAll
|
||||
|
||||
//assert
|
||||
require.True(t, ShouldNotify(device, smartAttrs, notifyLevel, notifyFilterAttributes))
|
||||
require.True(t, ShouldNotify(device, smartAttrs, statusThreshold, notifyFilterAttributes))
|
||||
}
|
||||
|
||||
func TestShouldNotify_NotifyLevelFailSmart_FailingSmartDevice(t *testing.T) {
|
||||
func TestShouldNotify_MetricsStatusThresholdSmart_FailingSmartDevice(t *testing.T) {
|
||||
t.Parallel()
|
||||
//setup
|
||||
device := models.Device{
|
||||
DeviceStatus: pkg.DeviceStatusFailedSmart,
|
||||
}
|
||||
smartAttrs := measurements.Smart{}
|
||||
notifyLevel := pkg.NotifyLevelFailSmart
|
||||
notifyFilterAttributes := pkg.NotifyFilterAttributesAll
|
||||
statusThreshold := pkg.MetricsStatusThresholdSmart
|
||||
notifyFilterAttributes := pkg.MetricsStatusFilterAttributesAll
|
||||
|
||||
//assert
|
||||
require.True(t, ShouldNotify(device, smartAttrs, notifyLevel, notifyFilterAttributes))
|
||||
require.True(t, ShouldNotify(device, smartAttrs, statusThreshold, notifyFilterAttributes))
|
||||
}
|
||||
|
||||
func TestShouldNotify_NotifyLevelFailScrutiny_FailingSmartDevice(t *testing.T) {
|
||||
func TestShouldNotify_MetricsStatusThresholdScrutiny_FailingSmartDevice(t *testing.T) {
|
||||
t.Parallel()
|
||||
//setup
|
||||
device := models.Device{
|
||||
DeviceStatus: pkg.DeviceStatusFailedSmart,
|
||||
}
|
||||
smartAttrs := measurements.Smart{}
|
||||
notifyLevel := pkg.NotifyLevelFailScrutiny
|
||||
notifyFilterAttributes := pkg.NotifyFilterAttributesAll
|
||||
statusThreshold := pkg.MetricsStatusThresholdScrutiny
|
||||
notifyFilterAttributes := pkg.MetricsStatusFilterAttributesAll
|
||||
|
||||
//assert
|
||||
require.False(t, ShouldNotify(device, smartAttrs, notifyLevel, notifyFilterAttributes))
|
||||
require.False(t, ShouldNotify(device, smartAttrs, statusThreshold, notifyFilterAttributes))
|
||||
}
|
||||
|
||||
func TestShouldNotify_NotifyFilterAttributesCritical_WithCriticalAttrs(t *testing.T) {
|
||||
func TestShouldNotify_MetricsStatusFilterAttributesCritical_WithCriticalAttrs(t *testing.T) {
|
||||
t.Parallel()
|
||||
//setup
|
||||
device := models.Device{
|
||||
@@ -75,14 +77,14 @@ func TestShouldNotify_NotifyFilterAttributesCritical_WithCriticalAttrs(t *testin
|
||||
Status: pkg.AttributeStatusFailedSmart,
|
||||
},
|
||||
}}
|
||||
notifyLevel := pkg.NotifyLevelFail
|
||||
notifyFilterAttributes := pkg.NotifyFilterAttributesCritical
|
||||
statusThreshold := pkg.MetricsStatusThresholdBoth
|
||||
notifyFilterAttributes := pkg.MetricsStatusFilterAttributesCritical
|
||||
|
||||
//assert
|
||||
require.True(t, ShouldNotify(device, smartAttrs, notifyLevel, notifyFilterAttributes))
|
||||
require.True(t, ShouldNotify(device, smartAttrs, statusThreshold, notifyFilterAttributes))
|
||||
}
|
||||
|
||||
func TestShouldNotify_NotifyFilterAttributesCritical_WithMultipleCriticalAttrs(t *testing.T) {
|
||||
func TestShouldNotify_MetricsStatusFilterAttributesCritical_WithMultipleCriticalAttrs(t *testing.T) {
|
||||
t.Parallel()
|
||||
//setup
|
||||
device := models.Device{
|
||||
@@ -96,14 +98,14 @@ func TestShouldNotify_NotifyFilterAttributesCritical_WithMultipleCriticalAttrs(t
|
||||
Status: pkg.AttributeStatusFailedScrutiny,
|
||||
},
|
||||
}}
|
||||
notifyLevel := pkg.NotifyLevelFail
|
||||
notifyFilterAttributes := pkg.NotifyFilterAttributesCritical
|
||||
statusThreshold := pkg.MetricsStatusThresholdBoth
|
||||
notifyFilterAttributes := pkg.MetricsStatusFilterAttributesCritical
|
||||
|
||||
//assert
|
||||
require.True(t, ShouldNotify(device, smartAttrs, notifyLevel, notifyFilterAttributes))
|
||||
require.True(t, ShouldNotify(device, smartAttrs, statusThreshold, notifyFilterAttributes))
|
||||
}
|
||||
|
||||
func TestShouldNotify_NotifyFilterAttributesCritical_WithNoCriticalAttrs(t *testing.T) {
|
||||
func TestShouldNotify_MetricsStatusFilterAttributesCritical_WithNoCriticalAttrs(t *testing.T) {
|
||||
t.Parallel()
|
||||
//setup
|
||||
device := models.Device{
|
||||
@@ -114,14 +116,14 @@ func TestShouldNotify_NotifyFilterAttributesCritical_WithNoCriticalAttrs(t *test
|
||||
Status: pkg.AttributeStatusFailedSmart,
|
||||
},
|
||||
}}
|
||||
notifyLevel := pkg.NotifyLevelFail
|
||||
notifyFilterAttributes := pkg.NotifyFilterAttributesCritical
|
||||
statusThreshold := pkg.MetricsStatusThresholdBoth
|
||||
notifyFilterAttributes := pkg.MetricsStatusFilterAttributesCritical
|
||||
|
||||
//assert
|
||||
require.False(t, ShouldNotify(device, smartAttrs, notifyLevel, notifyFilterAttributes))
|
||||
require.False(t, ShouldNotify(device, smartAttrs, statusThreshold, notifyFilterAttributes))
|
||||
}
|
||||
|
||||
func TestShouldNotify_NotifyFilterAttributesCritical_WithNoFailingCriticalAttrs(t *testing.T) {
|
||||
func TestShouldNotify_MetricsStatusFilterAttributesCritical_WithNoFailingCriticalAttrs(t *testing.T) {
|
||||
t.Parallel()
|
||||
//setup
|
||||
device := models.Device{
|
||||
@@ -132,14 +134,14 @@ func TestShouldNotify_NotifyFilterAttributesCritical_WithNoFailingCriticalAttrs(
|
||||
Status: pkg.AttributeStatusPassed,
|
||||
},
|
||||
}}
|
||||
notifyLevel := pkg.NotifyLevelFail
|
||||
notifyFilterAttributes := pkg.NotifyFilterAttributesCritical
|
||||
statusThreshold := pkg.MetricsStatusThresholdBoth
|
||||
notifyFilterAttributes := pkg.MetricsStatusFilterAttributesCritical
|
||||
|
||||
//assert
|
||||
require.False(t, ShouldNotify(device, smartAttrs, notifyLevel, notifyFilterAttributes))
|
||||
require.False(t, ShouldNotify(device, smartAttrs, statusThreshold, notifyFilterAttributes))
|
||||
}
|
||||
|
||||
func TestShouldNotify_NotifyFilterAttributesCritical_NotifyLevelFailSmart_WithCriticalAttrsFailingScrutiny(t *testing.T) {
|
||||
func TestShouldNotify_MetricsStatusFilterAttributesCritical_MetricsStatusThresholdSmart_WithCriticalAttrsFailingScrutiny(t *testing.T) {
|
||||
t.Parallel()
|
||||
//setup
|
||||
device := models.Device{
|
||||
@@ -153,9 +155,90 @@ func TestShouldNotify_NotifyFilterAttributesCritical_NotifyLevelFailSmart_WithCr
|
||||
Status: pkg.AttributeStatusFailedScrutiny,
|
||||
},
|
||||
}}
|
||||
notifyLevel := pkg.NotifyLevelFailSmart
|
||||
notifyFilterAttributes := pkg.NotifyFilterAttributesCritical
|
||||
statusThreshold := pkg.MetricsStatusThresholdSmart
|
||||
notifyFilterAttributes := pkg.MetricsStatusFilterAttributesCritical
|
||||
|
||||
//assert
|
||||
require.False(t, ShouldNotify(device, smartAttrs, notifyLevel, notifyFilterAttributes))
|
||||
require.False(t, ShouldNotify(device, smartAttrs, statusThreshold, notifyFilterAttributes))
|
||||
}
|
||||
|
||||
func TestNewPayload(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
//setup
|
||||
device := models.Device{
|
||||
SerialNumber: "FAKEWDDJ324KSO",
|
||||
DeviceType: pkg.DeviceProtocolAta,
|
||||
DeviceName: "/dev/sda",
|
||||
DeviceStatus: pkg.DeviceStatusFailedScrutiny,
|
||||
}
|
||||
currentTime := time.Now()
|
||||
//test
|
||||
|
||||
payload := NewPayload(device, false, currentTime)
|
||||
|
||||
//assert
|
||||
require.Equal(t, "Scrutiny SMART error (ScrutinyFailure) detected on device: /dev/sda", payload.Subject)
|
||||
require.Equal(t, fmt.Sprintf(`Scrutiny SMART error notification for device: /dev/sda
|
||||
Failure Type: ScrutinyFailure
|
||||
Device Name: /dev/sda
|
||||
Device Serial: FAKEWDDJ324KSO
|
||||
Device Type: ATA
|
||||
|
||||
Date: %s`, currentTime.Format(time.RFC3339)), payload.Message)
|
||||
}
|
||||
|
||||
func TestNewPayload_TestMode(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
//setup
|
||||
device := models.Device{
|
||||
SerialNumber: "FAKEWDDJ324KSO",
|
||||
DeviceType: pkg.DeviceProtocolAta,
|
||||
DeviceName: "/dev/sda",
|
||||
DeviceStatus: pkg.DeviceStatusFailedScrutiny,
|
||||
}
|
||||
currentTime := time.Now()
|
||||
//test
|
||||
|
||||
payload := NewPayload(device, true, currentTime)
|
||||
|
||||
//assert
|
||||
require.Equal(t, "Scrutiny SMART error (EmailTest) detected on device: /dev/sda", payload.Subject)
|
||||
require.Equal(t, fmt.Sprintf(`TEST NOTIFICATION:
|
||||
Scrutiny SMART error notification for device: /dev/sda
|
||||
Failure Type: EmailTest
|
||||
Device Name: /dev/sda
|
||||
Device Serial: FAKEWDDJ324KSO
|
||||
Device Type: ATA
|
||||
|
||||
Date: %s`, currentTime.Format(time.RFC3339)), payload.Message)
|
||||
}
|
||||
|
||||
func TestNewPayload_WithHostId(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
//setup
|
||||
device := models.Device{
|
||||
SerialNumber: "FAKEWDDJ324KSO",
|
||||
DeviceType: pkg.DeviceProtocolAta,
|
||||
DeviceName: "/dev/sda",
|
||||
DeviceStatus: pkg.DeviceStatusFailedScrutiny,
|
||||
HostId: "custom-host",
|
||||
}
|
||||
currentTime := time.Now()
|
||||
//test
|
||||
|
||||
payload := NewPayload(device, false, currentTime)
|
||||
|
||||
//assert
|
||||
require.Equal(t, "Scrutiny SMART error (ScrutinyFailure) detected on [host]device: [custom-host]/dev/sda", payload.Subject)
|
||||
require.Equal(t, fmt.Sprintf(`Scrutiny SMART error notification for device: /dev/sda
|
||||
Host Id: custom-host
|
||||
Failure Type: ScrutinyFailure
|
||||
Device Name: /dev/sda
|
||||
Device Serial: FAKEWDDJ324KSO
|
||||
Device Type: ATA
|
||||
|
||||
Date: %s`, currentTime.Format(time.RFC3339)), payload.Message)
|
||||
}
|
||||
|
||||
@@ -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.14"
|
||||
const VERSION = "0.7.0"
|
||||
|
||||
@@ -8,7 +8,7 @@ import (
|
||||
)
|
||||
|
||||
func DeleteDevice(c *gin.Context) {
|
||||
logger := c.MustGet("LOGGER").(logrus.FieldLogger)
|
||||
logger := c.MustGet("LOGGER").(*logrus.Entry)
|
||||
deviceRepo := c.MustGet("DEVICE_REPOSITORY").(database.DeviceRepo)
|
||||
|
||||
err := deviceRepo.DeleteDevice(c, c.Param("wwn"))
|
||||
|
||||
@@ -9,7 +9,7 @@ import (
|
||||
)
|
||||
|
||||
func GetDeviceDetails(c *gin.Context) {
|
||||
logger := c.MustGet("LOGGER").(logrus.FieldLogger)
|
||||
logger := c.MustGet("LOGGER").(*logrus.Entry)
|
||||
deviceRepo := c.MustGet("DEVICE_REPOSITORY").(database.DeviceRepo)
|
||||
|
||||
device, err := deviceRepo.GetDeviceDetails(c, c.Param("wwn"))
|
||||
|
||||
@@ -8,7 +8,7 @@ import (
|
||||
)
|
||||
|
||||
func GetDevicesSummary(c *gin.Context) {
|
||||
logger := c.MustGet("LOGGER").(logrus.FieldLogger)
|
||||
logger := c.MustGet("LOGGER").(*logrus.Entry)
|
||||
deviceRepo := c.MustGet("DEVICE_REPOSITORY").(database.DeviceRepo)
|
||||
|
||||
summary, err := deviceRepo.GetSummary(c)
|
||||
@@ -18,6 +18,7 @@ func GetDevicesSummary(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
//this must match DeviceSummaryWrapper (webapp/backend/pkg/models/device_summary.go)
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": true,
|
||||
"data": map[string]interface{}{
|
||||
|
||||
@@ -8,7 +8,7 @@ import (
|
||||
)
|
||||
|
||||
func GetDevicesSummaryTempHistory(c *gin.Context) {
|
||||
logger := c.MustGet("LOGGER").(logrus.FieldLogger)
|
||||
logger := c.MustGet("LOGGER").(*logrus.Entry)
|
||||
deviceRepo := c.MustGet("DEVICE_REPOSITORY").(database.DeviceRepo)
|
||||
|
||||
durationKey, exists := c.GetQuery("duration_key")
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"github.com/analogj/scrutiny/webapp/backend/pkg/database"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/sirupsen/logrus"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func GetSettings(c *gin.Context) {
|
||||
logger := c.MustGet("LOGGER").(*logrus.Entry)
|
||||
deviceRepo := c.MustGet("DEVICE_REPOSITORY").(database.DeviceRepo)
|
||||
|
||||
settings, err := deviceRepo.LoadSettings(c)
|
||||
if err != nil {
|
||||
logger.Errorln("An error occurred while retrieving settings", err)
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"success": false})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": true,
|
||||
"settings": settings,
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"github.com/analogj/scrutiny/webapp/backend/pkg/database"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/sirupsen/logrus"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func HealthCheck(c *gin.Context) {
|
||||
logger := c.MustGet("LOGGER").(*logrus.Entry)
|
||||
deviceRepo := c.MustGet("DEVICE_REPOSITORY").(database.DeviceRepo)
|
||||
logger.Infof("Checking Influxdb & Sqlite health")
|
||||
|
||||
//check sqlite and influxdb health
|
||||
err := deviceRepo.HealthCheck(c)
|
||||
if err != nil {
|
||||
logger.Errorln("An error occurred during healthcheck", err)
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"success": false, "error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
//TODO:
|
||||
// check if the /web folder is populated.
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": true,
|
||||
})
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"github.com/analogj/scrutiny/webapp/backend/pkg/database"
|
||||
"github.com/analogj/scrutiny/webapp/backend/pkg/models"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/samber/lo"
|
||||
"github.com/sirupsen/logrus"
|
||||
"net/http"
|
||||
)
|
||||
@@ -12,7 +13,7 @@ import (
|
||||
// This function is run everytime a collector is about to start a run. It can be used to update device metadata.
|
||||
func RegisterDevices(c *gin.Context) {
|
||||
deviceRepo := c.MustGet("DEVICE_REPOSITORY").(database.DeviceRepo)
|
||||
logger := c.MustGet("LOGGER").(logrus.FieldLogger)
|
||||
logger := c.MustGet("LOGGER").(*logrus.Entry)
|
||||
|
||||
var collectorDeviceWrapper models.DeviceWrapper
|
||||
err := c.BindJSON(&collectorDeviceWrapper)
|
||||
@@ -22,8 +23,13 @@ func RegisterDevices(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
//filter any device with empty wwn (they are invalid)
|
||||
detectedStorageDevices := lo.Filter[models.Device](collectorDeviceWrapper.Data, func(dev models.Device, _ int) bool {
|
||||
return len(dev.WWN) > 0
|
||||
})
|
||||
|
||||
errs := []error{}
|
||||
for _, dev := range collectorDeviceWrapper.Data {
|
||||
for _, dev := range detectedStorageDevices {
|
||||
//insert devices into DB (and update specified columns if device is already registered)
|
||||
// update device fields that may change: (DeviceType, HostID)
|
||||
if err := deviceRepo.RegisterDevice(c, dev); err != nil {
|
||||
@@ -40,7 +46,7 @@ func RegisterDevices(c *gin.Context) {
|
||||
} else {
|
||||
c.JSON(http.StatusOK, models.DeviceWrapper{
|
||||
Success: true,
|
||||
Data: collectorDeviceWrapper.Data,
|
||||
Data: detectedStorageDevices,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"github.com/analogj/scrutiny/webapp/backend/pkg/database"
|
||||
"github.com/analogj/scrutiny/webapp/backend/pkg/models"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/sirupsen/logrus"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func SaveSettings(c *gin.Context) {
|
||||
logger := c.MustGet("LOGGER").(*logrus.Entry)
|
||||
deviceRepo := c.MustGet("DEVICE_REPOSITORY").(database.DeviceRepo)
|
||||
|
||||
var settings models.Settings
|
||||
err := c.BindJSON(&settings)
|
||||
if err != nil {
|
||||
logger.Errorln("Cannot parse updated settings", err)
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"success": false})
|
||||
return
|
||||
}
|
||||
|
||||
err = deviceRepo.SaveSettings(c, settings)
|
||||
if err != nil {
|
||||
logger.Errorln("An error occurred while saving settings", err)
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"success": false})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": true,
|
||||
"settings": settings,
|
||||
})
|
||||
}
|
||||
@@ -13,7 +13,7 @@ import (
|
||||
// Send test notification
|
||||
func SendTestNotification(c *gin.Context) {
|
||||
appConfig := c.MustGet("CONFIG").(config.Interface)
|
||||
logger := c.MustGet("LOGGER").(logrus.FieldLogger)
|
||||
logger := c.MustGet("LOGGER").(*logrus.Entry)
|
||||
|
||||
testNotify := notify.New(
|
||||
logger,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/analogj/scrutiny/webapp/backend/pkg"
|
||||
"github.com/analogj/scrutiny/webapp/backend/pkg/config"
|
||||
"github.com/analogj/scrutiny/webapp/backend/pkg/database"
|
||||
@@ -13,13 +14,17 @@ import (
|
||||
|
||||
func UploadDeviceMetrics(c *gin.Context) {
|
||||
//db := c.MustGet("DB").(*gorm.DB)
|
||||
logger := c.MustGet("LOGGER").(logrus.FieldLogger)
|
||||
logger := c.MustGet("LOGGER").(*logrus.Entry)
|
||||
appConfig := c.MustGet("CONFIG").(config.Interface)
|
||||
//influxWriteDb := c.MustGet("INFLUXDB_WRITE").(*api.WriteAPIBlocking)
|
||||
deviceRepo := c.MustGet("DEVICE_REPOSITORY").(database.DeviceRepo)
|
||||
|
||||
//appConfig := c.MustGet("CONFIG").(config.Interface)
|
||||
|
||||
if c.Param("wwn") == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"success": false})
|
||||
}
|
||||
|
||||
var collectorSmartData collector.SmartInfo
|
||||
err := c.BindJSON(&collectorSmartData)
|
||||
if err != nil {
|
||||
@@ -63,7 +68,12 @@ func UploadDeviceMetrics(c *gin.Context) {
|
||||
}
|
||||
|
||||
//check for error
|
||||
if notify.ShouldNotify(updatedDevice, smartData, appConfig.GetString("notify.level"), appConfig.GetString("notify.filter_attributes")) {
|
||||
if notify.ShouldNotify(
|
||||
updatedDevice,
|
||||
smartData,
|
||||
pkg.MetricsStatusThreshold(appConfig.GetInt(fmt.Sprintf("%s.metrics.status_threshold", config.DB_USER_SETTINGS_SUBKEY))),
|
||||
pkg.MetricsStatusFilterAttributes(appConfig.GetInt(fmt.Sprintf("%s.metrics.status_filter_attributes", config.DB_USER_SETTINGS_SUBKEY))),
|
||||
) {
|
||||
//send notifications
|
||||
|
||||
liveNotify := notify.New(
|
||||
|
||||
@@ -28,11 +28,11 @@ import (
|
||||
var timeFormat = "02/Jan/2006:15:04:05 -0700"
|
||||
|
||||
// Logger is the logrus logger handler
|
||||
func LoggerMiddleware(logger logrus.FieldLogger) gin.HandlerFunc {
|
||||
func LoggerMiddleware(logger *logrus.Entry) gin.HandlerFunc {
|
||||
|
||||
hostname, err := os.Hostname()
|
||||
if err != nil {
|
||||
hostname = "unknow"
|
||||
hostname = "unknown"
|
||||
}
|
||||
|
||||
return func(c *gin.Context) {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/analogj/scrutiny/webapp/backend/pkg/config"
|
||||
"github.com/analogj/scrutiny/webapp/backend/pkg/database"
|
||||
"github.com/gin-gonic/gin"
|
||||
@@ -14,6 +15,14 @@ func RepositoryMiddleware(appConfig config.Interface, globalLogger logrus.FieldL
|
||||
panic(err)
|
||||
}
|
||||
|
||||
// ensure the settings have been loaded into the app config during startup.
|
||||
_, err = deviceRepo.LoadSettings(context.Background())
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
//settings.UpdateSettingEntries()
|
||||
|
||||
//TODO: determine where we can call defer deviceRepo.Close()
|
||||
return func(c *gin.Context) {
|
||||
c.Set("DEVICE_REPOSITORY", deviceRepo)
|
||||
|
||||
@@ -9,18 +9,17 @@ import (
|
||||
"github.com/analogj/scrutiny/webapp/backend/pkg/web/middleware"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/sirupsen/logrus"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type AppEngine struct {
|
||||
Config config.Interface
|
||||
Logger *logrus.Entry
|
||||
}
|
||||
|
||||
func (ae *AppEngine) Setup(logger logrus.FieldLogger) *gin.Engine {
|
||||
func (ae *AppEngine) Setup(logger *logrus.Entry) *gin.Engine {
|
||||
r := gin.New()
|
||||
|
||||
r.Use(middleware.LoggerMiddleware(logger))
|
||||
@@ -35,11 +34,7 @@ func (ae *AppEngine) Setup(logger logrus.FieldLogger) *gin.Engine {
|
||||
{
|
||||
api := base.Group("/api")
|
||||
{
|
||||
api.GET("/health", func(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": true,
|
||||
})
|
||||
})
|
||||
api.GET("/health", handler.HealthCheck)
|
||||
api.POST("/health/notify", handler.SendTestNotification) //check if notifications are configured correctly
|
||||
|
||||
api.POST("/devices/register", handler.RegisterDevices) //used by Collector to register new devices and retrieve filtered list
|
||||
@@ -50,6 +45,8 @@ func (ae *AppEngine) Setup(logger logrus.FieldLogger) *gin.Engine {
|
||||
api.GET("/device/:wwn/details", handler.GetDeviceDetails) //used by Details
|
||||
api.DELETE("/device/:wwn", handler.DeleteDevice) //used by UI to delete device
|
||||
|
||||
api.GET("/settings", handler.GetSettings) //used to get settings
|
||||
api.POST("/settings", handler.SaveSettings) //used to save settings
|
||||
}
|
||||
}
|
||||
|
||||
@@ -75,26 +72,6 @@ func (ae *AppEngine) Start() error {
|
||||
gin.SetMode(gin.DebugMode)
|
||||
}
|
||||
|
||||
logger := logrus.New()
|
||||
//set default log level
|
||||
logLevel, err := logrus.ParseLevel(ae.Config.GetString("log.level"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
logger.SetLevel(logLevel)
|
||||
//set the log file if present
|
||||
if len(ae.Config.GetString("log.file")) != 0 {
|
||||
logFile, err := os.OpenFile(ae.Config.GetString("log.file"), os.O_CREATE|os.O_WRONLY, 0644)
|
||||
defer logFile.Close()
|
||||
if err != nil {
|
||||
logrus.Errorf("Failed to open log file %s for output: %s", ae.Config.GetString("log.file"), err)
|
||||
return err
|
||||
}
|
||||
|
||||
//configure the logrus default
|
||||
logger.SetOutput(io.MultiWriter(os.Stderr, logFile))
|
||||
}
|
||||
|
||||
//check if the database parent directory exists, fail here rather than in a handler.
|
||||
if !utils.FileExists(filepath.Dir(ae.Config.GetString("web.database.location"))) {
|
||||
return errors.ConfigValidationError(fmt.Sprintf(
|
||||
@@ -102,7 +79,7 @@ func (ae *AppEngine) Start() error {
|
||||
filepath.Dir(ae.Config.GetString("web.database.location"))))
|
||||
}
|
||||
|
||||
r := ae.Setup(logger)
|
||||
r := ae.Setup(ae.Logger)
|
||||
|
||||
return r.Run(fmt.Sprintf("%s:%s", ae.Config.GetString("web.listen.host"), ae.Config.GetString("web.listen.port")))
|
||||
}
|
||||
|
||||
@@ -3,7 +3,9 @@ package web_test
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/analogj/scrutiny/webapp/backend/pkg"
|
||||
"github.com/analogj/scrutiny/webapp/backend/pkg/config"
|
||||
mock_config "github.com/analogj/scrutiny/webapp/backend/pkg/config/mock"
|
||||
"github.com/analogj/scrutiny/webapp/backend/pkg/models"
|
||||
"github.com/analogj/scrutiny/webapp/backend/pkg/models/collector"
|
||||
@@ -89,6 +91,8 @@ func (suite *ServerTestSuite) TestHealthRoute() {
|
||||
mockCtrl := gomock.NewController(suite.T())
|
||||
defer mockCtrl.Finish()
|
||||
fakeConfig := mock_config.NewMockInterface(mockCtrl)
|
||||
fakeConfig.EXPECT().SetDefault(gomock.Any(), gomock.Any()).AnyTimes()
|
||||
fakeConfig.EXPECT().UnmarshalKey(gomock.Any(), gomock.Any()).AnyTimes().Return(nil)
|
||||
fakeConfig.EXPECT().GetString("web.database.location").Return(path.Join(parentPath, "scrutiny_test.db")).AnyTimes()
|
||||
fakeConfig.EXPECT().GetString("web.src.frontend.path").Return(parentPath).AnyTimes()
|
||||
fakeConfig.EXPECT().GetString("web.listen.basepath").Return(suite.Basepath).AnyTimes()
|
||||
@@ -99,6 +103,7 @@ func (suite *ServerTestSuite) TestHealthRoute() {
|
||||
fakeConfig.EXPECT().GetString("web.influxdb.token").Return("my-super-secret-auth-token").AnyTimes()
|
||||
fakeConfig.EXPECT().GetString("web.influxdb.org").Return("scrutiny").AnyTimes()
|
||||
fakeConfig.EXPECT().GetString("web.influxdb.bucket").Return("metrics").AnyTimes()
|
||||
fakeConfig.EXPECT().GetBool("web.influxdb.tls.insecure_skip_verify").Return(false).AnyTimes()
|
||||
fakeConfig.EXPECT().GetBool("web.influxdb.retention_policy").Return(false).AnyTimes()
|
||||
if _, isGithubActions := os.LookupEnv("GITHUB_ACTIONS"); isGithubActions {
|
||||
// when running test suite in github actions, we run an influxdb service as a sidecar.
|
||||
@@ -111,7 +116,7 @@ func (suite *ServerTestSuite) TestHealthRoute() {
|
||||
Config: fakeConfig,
|
||||
}
|
||||
|
||||
router := ae.Setup(logrus.New())
|
||||
router := ae.Setup(logrus.WithField("test", suite.T().Name()))
|
||||
|
||||
//test
|
||||
w := httptest.NewRecorder()
|
||||
@@ -130,6 +135,8 @@ func (suite *ServerTestSuite) TestRegisterDevicesRoute() {
|
||||
mockCtrl := gomock.NewController(suite.T())
|
||||
defer mockCtrl.Finish()
|
||||
fakeConfig := mock_config.NewMockInterface(mockCtrl)
|
||||
fakeConfig.EXPECT().SetDefault(gomock.Any(), gomock.Any()).AnyTimes()
|
||||
fakeConfig.EXPECT().UnmarshalKey(gomock.Any(), gomock.Any()).AnyTimes().Return(nil)
|
||||
fakeConfig.EXPECT().GetString("web.database.location").Return(path.Join(parentPath, "scrutiny_test.db")).AnyTimes()
|
||||
fakeConfig.EXPECT().GetString("web.src.frontend.path").Return(parentPath).AnyTimes()
|
||||
fakeConfig.EXPECT().GetString("web.listen.basepath").Return(suite.Basepath).AnyTimes()
|
||||
@@ -139,6 +146,7 @@ func (suite *ServerTestSuite) TestRegisterDevicesRoute() {
|
||||
fakeConfig.EXPECT().GetString("web.influxdb.token").Return("my-super-secret-auth-token").AnyTimes()
|
||||
fakeConfig.EXPECT().GetString("web.influxdb.org").Return("scrutiny").AnyTimes()
|
||||
fakeConfig.EXPECT().GetString("web.influxdb.bucket").Return("metrics").AnyTimes()
|
||||
fakeConfig.EXPECT().GetBool("web.influxdb.tls.insecure_skip_verify").Return(false).AnyTimes()
|
||||
fakeConfig.EXPECT().GetBool("web.influxdb.retention_policy").Return(false).AnyTimes()
|
||||
if _, isGithubActions := os.LookupEnv("GITHUB_ACTIONS"); isGithubActions {
|
||||
// when running test suite in github actions, we run an influxdb service as a sidecar.
|
||||
@@ -150,7 +158,7 @@ func (suite *ServerTestSuite) TestRegisterDevicesRoute() {
|
||||
ae := web.AppEngine{
|
||||
Config: fakeConfig,
|
||||
}
|
||||
router := ae.Setup(logrus.New())
|
||||
router := ae.Setup(logrus.WithField("test", suite.T().Name()))
|
||||
file, err := os.Open("testdata/register-devices-req.json")
|
||||
require.NoError(suite.T(), err)
|
||||
|
||||
@@ -170,6 +178,8 @@ func (suite *ServerTestSuite) TestUploadDeviceMetricsRoute() {
|
||||
mockCtrl := gomock.NewController(suite.T())
|
||||
defer mockCtrl.Finish()
|
||||
fakeConfig := mock_config.NewMockInterface(mockCtrl)
|
||||
fakeConfig.EXPECT().SetDefault(gomock.Any(), gomock.Any()).AnyTimes()
|
||||
fakeConfig.EXPECT().UnmarshalKey(gomock.Any(), gomock.Any()).AnyTimes().Return(nil)
|
||||
fakeConfig.EXPECT().GetString("web.database.location").AnyTimes().Return(path.Join(parentPath, "scrutiny_test.db"))
|
||||
fakeConfig.EXPECT().GetString("web.src.frontend.path").AnyTimes().Return(parentPath)
|
||||
fakeConfig.EXPECT().GetString("web.listen.basepath").Return(suite.Basepath).AnyTimes()
|
||||
@@ -179,6 +189,7 @@ func (suite *ServerTestSuite) TestUploadDeviceMetricsRoute() {
|
||||
fakeConfig.EXPECT().GetString("web.influxdb.token").Return("my-super-secret-auth-token").AnyTimes()
|
||||
fakeConfig.EXPECT().GetString("web.influxdb.org").Return("scrutiny").AnyTimes()
|
||||
fakeConfig.EXPECT().GetString("web.influxdb.bucket").Return("metrics").AnyTimes()
|
||||
fakeConfig.EXPECT().GetBool("web.influxdb.tls.insecure_skip_verify").Return(false).AnyTimes()
|
||||
fakeConfig.EXPECT().GetBool("web.influxdb.retention_policy").Return(false).AnyTimes()
|
||||
if _, isGithubActions := os.LookupEnv("GITHUB_ACTIONS"); isGithubActions {
|
||||
// when running test suite in github actions, we run an influxdb service as a sidecar.
|
||||
@@ -186,13 +197,14 @@ func (suite *ServerTestSuite) TestUploadDeviceMetricsRoute() {
|
||||
} else {
|
||||
fakeConfig.EXPECT().GetString("web.influxdb.host").Return("localhost").AnyTimes()
|
||||
}
|
||||
fakeConfig.EXPECT().GetString("notify.level").AnyTimes().Return(pkg.NotifyLevelFail)
|
||||
fakeConfig.EXPECT().GetString("notify.filter_attributes").AnyTimes().Return(pkg.NotifyFilterAttributesAll)
|
||||
fakeConfig.EXPECT().GetInt(fmt.Sprintf("%s.metrics.notify_level", config.DB_USER_SETTINGS_SUBKEY)).AnyTimes().Return(int(pkg.MetricsNotifyLevelFail))
|
||||
fakeConfig.EXPECT().GetInt(fmt.Sprintf("%s.metrics.status_filter_attributes", config.DB_USER_SETTINGS_SUBKEY)).AnyTimes().Return(int(pkg.MetricsStatusFilterAttributesAll))
|
||||
fakeConfig.EXPECT().GetInt(fmt.Sprintf("%s.metrics.status_threshold", config.DB_USER_SETTINGS_SUBKEY)).AnyTimes().Return(int(pkg.MetricsStatusThresholdBoth))
|
||||
|
||||
ae := web.AppEngine{
|
||||
Config: fakeConfig,
|
||||
}
|
||||
router := ae.Setup(logrus.New())
|
||||
router := ae.Setup(logrus.WithField("test", suite.T().Name()))
|
||||
devicesfile, err := os.Open("testdata/register-devices-single-req.json")
|
||||
require.NoError(suite.T(), err)
|
||||
|
||||
@@ -219,10 +231,13 @@ func (suite *ServerTestSuite) TestPopulateMultiple() {
|
||||
mockCtrl := gomock.NewController(suite.T())
|
||||
defer mockCtrl.Finish()
|
||||
fakeConfig := mock_config.NewMockInterface(mockCtrl)
|
||||
fakeConfig.EXPECT().SetDefault(gomock.Any(), gomock.Any()).AnyTimes()
|
||||
fakeConfig.EXPECT().UnmarshalKey(gomock.Any(), gomock.Any()).AnyTimes().Return(nil)
|
||||
//fakeConfig.EXPECT().GetString("web.database.location").AnyTimes().Return("testdata/scrutiny_test.db")
|
||||
fakeConfig.EXPECT().GetStringSlice("notify.urls").Return([]string{}).AnyTimes()
|
||||
fakeConfig.EXPECT().GetString("notify.level").AnyTimes().Return(pkg.NotifyLevelFail)
|
||||
fakeConfig.EXPECT().GetString("notify.filter_attributes").AnyTimes().Return(pkg.NotifyFilterAttributesAll)
|
||||
fakeConfig.EXPECT().GetInt(fmt.Sprintf("%s.metrics.notify_level", config.DB_USER_SETTINGS_SUBKEY)).AnyTimes().Return(int(pkg.MetricsNotifyLevelFail))
|
||||
fakeConfig.EXPECT().GetInt(fmt.Sprintf("%s.metrics.status_filter_attributes", config.DB_USER_SETTINGS_SUBKEY)).AnyTimes().Return(int(pkg.MetricsStatusFilterAttributesAll))
|
||||
fakeConfig.EXPECT().GetInt(fmt.Sprintf("%s.metrics.status_threshold", config.DB_USER_SETTINGS_SUBKEY)).AnyTimes().Return(int(pkg.MetricsStatusThresholdBoth))
|
||||
fakeConfig.EXPECT().GetString("web.database.location").AnyTimes().Return(path.Join(parentPath, "scrutiny_test.db"))
|
||||
fakeConfig.EXPECT().GetString("web.src.frontend.path").AnyTimes().Return(parentPath)
|
||||
fakeConfig.EXPECT().GetString("web.listen.basepath").Return(suite.Basepath).AnyTimes()
|
||||
@@ -232,6 +247,7 @@ func (suite *ServerTestSuite) TestPopulateMultiple() {
|
||||
fakeConfig.EXPECT().GetString("web.influxdb.token").Return("my-super-secret-auth-token").AnyTimes()
|
||||
fakeConfig.EXPECT().GetString("web.influxdb.org").Return("scrutiny").AnyTimes()
|
||||
fakeConfig.EXPECT().GetString("web.influxdb.bucket").Return("metrics").AnyTimes()
|
||||
fakeConfig.EXPECT().GetBool("web.influxdb.tls.insecure_skip_verify").Return(false).AnyTimes()
|
||||
fakeConfig.EXPECT().GetBool("web.influxdb.retention_policy").Return(false).AnyTimes()
|
||||
if _, isGithubActions := os.LookupEnv("GITHUB_ACTIONS"); isGithubActions {
|
||||
// when running test suite in github actions, we run an influxdb service as a sidecar.
|
||||
@@ -243,7 +259,7 @@ func (suite *ServerTestSuite) TestPopulateMultiple() {
|
||||
ae := web.AppEngine{
|
||||
Config: fakeConfig,
|
||||
}
|
||||
router := ae.Setup(logrus.New())
|
||||
router := ae.Setup(logrus.WithField("test", suite.T().Name()))
|
||||
devicesfile, err := os.Open("testdata/register-devices-req.json")
|
||||
require.NoError(suite.T(), err)
|
||||
|
||||
@@ -319,6 +335,8 @@ func (suite *ServerTestSuite) TestSendTestNotificationRoute_WebhookFailure() {
|
||||
mockCtrl := gomock.NewController(suite.T())
|
||||
defer mockCtrl.Finish()
|
||||
fakeConfig := mock_config.NewMockInterface(mockCtrl)
|
||||
fakeConfig.EXPECT().SetDefault(gomock.Any(), gomock.Any()).AnyTimes()
|
||||
fakeConfig.EXPECT().UnmarshalKey(gomock.Any(), gomock.Any()).AnyTimes().Return(nil)
|
||||
fakeConfig.EXPECT().GetString("web.database.location").AnyTimes().Return(path.Join(parentPath, "scrutiny_test.db"))
|
||||
fakeConfig.EXPECT().GetString("web.src.frontend.path").AnyTimes().Return(parentPath)
|
||||
fakeConfig.EXPECT().GetString("web.listen.basepath").Return(suite.Basepath).AnyTimes()
|
||||
@@ -328,10 +346,12 @@ func (suite *ServerTestSuite) TestSendTestNotificationRoute_WebhookFailure() {
|
||||
fakeConfig.EXPECT().GetString("web.influxdb.token").Return("my-super-secret-auth-token").AnyTimes()
|
||||
fakeConfig.EXPECT().GetString("web.influxdb.org").Return("scrutiny").AnyTimes()
|
||||
fakeConfig.EXPECT().GetString("web.influxdb.bucket").Return("metrics").AnyTimes()
|
||||
fakeConfig.EXPECT().GetBool("web.influxdb.tls.insecure_skip_verify").Return(false).AnyTimes()
|
||||
fakeConfig.EXPECT().GetBool("web.influxdb.retention_policy").Return(false).AnyTimes()
|
||||
fakeConfig.EXPECT().GetStringSlice("notify.urls").AnyTimes().Return([]string{"https://unroutable.domain.example.asdfghj"})
|
||||
fakeConfig.EXPECT().GetString("notify.level").AnyTimes().Return(pkg.NotifyLevelFail)
|
||||
fakeConfig.EXPECT().GetString("notify.filter_attributes").AnyTimes().Return(pkg.NotifyFilterAttributesAll)
|
||||
fakeConfig.EXPECT().GetInt(fmt.Sprintf("%s.metrics.notify_level", config.DB_USER_SETTINGS_SUBKEY)).AnyTimes().Return(int(pkg.MetricsNotifyLevelFail))
|
||||
fakeConfig.EXPECT().GetInt(fmt.Sprintf("%s.metrics.status_filter_attributes", config.DB_USER_SETTINGS_SUBKEY)).AnyTimes().Return(int(pkg.MetricsStatusFilterAttributesAll))
|
||||
fakeConfig.EXPECT().GetInt(fmt.Sprintf("%s.metrics.status_threshold", config.DB_USER_SETTINGS_SUBKEY)).AnyTimes().Return(int(pkg.MetricsStatusThresholdBoth))
|
||||
|
||||
if _, isGithubActions := os.LookupEnv("GITHUB_ACTIONS"); isGithubActions {
|
||||
// when running test suite in github actions, we run an influxdb service as a sidecar.
|
||||
@@ -343,7 +363,7 @@ func (suite *ServerTestSuite) TestSendTestNotificationRoute_WebhookFailure() {
|
||||
ae := web.AppEngine{
|
||||
Config: fakeConfig,
|
||||
}
|
||||
router := ae.Setup(logrus.New())
|
||||
router := ae.Setup(logrus.WithField("test", suite.T().Name()))
|
||||
|
||||
//test
|
||||
wr := httptest.NewRecorder()
|
||||
@@ -361,6 +381,8 @@ func (suite *ServerTestSuite) TestSendTestNotificationRoute_ScriptFailure() {
|
||||
mockCtrl := gomock.NewController(suite.T())
|
||||
defer mockCtrl.Finish()
|
||||
fakeConfig := mock_config.NewMockInterface(mockCtrl)
|
||||
fakeConfig.EXPECT().SetDefault(gomock.Any(), gomock.Any()).AnyTimes()
|
||||
fakeConfig.EXPECT().UnmarshalKey(gomock.Any(), gomock.Any()).AnyTimes().Return(nil)
|
||||
fakeConfig.EXPECT().GetString("web.database.location").AnyTimes().Return(path.Join(parentPath, "scrutiny_test.db"))
|
||||
fakeConfig.EXPECT().GetString("web.src.frontend.path").AnyTimes().Return(parentPath)
|
||||
fakeConfig.EXPECT().GetString("web.listen.basepath").Return(suite.Basepath).AnyTimes()
|
||||
@@ -370,10 +392,12 @@ func (suite *ServerTestSuite) TestSendTestNotificationRoute_ScriptFailure() {
|
||||
fakeConfig.EXPECT().GetString("web.influxdb.token").Return("my-super-secret-auth-token").AnyTimes()
|
||||
fakeConfig.EXPECT().GetString("web.influxdb.org").Return("scrutiny").AnyTimes()
|
||||
fakeConfig.EXPECT().GetString("web.influxdb.bucket").Return("metrics").AnyTimes()
|
||||
fakeConfig.EXPECT().GetBool("web.influxdb.tls.insecure_skip_verify").Return(false).AnyTimes()
|
||||
fakeConfig.EXPECT().GetBool("web.influxdb.retention_policy").Return(false).AnyTimes()
|
||||
fakeConfig.EXPECT().GetStringSlice("notify.urls").AnyTimes().Return([]string{"script:///missing/path/on/disk"})
|
||||
fakeConfig.EXPECT().GetString("notify.level").AnyTimes().Return(pkg.NotifyLevelFail)
|
||||
fakeConfig.EXPECT().GetString("notify.filter_attributes").AnyTimes().Return(pkg.NotifyFilterAttributesAll)
|
||||
fakeConfig.EXPECT().GetInt(fmt.Sprintf("%s.metrics.notify_level", config.DB_USER_SETTINGS_SUBKEY)).AnyTimes().Return(int(pkg.MetricsNotifyLevelFail))
|
||||
fakeConfig.EXPECT().GetInt(fmt.Sprintf("%s.metrics.status_filter_attributes", config.DB_USER_SETTINGS_SUBKEY)).AnyTimes().Return(int(pkg.MetricsStatusFilterAttributesAll))
|
||||
fakeConfig.EXPECT().GetInt(fmt.Sprintf("%s.metrics.status_threshold", config.DB_USER_SETTINGS_SUBKEY)).AnyTimes().Return(int(pkg.MetricsStatusThresholdBoth))
|
||||
|
||||
if _, isGithubActions := os.LookupEnv("GITHUB_ACTIONS"); isGithubActions {
|
||||
// when running test suite in github actions, we run an influxdb service as a sidecar.
|
||||
@@ -385,7 +409,7 @@ func (suite *ServerTestSuite) TestSendTestNotificationRoute_ScriptFailure() {
|
||||
ae := web.AppEngine{
|
||||
Config: fakeConfig,
|
||||
}
|
||||
router := ae.Setup(logrus.New())
|
||||
router := ae.Setup(logrus.WithField("test", suite.T().Name()))
|
||||
|
||||
//test
|
||||
wr := httptest.NewRecorder()
|
||||
@@ -403,6 +427,8 @@ func (suite *ServerTestSuite) TestSendTestNotificationRoute_ScriptSuccess() {
|
||||
mockCtrl := gomock.NewController(suite.T())
|
||||
defer mockCtrl.Finish()
|
||||
fakeConfig := mock_config.NewMockInterface(mockCtrl)
|
||||
fakeConfig.EXPECT().SetDefault(gomock.Any(), gomock.Any()).AnyTimes()
|
||||
fakeConfig.EXPECT().UnmarshalKey(gomock.Any(), gomock.Any()).AnyTimes().Return(nil)
|
||||
fakeConfig.EXPECT().GetString("web.database.location").AnyTimes().Return(path.Join(parentPath, "scrutiny_test.db"))
|
||||
fakeConfig.EXPECT().GetString("web.src.frontend.path").AnyTimes().Return(parentPath)
|
||||
fakeConfig.EXPECT().GetString("web.listen.basepath").Return(suite.Basepath).AnyTimes()
|
||||
@@ -412,10 +438,12 @@ func (suite *ServerTestSuite) TestSendTestNotificationRoute_ScriptSuccess() {
|
||||
fakeConfig.EXPECT().GetString("web.influxdb.token").Return("my-super-secret-auth-token").AnyTimes()
|
||||
fakeConfig.EXPECT().GetString("web.influxdb.org").Return("scrutiny").AnyTimes()
|
||||
fakeConfig.EXPECT().GetString("web.influxdb.bucket").Return("metrics").AnyTimes()
|
||||
fakeConfig.EXPECT().GetBool("web.influxdb.tls.insecure_skip_verify").Return(false).AnyTimes()
|
||||
fakeConfig.EXPECT().GetBool("web.influxdb.retention_policy").Return(false).AnyTimes()
|
||||
fakeConfig.EXPECT().GetStringSlice("notify.urls").AnyTimes().Return([]string{"script:///usr/bin/env"})
|
||||
fakeConfig.EXPECT().GetString("notify.level").AnyTimes().Return(pkg.NotifyLevelFail)
|
||||
fakeConfig.EXPECT().GetString("notify.filter_attributes").AnyTimes().Return(pkg.NotifyFilterAttributesAll)
|
||||
fakeConfig.EXPECT().GetInt(fmt.Sprintf("%s.metrics.notify_level", config.DB_USER_SETTINGS_SUBKEY)).AnyTimes().Return(int(pkg.MetricsNotifyLevelFail))
|
||||
fakeConfig.EXPECT().GetInt(fmt.Sprintf("%s.metrics.status_filter_attributes", config.DB_USER_SETTINGS_SUBKEY)).AnyTimes().Return(int(pkg.MetricsStatusFilterAttributesAll))
|
||||
fakeConfig.EXPECT().GetInt(fmt.Sprintf("%s.metrics.status_threshold", config.DB_USER_SETTINGS_SUBKEY)).AnyTimes().Return(int(pkg.MetricsStatusThresholdBoth))
|
||||
|
||||
if _, isGithubActions := os.LookupEnv("GITHUB_ACTIONS"); isGithubActions {
|
||||
// when running test suite in github actions, we run an influxdb service as a sidecar.
|
||||
@@ -427,7 +455,7 @@ func (suite *ServerTestSuite) TestSendTestNotificationRoute_ScriptSuccess() {
|
||||
ae := web.AppEngine{
|
||||
Config: fakeConfig,
|
||||
}
|
||||
router := ae.Setup(logrus.New())
|
||||
router := ae.Setup(logrus.WithField("test", suite.T().Name()))
|
||||
|
||||
//test
|
||||
wr := httptest.NewRecorder()
|
||||
@@ -445,6 +473,8 @@ func (suite *ServerTestSuite) TestSendTestNotificationRoute_ShoutrrrFailure() {
|
||||
mockCtrl := gomock.NewController(suite.T())
|
||||
defer mockCtrl.Finish()
|
||||
fakeConfig := mock_config.NewMockInterface(mockCtrl)
|
||||
fakeConfig.EXPECT().SetDefault(gomock.Any(), gomock.Any()).AnyTimes()
|
||||
fakeConfig.EXPECT().UnmarshalKey(gomock.Any(), gomock.Any()).AnyTimes().Return(nil)
|
||||
fakeConfig.EXPECT().GetString("web.database.location").AnyTimes().Return(path.Join(parentPath, "scrutiny_test.db"))
|
||||
fakeConfig.EXPECT().GetString("web.src.frontend.path").AnyTimes().Return(parentPath)
|
||||
fakeConfig.EXPECT().GetString("web.listen.basepath").Return(suite.Basepath).AnyTimes()
|
||||
@@ -454,10 +484,12 @@ func (suite *ServerTestSuite) TestSendTestNotificationRoute_ShoutrrrFailure() {
|
||||
fakeConfig.EXPECT().GetString("web.influxdb.token").Return("my-super-secret-auth-token").AnyTimes()
|
||||
fakeConfig.EXPECT().GetString("web.influxdb.org").Return("scrutiny").AnyTimes()
|
||||
fakeConfig.EXPECT().GetString("web.influxdb.bucket").Return("metrics").AnyTimes()
|
||||
fakeConfig.EXPECT().GetBool("web.influxdb.tls.insecure_skip_verify").Return(false).AnyTimes()
|
||||
fakeConfig.EXPECT().GetBool("web.influxdb.retention_policy").Return(false).AnyTimes()
|
||||
fakeConfig.EXPECT().GetStringSlice("notify.urls").AnyTimes().Return([]string{"discord://invalidtoken@channel"})
|
||||
fakeConfig.EXPECT().GetString("notify.level").AnyTimes().Return(pkg.NotifyLevelFail)
|
||||
fakeConfig.EXPECT().GetString("notify.filter_attributes").AnyTimes().Return(pkg.NotifyFilterAttributesAll)
|
||||
fakeConfig.EXPECT().GetInt(fmt.Sprintf("%s.metrics.notify_level", config.DB_USER_SETTINGS_SUBKEY)).AnyTimes().Return(int(pkg.MetricsNotifyLevelFail))
|
||||
fakeConfig.EXPECT().GetInt(fmt.Sprintf("%s.metrics.status_filter_attributes", config.DB_USER_SETTINGS_SUBKEY)).AnyTimes().Return(int(pkg.MetricsStatusFilterAttributesAll))
|
||||
fakeConfig.EXPECT().GetInt(fmt.Sprintf("%s.metrics.status_threshold", config.DB_USER_SETTINGS_SUBKEY)).AnyTimes().Return(int(pkg.MetricsStatusThresholdBoth))
|
||||
|
||||
if _, isGithubActions := os.LookupEnv("GITHUB_ACTIONS"); isGithubActions {
|
||||
// when running test suite in github actions, we run an influxdb service as a sidecar.
|
||||
@@ -468,7 +500,7 @@ func (suite *ServerTestSuite) TestSendTestNotificationRoute_ShoutrrrFailure() {
|
||||
ae := web.AppEngine{
|
||||
Config: fakeConfig,
|
||||
}
|
||||
router := ae.Setup(logrus.New())
|
||||
router := ae.Setup(logrus.WithField("test", suite.T().Name()))
|
||||
|
||||
//test
|
||||
wr := httptest.NewRecorder()
|
||||
@@ -486,6 +518,8 @@ func (suite *ServerTestSuite) TestGetDevicesSummaryRoute_Nvme() {
|
||||
mockCtrl := gomock.NewController(suite.T())
|
||||
defer mockCtrl.Finish()
|
||||
fakeConfig := mock_config.NewMockInterface(mockCtrl)
|
||||
fakeConfig.EXPECT().SetDefault(gomock.Any(), gomock.Any()).AnyTimes()
|
||||
fakeConfig.EXPECT().UnmarshalKey(gomock.Any(), gomock.Any()).AnyTimes().Return(nil)
|
||||
fakeConfig.EXPECT().GetString("web.database.location").AnyTimes().Return(path.Join(parentPath, "scrutiny_test.db"))
|
||||
fakeConfig.EXPECT().GetString("web.src.frontend.path").AnyTimes().Return(parentPath)
|
||||
fakeConfig.EXPECT().GetString("web.listen.basepath").Return(suite.Basepath).AnyTimes()
|
||||
@@ -495,10 +529,13 @@ func (suite *ServerTestSuite) TestGetDevicesSummaryRoute_Nvme() {
|
||||
fakeConfig.EXPECT().GetString("web.influxdb.token").Return("my-super-secret-auth-token").AnyTimes()
|
||||
fakeConfig.EXPECT().GetString("web.influxdb.org").Return("scrutiny").AnyTimes()
|
||||
fakeConfig.EXPECT().GetString("web.influxdb.bucket").Return("metrics").AnyTimes()
|
||||
fakeConfig.EXPECT().GetBool("web.influxdb.tls.insecure_skip_verify").Return(false).AnyTimes()
|
||||
fakeConfig.EXPECT().GetBool("web.influxdb.retention_policy").Return(false).AnyTimes()
|
||||
fakeConfig.EXPECT().GetStringSlice("notify.urls").AnyTimes().Return([]string{})
|
||||
fakeConfig.EXPECT().GetString("notify.level").AnyTimes().Return(pkg.NotifyLevelFail)
|
||||
fakeConfig.EXPECT().GetString("notify.filter_attributes").AnyTimes().Return(pkg.NotifyFilterAttributesAll)
|
||||
fakeConfig.EXPECT().GetInt(fmt.Sprintf("%s.metrics.notify_level", config.DB_USER_SETTINGS_SUBKEY)).AnyTimes().Return(int(pkg.MetricsNotifyLevelFail))
|
||||
fakeConfig.EXPECT().GetInt(fmt.Sprintf("%s.metrics.status_filter_attributes", config.DB_USER_SETTINGS_SUBKEY)).AnyTimes().Return(int(pkg.MetricsStatusFilterAttributesAll))
|
||||
fakeConfig.EXPECT().GetInt(fmt.Sprintf("%s.metrics.status_threshold", config.DB_USER_SETTINGS_SUBKEY)).AnyTimes().Return(int(pkg.MetricsStatusThresholdBoth))
|
||||
|
||||
if _, isGithubActions := os.LookupEnv("GITHUB_ACTIONS"); isGithubActions {
|
||||
// when running test suite in github actions, we run an influxdb service as a sidecar.
|
||||
fakeConfig.EXPECT().GetString("web.influxdb.host").Return("influxdb").AnyTimes()
|
||||
@@ -509,7 +546,7 @@ func (suite *ServerTestSuite) TestGetDevicesSummaryRoute_Nvme() {
|
||||
ae := web.AppEngine{
|
||||
Config: fakeConfig,
|
||||
}
|
||||
router := ae.Setup(logrus.New())
|
||||
router := ae.Setup(logrus.WithField("test", suite.T().Name()))
|
||||
devicesfile, err := os.Open("testdata/register-devices-req-2.json")
|
||||
require.NoError(suite.T(), err)
|
||||
|
||||
|
||||
@@ -46,3 +46,5 @@ testem.log
|
||||
Thumbs.db
|
||||
|
||||
/dist
|
||||
|
||||
/coverage
|
||||
|
||||
@@ -52,7 +52,6 @@
|
||||
"optimization": true,
|
||||
"outputHashing": "all",
|
||||
"sourceMap": false,
|
||||
"extractCss": true,
|
||||
"namedChunks": false,
|
||||
"extractLicenses": true,
|
||||
"vendorChunk": false,
|
||||
@@ -101,10 +100,22 @@
|
||||
"src/favicon-32x32.png",
|
||||
"src/assets"
|
||||
],
|
||||
"stylePreprocessorOptions": {
|
||||
"includePaths": [
|
||||
"src/@treo/styles"
|
||||
]
|
||||
},
|
||||
"styles": [
|
||||
"src/styles.scss"
|
||||
"src/styles/vendors.scss",
|
||||
"src/@treo/styles/main.scss",
|
||||
"src/styles/styles.scss",
|
||||
"src/styles/tailwind.scss"
|
||||
],
|
||||
"scripts": []
|
||||
"scripts": [],
|
||||
"fileReplacements": [{
|
||||
"replace": "src/environments/environment.ts",
|
||||
"with": "src/environments/environment.prod.ts"
|
||||
}]
|
||||
}
|
||||
},
|
||||
"lint": {
|
||||
|
||||
@@ -10,24 +10,24 @@ module.exports = function (config)
|
||||
require('karma-jasmine'),
|
||||
require('karma-chrome-launcher'),
|
||||
require('karma-jasmine-html-reporter'),
|
||||
require('karma-coverage-istanbul-reporter'),
|
||||
require('karma-coverage'),
|
||||
require('@angular-devkit/build-angular/plugins/karma')
|
||||
],
|
||||
client : {
|
||||
client: {
|
||||
clearContext: false // leave Jasmine Spec Runner output visible in browser
|
||||
},
|
||||
coverageIstanbulReporter: {
|
||||
dir : require('path').join(__dirname, './coverage/treo'),
|
||||
reports : ['html', 'lcovonly', 'text-summary'],
|
||||
dir: require('path').join(__dirname, './coverage'),
|
||||
reports: ['html', 'lcovonly', 'text-summary'],
|
||||
fixWebpackSourcePaths: true
|
||||
},
|
||||
reporters : ['progress', 'kjhtml'],
|
||||
port : 9876,
|
||||
colors : true,
|
||||
logLevel : config.LOG_INFO,
|
||||
autoWatch : true,
|
||||
browsers : ['Chrome'],
|
||||
singleRun : false,
|
||||
restartOnFileChange : true
|
||||
reporters: ['progress', 'kjhtml'],
|
||||
port: 9876,
|
||||
colors: true,
|
||||
logLevel: config.LOG_INFO,
|
||||
autoWatch: true,
|
||||
browsers: ['Chrome'],
|
||||
singleRun: false,
|
||||
restartOnFileChange: true
|
||||
});
|
||||
};
|
||||
|
||||
Generated
+8291
-27143
File diff suppressed because it is too large
Load Diff
@@ -7,7 +7,7 @@
|
||||
"start": "ng serve --open",
|
||||
"start:mem": "node --max_old_space_size=6144 ./node_modules/@angular/cli/bin/ng serve --open",
|
||||
"build": "ng build",
|
||||
"build:prod": "ng build --prod",
|
||||
"build:prod": "ng build --configuration production",
|
||||
"build:prod:mem": "node --max_old_space_size=6144 ./node_modules/@angular/cli/bin/ng build --prod",
|
||||
"test": "ng test",
|
||||
"lint": "ng lint",
|
||||
@@ -20,66 +20,55 @@
|
||||
},
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@angular/animations": "9.1.4",
|
||||
"@angular/cdk": "9.2.2",
|
||||
"@angular/common": "9.1.4",
|
||||
"@angular/compiler": "9.1.4",
|
||||
"@angular/core": "9.1.4",
|
||||
"@angular/forms": "9.1.4",
|
||||
"@angular/material": "9.2.2",
|
||||
"@angular/material-moment-adapter": "9.2.2",
|
||||
"@angular/platform-browser": "9.1.4",
|
||||
"@angular/platform-browser-dynamic": "9.1.4",
|
||||
"@angular/router": "9.1.4",
|
||||
"@fullcalendar/angular": "4.4.5-beta",
|
||||
"@fullcalendar/core": "4.4.0",
|
||||
"@fullcalendar/daygrid": "4.4.0",
|
||||
"@fullcalendar/interaction": "4.4.0",
|
||||
"@fullcalendar/list": "4.4.0",
|
||||
"@fullcalendar/moment": "4.4.0",
|
||||
"@fullcalendar/rrule": "4.4.0",
|
||||
"@fullcalendar/timegrid": "4.4.0",
|
||||
"@types/humanize-duration": "^3.18.1",
|
||||
"apexcharts": "3.19.2",
|
||||
"crypto-js": "3.3.0",
|
||||
"highlight.js": "10.0.1",
|
||||
"humanize-duration": "^3.24.0",
|
||||
"lodash": "4.17.15",
|
||||
"moment": "2.24.0",
|
||||
"ng-apexcharts": "1.5.12",
|
||||
"ngx-markdown": "9.0.0",
|
||||
"ngx-quill": "9.1.0",
|
||||
"perfect-scrollbar": "1.5.0",
|
||||
"quill": "1.3.7",
|
||||
"rrule": "2.6.4",
|
||||
"rxjs": "6.5.5",
|
||||
"tslib": "1.11.1",
|
||||
"web-animations-js": "2.3.2",
|
||||
"zone.js": "0.10.3"
|
||||
"@angular/animations": "v13-lts",
|
||||
"@angular/cdk": "v13-lts",
|
||||
"@angular/common": "v13-lts",
|
||||
"@angular/compiler": "v13-lts",
|
||||
"@angular/core": "v13-lts",
|
||||
"@angular/forms": "v13-lts",
|
||||
"@angular/material": "v13-lts",
|
||||
"@angular/material-moment-adapter": "v13-lts",
|
||||
"@angular/platform-browser": "v13-lts",
|
||||
"@angular/platform-browser-dynamic": "v13-lts",
|
||||
"@angular/router": "v13-lts",
|
||||
"@types/humanize-duration": "^3.27.1",
|
||||
"crypto-js": "^4.1.1",
|
||||
"highlight.js": "^11.6.0",
|
||||
"humanize-duration": "^3.27.3",
|
||||
"lodash": "4.17.21",
|
||||
"moment": "^2.29.4",
|
||||
"ng-apexcharts": "^1.7.4",
|
||||
"ngx-markdown": "^13.1.0",
|
||||
"perfect-scrollbar": "^1.5.5",
|
||||
"quill": "^1.3.7",
|
||||
"rrule": "^2.7.1",
|
||||
"rxjs": "^7.5.7",
|
||||
"tslib": "^2.4.1",
|
||||
"web-animations-js": "^2.3.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@angular-devkit/build-angular": "0.901.4",
|
||||
"@angular/cli": "9.1.4",
|
||||
"@angular/compiler-cli": "9.1.4",
|
||||
"@angular/language-service": "9.1.4",
|
||||
"@types/crypto-js": "3.1.45",
|
||||
"@types/highlight.js": "9.12.3",
|
||||
"@types/jasmine": "3.5.10",
|
||||
"@types/jasminewd2": "2.0.8",
|
||||
"@types/lodash": "4.14.150",
|
||||
"@types/node": "12.12.37",
|
||||
"codelyzer": "5.2.2",
|
||||
"jasmine-core": "3.5.0",
|
||||
"jasmine-spec-reporter": "4.2.1",
|
||||
"karma": "5.0.4",
|
||||
"karma-chrome-launcher": "3.1.0",
|
||||
"karma-coverage-istanbul-reporter": "2.1.1",
|
||||
"karma-jasmine": "3.0.3",
|
||||
"karma-jasmine-html-reporter": "1.5.3",
|
||||
"protractor": "5.4.4",
|
||||
"tailwindcss": "1.4.4",
|
||||
"ts-node": "8.3.0",
|
||||
"tslint": "6.1.2",
|
||||
"typescript": "3.8.3"
|
||||
"@angular-devkit/build-angular": "v13-lts",
|
||||
"@angular/cli": "v13-lts",
|
||||
"@angular/compiler-cli": "v13-lts",
|
||||
"@angular/language-service": "v13-lts",
|
||||
"@types/crypto-js": "^4.1.1",
|
||||
"@types/highlight.js": "^10.1.0",
|
||||
"@types/jasmine": "^4.3.0",
|
||||
"@types/jasminewd2": "^2.0.10",
|
||||
"@types/lodash": "^4.14.188",
|
||||
"@types/node": "^18.11.9",
|
||||
"codelyzer": "^6.0.2",
|
||||
"jasmine-core": "^4.5.0",
|
||||
"jasmine-spec-reporter": "^7.0.0",
|
||||
"karma": "^6.4.1",
|
||||
"karma-chrome-launcher": "^3.1.1",
|
||||
"karma-coverage": "^2.2.0",
|
||||
"karma-jasmine": "^5.1.0",
|
||||
"karma-jasmine-html-reporter": "^2.0.0",
|
||||
"protractor": "^7.0.0",
|
||||
"tailwindcss": "^3.2.3",
|
||||
"ts-node": "^10.9.1",
|
||||
"tslint": "^6.1.3",
|
||||
"typescript": "^4.6.4"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -56,7 +56,7 @@ export class TreoDateRangeComponent implements ControlValueAccessor, OnInit, OnD
|
||||
private _timeFormat: string;
|
||||
private _timeRange: boolean;
|
||||
private readonly _timeRegExp: RegExp;
|
||||
private _unsubscribeAll: Subject<any>;
|
||||
private _unsubscribeAll: Subject<void>;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
|
||||
@@ -31,7 +31,7 @@ export class TreoMessageComponent implements OnInit, OnDestroy
|
||||
private _dismissed: null | boolean;
|
||||
private _showIcon: boolean;
|
||||
private _type: TreoMessageType;
|
||||
private _unsubscribeAll: Subject<any>;
|
||||
private _unsubscribeAll: Subject<void>;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
|
||||
+1
-1
@@ -23,7 +23,7 @@ export class TreoHorizontalNavigationBasicItemComponent implements OnInit, OnDes
|
||||
|
||||
// Private
|
||||
private _treoHorizontalNavigationComponent: TreoHorizontalNavigationComponent;
|
||||
private _unsubscribeAll: Subject<any>;
|
||||
private _unsubscribeAll: Subject<void>;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
|
||||
+1
-1
@@ -32,7 +32,7 @@ export class TreoHorizontalNavigationBranchItemComponent implements OnInit, OnDe
|
||||
|
||||
// Private
|
||||
private _treoHorizontalNavigationComponent: TreoHorizontalNavigationComponent;
|
||||
private _unsubscribeAll: Subject<any>;
|
||||
private _unsubscribeAll: Subject<void>;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
|
||||
+1
-1
@@ -23,7 +23,7 @@ export class TreoHorizontalNavigationDividerItemComponent implements OnInit, OnD
|
||||
|
||||
// Private
|
||||
private _treoHorizontalNavigationComponent: TreoHorizontalNavigationComponent;
|
||||
private _unsubscribeAll: Subject<any>;
|
||||
private _unsubscribeAll: Subject<void>;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
|
||||
+1
-1
@@ -23,7 +23,7 @@ export class TreoHorizontalNavigationSpacerItemComponent implements OnInit, OnDe
|
||||
|
||||
// Private
|
||||
private _treoHorizontalNavigationComponent: TreoHorizontalNavigationComponent;
|
||||
private _unsubscribeAll: Subject<any>;
|
||||
private _unsubscribeAll: Subject<void>;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
|
||||
@@ -23,7 +23,7 @@ export class TreoHorizontalNavigationComponent implements OnInit, OnDestroy
|
||||
|
||||
// Private
|
||||
private _navigation: TreoNavigationItem[];
|
||||
private _unsubscribeAll: Subject<any>;
|
||||
private _unsubscribeAll: Subject<void>;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
|
||||
+1
-1
@@ -35,7 +35,7 @@ export class TreoVerticalNavigationAsideItemComponent implements OnInit, OnDestr
|
||||
|
||||
// Private
|
||||
private _treoVerticalNavigationComponent: TreoVerticalNavigationComponent;
|
||||
private _unsubscribeAll: Subject<any>;
|
||||
private _unsubscribeAll: Subject<void>;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
|
||||
+1
-1
@@ -23,7 +23,7 @@ export class TreoVerticalNavigationBasicItemComponent implements OnInit, OnDestr
|
||||
|
||||
// Private
|
||||
private _treoVerticalNavigationComponent: TreoVerticalNavigationComponent;
|
||||
private _unsubscribeAll: Subject<any>;
|
||||
private _unsubscribeAll: Subject<void>;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
|
||||
+1
-1
@@ -38,7 +38,7 @@ export class TreoVerticalNavigationCollapsableItemComponent implements OnInit, O
|
||||
|
||||
// Private
|
||||
private _treoVerticalNavigationComponent: TreoVerticalNavigationComponent;
|
||||
private _unsubscribeAll: Subject<any>;
|
||||
private _unsubscribeAll: Subject<void>;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
|
||||
+1
-1
@@ -23,7 +23,7 @@ export class TreoVerticalNavigationDividerItemComponent implements OnInit, OnDes
|
||||
|
||||
// Private
|
||||
private _treoVerticalNavigationComponent: TreoVerticalNavigationComponent;
|
||||
private _unsubscribeAll: Subject<any>;
|
||||
private _unsubscribeAll: Subject<void>;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
|
||||
+1
-1
@@ -27,7 +27,7 @@ export class TreoVerticalNavigationGroupItemComponent implements OnInit, OnDestr
|
||||
|
||||
// Private
|
||||
private _treoVerticalNavigationComponent: TreoVerticalNavigationComponent;
|
||||
private _unsubscribeAll: Subject<any>;
|
||||
private _unsubscribeAll: Subject<void>;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
|
||||
+1
-1
@@ -23,7 +23,7 @@ export class TreoVerticalNavigationSpacerItemComponent implements OnInit, OnDest
|
||||
|
||||
// Private
|
||||
private _treoVerticalNavigationComponent: TreoVerticalNavigationComponent;
|
||||
private _unsubscribeAll: Subject<any>;
|
||||
private _unsubscribeAll: Subject<void>;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
|
||||
@@ -65,7 +65,7 @@ export class TreoVerticalNavigationComponent implements OnInit, AfterViewInit, O
|
||||
private _position: TreoVerticalNavigationPosition;
|
||||
private _scrollStrategy: ScrollStrategy;
|
||||
private _transparentOverlay: boolean | '';
|
||||
private _unsubscribeAll: Subject<any>;
|
||||
private _unsubscribeAll: Subject<void>;
|
||||
|
||||
@HostBinding('class.treo-vertical-navigation-animations-enabled')
|
||||
private _animationsEnabled: boolean;
|
||||
|
||||
@@ -12,7 +12,7 @@ export class TreoAutogrowDirective implements OnInit, OnDestroy
|
||||
|
||||
// Private
|
||||
private _padding: number;
|
||||
private _unsubscribeAll: Subject<any>;
|
||||
private _unsubscribeAll: Subject<void>;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
|
||||
@@ -24,7 +24,7 @@ export class TreoScrollbarDirective implements OnInit, OnDestroy
|
||||
private _animation: number | null;
|
||||
private _enabled: boolean;
|
||||
private _options: any;
|
||||
private _unsubscribeAll: Subject<any>;
|
||||
private _unsubscribeAll: Subject<void>;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
|
||||
@@ -20,7 +20,7 @@ export class TreoMockApiModule
|
||||
*
|
||||
* @param mockDataServices
|
||||
*/
|
||||
static forRoot(mockDataServices: any[]): ModuleWithProviders
|
||||
static forRoot(mockDataServices: any[]): ModuleWithProviders<TreoMockApiModule>
|
||||
{
|
||||
return {
|
||||
ngModule : TreoMockApiModule,
|
||||
|
||||
@@ -26,7 +26,6 @@
|
||||
|
||||
// 6. Overrides
|
||||
@import 'overrides/angular-material';
|
||||
@import 'overrides/fullcalendar';
|
||||
@import 'overrides/highlightjs';
|
||||
@import 'overrides/perfect-scrollbar';
|
||||
@import 'overrides/quill';
|
||||
|
||||
@@ -1,878 +0,0 @@
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
// @ FullCalendar overrides
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
|
||||
.fc {
|
||||
|
||||
.fc-view-container {
|
||||
|
||||
// Day Grid - Month view
|
||||
.fc-view.fc-dayGridMonth-view {
|
||||
|
||||
.fc-head {
|
||||
|
||||
> tr > .fc-head-container {
|
||||
border: none;
|
||||
|
||||
.fc-row {
|
||||
|
||||
.fc-day-header {
|
||||
|
||||
span {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding-top: 8px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.fc-body {
|
||||
|
||||
> tr > .fc-widget-content {
|
||||
border: none;
|
||||
|
||||
.fc-day-grid {
|
||||
|
||||
.fc-week {
|
||||
|
||||
.fc-content-skeleton {
|
||||
|
||||
.fc-day-top {
|
||||
text-align: center;
|
||||
|
||||
&.fc-other-month {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.fc-day-number {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 22px;
|
||||
height: 21px;
|
||||
margin: 4px 0;
|
||||
font-size: 12px;
|
||||
border-radius: 50%;
|
||||
float: none;
|
||||
}
|
||||
}
|
||||
|
||||
.fc-event-container {
|
||||
|
||||
.fc-day-grid-event {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 22px;
|
||||
min-height: 22px;
|
||||
max-height: 22px;
|
||||
margin: 0 6px 4px 6px;
|
||||
padding: 0 8px;
|
||||
font-size: 12px;
|
||||
border-radius: 4px;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
|
||||
@include treo-breakpoint('xs') {
|
||||
padding: 0 5px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.fc-more {
|
||||
padding: 0 6px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
white-space: nowrap;
|
||||
|
||||
@include treo-breakpoint('xs') {
|
||||
padding: 0 3px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.fc-highlight-skeleton {
|
||||
|
||||
.fc-highlight {
|
||||
position: relative;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.fc-popover {
|
||||
|
||||
&.fc-more-popover {
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
@include treo-elevation('2xl');
|
||||
|
||||
.fc-header {
|
||||
height: 32px;
|
||||
min-height: 32px;
|
||||
max-height: 32px;
|
||||
padding: 0 8px;
|
||||
|
||||
.fc-title {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.fc-body {
|
||||
max-height: 160px;
|
||||
overflow: hidden auto;
|
||||
|
||||
.fc-event-container {
|
||||
padding: 8px;
|
||||
|
||||
.fc-day-grid-event {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 22px;
|
||||
min-height: 22px;
|
||||
max-height: 22px;
|
||||
margin: 0 0 6px 0;
|
||||
padding: 0 8px;
|
||||
font-size: 12px;
|
||||
line-height: 1;
|
||||
border-radius: 4px;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Time Grid - Week view
|
||||
.fc-view.fc-timeGridWeek-view {
|
||||
|
||||
.fc-head {
|
||||
|
||||
> tr > .fc-head-container {
|
||||
border: none;
|
||||
|
||||
.fc-row {
|
||||
|
||||
.fc-axis {
|
||||
width: 48px !important;
|
||||
}
|
||||
|
||||
.fc-day-header {
|
||||
|
||||
span {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
|
||||
&.fc-weekday {
|
||||
padding-top: 16px;
|
||||
font-size: 12px;
|
||||
letter-spacing: 0.055em;
|
||||
text-transform: uppercase;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
&.fc-date {
|
||||
padding-bottom: 12px;
|
||||
font-size: 26px;
|
||||
font-weight: 300;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.fc-body {
|
||||
|
||||
> tr > .fc-widget-content {
|
||||
border: none;
|
||||
|
||||
.fc-day-grid {
|
||||
|
||||
.fc-row {
|
||||
min-height: 0;
|
||||
|
||||
.fc-bg {
|
||||
|
||||
.fc-axis {
|
||||
width: 48px !important;
|
||||
}
|
||||
}
|
||||
|
||||
.fc-content-skeleton {
|
||||
padding-bottom: 0;
|
||||
|
||||
.fc-axis {
|
||||
width: 48px !important;
|
||||
}
|
||||
|
||||
.fc-event-container {
|
||||
|
||||
.fc-day-grid-event {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 22px;
|
||||
min-height: 22px;
|
||||
max-height: 22px;
|
||||
margin: 0 6px 6px 6px;
|
||||
padding: 0 8px;
|
||||
font-size: 12px;
|
||||
line-height: 1;
|
||||
border-radius: 4px;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.fc-divider {
|
||||
border: none;
|
||||
}
|
||||
|
||||
.fc-time-grid {
|
||||
|
||||
.fc-bg {
|
||||
|
||||
.fc-axis {
|
||||
border: none;
|
||||
width: 48px !important;
|
||||
|
||||
+ .fc-day {
|
||||
border: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.fc-slats {
|
||||
|
||||
.fc-axis {
|
||||
width: 48px !important;
|
||||
height: 48px;
|
||||
text-align: center;
|
||||
|
||||
span {
|
||||
font-size: 12px;
|
||||
width: 48px;
|
||||
min-width: 48px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.fc-content-skeleton {
|
||||
|
||||
.fc-axis {
|
||||
width: 48px !important;
|
||||
}
|
||||
|
||||
.fc-event-container {
|
||||
margin: 0 12px 0 0;
|
||||
|
||||
.fc-time-grid-event {
|
||||
display: flex;
|
||||
padding: 8px;
|
||||
border-radius: 4px;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
|
||||
.fc-time,
|
||||
.fc-title {
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Time Grid - Day view
|
||||
.fc-view.fc-timeGridDay-view {
|
||||
|
||||
.fc-head {
|
||||
|
||||
> tr > .fc-head-container {
|
||||
border: none;
|
||||
|
||||
.fc-row {
|
||||
|
||||
.fc-axis {
|
||||
width: 48px !important;
|
||||
}
|
||||
|
||||
.fc-day-header {
|
||||
|
||||
span {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
|
||||
&.fc-weekday {
|
||||
padding-top: 16px;
|
||||
font-size: 12px;
|
||||
letter-spacing: 0.055em;
|
||||
text-transform: uppercase;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
&.fc-date {
|
||||
padding-bottom: 12px;
|
||||
font-size: 26px;
|
||||
font-weight: 300;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.fc-body {
|
||||
|
||||
> tr > .fc-widget-content {
|
||||
border: none;
|
||||
|
||||
.fc-day-grid {
|
||||
|
||||
.fc-row {
|
||||
min-height: 0;
|
||||
|
||||
.fc-bg {
|
||||
|
||||
.fc-axis {
|
||||
width: 48px !important;
|
||||
}
|
||||
}
|
||||
|
||||
.fc-content-skeleton {
|
||||
padding-bottom: 0;
|
||||
|
||||
.fc-axis {
|
||||
width: 48px !important;
|
||||
}
|
||||
|
||||
.fc-event-container {
|
||||
|
||||
.fc-day-grid-event {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 22px;
|
||||
min-height: 22px;
|
||||
max-height: 22px;
|
||||
margin: 0 6px 6px 6px;
|
||||
padding: 0 8px;
|
||||
font-size: 12px;
|
||||
line-height: 1;
|
||||
border-radius: 4px;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.fc-divider {
|
||||
border: none;
|
||||
}
|
||||
|
||||
.fc-time-grid {
|
||||
|
||||
.fc-bg {
|
||||
|
||||
.fc-axis {
|
||||
border: none;
|
||||
width: 48px !important;
|
||||
|
||||
+ .fc-day {
|
||||
border: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.fc-slats {
|
||||
|
||||
.fc-axis {
|
||||
width: 48px !important;
|
||||
height: 48px;
|
||||
text-align: center;
|
||||
|
||||
span {
|
||||
font-size: 12px;
|
||||
width: 48px;
|
||||
min-width: 48px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.fc-content-skeleton {
|
||||
|
||||
.fc-axis {
|
||||
width: 48px !important;
|
||||
}
|
||||
|
||||
.fc-event-container {
|
||||
margin: 0 12px 0 0;
|
||||
|
||||
.fc-time-grid-event {
|
||||
display: flex;
|
||||
padding: 8px;
|
||||
border-radius: 4px;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
|
||||
.fc-time,
|
||||
.fc-title {
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// List - Year view
|
||||
.fc-view.fc-listYear-view {
|
||||
border: none;
|
||||
|
||||
.fc-list-table {
|
||||
|
||||
.fc-list-heading {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.fc-list-item {
|
||||
display: flex;
|
||||
cursor: pointer;
|
||||
|
||||
td {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: auto;
|
||||
height: 48px;
|
||||
min-height: 48px;
|
||||
padding: 0 8px;
|
||||
border-width: 0 0 1px 0;
|
||||
|
||||
&.fc-list-item-date {
|
||||
order: 1;
|
||||
padding-left: 16px;
|
||||
width: 120px;
|
||||
min-width: 120px;
|
||||
max-width: 120px;
|
||||
|
||||
@include treo-breakpoint('xs') {
|
||||
width: 100px;
|
||||
min-width: 100px;
|
||||
max-width: 100px;
|
||||
}
|
||||
|
||||
> span {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
|
||||
span {
|
||||
|
||||
&:first-child {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding-right: 8px;
|
||||
width: 32px;
|
||||
min-width: 32px;
|
||||
max-width: 32px;
|
||||
font-size: 18px;
|
||||
|
||||
@include treo-breakpoint('xs') {
|
||||
padding-right: 2px;
|
||||
}
|
||||
|
||||
+ span {
|
||||
display: flex;
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
letter-spacing: 0.055em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.fc-list-item-time {
|
||||
flex: 0 0 auto;
|
||||
order: 3;
|
||||
width: 160px;
|
||||
min-width: 160px;
|
||||
max-width: 160px;
|
||||
|
||||
@include treo-breakpoint('xs') {
|
||||
width: 120px;
|
||||
min-width: 120px;
|
||||
max-width: 120px;
|
||||
}
|
||||
}
|
||||
|
||||
&.fc-list-item-marker {
|
||||
flex: 0 0 auto;
|
||||
order: 2;
|
||||
|
||||
.fc-event-dot {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
}
|
||||
|
||||
&.fc-list-item-title {
|
||||
flex: 1 1 auto;
|
||||
order: 4;
|
||||
padding-right: 24px;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Day grid event - Dragging
|
||||
.fc-day-grid-event {
|
||||
|
||||
&.fc-dragging,
|
||||
&.fc-resizing {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 22px;
|
||||
min-height: 22px;
|
||||
max-height: 22px;
|
||||
margin: 0 6px 4px 6px;
|
||||
padding: 0 8px;
|
||||
font-size: 12px;
|
||||
line-height: 1;
|
||||
border-radius: 4px;
|
||||
border: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
// @ Theming
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
@include treo-theme {
|
||||
|
||||
$background: map-get($theme, background);
|
||||
$foreground: map-get($theme, foreground);
|
||||
$primary: map-get($theme, primary);
|
||||
|
||||
.fc {
|
||||
|
||||
.fc-view-container {
|
||||
|
||||
// Day Grid - Month view
|
||||
.fc-view.fc-dayGridMonth-view {
|
||||
|
||||
.fc-head {
|
||||
|
||||
> tr > .fc-head-container {
|
||||
|
||||
.fc-row {
|
||||
|
||||
.fc-day-header {
|
||||
border-color: map-get($foreground, divider);
|
||||
|
||||
span {
|
||||
color: map-get($foreground, secondary-text);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.fc-body {
|
||||
|
||||
> tr > .fc-widget-content {
|
||||
|
||||
.fc-day-grid {
|
||||
|
||||
.fc-week {
|
||||
|
||||
.fc-bg {
|
||||
|
||||
.fc-day {
|
||||
border-color: map-get($foreground, divider);
|
||||
|
||||
&.fc-today {
|
||||
background: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.fc-content-skeleton {
|
||||
|
||||
.fc-day-top {
|
||||
|
||||
&.fc-other-month {
|
||||
|
||||
.fc-day-number {
|
||||
color: map-get($foreground, hint-text);
|
||||
}
|
||||
}
|
||||
|
||||
&.fc-today {
|
||||
|
||||
.fc-day-number {
|
||||
background: map-get($primary, default);
|
||||
color: map-get($primary, default-contrast);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.fc-more {
|
||||
color: map-get($foreground, secondary-text);
|
||||
}
|
||||
}
|
||||
|
||||
.fc-highlight-skeleton {
|
||||
|
||||
.fc-highlight {
|
||||
background: treo-color('cool-gray', 100);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.fc-popover {
|
||||
background: map-get($background, card);
|
||||
|
||||
&.fc-more-popover {
|
||||
|
||||
.fc-header {
|
||||
background: map-get($background, hover);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Time Grid - Week view
|
||||
.fc-view.fc-timeGridWeek-view {
|
||||
|
||||
.fc-head {
|
||||
|
||||
> tr > .fc-head-container {
|
||||
|
||||
.fc-row {
|
||||
|
||||
.fc-axis {
|
||||
border-color: map-get($foreground, divider);
|
||||
}
|
||||
|
||||
.fc-day-header {
|
||||
border-color: map-get($foreground, divider);
|
||||
|
||||
span {
|
||||
color: map-get($foreground, secondary-text);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.fc-body {
|
||||
|
||||
> tr > .fc-widget-content {
|
||||
border: none;
|
||||
|
||||
.fc-day-grid {
|
||||
|
||||
.fc-bg {
|
||||
|
||||
.fc-axis {
|
||||
border-color: map-get($foreground, divider);
|
||||
}
|
||||
|
||||
.fc-day {
|
||||
border-color: map-get($foreground, divider);
|
||||
|
||||
&.fc-today {
|
||||
background: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.fc-divider {
|
||||
background: map-get($foreground, divider);
|
||||
}
|
||||
|
||||
.fc-time-grid {
|
||||
|
||||
.fc-bg {
|
||||
|
||||
.fc-day {
|
||||
border-color: map-get($foreground, divider);
|
||||
|
||||
&.fc-today {
|
||||
background: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.fc-slats {
|
||||
|
||||
.fc-time {
|
||||
border-color: map-get($foreground, divider);
|
||||
}
|
||||
|
||||
.fc-widget-content {
|
||||
border-color: map-get($foreground, divider);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Time Grid - Day view
|
||||
.fc-view.fc-timeGridDay-view {
|
||||
|
||||
.fc-head {
|
||||
|
||||
> tr > .fc-head-container {
|
||||
|
||||
.fc-row {
|
||||
|
||||
.fc-axis {
|
||||
border-color: map-get($foreground, divider);
|
||||
}
|
||||
|
||||
.fc-day-header {
|
||||
border-color: map-get($foreground, divider);
|
||||
|
||||
span {
|
||||
color: map-get($foreground, secondary-text);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.fc-body {
|
||||
|
||||
> tr > .fc-widget-content {
|
||||
border: none;
|
||||
|
||||
.fc-day-grid {
|
||||
|
||||
.fc-bg {
|
||||
|
||||
.fc-axis {
|
||||
border-color: map-get($foreground, divider);
|
||||
}
|
||||
|
||||
.fc-day {
|
||||
border-color: map-get($foreground, divider);
|
||||
|
||||
&.fc-today {
|
||||
background: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.fc-divider {
|
||||
background: map-get($foreground, divider);
|
||||
}
|
||||
|
||||
.fc-time-grid {
|
||||
|
||||
.fc-bg {
|
||||
|
||||
.fc-day {
|
||||
border-color: map-get($foreground, divider);
|
||||
|
||||
&.fc-today {
|
||||
background: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.fc-slats {
|
||||
|
||||
.fc-time {
|
||||
border-color: map-get($foreground, divider);
|
||||
}
|
||||
|
||||
.fc-widget-content {
|
||||
border-color: map-get($foreground, divider);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// List - Year view
|
||||
.fc-view.fc-listYear-view {
|
||||
|
||||
.fc-list-table {
|
||||
|
||||
.fc-list-item {
|
||||
|
||||
&:hover {
|
||||
|
||||
td {
|
||||
background-color: map-get($background, hover);
|
||||
}
|
||||
}
|
||||
|
||||
td {
|
||||
border-color: map-get($foreground, divider);
|
||||
|
||||
&.fc-list-item-date {
|
||||
|
||||
> span {
|
||||
|
||||
span {
|
||||
|
||||
&:first-child {
|
||||
|
||||
+ span {
|
||||
color: map-get($foreground, secondary-text);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,22 +1,22 @@
|
||||
import { NgModule, enableProdMode } from '@angular/core';
|
||||
import { BrowserModule } from '@angular/platform-browser';
|
||||
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
|
||||
import { ExtraOptions, PreloadAllModules, RouterModule } from '@angular/router';
|
||||
import { APP_BASE_HREF } from '@angular/common';
|
||||
import { MarkdownModule } from 'ngx-markdown';
|
||||
import { TreoModule } from '@treo';
|
||||
import { TreoConfigModule } from '@treo/services/config';
|
||||
import { TreoMockApiModule } from '@treo/lib/mock-api';
|
||||
import { CoreModule } from 'app/core/core.module';
|
||||
import { appConfig } from 'app/core/config/app.config';
|
||||
import { mockDataServices } from 'app/data/mock';
|
||||
import { LayoutModule } from 'app/layout/layout.module';
|
||||
import { AppComponent } from 'app/app.component';
|
||||
import { appRoutes, getAppBaseHref } from 'app/app.routing';
|
||||
import {enableProdMode, NgModule} from '@angular/core';
|
||||
import {BrowserModule} from '@angular/platform-browser';
|
||||
import {BrowserAnimationsModule} from '@angular/platform-browser/animations';
|
||||
import {ExtraOptions, PreloadAllModules, RouterModule} from '@angular/router';
|
||||
import {APP_BASE_HREF} from '@angular/common';
|
||||
import {MarkdownModule} from 'ngx-markdown';
|
||||
import {TreoModule} from '@treo';
|
||||
import {ScrutinyConfigModule} from 'app/core/config/scrutiny-config.module';
|
||||
import {TreoMockApiModule} from '@treo/lib/mock-api';
|
||||
import {CoreModule} from 'app/core/core.module';
|
||||
import {appConfig} from 'app/core/config/app.config';
|
||||
import {mockDataServices} from 'app/data/mock';
|
||||
import {LayoutModule} from 'app/layout/layout.module';
|
||||
import {AppComponent} from 'app/app.component';
|
||||
import {appRoutes, getAppBaseHref} from 'app/app.routing';
|
||||
|
||||
const routerConfig: ExtraOptions = {
|
||||
scrollPositionRestoration: 'enabled',
|
||||
preloadingStrategy : PreloadAllModules
|
||||
preloadingStrategy: PreloadAllModules
|
||||
};
|
||||
|
||||
let dev = [
|
||||
@@ -41,7 +41,7 @@ if (process.env.NODE_ENV === 'production') {
|
||||
|
||||
// Treo & Treo Mock API
|
||||
TreoModule,
|
||||
TreoConfigModule.forRoot(appConfig),
|
||||
ScrutinyConfigModule.forRoot(appConfig),
|
||||
...dev,
|
||||
|
||||
// Core
|
||||
|
||||
@@ -1,22 +1,62 @@
|
||||
import { Layout } from 'app/layout/layout.types';
|
||||
import {Layout} from 'app/layout/layout.types';
|
||||
|
||||
// Theme type
|
||||
export type Theme = 'light' | 'dark' | 'system';
|
||||
|
||||
// Device title to display on the dashboard
|
||||
export type DashboardDisplay = 'name' | 'serial_id' | 'uuid' | 'label'
|
||||
|
||||
export type DashboardSort = 'status' | 'title' | 'age'
|
||||
|
||||
export type TemperatureUnit = 'celsius' | 'fahrenheit'
|
||||
|
||||
export type LineStroke = 'smooth' | 'straight' | 'stepline'
|
||||
|
||||
|
||||
export enum MetricsNotifyLevel {
|
||||
Warn = 1,
|
||||
Fail = 2
|
||||
}
|
||||
|
||||
export enum MetricsStatusFilterAttributes {
|
||||
All = 0,
|
||||
Critical = 1
|
||||
}
|
||||
|
||||
export enum MetricsStatusThreshold {
|
||||
Smart = 1,
|
||||
Scrutiny = 2,
|
||||
|
||||
// shortcut
|
||||
Both = 3
|
||||
}
|
||||
|
||||
/**
|
||||
* AppConfig interface. Update this interface to strictly type your config
|
||||
* object.
|
||||
*/
|
||||
export interface AppConfig
|
||||
{
|
||||
theme: Theme;
|
||||
layout: Layout;
|
||||
export interface AppConfig {
|
||||
theme?: Theme;
|
||||
layout?: Layout;
|
||||
|
||||
// Dashboard options
|
||||
dashboardDisplay: string;
|
||||
dashboardSort: string;
|
||||
dashboard_display?: DashboardDisplay;
|
||||
dashboard_sort?: DashboardSort;
|
||||
|
||||
temperature_unit?: TemperatureUnit;
|
||||
|
||||
file_size_si_units?: boolean;
|
||||
|
||||
line_stroke?: LineStroke;
|
||||
|
||||
// Settings from Scrutiny API
|
||||
|
||||
metrics?: {
|
||||
notify_level?: MetricsNotifyLevel
|
||||
status_filter_attributes?: MetricsStatusFilterAttributes
|
||||
status_threshold?: MetricsStatusThreshold
|
||||
}
|
||||
|
||||
temperatureUnit: string;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -28,12 +68,21 @@ export interface AppConfig
|
||||
* "ConfigService".
|
||||
*/
|
||||
export const appConfig: AppConfig = {
|
||||
theme : 'light',
|
||||
theme: 'light',
|
||||
layout: 'material',
|
||||
|
||||
dashboardDisplay: 'name',
|
||||
dashboardSort: 'status',
|
||||
dashboard_display: 'name',
|
||||
dashboard_sort: 'status',
|
||||
|
||||
temperatureUnit: 'celsius',
|
||||
temperature_unit: 'celsius',
|
||||
file_size_si_units: false,
|
||||
|
||||
line_stroke: 'smooth',
|
||||
|
||||
metrics: {
|
||||
notify_level: MetricsNotifyLevel.Fail,
|
||||
status_filter_attributes: MetricsStatusFilterAttributes.All,
|
||||
status_threshold: MetricsStatusThreshold.Both
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
import {ModuleWithProviders, NgModule} from '@angular/core';
|
||||
import {ScrutinyConfigService} from 'app/core/config/scrutiny-config.service';
|
||||
import {TREO_APP_CONFIG} from '@treo/services/config/config.constants';
|
||||
|
||||
@NgModule()
|
||||
export class ScrutinyConfigModule {
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param {ScrutinyConfigService} _scrutinyConfigService
|
||||
*/
|
||||
constructor(
|
||||
private _scrutinyConfigService: ScrutinyConfigService
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* forRoot method for setting user configuration
|
||||
*
|
||||
* @param config
|
||||
*/
|
||||
static forRoot(config: any): ModuleWithProviders<ScrutinyConfigModule> {
|
||||
return {
|
||||
ngModule: ScrutinyConfigModule,
|
||||
providers: [
|
||||
{
|
||||
provide: TREO_APP_CONFIG,
|
||||
useValue: config
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user