diff --git a/.bra.toml b/.bra.toml
index dcf316466d6..15961e1e3fd 100644
--- a/.bra.toml
+++ b/.bra.toml
@@ -9,7 +9,7 @@ watch_dirs = [
"$WORKDIR/public/views",
"$WORKDIR/conf",
]
-watch_exts = [".go", ".ini", ".toml"]
+watch_exts = [".go", ".ini", ".toml", ".template.html"]
build_delay = 1500
cmds = [
["go", "run", "build.go", "-dev", "build-server"],
diff --git a/.circleci/config.yml b/.circleci/config.yml
index 44f34d42926..e631e0a8d33 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -5,9 +5,11 @@ aliases:
ignore: /.*/
tags:
only: /^v[0-9]+(\.[0-9]+){2}(-.+|[^-.]*)$/
- - &filter-not-release
+ - &filter-not-release-or-master
tags:
ignore: /^v[0-9]+(\.[0-9]+){2}(-.+|[^-.]*)$/
+ branches:
+ ignore: master
- &filter-only-master
branches:
only: master
@@ -17,7 +19,7 @@ version: 2
jobs:
mysql-integration-test:
docker:
- - image: circleci/golang:1.10
+ - image: circleci/golang:1.11
- image: circleci/mysql:5.6-ram
environment:
MYSQL_ROOT_PASSWORD: rootpass
@@ -30,14 +32,14 @@ jobs:
- run: sudo apt update
- run: sudo apt install -y mysql-client
- run: dockerize -wait tcp://127.0.0.1:3306 -timeout 120s
- - run: cat docker/blocks/mysql_tests/setup.sql | mysql -h 127.0.0.1 -P 3306 -u root -prootpass
+ - run: cat devenv/docker/blocks/mysql_tests/setup.sql | mysql -h 127.0.0.1 -P 3306 -u root -prootpass
- run:
name: mysql integration tests
command: 'GRAFANA_TEST_DB=mysql go test ./pkg/services/sqlstore/... ./pkg/tsdb/mysql/... '
postgres-integration-test:
docker:
- - image: circleci/golang:1.10
+ - image: circleci/golang:1.11
- image: circleci/postgres:9.3-ram
environment:
POSTGRES_USER: grafanatest
@@ -49,7 +51,7 @@ jobs:
- run: sudo apt update
- run: sudo apt install -y postgresql-client
- run: dockerize -wait tcp://127.0.0.1:5432 -timeout 120s
- - run: 'PGPASSWORD=grafanatest psql -p 5432 -h 127.0.0.1 -U grafanatest -d grafanatest -f docker/blocks/postgres_tests/setup.sql'
+ - run: 'PGPASSWORD=grafanatest psql -p 5432 -h 127.0.0.1 -U grafanatest -d grafanatest -f devenv/docker/blocks/postgres_tests/setup.sql'
- run:
name: postgres integration tests
command: 'GRAFANA_TEST_DB=postgres go test ./pkg/services/sqlstore/... ./pkg/tsdb/postgres/...'
@@ -72,14 +74,14 @@ jobs:
gometalinter:
docker:
- - image: circleci/golang:1.10
+ - image: circleci/golang:1.11
environment:
# we need CGO because of go-sqlite3
CGO_ENABLED: 1
working_directory: /go/src/github.com/grafana/grafana
steps:
- checkout
- - run: 'go get -u gopkg.in/alecthomas/gometalinter.v2'
+ - run: 'go get -u github.com/alecthomas/gometalinter'
- run: 'go get -u github.com/tsenart/deadcode'
- run: 'go get -u github.com/gordonklaus/ineffassign'
- run: 'go get -u github.com/opennota/check/cmd/structcheck'
@@ -87,9 +89,9 @@ jobs:
- run: 'go get -u github.com/opennota/check/cmd/varcheck'
- run:
name: run linters
- command: 'gometalinter.v2 --enable-gc --vendor --deadline 10m --disable-all --enable=deadcode --enable=ineffassign --enable=structcheck --enable=unconvert --enable=varcheck ./...'
+ command: 'gometalinter --enable-gc --vendor --deadline 10m --disable-all --enable=deadcode --enable=ineffassign --enable=structcheck --enable=unconvert --enable=varcheck ./...'
- run:
- name: run go vet
+ name: run go vet
command: 'go vet ./pkg/...'
test-frontend:
@@ -102,6 +104,7 @@ jobs:
- run:
name: yarn install
command: 'yarn install --pure-lockfile --no-progress'
+ no_output_timeout: 15m
- save_cache:
key: dependency-cache-{{ checksum "yarn.lock" }}
paths:
@@ -112,7 +115,7 @@ jobs:
test-backend:
docker:
- - image: circleci/golang:1.10
+ - image: circleci/golang:1.11
working_directory: /go/src/github.com/grafana/grafana
steps:
- checkout
@@ -122,7 +125,7 @@ jobs:
build-all:
docker:
- - image: grafana/build-container:1.0.0
+ - image: grafana/build-container:1.1.0
working_directory: /go/src/github.com/grafana/grafana
steps:
- checkout
@@ -144,6 +147,12 @@ jobs:
- run:
name: sign packages
command: './scripts/build/sign_packages.sh'
+ - run:
+ name: verify signed packages
+ command: |
+ mkdir -p ~/.rpmdb/pubkeys
+ curl -s https://grafanarel.s3.amazonaws.com/RPM-GPG-KEY-grafana > ~/.rpmdb/pubkeys/grafana.key
+ ./scripts/build/verify_signed_packages.sh dist/*.rpm
- run:
name: sha-sum packages
command: 'go run build.go sha-dist'
@@ -156,8 +165,65 @@ jobs:
- dist/grafana*
- scripts/*.sh
- scripts/publish
- - store_artifacts:
- path: dist
+
+ build:
+ docker:
+ - image: grafana/build-container:1.1.0
+ working_directory: /go/src/github.com/grafana/grafana
+ steps:
+ - checkout
+ - run:
+ name: prepare build tools
+ command: '/tmp/bootstrap.sh'
+ - run:
+ name: build and package grafana
+ command: './scripts/build/build.sh'
+ - run:
+ name: sign packages
+ command: './scripts/build/sign_packages.sh'
+ - run:
+ name: sha-sum packages
+ command: 'go run build.go sha-dist'
+ - persist_to_workspace:
+ root: .
+ paths:
+ - dist/grafana*
+
+ grafana-docker-master:
+ docker:
+ - image: docker:stable-git
+ steps:
+ - checkout
+ - attach_workspace:
+ at: .
+ - setup_remote_docker
+ - run: docker info
+ - run: cp dist/grafana-latest.linux-x64.tar.gz packaging/docker
+ - run: cd packaging/docker && ./build-deploy.sh "master-${CIRCLE_SHA1}"
+
+ grafana-docker-pr:
+ docker:
+ - image: docker:stable-git
+ steps:
+ - checkout
+ - attach_workspace:
+ at: .
+ - setup_remote_docker
+ - run: docker info
+ - run: cp dist/grafana-latest.linux-x64.tar.gz packaging/docker
+ - run: cd packaging/docker && ./build.sh "${CIRCLE_SHA1}"
+
+ grafana-docker-release:
+ docker:
+ - image: docker:stable-git
+ steps:
+ - checkout
+ - attach_workspace:
+ at: .
+ - setup_remote_docker
+ - run: docker info
+ - run: cp dist/grafana-latest.linux-x64.tar.gz packaging/docker
+ - run: cd packaging/docker && ./build-deploy.sh "${CIRCLE_TAG}"
build-enterprise:
docker:
@@ -213,9 +279,6 @@ jobs:
- run:
name: Trigger Windows build
command: './scripts/trigger_windows_build.sh ${APPVEYOR_TOKEN} ${CIRCLE_SHA1} master'
- - run:
- name: Trigger Docker build
- command: './scripts/trigger_docker_build.sh ${TRIGGER_GRAFANA_PACKER_CIRCLECI_TOKEN} master-$(echo "${CIRCLE_SHA1}" | cut -b1-7)'
- run:
name: Publish to Grafana.com
command: |
@@ -237,30 +300,27 @@ jobs:
- run:
name: Trigger Windows build
command: './scripts/trigger_windows_build.sh ${APPVEYOR_TOKEN} ${CIRCLE_SHA1} release'
- - run:
- name: Trigger Docker build
- command: './scripts/trigger_docker_build.sh ${TRIGGER_GRAFANA_PACKER_CIRCLECI_TOKEN} ${CIRCLE_TAG}'
workflows:
version: 2
- test-and-build:
+ build-master:
jobs:
- build-all:
filters: *filter-only-master
- build-enterprise:
filters: *filter-only-master
- codespell:
- filters: *filter-not-release
+ filters: *filter-only-master
- gometalinter:
- filters: *filter-not-release
+ filters: *filter-only-master
- test-frontend:
- filters: *filter-not-release
+ filters: *filter-only-master
- test-backend:
- filters: *filter-not-release
+ filters: *filter-only-master
- mysql-integration-test:
- filters: *filter-not-release
+ filters: *filter-only-master
- postgres-integration-test:
- filters: *filter-not-release
+ filters: *filter-only-master
- deploy-master:
requires:
- build-all
@@ -270,7 +330,17 @@ workflows:
- gometalinter
- mysql-integration-test
- postgres-integration-test
- filters: *filter-only-master
+ filters: *filter-only-master
+ - grafana-docker-master:
+ requires:
+ - build-all
+ - test-backend
+ - test-frontend
+ - codespell
+ - gometalinter
+ - mysql-integration-test
+ - postgres-integration-test
+ filters: *filter-only-master
- deploy-enterprise-master:
requires:
- build-all
@@ -309,3 +379,40 @@ workflows:
- mysql-integration-test
- postgres-integration-test
filters: *filter-only-release
+ - grafana-docker-release:
+ requires:
+ - build-all
+ - test-backend
+ - test-frontend
+ - codespell
+ - gometalinter
+ - mysql-integration-test
+ - postgres-integration-test
+ filters: *filter-only-release
+
+ build-branches-and-prs:
+ jobs:
+ - build:
+ filters: *filter-not-release-or-master
+ - codespell:
+ filters: *filter-not-release-or-master
+ - gometalinter:
+ filters: *filter-not-release-or-master
+ - test-frontend:
+ filters: *filter-not-release-or-master
+ - test-backend:
+ filters: *filter-not-release-or-master
+ - mysql-integration-test:
+ filters: *filter-not-release-or-master
+ - postgres-integration-test:
+ filters: *filter-not-release-or-master
+ - grafana-docker-pr:
+ requires:
+ - build
+ - test-backend
+ - test-frontend
+ - codespell
+ - gometalinter
+ - mysql-integration-test
+ - postgres-integration-test
+ filters: *filter-not-release-or-master
diff --git a/.dockerignore b/.dockerignore
index e50dfd86aa3..c535fa427b5 100644
--- a/.dockerignore
+++ b/.dockerignore
@@ -3,9 +3,12 @@
.git
.gitignore
.github
+.vscode
+bin
data*
dist
docker
+Dockerfile
docs
dump.rdb
node_modules
diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md
index fe0a1d6c548..769ba2a519b 100644
--- a/.github/CONTRIBUTING.md
+++ b/.github/CONTRIBUTING.md
@@ -2,12 +2,12 @@ Follow the setup guide in README.md
### Rebuild frontend assets on source change
```
-grunt && grunt watch
+yarn watch
```
### Rerun tests on source change
```
-grunt karma:dev
+yarn jest
```
### Run tests for backend assets before commit
@@ -17,6 +17,6 @@ test -z "$(gofmt -s -l . | grep -v -E 'vendor/(github.com|golang.org|gopkg.in)'
### Run tests for frontend assets before commit
```
-npm test
+yarn test
go test -v ./pkg/...
```
diff --git a/.gitignore b/.gitignore
index 11df66360d9..78b8d075ef6 100644
--- a/.gitignore
+++ b/.gitignore
@@ -40,8 +40,8 @@ public/css/*.min.css
conf/custom.ini
fig.yml
-docker-compose.yml
-docker-compose.yaml
+devenv/docker-compose.yml
+devenv/docker-compose.yaml
/conf/provisioning/**/custom.yaml
/conf/provisioning/**/dev.yaml
/conf/ldap_dev.toml
@@ -58,6 +58,7 @@ debug.test
/examples/*/dist
/packaging/**/*.rpm
/packaging/**/*.deb
+/packaging/**/*.tar.gz
# Ignore OSX indexing
.DS_Store
@@ -70,4 +71,4 @@ debug.test
/vendor/**/appengine*
*.orig
-/devenv/dashboards/bulk-testing/*.json
+/devenv/bulk-dashboards/*.json
diff --git a/.jscs.json b/.jscs.json
deleted file mode 100644
index 8fdad332de5..00000000000
--- a/.jscs.json
+++ /dev/null
@@ -1,13 +0,0 @@
-{
- "disallowImplicitTypeConversion": ["string"],
- "disallowKeywords": ["with"],
- "disallowMultipleLineBreaks": true,
- "disallowMixedSpacesAndTabs": true,
- "disallowTrailingWhitespace": true,
- "requireSpacesInFunctionExpression": {
- "beforeOpeningCurlyBrace": true
- },
- "disallowSpacesInsideArrayBrackets": true,
- "disallowSpacesInsideParentheses": true,
- "validateIndentation": 2
-}
diff --git a/.jshintrc b/.jshintrc
deleted file mode 100644
index 1d8fad63173..00000000000
--- a/.jshintrc
+++ /dev/null
@@ -1,37 +0,0 @@
-{
- "browser": true,
- "esversion": 6,
- "bitwise":false,
- "curly": true,
- "eqnull": true,
- "strict": false,
- "devel": true,
- "eqeqeq": true,
- "forin": false,
- "immed": true,
- "supernew": true,
- "expr": true,
- "indent": 2,
- "latedef": false,
- "newcap": true,
- "noarg": true,
- "noempty": true,
- "undef": true,
- "boss": true,
- "trailing": true,
- "laxbreak": true,
- "laxcomma": true,
- "sub": true,
- "unused": true,
- "maxdepth": 6,
- "maxlen": 140,
-
- "globals": {
- "System": true,
- "Promise": true,
- "define": true,
- "require": true,
- "Chromath": false,
- "setImmediate": true
- }
-}
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 6409f094f65..ace4348af99 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,28 +1,124 @@
-# 5.3.0 (unreleased)
+# 5.4.0 (unreleased)
-* **Dataproxy**: Pass configured/auth headers to a Datasource [#10971](https://github.com/grafana/grafana/issues/10971), thx [@mrsiano](https://github.com/mrsiano)
-* **Cleanup**: Make temp file time to live configurable [#11607](https://github.com/grafana/grafana/issues/11607), thx [@xapon](https://github.com/xapon)
-* **LDAP**: Define Grafana Admin permission in ldap group mappings [#2469](https://github.com/grafana/grafana/issues/2496), PR [#12622](https://github.com/grafana/grafana/issues/12622)
-* **Cloudwatch**: CloudWatch GetMetricData support [#11487](https://github.com/grafana/grafana/issues/11487), thx [@mtanda](https://github.com/mtanda)
+### New Features
+
+* **Annotations**: Enable template variables in tagged annotations queries [#9735](https://github.com/grafana/grafana/issues/9735)
### Minor
-* **Api**: Delete nonexistent datasource should return 404 [#12313](https://github.com/grafana/grafana/issues/12313), thx [@AustinWinstanley](https://github.com/AustinWinstanley)
-* **Dashboard**: Fix selecting current dashboard from search should not reload dashboard [#12248](https://github.com/grafana/grafana/issues/12248)
-* **Singlestat**: Make colorization of prefix and postfix optional in singlestat [#11892](https://github.com/grafana/grafana/pull/11892), thx [@ApsOps](https://github.com/ApsOps)
-* **Table**: Make table sorting stable when null values exist [#12362](https://github.com/grafana/grafana/pull/12362), thx [@bz2](https://github.com/bz2)
+* **OAuth**: Allow oauth email attribute name to be configurable [#12986](https://github.com/grafana/grafana/issues/12986), thx [@bobmshannon](https://github.com/bobmshannon)
+* **Tags**: Default sort order for GetDashboardTags [#11681](https://github.com/grafana/grafana/pull/11681), thx [@Jonnymcc](https://github.com/Jonnymcc)
+* **Prometheus**: Label completion queries respect dashboard time range [#12251](https://github.com/grafana/grafana/pull/12251), thx [@mtanda](https://github.com/mtanda)
+* **Prometheus**: Allow to display annotations based on Prometheus series value [#10159](https://github.com/grafana/grafana/issues/10159), thx [@mtanda](https://github.com/mtanda)
+* **Prometheus**: Adhoc-filtering for Prometheus dashboards [#13212](https://github.com/grafana/grafana/issues/13212)
+* **Singlestat**: Fix gauge display accuracy for percents [#13270](https://github.com/grafana/grafana/issues/13270), thx [@tianon](https://github.com/tianon)
+
+# 5.3.0 (unreleased)
+
+### Minor
+
+* **Alerting**: Link to view full size image in Microsoft Teams alert notifier [#13121](https://github.com/grafana/grafana/issues/13121), thx [@holiiveira](https://github.com/holiiveira)
+* **Postgres/MySQL/MSSQL**: Add support for replacing $__interval and $__interval_ms in alert queries [#11555](https://github.com/grafana/grafana/issues/11555), thx [@svenklemm](https://github.com/svenklemm)
+
+# 5.3.0-beta1 (2018-09-06)
+
+### New Major Features
+
+* **Alerting**: Notification reminders [#7330](https://github.com/grafana/grafana/issues/7330), thx [@jbaublitz](https://github.com/jbaublitz)
+* **Dashboard**: TV & Kiosk mode changes, new cycle view mode button in dashboard toolbar [#13025](https://github.com/grafana/grafana/pull/13025)
+* **OAuth**: Gitlab OAuth with support for filter by groups [#5623](https://github.com/grafana/grafana/issues/5623), thx [@BenoitKnecht](https://github.com/BenoitKnecht)
+* **Postgres**: Graphical query builder [#10095](https://github.com/grafana/grafana/issues/10095), thx [svenklemm](https://github.com/svenklemm)
+
+### New Features
+
+* **LDAP**: Define Grafana Admin permission in ldap group mappings [#2469](https://github.com/grafana/grafana/issues/2496), PR [#12622](https://github.com/grafana/grafana/issues/12622)
+* **LDAP**: Client certificates support [#12805](https://github.com/grafana/grafana/issues/12805), thx [@nyxi](https://github.com/nyxi)
+* **Profile**: List teams that the user is member of in current/active organization [#12476](https://github.com/grafana/grafana/issues/12476)
+* **Configuration**: Allow auto-assigning users to specific organization (other than Main. Org) [#1823](https://github.com/grafana/grafana/issues/1823) [#12801](https://github.com/grafana/grafana/issues/12801), thx [@gzzo](https://github.com/gzzo) and [@ofosos](https://github.com/ofosos)
+* **Dataproxy**: Pass configured/auth headers to a Datasource [#10971](https://github.com/grafana/grafana/issues/10971), thx [@mrsiano](https://github.com/mrsiano)
+* **Cloudwatch**: CloudWatch GetMetricData support [#11487](https://github.com/grafana/grafana/issues/11487), thx [@mtanda](https://github.com/mtanda)
+* **Postgres**: TimescaleDB support, e.g. use `time_bucket` for grouping by time when option enabled [#12680](https://github.com/grafana/grafana/pull/12680), thx [svenklemm](https://github.com/svenklemm)
+* **Cleanup**: Make temp file time to live configurable [#11607](https://github.com/grafana/grafana/issues/11607), thx [@xapon](https://github.com/xapon)
+
+### Minor
+
+* **Alerting**: Its now possible to configure the default value for how to handle errors and no data in alerting. [#10424](https://github.com/grafana/grafana/issues/10424)
+* **Alerting**: Fix diff and percent_diff reducers [#11563](https://github.com/grafana/grafana/issues/11563), thx [@jessetane](https://github.com/jessetane)
+* **Alerting**: Fix rendering timeout which could cause notifications to not be sent due to rendering timing out [#12151](https://github.com/grafana/grafana/issues/12151)
+* **Docker**: Make it possible to set a specific plugin url [#12861](https://github.com/grafana/grafana/pull/12861), thx [ClementGautier](https://github.com/ClementGautier)
+* **GrafanaCli**: Fixed issue with grafana-cli install plugin resulting in corrupt http response from source error. Fixes [#13079](https://github.com/grafana/grafana/issues/13079)
+* **Provisioning**: Should allow one default datasource per organisation [#12229](https://github.com/grafana/grafana/issues/12229)
+* **Github OAuth**: Allow changes of user info at Github to be synched to Grafana when signing in [#11818](https://github.com/grafana/grafana/issues/11818), thx [@rwaweber](https://github.com/rwaweber)
+* **OAuth**: Fix overriding tls_skip_verify_insecure using environment variable [#12747](https://github.com/grafana/grafana/issues/12747), thx [@jangaraj](https://github.com/jangaraj)
* **Prometheus**: Fix graph panel bar width issue in aligned prometheus queries [#12379](https://github.com/grafana/grafana/issues/12379)
* **Prometheus**: Heatmap - fix unhandled error when some points are missing [#12484](https://github.com/grafana/grafana/issues/12484)
-* **Prometheus**: Add $interval, $interval_ms, $range, and $range_ms support for dashboard and template queries [#12597](https://github.com/grafana/grafana/issues/12597)
-* **Variables**: Skip unneeded extra query request when de-selecting variable values used for repeated panels [#8186](https://github.com/grafana/grafana/issues/8186), thx [@mtanda](https://github.com/mtanda)
+* **Prometheus**: Add $__interval, $__interval_ms, $__range, $__range_s & $__range_ms support for dashboard and template queries [#12597](https://github.com/grafana/grafana/issues/12597) [#12882](https://github.com/grafana/grafana/issues/12882), thx [@roidelapluie](https://github.com/roidelapluie)
+* **Elasticsearch**: For alerting/backend, support having index name to the right of pattern in index pattern [#12731](https://github.com/grafana/grafana/issues/12731)
+* **Graphite**: Fix for quoting of int function parameters (when using variables) [#11927](https://github.com/grafana/grafana/pull/11927)
+* **InfluxDB**: Support timeFilter in query templating for InfluxDB [#12598](https://github.com/grafana/grafana/pull/12598), thx [kichristensen](https://github.com/kichristensen)
+* **Postgres/MySQL/MSSQL**: New $__unixEpochGroup and $__unixEpochGroupAlias macros [#12892](https://github.com/grafana/grafana/issues/12892), thx [@svenklemm](https://github.com/svenklemm)
+* **Postgres/MySQL/MSSQL**: Add previous fill mode to $__timeGroup macro which will fill in previously seen value when point is missing [#12756](https://github.com/grafana/grafana/issues/12756), thx [@svenklemm](https://github.com/svenklemm)
* **Postgres/MySQL/MSSQL**: Use floor rounding in $__timeGroup macro function [#12460](https://github.com/grafana/grafana/issues/12460), thx [@svenklemm](https://github.com/svenklemm)
+* **Postgres/MySQL/MSSQL**: Use metric column as prefix when returning multiple value columns [#12727](https://github.com/grafana/grafana/issues/12727), thx [@svenklemm](https://github.com/svenklemm)
+* **Postgres/MySQL/MSSQL**: New $__timeGroupAlias macro. Postgres $__timeGroup no longer automatically adds time column alias [#12749](https://github.com/grafana/grafana/issues/12749), thx [@svenklemm](https://github.com/svenklemm)
+* **Postgres/MySQL/MSSQL**: Escape single quotes in variables [#12785](https://github.com/grafana/grafana/issues/12785), thx [@eMerzh](https://github.com/eMerzh)
+* **Postgres/MySQL/MSSQL**: Min time interval support [#13157](https://github.com/grafana/grafana/issues/13157), thx [@svenklemm](https://github.com/svenklemm)
* **MySQL/MSSQL**: Use datetime format instead of epoch for $__timeFilter, $__timeFrom and $__timeTo macros [#11618](https://github.com/grafana/grafana/issues/11618) [#11619](https://github.com/grafana/grafana/issues/11619), thx [@AustinWinstanley](https://github.com/AustinWinstanley)
* **Postgres**: Escape ssl mode parameter in connectionstring [#12644](https://github.com/grafana/grafana/issues/12644), thx [@yogyrahmawan](https://github.com/yogyrahmawan)
-* **Github OAuth**: Allow changes of user info at Github to be synched to Grafana when signing in [#11818](https://github.com/grafana/grafana/issues/11818), thx [@rwaweber](https://github.com/rwaweber)
-* **Alerting**: Fix diff and percent_diff reducers [#11563](https://github.com/grafana/grafana/issues/11563), thx [@jessetane](https://github.com/jessetane)
-* **Units**: Polish złoty currency [#12691](https://github.com/grafana/grafana/pull/12691), thx [@mwegrzynek](https://github.com/mwegrzynek)
* **Cloudwatch**: Improved error handling [#12489](https://github.com/grafana/grafana/issues/12489), thx [@mtanda](https://github.com/mtanda)
+* **Cloudwatch**: AppSync metrics and dimensions [#12300](https://github.com/grafana/grafana/issues/12300), thx [@franciscocpg](https://github.com/franciscocpg)
+* **Cloudwatch**: Direct Connect metrics and dimensions [#12762](https://github.com/grafana/grafana/pulls/12762), thx [@mindriot88](https://github.com/mindriot88)
+* **Cloudwatch**: Added BurstBalance metric to list of AWS RDS metrics [#12561](https://github.com/grafana/grafana/pulls/12561), thx [@activeshadow](https://github.com/activeshadow)
+* **Cloudwatch**: Add new Redshift metrics and dimensions [#12063](https://github.com/grafana/grafana/pulls/12063), thx [@A21z](https://github.com/A21z)
+* **Dashboard**: Fix selecting current dashboard from search should not reload dashboard [#12248](https://github.com/grafana/grafana/issues/12248)
+* **Dashboard**: Use uid when linking to dashboards internally in a dashboard [#10705](https://github.com/grafana/grafana/issues/10705)
+* **Graph**: Option to hide series from tooltip [#3341](https://github.com/grafana/grafana/issues/3341), thx [@mtanda](https://github.com/mtanda)
+* **Singlestat**: Make colorization of prefix and postfix optional in singlestat [#11892](https://github.com/grafana/grafana/pull/11892), thx [@ApsOps](https://github.com/ApsOps)
* **Table**: Adjust header contrast for the light theme [#12668](https://github.com/grafana/grafana/issues/12668)
+* **Table**: Fix link color when using light theme and thresholds in use [#12766](https://github.com/grafana/grafana/issues/12766)
+* **Table**: Fix for useless horizontal scrollbar for table panel [#9964](https://github.com/grafana/grafana/issues/9964)
+* **Table**: Make table sorting stable when null values exist [#12362](https://github.com/grafana/grafana/pull/12362), thx [@bz2](https://github.com/bz2)
+* **Heatmap**: Fix broken tooltip and crosshair on Firefox [#12486](https://github.com/grafana/grafana/issues/12486)
+* **Datasource**: Fix UI issue with secret fields after updating datasource [#11270](https://github.com/grafana/grafana/issues/11270)
+* **Variables**: Skip unneeded extra query request when de-selecting variable values used for repeated panels [#8186](https://github.com/grafana/grafana/issues/8186), thx [@mtanda](https://github.com/mtanda)
+* **Variables**: Limit amount of queries executed when updating variable that other variable(s) are dependent on [#11890](https://github.com/grafana/grafana/issues/11890)
+* **Variables**: Support query variable refresh when another variable referenced in `Regex` field change its value [#12952](https://github.com/grafana/grafana/issues/12952), thx [@franciscocpg](https://github.com/franciscocpg)
+* **Variables**: Support variables in query variable `Custom all value` field [#12965](https://github.com/grafana/grafana/issues/12965), thx [@franciscocpg](https://github.com/franciscocpg)
+* **Units**: Change units to include characters for power of 2 and 3 [#12744](https://github.com/grafana/grafana/pull/12744), thx [@Worty](https://github.com/Worty)
+* **Units**: Polish złoty currency [#12691](https://github.com/grafana/grafana/pull/12691), thx [@mwegrzynek](https://github.com/mwegrzynek)
+* **Units**: Adds bitcoin axes unit. [#13125](https://github.com/grafana/grafana/pull/13125)
+* **Api**: Delete nonexistent datasource should return 404 [#12313](https://github.com/grafana/grafana/issues/12313), thx [@AustinWinstanley](https://github.com/AustinWinstanley)
+* **Logging**: Reopen log files after receiving a SIGHUP signal [#13112](https://github.com/grafana/grafana/pull/13112), thx [@filewalkwithme](https://github.com/filewalkwithme)
+* **Login**: Show loading animation while waiting for authentication response on login [#12865](https://github.com/grafana/grafana/issues/12865)
+* **UI**: Fix iOS home screen "app" icon and Windows 10 app experience [#12752](https://github.com/grafana/grafana/issues/12752), thx [@andig](https://github.com/andig)
+* **Plugins**: Convert URL-like text to links in plugins readme [#12843](https://github.com/grafana/grafana/pull/12843), thx [pgiraud](https://github.com/pgiraud)
+
+### Breaking changes
+
+* Postgres datasource no longer automatically adds time column alias when using the $__timeGroup alias. However, there's code in place which should make this change backward compatible and shouldn't create any issues.
+* Kiosk mode now also hides submenu (variables)
+* ?inactive url parameter no longer supported, replaced with kiosk=tv url parameter
+
+### New experimental features
+
+These are new features that's still being worked on and are in an experimental phase. We encourage users to try these out and provide any feedback in related issue.
+
+* **Dashboard**: Auto fit dashboard panels to optimize space used for current TV / Monitor [#12768](https://github.com/grafana/grafana/issues/12768)
+
+### Tech
+
+* **Frontend**: Convert all Frontend Karma tests to Jest tests [#12224](https://github.com/grafana/grafana/issues/12224)
+* **Backend**: Upgrade to golang 1.11 [#13030](https://github.com/grafana/grafana/issues/13030)
+
+# 5.2.4 (2018-09-07)
+
+* **GrafanaCli**: Fixed issue with grafana-cli install plugin resulting in corrupt http response from source error. Fixes [#13079](https://github.com/grafana/grafana/issues/13079)
+
+# 5.2.3 (2018-08-29)
+
+### Important fix for LDAP & OAuth login vulnerability
+
+See [security announcement](https://community.grafana.com/t/grafana-5-2-3-and-4-6-4-security-update/10050) for details.
# 5.2.2 (2018-07-25)
@@ -222,7 +318,7 @@
* **Dashboard**: Sizing and positioning of settings menu icons [#11572](https://github.com/grafana/grafana/pull/11572)
* **Dashboard**: Add search filter/tabs to new panel control [#10427](https://github.com/grafana/grafana/issues/10427)
* **Folders**: User with org viewer role should not be able to save/move dashboards in/to general folder [#11553](https://github.com/grafana/grafana/issues/11553)
-* **Influxdb**: Dont assume the first column in table response is time. [#11476](https://github.com/grafana/grafana/issues/11476), thx [@hahnjo](https://github.com/hahnjo)
+* **Influxdb**: Don't assume the first column in table response is time. [#11476](https://github.com/grafana/grafana/issues/11476), thx [@hahnjo](https://github.com/hahnjo)
### Tech
* Backend code simplification [#11613](https://github.com/grafana/grafana/pull/11613), thx [@knweiss](https://github.com/knweiss)
@@ -392,6 +488,12 @@ The following properties have been deprecated and will be removed in a future re
- `uri` property in `GET /api/search` -> Use new `url` or `uid` property instead
- `meta.slug` property in `GET /api/dashboards/uid/:uid` and `GET /api/dashboards/db/:slug` -> Use new `meta.url` or `dashboard.uid` property instead
+# 4.6.4 (2018-08-29)
+
+### Important fix for LDAP & OAuth login vulnerability
+
+See [security announcement](https://community.grafana.com/t/grafana-5-2-3-and-4-6-4-security-update/10050) for details.
+
# 4.6.3 (2017-12-14)
## Fixes
@@ -1362,7 +1464,7 @@ Grafana 2.x is fundamentally different from 1.x; it now ships with an integrated
**New features**
- [Issue #1623](https://github.com/grafana/grafana/issues/1623). Share Dashboard: Dashboard snapshot sharing (dash and data snapshot), save to local or save to public snapshot dashboard snapshots.raintank.io site
-- [Issue #1622](https://github.com/grafana/grafana/issues/1622). Share Panel: The share modal now has an embed option, gives you an iframe that you can use to embedd a single graph on another web site
+- [Issue #1622](https://github.com/grafana/grafana/issues/1622). Share Panel: The share modal now has an embed option, gives you an iframe that you can use to embed a single graph on another web site
- [Issue #718](https://github.com/grafana/grafana/issues/718). Dashboard: When saving a dashboard and another user has made changes in between the user is prompted with a warning if he really wants to overwrite the other's changes
- [Issue #1331](https://github.com/grafana/grafana/issues/1331). Graph & Singlestat: New axis/unit format selector and more units (kbytes, Joule, Watt, eV), and new design for graph axis & grid tab and single stat options tab views
- [Issue #1241](https://github.com/grafana/grafana/issues/1242). Timepicker: New option in timepicker (under dashboard settings), to change ``now`` to be for example ``now-1m``, useful when you want to ignore last minute because it contains incomplete data
diff --git a/Dockerfile b/Dockerfile
new file mode 100644
index 00000000000..28dd71952af
--- /dev/null
+++ b/Dockerfile
@@ -0,0 +1,82 @@
+# Golang build container
+FROM golang:1.11
+
+WORKDIR $GOPATH/src/github.com/grafana/grafana
+
+COPY Gopkg.toml Gopkg.lock ./
+COPY vendor vendor
+
+ARG DEP_ENSURE=""
+RUN if [ ! -z "${DEP_ENSURE}" ]; then \
+ go get -u github.com/golang/dep/cmd/dep && \
+ dep ensure --vendor-only; \
+ fi
+
+COPY pkg pkg
+COPY build.go build.go
+COPY package.json package.json
+
+RUN go run build.go build
+
+# Node build container
+FROM node:8
+
+WORKDIR /usr/src/app/
+
+COPY package.json yarn.lock ./
+RUN yarn install --pure-lockfile --no-progress
+
+COPY Gruntfile.js tsconfig.json tslint.json ./
+COPY public public
+COPY scripts scripts
+COPY emails emails
+
+ENV NODE_ENV production
+RUN ./node_modules/.bin/grunt build
+
+# Final container
+FROM debian:stretch-slim
+
+ARG GF_UID="472"
+ARG GF_GID="472"
+
+ENV PATH=/usr/share/grafana/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin \
+ GF_PATHS_CONFIG="/etc/grafana/grafana.ini" \
+ GF_PATHS_DATA="/var/lib/grafana" \
+ GF_PATHS_HOME="/usr/share/grafana" \
+ GF_PATHS_LOGS="/var/log/grafana" \
+ GF_PATHS_PLUGINS="/var/lib/grafana/plugins" \
+ GF_PATHS_PROVISIONING="/etc/grafana/provisioning"
+
+WORKDIR $GF_PATHS_HOME
+
+RUN apt-get update && apt-get install -qq -y libfontconfig ca-certificates && \
+ apt-get autoremove -y && \
+ rm -rf /var/lib/apt/lists/*
+
+COPY conf ./conf
+
+RUN mkdir -p "$GF_PATHS_HOME/.aws" && \
+ groupadd -r -g $GF_GID grafana && \
+ useradd -r -u $GF_UID -g grafana grafana && \
+ mkdir -p "$GF_PATHS_PROVISIONING/datasources" \
+ "$GF_PATHS_PROVISIONING/dashboards" \
+ "$GF_PATHS_LOGS" \
+ "$GF_PATHS_PLUGINS" \
+ "$GF_PATHS_DATA" && \
+ cp "$GF_PATHS_HOME/conf/sample.ini" "$GF_PATHS_CONFIG" && \
+ cp "$GF_PATHS_HOME/conf/ldap.toml" /etc/grafana/ldap.toml && \
+ chown -R grafana:grafana "$GF_PATHS_DATA" "$GF_PATHS_HOME/.aws" "$GF_PATHS_LOGS" "$GF_PATHS_PLUGINS" && \
+ chmod 777 "$GF_PATHS_DATA" "$GF_PATHS_HOME/.aws" "$GF_PATHS_LOGS" "$GF_PATHS_PLUGINS"
+
+COPY --from=0 /go/src/github.com/grafana/grafana/bin/linux-amd64/grafana-server /go/src/github.com/grafana/grafana/bin/linux-amd64/grafana-cli ./bin/
+COPY --from=1 /usr/src/app/public ./public
+COPY --from=1 /usr/src/app/tools ./tools
+COPY tools/phantomjs/render.js ./tools/phantomjs/render.js
+
+EXPOSE 3000
+
+COPY ./packaging/docker/run.sh /run.sh
+
+USER grafana
+ENTRYPOINT [ "/run.sh" ]
diff --git a/Gopkg.lock b/Gopkg.lock
index 6f08e208ecd..bd247d691dd 100644
--- a/Gopkg.lock
+++ b/Gopkg.lock
@@ -427,12 +427,6 @@
revision = "1744e2970ca51c86172c8190fadad617561ed6e7"
version = "v1.0.0"
-[[projects]]
- branch = "master"
- name = "github.com/shurcooL/sanitized_anchor_name"
- packages = ["."]
- revision = "86672fcb3f950f35f2e675df2240550f2a50762f"
-
[[projects]]
name = "github.com/smartystreets/assertions"
packages = [
@@ -679,6 +673,6 @@
[solve-meta]
analyzer-name = "dep"
analyzer-version = 1
- inputs-digest = "cb8e7fd81f23ec987fc4d5dd9d31ae0f1164bc2f30cbea2fe86e0d97dd945beb"
+ inputs-digest = "81a37e747b875cf870c1b9486fa3147e704dea7db8ba86f7cb942d3ddc01d3e3"
solver-name = "gps-cdcl"
solver-version = 1
diff --git a/Gruntfile.js b/Gruntfile.js
index 23276e8a122..2d5990b5f58 100644
--- a/Gruntfile.js
+++ b/Gruntfile.js
@@ -1,4 +1,3 @@
-/* jshint node:true */
'use strict';
module.exports = function (grunt) {
var os = require('os');
@@ -26,7 +25,6 @@ module.exports = function (grunt) {
}
}
- config.coverage = grunt.option('coverage');
config.phjs = grunt.option('phjsToRelease');
config.pkg.version = grunt.option('pkgVer') || config.pkg.version;
diff --git a/Makefile b/Makefile
index c1d755d247d..c9e51d897f3 100644
--- a/Makefile
+++ b/Makefile
@@ -24,6 +24,15 @@ build-js:
build: build-go build-js
+build-docker-dev:
+ @echo "\033[92mInfo:\033[0m the frontend code is expected to be built already."
+ go run build.go -goos linux -pkg-arch amd64 ${OPT} build package-only latest
+ cp dist/grafana-latest.linux-x64.tar.gz packaging/docker
+ cd packaging/docker && docker build --tag grafana/grafana:dev .
+
+build-docker-full:
+ docker build --tag grafana/grafana:dev .
+
test-go:
go test -v ./pkg/...
@@ -34,6 +43,3 @@ test: test-go test-js
run:
./bin/grafana-server
-
-protoc:
- protoc -I pkg/tsdb/models pkg/tsdb/models/*.proto --go_out=plugins=grpc:pkg/tsdb/models/.
\ No newline at end of file
diff --git a/NOTICE.md b/NOTICE.md
index ca148971b62..899b2a3c3f9 100644
--- a/NOTICE.md
+++ b/NOTICE.md
@@ -1,5 +1,5 @@
-Copyright 2014-2017 Grafana Labs
+Copyright 2014-2018 Grafana Labs
This software is based on Kibana:
Copyright 2012-2013 Elasticsearch BV
diff --git a/README.md b/README.md
index 322523d703b..133d9e50d07 100644
--- a/README.md
+++ b/README.md
@@ -24,7 +24,7 @@ the latest master builds [here](https://grafana.com/grafana/download)
### Dependencies
-- Go 1.10
+- Go 1.11
- NodeJS LTS
### Building the backend
@@ -43,7 +43,7 @@ To build the assets, rebuild on file change, and serve them by Grafana's webserv
```bash
npm install -g yarn
yarn install --pure-lockfile
-npm run watch
+yarn watch
```
Build the assets, rebuild on file change with Hot Module Replacement (HMR), and serve them by webpack-dev-server (http://localhost:3333):
@@ -54,14 +54,9 @@ env GRAFANA_THEME=light yarn start
```
Note: HMR for Angular is not supported. If you edit files in the Angular part of the app, the whole page will reload.
-Run tests
+Run tests
```bash
-npm run jest
-```
-
-Run karma tests
-```bash
-npm run karma
+yarn jest
```
### Recompile backend on source change
@@ -74,6 +69,15 @@ bra run
Open grafana in your browser (default: `http://localhost:3000`) and login with admin user (default: `user/pass = admin/admin`).
+### Building a docker image (on linux/amd64)
+
+This builds a docker image from your local sources:
+
+1. Build the frontend `go run build.go build-frontend`
+2. Build the docker image `make build-docker-dev`
+
+The resulting image will be tagged as `grafana/grafana:dev`
+
### Dev config
Create a custom.ini in the conf directory to override default configuration options.
@@ -89,30 +93,38 @@ In your custom.ini uncomment (remove the leading `;`) sign. And set `app_mode =
#### Frontend
Execute all frontend tests
```bash
-npm run test
+yarn test
```
-Writing & watching frontend tests (we have two test runners)
+Writing & watching frontend tests
-- jest for all new tests that do not require browser context (React+more)
- - Start watcher: `npm run jest`
- - Jest will run all test files that end with the name ".jest.ts"
-- karma + mocha is used for testing angularjs components. We do want to migrate these test to jest over time (if possible).
- - Start watcher: `npm run karma`
- - Karma+Mocha runs all files that end with the name "_specs.ts".
+- Start watcher: `yarn jest`
+- Jest will run all test files that end with the name ".test.ts"
#### Backend
```bash
# Run Golang tests using sqlite3 as database (default)
-go test ./pkg/...
+go test ./pkg/...
# Run Golang tests using mysql as database - convenient to use /docker/blocks/mysql_tests
-GRAFANA_TEST_DB=mysql go test ./pkg/...
+GRAFANA_TEST_DB=mysql go test ./pkg/...
# Run Golang tests using postgres as database - convenient to use /docker/blocks/postgres_tests
-GRAFANA_TEST_DB=postgres go test ./pkg/...
+GRAFANA_TEST_DB=postgres go test ./pkg/...
```
+## Building custom docker image
+
+You can build a custom image using Docker, which doesn't require installing any dependencies besides docker itself.
+```bash
+git clone https://github.com/grafana/grafana
+cd grafana
+docker build -t grafana:dev .
+docker run -d --name=grafana -p 3000:3000 grafana:dev
+```
+
+Open grafana in your browser (default: `http://localhost:3000`) and login with admin user (default: `user/pass = admin/admin`).
+
## Contribute
If you have any idea for an improvement or found a bug, do not hesitate to open an issue.
diff --git a/ROADMAP.md b/ROADMAP.md
index 6f8111fd2d4..891bc9f790b 100644
--- a/ROADMAP.md
+++ b/ROADMAP.md
@@ -1,9 +1,10 @@
-# Roadmap (2018-06-26)
+# Roadmap (2018-08-07)
This roadmap is a tentative plan for the core development team. Things change constantly as PRs come in and priorities change.
But it will give you an idea of our current vision and plan.
### Short term (1-2 months)
+ - PRs & Bugs
- Multi-Stat panel
- Metrics & Log Explore UI
@@ -11,17 +12,16 @@ But it will give you an idea of our current vision and plan.
- React Panels
- Change visualization (panel type) on the fly.
- Templating Query Editor UI Plugin hook
+ - Backend plugins
### Long term (4 - 8 months)
-
-- Alerting improvements (silence, per series tracking, etc)
-- Progress on React migration
+ - Alerting improvements (silence, per series tracking, etc)
+ - Progress on React migration
### In a distant future far far away
-
-- Meta queries
-- Integrated light weight TSDB
-- Web socket & live data sources
+ - Meta queries
+ - Integrated light weight TSDB
+ - Web socket & live data sources
### Outside contributions
We know this is being worked on right now by contributors (and we hope to merge it when it's ready).
diff --git a/appveyor.yml b/appveyor.yml
index 5cdec1b8bf5..52f23162033 100644
--- a/appveyor.yml
+++ b/appveyor.yml
@@ -7,7 +7,7 @@ clone_folder: c:\gopath\src\github.com\grafana\grafana
environment:
nodejs_version: "6"
GOPATH: C:\gopath
- GOVERSION: 1.10
+ GOVERSION: 1.11
install:
- rmdir c:\go /s /q
diff --git a/build.go b/build.go
index bcb9b2ddf7d..9502f52be11 100644
--- a/build.go
+++ b/build.go
@@ -64,6 +64,10 @@ func main() {
readVersionFromPackageJson()
+ if pkgArch == "" {
+ pkgArch = goarch
+ }
+
log.Printf("Version: %s, Linux Version: %s, Package Iteration: %s\n", version, linuxPackageVersion, linuxPackageIteration)
if flag.NArg() == 0 {
@@ -105,10 +109,16 @@ func main() {
case "package":
grunt(gruntBuildArg("build")...)
- packageGrafana()
+ grunt(gruntBuildArg("package")...)
+ if goos == "linux" {
+ createLinuxPackages()
+ }
case "package-only":
- packageGrafana()
+ grunt(gruntBuildArg("package")...)
+ if goos == "linux" {
+ createLinuxPackages()
+ }
case "pkg-rpm":
grunt(gruntBuildArg("release")...)
@@ -133,22 +143,6 @@ func main() {
}
}
-func packageGrafana() {
- platformArg := fmt.Sprintf("--platform=%v", goos)
- previousPkgArch := pkgArch
- if pkgArch == "" {
- pkgArch = goarch
- }
- postProcessArgs := gruntBuildArg("package")
- postProcessArgs = append(postProcessArgs, platformArg)
- grunt(postProcessArgs...)
- pkgArch = previousPkgArch
-
- if goos == "linux" {
- createLinuxPackages()
- }
-}
-
func makeLatestDistCopies() {
files, err := ioutil.ReadDir("dist")
if err != nil {
@@ -404,6 +398,8 @@ func gruntBuildArg(task string) []string {
if phjsToRelease != "" {
args = append(args, fmt.Sprintf("--phjsToRelease=%v", phjsToRelease))
}
+ args = append(args, fmt.Sprintf("--platform=%v", goos))
+
return args
}
@@ -420,7 +416,7 @@ func test(pkg string) {
func build(binaryName, pkg string, tags []string) {
binary := fmt.Sprintf("./bin/%s-%s/%s", goos, goarch, binaryName)
if isDev {
- //dont include os and arch in output path in dev environment
+ //don't include os and arch in output path in dev environment
binary = fmt.Sprintf("./bin/%s", binaryName)
}
diff --git a/codecov.yml b/codecov.yml
deleted file mode 100644
index b2a839365ac..00000000000
--- a/codecov.yml
+++ /dev/null
@@ -1,11 +0,0 @@
-coverage:
- precision: 2
- round: down
- range: "50...100"
-
- status:
- project: yes
- patch: yes
- changes: no
-
-comment: off
diff --git a/conf/defaults.ini b/conf/defaults.ini
index 5faba3ea7bd..15b8927e65a 100644
--- a/conf/defaults.ini
+++ b/conf/defaults.ini
@@ -213,6 +213,9 @@ allow_org_create = false
# Set to true to automatically assign new users to the default organization (id 1)
auto_assign_org = true
+# Set this value to automatically add new users to the provided organization (if auto_assign_org above is set to true)
+auto_assign_org_id = 1
+
# Default role new users will be automatically assigned (if auto_assign_org above is set to true)
auto_assign_org_role = Viewer
@@ -267,6 +270,18 @@ api_url = https://api.github.com/user
team_ids =
allowed_organizations =
+#################################### GitLab Auth #########################
+[auth.gitlab]
+enabled = false
+allow_sign_up = true
+client_id = some_id
+client_secret = some_secret
+scopes = api
+auth_url = https://gitlab.com/oauth/authorize
+token_url = https://gitlab.com/oauth/token
+api_url = https://gitlab.com/api/v4
+allowed_groups =
+
#################################### Google Auth #########################
[auth.google]
enabled = false
@@ -306,11 +321,16 @@ allow_sign_up = true
client_id = some_id
client_secret = some_secret
scopes = user:email
+email_attribute_name = email:primary
auth_url =
token_url =
api_url =
team_ids =
allowed_organizations =
+tls_skip_verify_insecure = false
+tls_client_cert =
+tls_client_key =
+tls_client_ca =
#################################### Basic Auth ##########################
[auth.basic]
@@ -448,6 +468,12 @@ enabled = true
# Makes it possible to turn off alert rule execution but alerting UI is visible
execute_alerts = true
+# Default setting for new alert rules. Defaults to categorize error and timeouts as alerting. (alerting, keep_state)
+error_or_timeout = alerting
+
+# Default setting for how Grafana handles nodata or null values in alerting. (alerting, no_data, keep_state, ok)
+nodata_or_nullvalues = no_data
+
#################################### Explore #############################
[explore]
# Enable the Explore section
@@ -519,3 +545,8 @@ container_name =
[external_image_storage.local]
# does not require any configuration
+
+[rendering]
+# Options to configure external image rendering server like https://github.com/grafana/grafana-image-renderer
+server_url =
+callback_url =
diff --git a/conf/ldap.toml b/conf/ldap.toml
index a74b2b6cc2c..b684f2556d5 100644
--- a/conf/ldap.toml
+++ b/conf/ldap.toml
@@ -15,6 +15,9 @@ start_tls = false
ssl_skip_verify = false
# set to the path to your root CA certificate or leave unset to use system defaults
# root_ca_cert = "/path/to/certificate.crt"
+# Authentication against LDAP servers requiring client certificates
+# client_cert = "/path/to/client.crt"
+# client_key = "/path/to/client.key"
# Search user bind dn
bind_dn = "cn=admin,dc=grafana,dc=org"
@@ -28,37 +31,11 @@ search_filter = "(cn=%s)"
# An array of base dns to search through
search_base_dns = ["dc=grafana,dc=org"]
-# In POSIX LDAP schemas, without memberOf attribute a secondary query must be made for groups.
-# This is done by enabling group_search_filter below. You must also set member_of= "cn"
-# in [servers.attributes] below.
-
-# Users with nested/recursive group membership and an LDAP server that supports LDAP_MATCHING_RULE_IN_CHAIN
-# can set group_search_filter, group_search_filter_user_attribute, group_search_base_dns and member_of
-# below in such a way that the user's recursive group membership is considered.
-#
-# Nested Groups + Active Directory (AD) Example:
-#
-# AD groups store the Distinguished Names (DNs) of members, so your filter must
-# recursively search your groups for the authenticating user's DN. For example:
-#
-# group_search_filter = "(member:1.2.840.113556.1.4.1941:=%s)"
-# group_search_filter_user_attribute = "distinguishedName"
-# group_search_base_dns = ["ou=groups,dc=grafana,dc=org"]
-#
-# [servers.attributes]
-# ...
-# member_of = "distinguishedName"
-
-## Group search filter, to retrieve the groups of which the user is a member (only set if memberOf attribute is not available)
+## For Posix or LDAP setups that does not support member_of attribute you can define the below settings
+## Please check grafana LDAP docs for examples
# group_search_filter = "(&(objectClass=posixGroup)(memberUid=%s))"
-## Group search filter user attribute defines what user attribute gets substituted for %s in group_search_filter.
-## Defaults to the value of username in [server.attributes]
-## Valid options are any of your values in [servers.attributes]
-## If you are using nested groups you probably want to set this and member_of in
-## [servers.attributes] to "distinguishedName"
-# group_search_filter_user_attribute = "distinguishedName"
-## An array of the base DNs to search through for groups. Typically uses ou=groups
# group_search_base_dns = ["ou=groups,dc=grafana,dc=org"]
+# group_search_filter_user_attribute = "uid"
# Specify names of the ldap attributes your ldap uses
[servers.attributes]
diff --git a/conf/sample.ini b/conf/sample.ini
index 87544a5ac39..2ef254f79b9 100644
--- a/conf/sample.ini
+++ b/conf/sample.ini
@@ -272,6 +272,10 @@ log_queries =
;api_url = https://foo.bar/user
;team_ids =
;allowed_organizations =
+;tls_skip_verify_insecure = false
+;tls_client_cert =
+;tls_client_key =
+;tls_client_ca =
#################################### Grafana.com Auth ####################
[auth.grafana_com]
@@ -383,6 +387,12 @@ log_queries =
# Makes it possible to turn off alert rule execution but alerting UI is visible
;execute_alerts = true
+# Default setting for new alert rules. Defaults to categorize error and timeouts as alerting. (alerting, keep_state)
+;error_or_timeout = alerting
+
+# Default setting for how Grafana handles nodata or null values in alerting. (alerting, no_data, keep_state, ok)
+;nodata_or_nullvalues = no_data
+
#################################### Explore #############################
[explore]
# Enable the Explore section
@@ -456,3 +466,8 @@ log_queries =
[external_image_storage.local]
# does not require any configuration
+
+[rendering]
+# Options to configure external image rendering server like https://github.com/grafana/grafana-image-renderer
+;server_url =
+;callback_url =
diff --git a/scripts/benchmarks/ab/ab_test.sh b/devenv/benchmarks/ab/ab_test.sh
similarity index 100%
rename from scripts/benchmarks/ab/ab_test.sh
rename to devenv/benchmarks/ab/ab_test.sh
diff --git a/devenv/bulk-dashboards/bulk-dashboards.yaml b/devenv/bulk-dashboards/bulk-dashboards.yaml
index e0ba8a88e68..65557901f42 100644
--- a/devenv/bulk-dashboards/bulk-dashboards.yaml
+++ b/devenv/bulk-dashboards/bulk-dashboards.yaml
@@ -5,5 +5,5 @@ providers:
folder: 'Bulk dashboards'
type: file
options:
- path: devenv/dashboards/bulk-testing
+ path: devenv/bulk-dashboards
diff --git a/docker/create_docker_compose.sh b/devenv/create_docker_compose.sh
similarity index 94%
rename from docker/create_docker_compose.sh
rename to devenv/create_docker_compose.sh
index 9d28ede8e7e..5da9e8f5c8f 100755
--- a/docker/create_docker_compose.sh
+++ b/devenv/create_docker_compose.sh
@@ -1,13 +1,13 @@
#!/bin/bash
-blocks_dir=blocks
+blocks_dir=docker/blocks
docker_dir=docker
template_dir=templates
grafana_config_file=conf.tmp
grafana_config=config
-compose_header_file=compose_header.yml
+compose_header_file=docker/compose_header.yml
fig_file=docker-compose.yaml
fig_config=docker-compose.yaml
diff --git a/devenv/datasources.yaml b/devenv/datasources.yaml
index 241381097b1..a4e9bf05641 100644
--- a/devenv/datasources.yaml
+++ b/devenv/datasources.yaml
@@ -51,12 +51,28 @@ datasources:
user: grafana
password: password
+ - name: gdev-mysql-ds-tests
+ type: mysql
+ url: localhost:3306
+ database: grafana_ds_tests
+ user: grafana
+ password: password
+
- name: gdev-mssql
type: mssql
url: localhost:1433
database: grafana
user: grafana
- password: "Password!"
+ secureJsonData:
+ password: Password!
+
+ - name: gdev-mssql-ds-tests
+ type: mssql
+ url: localhost:1433
+ database: grafanatest
+ user: grafana
+ secureJsonData:
+ password: Password!
- name: gdev-postgres
type: postgres
@@ -68,6 +84,16 @@ datasources:
jsonData:
sslmode: "disable"
+ - name: gdev-postgres-ds-tests
+ type: postgres
+ url: localhost:5432
+ database: grafanadstest
+ user: grafanatest
+ secureJsonData:
+ password: grafanatest
+ jsonData:
+ sslmode: "disable"
+
- name: gdev-cloudwatch
type: cloudwatch
editable: true
diff --git a/docker/blocks/mssql/dashboard.json b/devenv/dev-dashboards/datasource_tests_mssql_fakedata.json
similarity index 92%
rename from docker/blocks/mssql/dashboard.json
rename to devenv/dev-dashboards/datasource_tests_mssql_fakedata.json
index ce9aa141a75..e810a686134 100644
--- a/docker/blocks/mssql/dashboard.json
+++ b/devenv/dev-dashboards/datasource_tests_mssql_fakedata.json
@@ -1,40 +1,4 @@
{
- "__inputs": [
- {
- "name": "DS_MSSQL",
- "label": "MSSQL",
- "description": "",
- "type": "datasource",
- "pluginId": "mssql",
- "pluginName": "MSSQL"
- }
- ],
- "__requires": [
- {
- "type": "grafana",
- "id": "grafana",
- "name": "Grafana",
- "version": "5.0.0"
- },
- {
- "type": "panel",
- "id": "graph",
- "name": "Graph",
- "version": "5.0.0"
- },
- {
- "type": "datasource",
- "id": "mssql",
- "name": "MSSQL",
- "version": "1.0.0"
- },
- {
- "type": "panel",
- "id": "table",
- "name": "Table",
- "version": "5.0.0"
- }
- ],
"annotations": {
"list": [
{
@@ -52,8 +16,7 @@
"editable": true,
"gnetId": null,
"graphTooltip": 0,
- "id": null,
- "iteration": 1520976748896,
+ "iteration": 1532618661457,
"links": [],
"panels": [
{
@@ -63,7 +26,7 @@
"bars": false,
"dashLength": 10,
"dashes": false,
- "datasource": "${DS_MSSQL}",
+ "datasource": "gdev-mssql",
"fill": 2,
"gridPos": {
"h": 9,
@@ -149,14 +112,18 @@
"min": null,
"show": true
}
- ]
+ ],
+ "yaxis": {
+ "align": false,
+ "alignLevel": null
+ }
},
{
"aliasColors": {},
"bars": false,
"dashLength": 10,
"dashes": false,
- "datasource": "${DS_MSSQL}",
+ "datasource": "gdev-mssql",
"fill": 2,
"gridPos": {
"h": 18,
@@ -234,14 +201,18 @@
"min": null,
"show": true
}
- ]
+ ],
+ "yaxis": {
+ "align": false,
+ "alignLevel": null
+ }
},
{
"aliasColors": {},
"bars": false,
"dashLength": 10,
"dashes": false,
- "datasource": "${DS_MSSQL}",
+ "datasource": "gdev-mssql",
"fill": 2,
"gridPos": {
"h": 9,
@@ -313,11 +284,15 @@
"min": null,
"show": true
}
- ]
+ ],
+ "yaxis": {
+ "align": false,
+ "alignLevel": null
+ }
},
{
"columns": [],
- "datasource": "${DS_MSSQL}",
+ "datasource": "gdev-mssql",
"fontSize": "100%",
"gridPos": {
"h": 10,
@@ -371,13 +346,13 @@
],
"schemaVersion": 16,
"style": "dark",
- "tags": [],
+ "tags": ["gdev", "mssql", "fake-data-gen"],
"templating": {
"list": [
{
"allValue": null,
"current": {},
- "datasource": "${DS_MSSQL}",
+ "datasource": "gdev-mssql",
"hide": 0,
"includeAll": false,
"label": "Datacenter",
@@ -387,6 +362,7 @@
"query": "SELECT DISTINCT datacenter FROM grafana_metric",
"refresh": 1,
"regex": "",
+ "skipUrlSync": false,
"sort": 1,
"tagValuesQuery": "",
"tags": [],
@@ -397,7 +373,7 @@
{
"allValue": null,
"current": {},
- "datasource": "${DS_MSSQL}",
+ "datasource": "gdev-mssql",
"hide": 0,
"includeAll": true,
"label": "Hostname",
@@ -407,6 +383,7 @@
"query": "SELECT DISTINCT hostname FROM grafana_metric WHERE datacenter='$datacenter'",
"refresh": 1,
"regex": "",
+ "skipUrlSync": false,
"sort": 1,
"tagValuesQuery": "",
"tags": [],
@@ -499,6 +476,7 @@
],
"query": "1s,10s,30s,1m,5m,10m,30m,1h,6h,12h,1d,7d,14d,30d",
"refresh": 2,
+ "skipUrlSync": false,
"type": "interval"
}
]
@@ -533,7 +511,7 @@
]
},
"timezone": "",
- "title": "Grafana Fake Data Gen - MSSQL",
+ "title": "Datasource tests - MSSQL",
"uid": "86Js1xRmk",
- "version": 11
+ "version": 1
}
\ No newline at end of file
diff --git a/docker/blocks/mysql_tests/dashboard.json b/devenv/dev-dashboards/datasource_tests_mssql_unittest.json
similarity index 70%
rename from docker/blocks/mysql_tests/dashboard.json
rename to devenv/dev-dashboards/datasource_tests_mssql_unittest.json
index 53f313315bd..b2d757ae188 100644
--- a/docker/blocks/mysql_tests/dashboard.json
+++ b/devenv/dev-dashboards/datasource_tests_mssql_unittest.json
@@ -1,40 +1,4 @@
{
- "__inputs": [
- {
- "name": "DS_MYSQL_TEST",
- "label": "MySQL TEST",
- "description": "",
- "type": "datasource",
- "pluginId": "mysql",
- "pluginName": "MySQL"
- }
- ],
- "__requires": [
- {
- "type": "grafana",
- "id": "grafana",
- "name": "Grafana",
- "version": "5.0.0"
- },
- {
- "type": "panel",
- "id": "graph",
- "name": "Graph",
- "version": "5.0.0"
- },
- {
- "type": "datasource",
- "id": "mysql",
- "name": "MySQL",
- "version": "5.0.0"
- },
- {
- "type": "panel",
- "id": "table",
- "name": "Table",
- "version": "5.0.0"
- }
- ],
"annotations": {
"list": [
{
@@ -47,31 +11,31 @@
"type": "dashboard"
},
{
- "datasource": "${DS_MYSQL_TEST}",
+ "datasource": "gdev-mssql-ds-tests",
"enable": false,
"hide": false,
"iconColor": "#6ed0e0",
"limit": 100,
"name": "Deploys",
- "rawQuery": "SELECT\n time_sec,\n description as text,\n tags\n FROM event\n WHERE $__unixEpochFilter(time_sec) AND tags='deploy'\n ORDER BY 1 ASC\n ",
+ "rawQuery": "SELECT\n $__time(time_sec),\n description as [text],\n tags\n FROM [event]\n WHERE $__unixEpochFilter(time_sec) AND tags='deploy'\n ORDER BY 1 ASC\n ",
"showIn": 0,
"tags": [],
"type": "tags"
},
{
- "datasource": "${DS_MYSQL_TEST}",
+ "datasource": "gdev-mssql-ds-tests",
"enable": false,
"hide": false,
"iconColor": "rgba(255, 96, 96, 1)",
"limit": 100,
"name": "Tickets",
- "rawQuery": "SELECT\n time_sec as time,\n description as text,\n tags\n FROM event\n WHERE $__unixEpochFilter(time_sec) AND tags='ticket'\n ORDER BY 1 ASC\n ",
+ "rawQuery": "SELECT\n $__time(time_sec),\n description as [text],\n tags\n FROM [event]\n WHERE $__unixEpochFilter(time_sec) AND tags='ticket'\n ORDER BY 1 ASC\n ",
"showIn": 0,
"tags": [],
"type": "tags"
},
{
- "datasource": "${DS_MYSQL_TEST}",
+ "datasource": "gdev-mssql-ds-tests",
"enable": false,
"hide": false,
"iconColor": "#7eb26d",
@@ -83,7 +47,7 @@
"type": "tags"
},
{
- "datasource": "${DS_MYSQL_TEST}",
+ "datasource": "gdev-mssql-ds-tests",
"enable": false,
"hide": false,
"iconColor": "#1f78c1",
@@ -96,16 +60,16 @@
}
]
},
+ "description": "Run the mssql unit tests to generate the data backing this dashboard",
"editable": true,
"gnetId": null,
"graphTooltip": 0,
- "id": null,
- "iteration": 1523320712115,
+ "iteration": 1534507501976,
"links": [],
"panels": [
{
"columns": [],
- "datasource": "${DS_MYSQL_TEST}",
+ "datasource": "gdev-mssql-ds-tests",
"fontSize": "100%",
"gridPos": {
"h": 4,
@@ -142,7 +106,7 @@
{
"alias": "",
"format": "table",
- "rawSql": "SELECT * from mysql_types",
+ "rawSql": "SELECT * from mssql_types",
"refId": "A"
}
],
@@ -152,7 +116,7 @@
},
{
"columns": [],
- "datasource": "${DS_MYSQL_TEST}",
+ "datasource": "gdev-mssql-ds-tests",
"fontSize": "100%",
"gridPos": {
"h": 3,
@@ -173,7 +137,7 @@
{
"alias": "Time",
"dateFormat": "YYYY-MM-DD HH:mm:ss",
- "pattern": "time_sec",
+ "pattern": "time",
"type": "date"
},
{
@@ -195,18 +159,18 @@
{
"alias": "",
"format": "table",
- "rawSql": "SELECT cast(null as unsigned integer) as time_sec",
+ "rawSql": "SELECT cast(null as bigint) as time",
"refId": "A",
"target": ""
}
],
- "title": "cast(null as unsigned integer) as time",
+ "title": "cast(null as bigint) as time",
"transform": "table",
"type": "table"
},
{
"columns": [],
- "datasource": "${DS_MYSQL_TEST}",
+ "datasource": "gdev-mssql-ds-tests",
"fontSize": "100%",
"gridPos": {
"h": 3,
@@ -227,7 +191,7 @@
{
"alias": "Time",
"dateFormat": "YYYY-MM-DD HH:mm:ss",
- "pattern": "time_sec",
+ "pattern": "time",
"type": "date"
},
{
@@ -249,7 +213,7 @@
{
"alias": "",
"format": "table",
- "rawSql": "SELECT cast(null as datetime) as time_sec",
+ "rawSql": "SELECT cast(null as datetime) as time",
"refId": "A",
"target": ""
}
@@ -260,7 +224,7 @@
},
{
"columns": [],
- "datasource": "${DS_MYSQL_TEST}",
+ "datasource": "gdev-mssql-ds-tests",
"fontSize": "100%",
"gridPos": {
"h": 3,
@@ -281,7 +245,7 @@
{
"alias": "Time",
"dateFormat": "YYYY-MM-DD HH:mm:ss",
- "pattern": "time_sec",
+ "pattern": "time",
"type": "date"
},
{
@@ -303,18 +267,18 @@
{
"alias": "",
"format": "table",
- "rawSql": "SELECT cast(NOW() as datetime) as time_sec",
+ "rawSql": "SELECT GETDATE() as time",
"refId": "A",
"target": ""
}
],
- "title": "cast()NOW() as datetime) as time",
+ "title": "GETDATE() as time",
"transform": "table",
"type": "table"
},
{
"columns": [],
- "datasource": "${DS_MYSQL_TEST}",
+ "datasource": "gdev-mssql-ds-tests",
"fontSize": "100%",
"gridPos": {
"h": 3,
@@ -357,12 +321,12 @@
{
"alias": "",
"format": "table",
- "rawSql": "SELECT NOW() as time",
+ "rawSql": "SELECT GETUTCDATE() as time",
"refId": "A",
"target": ""
}
],
- "title": "NOW() as time",
+ "title": "GETUTCDATE() as time",
"transform": "table",
"type": "table"
},
@@ -371,11 +335,11 @@
"bars": false,
"dashLength": 10,
"dashes": false,
- "datasource": "${DS_MYSQL_TEST}",
+ "datasource": "gdev-mssql-ds-tests",
"fill": 2,
"gridPos": {
- "h": 9,
- "w": 8,
+ "h": 6,
+ "w": 6,
"x": 0,
"y": 7
},
@@ -405,7 +369,7 @@
{
"alias": "",
"format": "time_series",
- "rawSql": "SELECT $__timeGroup(time, '5m') AS time, avg(value) as value FROM metric WHERE $__timeFilter(time) GROUP BY 1 ORDER BY 1",
+ "rawSql": "SELECT $__timeGroupAlias(time, '5m'), avg(value) as value FROM metric WHERE $__timeFilter(time) GROUP BY $__timeGroup(time, '5m') ORDER BY 1",
"refId": "A"
}
],
@@ -454,12 +418,12 @@
"bars": false,
"dashLength": 10,
"dashes": false,
- "datasource": "${DS_MYSQL_TEST}",
+ "datasource": "gdev-mssql-ds-tests",
"fill": 2,
"gridPos": {
- "h": 9,
- "w": 8,
- "x": 8,
+ "h": 6,
+ "w": 6,
+ "x": 6,
"y": 7
},
"id": 9,
@@ -488,7 +452,7 @@
{
"alias": "",
"format": "time_series",
- "rawSql": "SELECT $__timeGroup(time, '5m', NULL) AS time, avg(value) as value FROM metric WHERE $__timeFilter(time) GROUP BY 1 ORDER BY 1",
+ "rawSql": "SELECT $__timeGroupAlias(time, '5m', NULL), avg(value) as value FROM metric WHERE $__timeFilter(time) GROUP BY $__timeGroup(time, '5m') ORDER BY 1",
"refId": "A"
}
],
@@ -537,12 +501,12 @@
"bars": false,
"dashLength": 10,
"dashes": false,
- "datasource": "${DS_MYSQL_TEST}",
+ "datasource": "gdev-mssql-ds-tests",
"fill": 2,
"gridPos": {
- "h": 9,
- "w": 8,
- "x": 16,
+ "h": 6,
+ "w": 6,
+ "x": 12,
"y": 7
},
"id": 10,
@@ -571,7 +535,7 @@
{
"alias": "",
"format": "time_series",
- "rawSql": "SELECT $__timeGroup(time, '5m', 10.0) AS time, avg(value) as value FROM metric WHERE $__timeFilter(time) GROUP BY 1 ORDER BY 1",
+ "rawSql": "SELECT $__timeGroupAlias(time, '5m', 10.0), avg(value) as value FROM metric WHERE $__timeFilter(time) GROUP BY $__timeGroup(time, '5m') ORDER BY 1",
"refId": "A"
}
],
@@ -615,18 +579,101 @@
"alignLevel": null
}
},
+ {
+ "aliasColors": {},
+ "bars": false,
+ "dashLength": 10,
+ "dashes": false,
+ "datasource": "gdev-mssql-ds-tests",
+ "fill": 2,
+ "gridPos": {
+ "h": 6,
+ "w": 6,
+ "x": 18,
+ "y": 7
+ },
+ "id": 36,
+ "legend": {
+ "avg": false,
+ "current": false,
+ "max": false,
+ "min": false,
+ "show": true,
+ "total": false,
+ "values": false
+ },
+ "lines": true,
+ "linewidth": 2,
+ "links": [],
+ "nullPointMode": "null as zero",
+ "percentage": false,
+ "pointradius": 3,
+ "points": true,
+ "renderer": "flot",
+ "seriesOverrides": [],
+ "spaceLength": 10,
+ "stack": false,
+ "steppedLine": true,
+ "targets": [
+ {
+ "alias": "",
+ "format": "time_series",
+ "rawSql": "SELECT $__timeGroupAlias(time, '5m', previous), avg(value) as value FROM metric WHERE $__timeFilter(time) GROUP BY $__timeGroup(time, '5m') ORDER BY 1",
+ "refId": "A"
+ }
+ ],
+ "thresholds": [],
+ "timeFrom": null,
+ "timeShift": null,
+ "title": "timeGroup macro 5m with fill(previous) and null as zero",
+ "tooltip": {
+ "shared": true,
+ "sort": 0,
+ "value_type": "individual"
+ },
+ "type": "graph",
+ "xaxis": {
+ "buckets": null,
+ "mode": "time",
+ "name": null,
+ "show": true,
+ "values": []
+ },
+ "yaxes": [
+ {
+ "format": "short",
+ "label": null,
+ "logBase": 1,
+ "max": null,
+ "min": "0",
+ "show": true
+ },
+ {
+ "format": "short",
+ "label": null,
+ "logBase": 1,
+ "max": null,
+ "min": null,
+ "show": true
+ }
+ ],
+ "yaxis": {
+ "align": false,
+ "alignLevel": null
+ }
+ },
{
"aliasColors": {},
"bars": true,
"dashLength": 10,
"dashes": false,
- "datasource": "${DS_MYSQL_TEST}",
+ "datasource": "gdev-mssql-ds-tests",
"fill": 2,
"gridPos": {
- "h": 9,
- "w": 8,
+ "h": 6,
+ "w": 6,
"x": 0,
- "y": 16
+ "y": 13
},
"id": 16,
"legend": {
@@ -654,7 +701,7 @@
{
"alias": "",
"format": "time_series",
- "rawSql": "SELECT $__timeGroup(time, '$summarize') AS time, avg(value) as value FROM metric WHERE $__timeFilter(time) GROUP BY 1 ORDER BY 1",
+ "rawSql": "SELECT $__timeGroupAlias(time, '$summarize'), avg(value) as value FROM metric WHERE $__timeFilter(time) GROUP BY $__timeGroup(time, '$summarize') ORDER BY 1",
"refId": "A"
}
],
@@ -703,13 +750,13 @@
"bars": true,
"dashLength": 10,
"dashes": false,
- "datasource": "${DS_MYSQL_TEST}",
+ "datasource": "gdev-mssql-ds-tests",
"fill": 2,
"gridPos": {
- "h": 9,
- "w": 8,
- "x": 8,
- "y": 16
+ "h": 6,
+ "w": 6,
+ "x": 6,
+ "y": 13
},
"id": 12,
"legend": {
@@ -737,7 +784,7 @@
{
"alias": "",
"format": "time_series",
- "rawSql": "SELECT $__timeGroup(time, '$summarize', NULL) AS time, sum(value) as value FROM metric WHERE $__timeFilter(time) GROUP BY 1 ORDER BY 1",
+ "rawSql": "SELECT $__timeGroupAlias(time, '$summarize', NULL), sum(value) as value FROM metric WHERE $__timeFilter(time) GROUP BY $__timeGroup(time, '$summarize') ORDER BY 1",
"refId": "A"
}
],
@@ -786,13 +833,13 @@
"bars": true,
"dashLength": 10,
"dashes": false,
- "datasource": "${DS_MYSQL_TEST}",
+ "datasource": "gdev-mssql-ds-tests",
"fill": 2,
"gridPos": {
- "h": 9,
- "w": 8,
- "x": 16,
- "y": 16
+ "h": 6,
+ "w": 6,
+ "x": 12,
+ "y": 13
},
"id": 13,
"legend": {
@@ -820,7 +867,7 @@
{
"alias": "",
"format": "time_series",
- "rawSql": "SELECT $__timeGroup(time, '$summarize', 100.0) AS time, sum(value) as value FROM metric WHERE $__timeFilter(time) GROUP BY 1 ORDER BY 1",
+ "rawSql": "SELECT $__timeGroupAlias(time, '$summarize', 100.0), sum(value) as value FROM metric WHERE $__timeFilter(time) GROUP BY $__timeGroup(time, '$summarize') ORDER BY 1",
"refId": "A"
}
],
@@ -864,18 +911,101 @@
"alignLevel": null
}
},
+ {
+ "aliasColors": {},
+ "bars": true,
+ "dashLength": 10,
+ "dashes": false,
+ "datasource": "gdev-mssql-ds-tests",
+ "fill": 2,
+ "gridPos": {
+ "h": 6,
+ "w": 6,
+ "x": 18,
+ "y": 13
+ },
+ "id": 37,
+ "legend": {
+ "avg": false,
+ "current": false,
+ "max": false,
+ "min": false,
+ "show": true,
+ "total": false,
+ "values": false
+ },
+ "lines": false,
+ "linewidth": 2,
+ "links": [],
+ "nullPointMode": "null",
+ "percentage": false,
+ "pointradius": 3,
+ "points": false,
+ "renderer": "flot",
+ "seriesOverrides": [],
+ "spaceLength": 10,
+ "stack": false,
+ "steppedLine": true,
+ "targets": [
+ {
+ "alias": "",
+ "format": "time_series",
+ "rawSql": "SELECT $__timeGroupAlias(time, '$summarize', previous), sum(value) as value FROM metric WHERE $__timeFilter(time) GROUP BY $__timeGroup(time, '$summarize') ORDER BY 1",
+ "refId": "A"
+ }
+ ],
+ "thresholds": [],
+ "timeFrom": null,
+ "timeShift": null,
+ "title": "Metrics - timeGroup macro $summarize with fill(previous)",
+ "tooltip": {
+ "shared": true,
+ "sort": 0,
+ "value_type": "individual"
+ },
+ "type": "graph",
+ "xaxis": {
+ "buckets": null,
+ "mode": "time",
+ "name": null,
+ "show": true,
+ "values": []
+ },
+ "yaxes": [
+ {
+ "format": "short",
+ "label": null,
+ "logBase": 1,
+ "max": null,
+ "min": null,
+ "show": true
+ },
+ {
+ "format": "short",
+ "label": null,
+ "logBase": 1,
+ "max": null,
+ "min": null,
+ "show": true
+ }
+ ],
+ "yaxis": {
+ "align": false,
+ "alignLevel": null
+ }
+ },
{
"aliasColors": {},
"bars": false,
"dashLength": 10,
"dashes": false,
- "datasource": "${DS_MYSQL_TEST}",
+ "datasource": "gdev-mssql-ds-tests",
"fill": 2,
"gridPos": {
"h": 8,
"w": 12,
"x": 0,
- "y": 25
+ "y": 19
},
"id": 27,
"legend": {
@@ -907,14 +1037,8 @@
{
"alias": "",
"format": "time_series",
- "rawSql": "SELECT \n $__timeGroup(time, '$summarize') as time, \n CONCAT(measurement, ' - value one') as metric, \n avg(valueOne) as valueOne\nFROM\n metric_values \nWHERE\n $__timeFilter(time) AND\n measurement IN($metric)\nGROUP BY 1, 2\nORDER BY 1",
+ "rawSql": "SELECT \n $__timeGroupAlias(time, '$summarize'), \n measurement as metric, \n avg(valueOne) as valueOne,\n avg(valueTwo) as valueTwo\nFROM\n metric_values \nWHERE\n $__timeFilter(time) AND\n ($metric = 'ALL' OR measurement = $metric)\nGROUP BY \n $__timeGroup(time, '$summarize'), \n measurement \nORDER BY 1",
"refId": "A"
- },
- {
- "alias": "",
- "format": "time_series",
- "rawSql": "SELECT \n $__timeGroup(time, '$summarize') as time, \n CONCAT(measurement, ' - value two') as metric, \n avg(valueTwo) as valueTwo \nFROM\n metric_values\nWHERE\n $__timeFilter(time) AND\n measurement IN($metric)\nGROUP BY 1,2\nORDER BY 1",
- "refId": "B"
}
],
"thresholds": [],
@@ -962,13 +1086,13 @@
"bars": false,
"dashLength": 10,
"dashes": false,
- "datasource": "${DS_MYSQL_TEST}",
+ "datasource": "gdev-mssql-ds-tests",
"fill": 2,
"gridPos": {
"h": 8,
"w": 12,
"x": 12,
- "y": 25
+ "y": 19
},
"id": 5,
"legend": {
@@ -1010,8 +1134,14 @@
{
"alias": "",
"format": "time_series",
- "rawSql": "SELECT \n $__timeGroup(time, '$summarize') as time, \n avg(valueOne) as valueOne, \n avg(valueTwo) as valueTwo \nFROM\n metric_values \nWHERE \n $__timeFilter(time) AND \n measurement IN($metric)\nGROUP BY 1\nORDER BY 1",
+ "rawSql": "SELECT \n $__timeGroupAlias(time, '$summarize'), \n avg(valueOne) as valueOne, \n avg(valueTwo) as valueTwo \nFROM\n metric_values \nWHERE \n $__timeFilter(time) AND \n ($metric = 'ALL' OR measurement = $metric)\nGROUP BY \n $__timeGroup(time, '$summarize')\nORDER BY 1",
"refId": "A"
+ },
+ {
+ "alias": "",
+ "format": "time_series",
+ "rawSql": "SELECT \n time,\n avg(valueOne) OVER (ORDER BY time ROWS BETWEEN 6 PRECEDING AND 6 FOLLOWING) as MovingAverageValueOne,\n avg(valueTwo) OVER (ORDER BY time ROWS BETWEEN 6 PRECEDING AND 6 FOLLOWING) as MovingAverageValueTwo\nFROM\n metric_values \nWHERE \n $__timeFilter(time) AND \n ($metric = 'ALL' OR measurement = $metric)\nORDER BY 1",
+ "refId": "B"
}
],
"thresholds": [],
@@ -1059,13 +1189,203 @@
"bars": false,
"dashLength": 10,
"dashes": false,
- "datasource": "${DS_MYSQL_TEST}",
+ "datasource": "gdev-mssql-ds-tests",
"fill": 2,
"gridPos": {
"h": 8,
"w": 12,
"x": 0,
- "y": 33
+ "y": 27
+ },
+ "id": 38,
+ "legend": {
+ "alignAsTable": true,
+ "avg": true,
+ "current": true,
+ "hideEmpty": false,
+ "hideZero": false,
+ "max": true,
+ "min": true,
+ "rightSide": true,
+ "show": true,
+ "total": true,
+ "values": true
+ },
+ "lines": true,
+ "linewidth": 2,
+ "links": [],
+ "nullPointMode": "null",
+ "percentage": false,
+ "pointradius": 3,
+ "points": false,
+ "renderer": "flot",
+ "seriesOverrides": [],
+ "spaceLength": 10,
+ "stack": false,
+ "steppedLine": false,
+ "targets": [
+ {
+ "alias": "",
+ "format": "time_series",
+ "rawSql": "SELECT \n $__unixEpochGroupAlias(timeInt32, '$summarize'), \n measurement as metric, \n avg(valueOne) as valueOne,\n avg(valueTwo) as valueTwo\nFROM\n metric_values \nWHERE\n $__unixEpochFilter(timeInt32) AND\n ($metric = 'ALL' OR measurement = $metric)\nGROUP BY \n $__unixEpochGroup(timeInt32, '$summarize'), \n measurement \nORDER BY 1",
+ "refId": "A"
+ }
+ ],
+ "thresholds": [],
+ "timeFrom": null,
+ "timeShift": null,
+ "title": "Multiple series with metric column using unixEpochGroup macro ($summarize)",
+ "tooltip": {
+ "shared": true,
+ "sort": 0,
+ "value_type": "individual"
+ },
+ "type": "graph",
+ "xaxis": {
+ "buckets": null,
+ "mode": "time",
+ "name": null,
+ "show": true,
+ "values": []
+ },
+ "yaxes": [
+ {
+ "format": "short",
+ "label": null,
+ "logBase": 1,
+ "max": null,
+ "min": "0",
+ "show": true
+ },
+ {
+ "format": "short",
+ "label": null,
+ "logBase": 1,
+ "max": null,
+ "min": null,
+ "show": true
+ }
+ ],
+ "yaxis": {
+ "align": false,
+ "alignLevel": null
+ }
+ },
+ {
+ "aliasColors": {},
+ "bars": false,
+ "dashLength": 10,
+ "dashes": false,
+ "datasource": "gdev-mssql-ds-tests",
+ "fill": 2,
+ "gridPos": {
+ "h": 8,
+ "w": 12,
+ "x": 12,
+ "y": 27
+ },
+ "id": 39,
+ "legend": {
+ "alignAsTable": true,
+ "avg": true,
+ "current": true,
+ "max": true,
+ "min": true,
+ "rightSide": true,
+ "show": true,
+ "total": true,
+ "values": true
+ },
+ "lines": true,
+ "linewidth": 2,
+ "links": [],
+ "nullPointMode": "null",
+ "percentage": false,
+ "pointradius": 3,
+ "points": false,
+ "renderer": "flot",
+ "seriesOverrides": [
+ {
+ "alias": "MovingAverageValueOne",
+ "dashes": true,
+ "lines": false
+ },
+ {
+ "alias": "MovingAverageValueTwo",
+ "dashes": true,
+ "lines": false,
+ "yaxis": 1
+ }
+ ],
+ "spaceLength": 10,
+ "stack": false,
+ "steppedLine": false,
+ "targets": [
+ {
+ "alias": "",
+ "format": "time_series",
+ "rawSql": "SELECT \n $__unixEpochGroupAlias(timeInt32, '$summarize'), \n avg(valueOne) as valueOne,\n avg(valueTwo) as valueTwo\nFROM\n metric_values \nWHERE\n $__unixEpochFilter(timeInt32) AND\n ($metric = 'ALL' OR measurement = $metric)\nGROUP BY \n $__unixEpochGroup(timeInt32, '$summarize')\nORDER BY 1",
+ "refId": "A"
+ },
+ {
+ "alias": "",
+ "format": "time_series",
+ "rawSql": "SELECT \n time,\n avg(valueOne) OVER (ORDER BY time ROWS BETWEEN 6 PRECEDING AND 6 FOLLOWING) as MovingAverageValueOne,\n avg(valueTwo) OVER (ORDER BY time ROWS BETWEEN 6 PRECEDING AND 6 FOLLOWING) as MovingAverageValueTwo\nFROM\n metric_values \nWHERE \n $__timeFilter(time) AND \n ($metric = 'ALL' OR measurement = $metric)\nORDER BY 1",
+ "refId": "B"
+ }
+ ],
+ "thresholds": [],
+ "timeFrom": null,
+ "timeShift": null,
+ "title": "Multiple series without metric column using unixEpochGroup macro ($summarize)",
+ "tooltip": {
+ "shared": true,
+ "sort": 0,
+ "value_type": "individual"
+ },
+ "type": "graph",
+ "xaxis": {
+ "buckets": null,
+ "mode": "time",
+ "name": null,
+ "show": true,
+ "values": []
+ },
+ "yaxes": [
+ {
+ "format": "short",
+ "label": null,
+ "logBase": 1,
+ "max": null,
+ "min": "0",
+ "show": true
+ },
+ {
+ "format": "short",
+ "label": null,
+ "logBase": 1,
+ "max": null,
+ "min": null,
+ "show": true
+ }
+ ],
+ "yaxis": {
+ "align": false,
+ "alignLevel": null
+ }
+ },
+ {
+ "aliasColors": {},
+ "bars": false,
+ "dashLength": 10,
+ "dashes": false,
+ "datasource": "gdev-mssql-ds-tests",
+ "fill": 2,
+ "gridPos": {
+ "h": 8,
+ "w": 12,
+ "x": 0,
+ "y": 35
},
"id": 4,
"legend": {
@@ -1097,14 +1417,8 @@
{
"alias": "",
"format": "time_series",
- "rawSql": "SELECT $__time(time), CONCAT(measurement, ' - value one') as metric, valueOne FROM metric_values WHERE $__timeFilter(time) AND measurement IN($metric) ORDER BY 1",
+ "rawSql": "SELECT $__timeEpoch(time), measurement as metric, valueOne, valueTwo FROM metric_values WHERE $__timeFilter(time) AND ($metric = 'ALL' OR measurement = $metric) ORDER BY 1",
"refId": "A"
- },
- {
- "alias": "",
- "format": "time_series",
- "rawSql": "SELECT $__time(time), CONCAT(measurement, ' - value two') as metric, valueTwo FROM metric_values WHERE $__timeFilter(time) AND measurement IN($metric) ORDER BY 1",
- "refId": "B"
}
],
"thresholds": [],
@@ -1152,13 +1466,13 @@
"bars": false,
"dashLength": 10,
"dashes": false,
- "datasource": "${DS_MYSQL_TEST}",
+ "datasource": "gdev-mssql-ds-tests",
"fill": 2,
"gridPos": {
"h": 8,
"w": 12,
"x": 12,
- "y": 33
+ "y": 35
},
"id": 28,
"legend": {
@@ -1188,7 +1502,7 @@
{
"alias": "",
"format": "time_series",
- "rawSql": "SELECT $__time(time), valueOne, valueTwo FROM metric_values ORDER BY 1",
+ "rawSql": "SELECT $__timeEpoch(time), valueOne, valueTwo FROM metric_values ORDER BY 1",
"refId": "A"
}
],
@@ -1237,13 +1551,13 @@
"bars": false,
"dashLength": 10,
"dashes": false,
- "datasource": "${DS_MYSQL_TEST}",
+ "datasource": "gdev-mssql-ds-tests",
"fill": 2,
"gridPos": {
"h": 8,
"w": 12,
"x": 0,
- "y": 41
+ "y": 43
},
"id": 19,
"legend": {
@@ -1275,14 +1589,8 @@
{
"alias": "",
"format": "time_series",
- "rawSql": "SELECT $__time(time), CONCAT(measurement, ' - value one') as metric, valueOne FROM metric_values WHERE $__timeFilter(time) AND measurement IN($metric) ORDER BY 1",
+ "rawSql": "SELECT $__timeEpoch(time), measurement as metric, valueOne, valueTwo FROM metric_values WHERE $__timeFilter(time) AND ($metric = 'ALL' OR measurement = $metric) ORDER BY 1",
"refId": "A"
- },
- {
- "alias": "",
- "format": "time_series",
- "rawSql": "SELECT $__time(time), CONCAT(measurement, ' - value two') as metric, valueTwo FROM metric_values WHERE $__timeFilter(time) AND measurement IN($metric) ORDER BY 1",
- "refId": "B"
}
],
"thresholds": [],
@@ -1330,13 +1638,13 @@
"bars": false,
"dashLength": 10,
"dashes": false,
- "datasource": "${DS_MYSQL_TEST}",
+ "datasource": "gdev-mssql-ds-tests",
"fill": 2,
"gridPos": {
"h": 8,
"w": 12,
"x": 12,
- "y": 41
+ "y": 43
},
"id": 18,
"legend": {
@@ -1366,7 +1674,7 @@
{
"alias": "",
"format": "time_series",
- "rawSql": "SELECT $__timeEpoch(time), valueOne, valueTwo FROM metric_values\nWHERE $__timeFilter(time) AND measurement IN($metric) ORDER BY 1",
+ "rawSql": "SELECT $__timeEpoch(time), valueOne, valueTwo FROM metric_values\nWHERE $__timeFilter(time) AND ($metric = 'ALL' OR measurement = $metric) ORDER BY 1",
"refId": "A"
}
],
@@ -1415,13 +1723,13 @@
"bars": false,
"dashLength": 10,
"dashes": false,
- "datasource": "${DS_MYSQL_TEST}",
+ "datasource": "gdev-mssql-ds-tests",
"fill": 2,
"gridPos": {
"h": 8,
"w": 12,
"x": 0,
- "y": 49
+ "y": 51
},
"id": 17,
"legend": {
@@ -1453,14 +1761,8 @@
{
"alias": "",
"format": "time_series",
- "rawSql": "SELECT $__time(time), CONCAT(measurement, ' - value one') as metric, valueOne FROM metric_values WHERE $__timeFilter(time) AND measurement IN($metric) ORDER BY 1",
+ "rawSql": "SELECT $__timeEpoch(time), measurement as metric, valueOne, valueTwo FROM metric_values WHERE $__timeFilter(time) AND ($metric = 'ALL' OR measurement = $metric) ORDER BY 1",
"refId": "A"
- },
- {
- "alias": "",
- "format": "time_series",
- "rawSql": "SELECT $__time(time), CONCAT(measurement, ' - value two') as metric, valueTwo FROM metric_values WHERE $__timeFilter(time) AND measurement IN($metric) ORDER BY 1",
- "refId": "B"
}
],
"thresholds": [],
@@ -1508,13 +1810,13 @@
"bars": false,
"dashLength": 10,
"dashes": false,
- "datasource": "${DS_MYSQL_TEST}",
+ "datasource": "gdev-mssql-ds-tests",
"fill": 2,
"gridPos": {
"h": 8,
"w": 12,
"x": 12,
- "y": 49
+ "y": 51
},
"id": 20,
"legend": {
@@ -1544,7 +1846,7 @@
{
"alias": "",
"format": "time_series",
- "rawSql": "SELECT $__timeEpoch(time), valueOne, valueTwo FROM metric_values\nWHERE $__timeFilter(time) AND measurement IN($metric) ORDER BY 1",
+ "rawSql": "SELECT $__timeEpoch(time), valueOne, valueTwo FROM metric_values\nWHERE $__timeFilter(time) AND ($metric = 'ALL' OR measurement = $metric) ORDER BY 1",
"refId": "A"
}
],
@@ -1588,18 +1890,192 @@
"alignLevel": null
}
},
+ {
+ "aliasColors": {},
+ "bars": false,
+ "dashLength": 10,
+ "dashes": false,
+ "datasource": "gdev-mssql-ds-tests",
+ "fill": 2,
+ "gridPos": {
+ "h": 8,
+ "w": 12,
+ "x": 0,
+ "y": 59
+ },
+ "id": 29,
+ "legend": {
+ "alignAsTable": true,
+ "avg": true,
+ "current": true,
+ "hideEmpty": false,
+ "hideZero": false,
+ "max": true,
+ "min": true,
+ "rightSide": true,
+ "show": true,
+ "total": true,
+ "values": true
+ },
+ "lines": true,
+ "linewidth": 2,
+ "links": [],
+ "nullPointMode": "null",
+ "percentage": false,
+ "pointradius": 3,
+ "points": false,
+ "renderer": "flot",
+ "seriesOverrides": [],
+ "spaceLength": 10,
+ "stack": false,
+ "steppedLine": false,
+ "targets": [
+ {
+ "alias": "",
+ "format": "time_series",
+ "rawSql": "DECLARE\n @from int = $__unixEpochFrom(), \n @to int = $__unixEpochTo(), \n @interval nvarchar(50) = '$summarize', \n @metric nvarchar(200) = $metric\n \nEXEC dbo.sp_test_epoch @from, @to, @interval, @metric",
+ "refId": "A"
+ }
+ ],
+ "thresholds": [],
+ "timeFrom": null,
+ "timeShift": null,
+ "title": "Stored procedure support using epoch",
+ "tooltip": {
+ "shared": true,
+ "sort": 0,
+ "value_type": "individual"
+ },
+ "type": "graph",
+ "xaxis": {
+ "buckets": null,
+ "mode": "time",
+ "name": null,
+ "show": true,
+ "values": []
+ },
+ "yaxes": [
+ {
+ "format": "short",
+ "label": null,
+ "logBase": 1,
+ "max": null,
+ "min": "0",
+ "show": true
+ },
+ {
+ "format": "short",
+ "label": null,
+ "logBase": 1,
+ "max": null,
+ "min": null,
+ "show": true
+ }
+ ],
+ "yaxis": {
+ "align": false,
+ "alignLevel": null
+ }
+ },
+ {
+ "aliasColors": {},
+ "bars": false,
+ "dashLength": 10,
+ "dashes": false,
+ "datasource": "gdev-mssql-ds-tests",
+ "fill": 2,
+ "gridPos": {
+ "h": 8,
+ "w": 12,
+ "x": 12,
+ "y": 59
+ },
+ "id": 30,
+ "legend": {
+ "alignAsTable": true,
+ "avg": true,
+ "current": true,
+ "hideEmpty": false,
+ "hideZero": false,
+ "max": true,
+ "min": true,
+ "rightSide": true,
+ "show": true,
+ "total": true,
+ "values": true
+ },
+ "lines": true,
+ "linewidth": 2,
+ "links": [],
+ "nullPointMode": "null",
+ "percentage": false,
+ "pointradius": 3,
+ "points": false,
+ "renderer": "flot",
+ "seriesOverrides": [],
+ "spaceLength": 10,
+ "stack": false,
+ "steppedLine": false,
+ "targets": [
+ {
+ "alias": "",
+ "format": "time_series",
+ "rawSql": "DECLARE\n @from datetime = $__timeFrom(), \n @to datetime = $__timeTo(), \n @interval nvarchar(50) = '$summarize', \n @metric nvarchar(200) = $metric\n \nEXEC dbo.sp_test_datetime @from, @to, @interval, @metric",
+ "refId": "A"
+ }
+ ],
+ "thresholds": [],
+ "timeFrom": null,
+ "timeShift": null,
+ "title": "Stored procedure support using datetime",
+ "tooltip": {
+ "shared": true,
+ "sort": 0,
+ "value_type": "individual"
+ },
+ "type": "graph",
+ "xaxis": {
+ "buckets": null,
+ "mode": "time",
+ "name": null,
+ "show": true,
+ "values": []
+ },
+ "yaxes": [
+ {
+ "format": "short",
+ "label": null,
+ "logBase": 1,
+ "max": null,
+ "min": "0",
+ "show": true
+ },
+ {
+ "format": "short",
+ "label": null,
+ "logBase": 1,
+ "max": null,
+ "min": null,
+ "show": true
+ }
+ ],
+ "yaxis": {
+ "align": false,
+ "alignLevel": null
+ }
+ },
{
"aliasColors": {},
"bars": true,
"dashLength": 10,
"dashes": false,
- "datasource": "${DS_MYSQL_TEST}",
+ "datasource": "gdev-mssql-ds-tests",
"fill": 1,
"gridPos": {
"h": 8,
"w": 12,
"x": 0,
- "y": 57
+ "y": 67
},
"id": 14,
"legend": {
@@ -1629,14 +2105,8 @@
{
"alias": "",
"format": "time_series",
- "rawSql": "SELECT $__timeEpoch(time), CONCAT(measurement, ' - value one') as metric, valueOne FROM metric_values \nWHERE $__timeFilter(time) AND measurement IN($metric) ORDER BY 1",
+ "rawSql": "SELECT $__timeEpoch(time), measurement as metric, valueOne, valueTwo FROM metric_values WHERE $__timeFilter(time) AND ($metric = 'ALL' OR measurement = $metric) ORDER BY 1",
"refId": "A"
- },
- {
- "alias": "",
- "format": "time_series",
- "rawSql": "SELECT $__timeEpoch(time), CONCAT(measurement, ' - value two') as metric, valueTwo FROM metric_values \nWHERE $__timeFilter(time) AND measurement IN($metric) ORDER BY 1",
- "refId": "B"
}
],
"thresholds": [],
@@ -1687,13 +2157,13 @@
"bars": true,
"dashLength": 10,
"dashes": false,
- "datasource": "${DS_MYSQL_TEST}",
+ "datasource": "gdev-mssql-ds-tests",
"fill": 1,
"gridPos": {
"h": 8,
"w": 12,
"x": 12,
- "y": 57
+ "y": 67
},
"id": 15,
"legend": {
@@ -1723,7 +2193,7 @@
{
"alias": "",
"format": "time_series",
- "rawSql": "SELECT $__timeEpoch(time), valueOne, valueTwo FROM metric_values\nWHERE $__timeFilter(time) AND measurement IN($metric) ORDER BY 1",
+ "rawSql": "SELECT $__timeEpoch(time), valueOne, valueTwo FROM metric_values\nWHERE $__timeFilter(time) AND ($metric = 'ALL' OR measurement = $metric) ORDER BY 1",
"refId": "A"
}
],
@@ -1774,13 +2244,13 @@
"bars": true,
"dashLength": 10,
"dashes": false,
- "datasource": "${DS_MYSQL_TEST}",
+ "datasource": "gdev-mssql-ds-tests",
"fill": 1,
"gridPos": {
"h": 8,
"w": 12,
"x": 0,
- "y": 65
+ "y": 75
},
"id": 25,
"legend": {
@@ -1810,14 +2280,8 @@
{
"alias": "",
"format": "time_series",
- "rawSql": "SELECT $__timeEpoch(time), CONCAT(measurement, ' - value one') as metric, valueOne FROM metric_values \nWHERE $__timeFilter(time) AND measurement IN($metric) ORDER BY 1",
+ "rawSql": "SELECT $__timeEpoch(time), measurement as metric, valueOne, valueTwo FROM metric_values WHERE $__timeFilter(time) AND ($metric = 'ALL' OR measurement = $metric) ORDER BY 1",
"refId": "A"
- },
- {
- "alias": "",
- "format": "time_series",
- "rawSql": "SELECT $__timeEpoch(time), CONCAT(measurement, ' - value two') as metric, valueTwo FROM metric_values \nWHERE $__timeFilter(time) AND measurement IN($metric) ORDER BY 1",
- "refId": "B"
}
],
"thresholds": [],
@@ -1868,13 +2332,13 @@
"bars": true,
"dashLength": 10,
"dashes": false,
- "datasource": "${DS_MYSQL_TEST}",
+ "datasource": "gdev-mssql-ds-tests",
"fill": 1,
"gridPos": {
"h": 8,
"w": 12,
"x": 12,
- "y": 65
+ "y": 75
},
"id": 22,
"legend": {
@@ -1904,7 +2368,7 @@
{
"alias": "",
"format": "time_series",
- "rawSql": "SELECT $__timeEpoch(time), valueOne, valueTwo FROM metric_values\nWHERE $__timeFilter(time) AND measurement IN($metric) ORDER BY 1",
+ "rawSql": "SELECT $__timeEpoch(time), valueOne, valueTwo FROM metric_values\nWHERE $__timeFilter(time) AND ($metric = 'ALL' OR measurement = $metric) ORDER BY 1",
"refId": "A"
}
],
@@ -1955,13 +2419,13 @@
"bars": true,
"dashLength": 10,
"dashes": false,
- "datasource": "${DS_MYSQL_TEST}",
+ "datasource": "gdev-mssql-ds-tests",
"fill": 1,
"gridPos": {
"h": 8,
"w": 12,
"x": 0,
- "y": 73
+ "y": 83
},
"id": 21,
"legend": {
@@ -1991,14 +2455,8 @@
{
"alias": "",
"format": "time_series",
- "rawSql": "SELECT $__timeEpoch(time), CONCAT(measurement, ' - value one') as metric, valueOne FROM metric_values \nWHERE $__timeFilter(time) AND measurement IN($metric) ORDER BY 1",
+ "rawSql": "SELECT $__timeEpoch(time), measurement as metric, valueOne, valueTwo FROM metric_values WHERE $__timeFilter(time) AND ($metric = 'ALL' OR measurement = $metric) ORDER BY 1",
"refId": "A"
- },
- {
- "alias": "",
- "format": "time_series",
- "rawSql": "SELECT $__timeEpoch(time), CONCAT(measurement, ' - value two') as metric, valueTwo FROM metric_values \nWHERE $__timeFilter(time) AND measurement IN($metric) ORDER BY 1",
- "refId": "B"
}
],
"thresholds": [],
@@ -2049,13 +2507,13 @@
"bars": true,
"dashLength": 10,
"dashes": false,
- "datasource": "${DS_MYSQL_TEST}",
+ "datasource": "gdev-mssql-ds-tests",
"fill": 1,
"gridPos": {
"h": 8,
"w": 12,
"x": 12,
- "y": 73
+ "y": 83
},
"id": 26,
"legend": {
@@ -2085,7 +2543,7 @@
{
"alias": "",
"format": "time_series",
- "rawSql": "SELECT $__timeEpoch(time), valueOne, valueTwo FROM metric_values\nWHERE $__timeFilter(time) AND measurement IN($metric) ORDER BY 1",
+ "rawSql": "SELECT $__timeEpoch(time), valueOne, valueTwo FROM metric_values \nWHERE $__timeFilter(time) AND ($metric = 'ALL' OR measurement = $metric) ORDER BY 1",
"refId": "A"
}
],
@@ -2136,13 +2594,13 @@
"bars": true,
"dashLength": 10,
"dashes": false,
- "datasource": "${DS_MYSQL_TEST}",
+ "datasource": "gdev-mssql-ds-tests",
"fill": 1,
"gridPos": {
"h": 8,
"w": 12,
"x": 0,
- "y": 81
+ "y": 91
},
"id": 23,
"legend": {
@@ -2172,14 +2630,8 @@
{
"alias": "",
"format": "time_series",
- "rawSql": "SELECT $__timeEpoch(time), CONCAT(measurement, ' - value one') as metric, valueOne FROM metric_values \nWHERE $__timeFilter(time) AND measurement IN($metric) ORDER BY 1",
+ "rawSql": "SELECT $__timeEpoch(time), measurement as metric, valueOne, valueTwo FROM metric_values WHERE $__timeFilter(time) AND ($metric = 'ALL' OR measurement = $metric) ORDER BY 1",
"refId": "A"
- },
- {
- "alias": "",
- "format": "time_series",
- "rawSql": "SELECT $__timeEpoch(time), CONCAT(measurement, ' - value two') as metric, valueTwo FROM metric_values \nWHERE $__timeFilter(time) AND measurement IN($metric) ORDER BY 1",
- "refId": "B"
}
],
"thresholds": [],
@@ -2230,13 +2682,13 @@
"bars": true,
"dashLength": 10,
"dashes": false,
- "datasource": "${DS_MYSQL_TEST}",
+ "datasource": "gdev-mssql-ds-tests",
"fill": 1,
"gridPos": {
"h": 8,
"w": 12,
"x": 12,
- "y": 81
+ "y": 91
},
"id": 24,
"legend": {
@@ -2266,7 +2718,7 @@
{
"alias": "",
"format": "time_series",
- "rawSql": "SELECT $__timeEpoch(time), valueOne, valueTwo FROM metric_values\nWHERE $__timeFilter(time) AND measurement IN($metric) ORDER BY 1",
+ "rawSql": "SELECT $__timeEpoch(time), valueOne, valueTwo FROM metric_values \nWHERE $__timeFilter(time) AND ($metric = 'ALL' OR measurement = $metric) ORDER BY 1",
"refId": "A"
}
],
@@ -2316,22 +2768,47 @@
"refresh": false,
"schemaVersion": 16,
"style": "dark",
- "tags": [],
+ "tags": [
+ "gdev",
+ "mssql"
+ ],
"templating": {
"list": [
{
- "allValue": "",
- "current": {},
- "datasource": "${DS_MYSQL_TEST}",
+ "allValue": "'ALL'",
+ "current": {
+ "selected": true,
+ "tags": [],
+ "text": "All",
+ "value": "$__all"
+ },
+ "datasource": "gdev-mssql-ds-tests",
"hide": 0,
"includeAll": true,
"label": "Metric",
- "multi": true,
+ "multi": false,
"name": "metric",
- "options": [],
+ "options": [
+ {
+ "selected": true,
+ "text": "All",
+ "value": "$__all"
+ },
+ {
+ "selected": false,
+ "text": "Metric A",
+ "value": "Metric A"
+ },
+ {
+ "selected": false,
+ "text": "Metric B",
+ "value": "Metric B"
+ }
+ ],
"query": "SELECT DISTINCT measurement FROM metric_values",
- "refresh": 1,
+ "refresh": 0,
"regex": "",
+ "skipUrlSync": false,
"sort": 0,
"tagValuesQuery": "",
"tags": [],
@@ -2384,6 +2861,7 @@
],
"query": "1s,10s,30s,1m,5m,10m",
"refresh": 2,
+ "skipUrlSync": false,
"type": "interval"
}
]
@@ -2418,7 +2896,7 @@
]
},
"timezone": "",
- "title": "MySQL Data Source Test",
- "uid": "Hmf8FDkmz",
- "version": 12
+ "title": "Datasource tests - MSSQL (unit test)",
+ "uid": "GlAqcPgmz",
+ "version": 2
}
\ No newline at end of file
diff --git a/docker/blocks/mysql/dashboard.json b/devenv/dev-dashboards/datasource_tests_mysql_fakedata.json
similarity index 92%
rename from docker/blocks/mysql/dashboard.json
rename to devenv/dev-dashboards/datasource_tests_mysql_fakedata.json
index dba7847cc72..ebeb452fc4c 100644
--- a/docker/blocks/mysql/dashboard.json
+++ b/devenv/dev-dashboards/datasource_tests_mysql_fakedata.json
@@ -1,40 +1,4 @@
{
- "__inputs": [
- {
- "name": "DS_MYSQL",
- "label": "MySQL",
- "description": "",
- "type": "datasource",
- "pluginId": "mysql",
- "pluginName": "MySQL"
- }
- ],
- "__requires": [
- {
- "type": "grafana",
- "id": "grafana",
- "name": "Grafana",
- "version": "5.0.0"
- },
- {
- "type": "panel",
- "id": "graph",
- "name": "Graph",
- "version": "5.0.0"
- },
- {
- "type": "datasource",
- "id": "mysql",
- "name": "MySQL",
- "version": "5.0.0"
- },
- {
- "type": "panel",
- "id": "table",
- "name": "Table",
- "version": "5.0.0"
- }
- ],
"annotations": {
"list": [
{
@@ -52,8 +16,7 @@
"editable": true,
"gnetId": null,
"graphTooltip": 0,
- "id": null,
- "iteration": 1523372133566,
+ "iteration": 1532620738041,
"links": [],
"panels": [
{
@@ -63,7 +26,7 @@
"bars": false,
"dashLength": 10,
"dashes": false,
- "datasource": "${DS_MYSQL}",
+ "datasource": "gdev-mysql",
"fill": 2,
"gridPos": {
"h": 9,
@@ -161,7 +124,7 @@
"bars": false,
"dashLength": 10,
"dashes": false,
- "datasource": "${DS_MYSQL}",
+ "datasource": "gdev-mysql",
"fill": 2,
"gridPos": {
"h": 18,
@@ -251,7 +214,7 @@
"bars": false,
"dashLength": 10,
"dashes": false,
- "datasource": "${DS_MYSQL}",
+ "datasource": "gdev-mysql",
"fill": 2,
"gridPos": {
"h": 9,
@@ -332,7 +295,7 @@
},
{
"columns": [],
- "datasource": "${DS_MYSQL}",
+ "datasource": "gdev-mysql",
"fontSize": "100%",
"gridPos": {
"h": 9,
@@ -390,6 +353,7 @@
"schemaVersion": 16,
"style": "dark",
"tags": [
+ "gdev",
"fake-data-gen",
"mysql"
],
@@ -397,8 +361,11 @@
"list": [
{
"allValue": null,
- "current": {},
- "datasource": "${DS_MYSQL}",
+ "current": {
+ "text": "America",
+ "value": "America"
+ },
+ "datasource": "gdev-mysql",
"hide": 0,
"includeAll": false,
"label": "Datacenter",
@@ -408,6 +375,7 @@
"query": "SELECT DISTINCT datacenter FROM grafana_metric",
"refresh": 1,
"regex": "",
+ "skipUrlSync": false,
"sort": 1,
"tagValuesQuery": "",
"tags": [],
@@ -417,8 +385,11 @@
},
{
"allValue": null,
- "current": {},
- "datasource": "${DS_MYSQL}",
+ "current": {
+ "text": "All",
+ "value": "$__all"
+ },
+ "datasource": "gdev-mysql",
"hide": 0,
"includeAll": true,
"label": "Hostname",
@@ -428,6 +399,7 @@
"query": "SELECT DISTINCT hostname FROM grafana_metric WHERE datacenter='$datacenter'",
"refresh": 1,
"regex": "",
+ "skipUrlSync": false,
"sort": 1,
"tagValuesQuery": "",
"tags": [],
@@ -520,6 +492,7 @@
],
"query": "1s,10s,30s,1m,5m,10m,30m,1h,6h,12h,1d,7d,14d,30d",
"refresh": 2,
+ "skipUrlSync": false,
"type": "interval"
}
]
@@ -554,7 +527,7 @@
]
},
"timezone": "",
- "title": "Grafana Fake Data Gen - MySQL",
+ "title": "Datasource tests - MySQL",
"uid": "DGsCac3kz",
"version": 8
}
\ No newline at end of file
diff --git a/docker/blocks/mssql_tests/dashboard.json b/devenv/dev-dashboards/datasource_tests_mysql_unittest.json
similarity index 81%
rename from docker/blocks/mssql_tests/dashboard.json
rename to devenv/dev-dashboards/datasource_tests_mysql_unittest.json
index 80994254093..0255f3c0c91 100644
--- a/docker/blocks/mssql_tests/dashboard.json
+++ b/devenv/dev-dashboards/datasource_tests_mysql_unittest.json
@@ -1,40 +1,4 @@
{
- "__inputs": [
- {
- "name": "DS_MSSQL_TEST",
- "label": "MSSQL Test",
- "description": "",
- "type": "datasource",
- "pluginId": "mssql",
- "pluginName": "Microsoft SQL Server"
- }
- ],
- "__requires": [
- {
- "type": "grafana",
- "id": "grafana",
- "name": "Grafana",
- "version": "5.0.0"
- },
- {
- "type": "panel",
- "id": "graph",
- "name": "Graph",
- "version": "5.0.0"
- },
- {
- "type": "datasource",
- "id": "mssql",
- "name": "Microsoft SQL Server",
- "version": "1.0.0"
- },
- {
- "type": "panel",
- "id": "table",
- "name": "Table",
- "version": "5.0.0"
- }
- ],
"annotations": {
"list": [
{
@@ -47,31 +11,31 @@
"type": "dashboard"
},
{
- "datasource": "${DS_MSSQL_TEST}",
+ "datasource": "gdev-mysql-ds-tests",
"enable": false,
"hide": false,
"iconColor": "#6ed0e0",
"limit": 100,
"name": "Deploys",
- "rawQuery": "SELECT\n $__time(time_sec),\n description as [text],\n tags\n FROM [event]\n WHERE $__unixEpochFilter(time_sec) AND tags='deploy'\n ORDER BY 1 ASC\n ",
+ "rawQuery": "SELECT\n time_sec,\n description as text,\n tags\n FROM event\n WHERE $__unixEpochFilter(time_sec) AND tags='deploy'\n ORDER BY 1 ASC\n ",
"showIn": 0,
"tags": [],
"type": "tags"
},
{
- "datasource": "${DS_MSSQL_TEST}",
+ "datasource": "gdev-mysql-ds-tests",
"enable": false,
"hide": false,
"iconColor": "rgba(255, 96, 96, 1)",
"limit": 100,
"name": "Tickets",
- "rawQuery": "SELECT\n $__time(time_sec),\n description as [text],\n tags\n FROM [event]\n WHERE $__unixEpochFilter(time_sec) AND tags='ticket'\n ORDER BY 1 ASC\n ",
+ "rawQuery": "SELECT\n time_sec as time,\n description as text,\n tags\n FROM event\n WHERE $__unixEpochFilter(time_sec) AND tags='ticket'\n ORDER BY 1 ASC\n ",
"showIn": 0,
"tags": [],
"type": "tags"
},
{
- "datasource": "${DS_MSSQL_TEST}",
+ "datasource": "gdev-mysql-ds-tests",
"enable": false,
"hide": false,
"iconColor": "#7eb26d",
@@ -83,7 +47,7 @@
"type": "tags"
},
{
- "datasource": "${DS_MSSQL_TEST}",
+ "datasource": "gdev-mysql-ds-tests",
"enable": false,
"hide": false,
"iconColor": "#1f78c1",
@@ -96,16 +60,16 @@
}
]
},
+ "description": "Run the mysql unit tests to generate the data backing this dashboard",
"editable": true,
"gnetId": null,
"graphTooltip": 0,
- "id": null,
- "iteration": 1523320861623,
+ "iteration": 1534508678095,
"links": [],
"panels": [
{
"columns": [],
- "datasource": "${DS_MSSQL_TEST}",
+ "datasource": "gdev-mysql-ds-tests",
"fontSize": "100%",
"gridPos": {
"h": 4,
@@ -142,7 +106,7 @@
{
"alias": "",
"format": "table",
- "rawSql": "SELECT * from mssql_types",
+ "rawSql": "SELECT * from mysql_types",
"refId": "A"
}
],
@@ -152,7 +116,7 @@
},
{
"columns": [],
- "datasource": "${DS_MSSQL_TEST}",
+ "datasource": "gdev-mysql-ds-tests",
"fontSize": "100%",
"gridPos": {
"h": 3,
@@ -173,7 +137,7 @@
{
"alias": "Time",
"dateFormat": "YYYY-MM-DD HH:mm:ss",
- "pattern": "time",
+ "pattern": "time_sec",
"type": "date"
},
{
@@ -195,18 +159,18 @@
{
"alias": "",
"format": "table",
- "rawSql": "SELECT cast(null as bigint) as time",
+ "rawSql": "SELECT cast(null as unsigned integer) as time_sec",
"refId": "A",
"target": ""
}
],
- "title": "cast(null as bigint) as time",
+ "title": "cast(null as unsigned integer) as time",
"transform": "table",
"type": "table"
},
{
"columns": [],
- "datasource": "${DS_MSSQL_TEST}",
+ "datasource": "gdev-mysql-ds-tests",
"fontSize": "100%",
"gridPos": {
"h": 3,
@@ -227,7 +191,7 @@
{
"alias": "Time",
"dateFormat": "YYYY-MM-DD HH:mm:ss",
- "pattern": "time",
+ "pattern": "time_sec",
"type": "date"
},
{
@@ -249,7 +213,7 @@
{
"alias": "",
"format": "table",
- "rawSql": "SELECT cast(null as datetime) as time",
+ "rawSql": "SELECT cast(null as datetime) as time_sec",
"refId": "A",
"target": ""
}
@@ -260,7 +224,7 @@
},
{
"columns": [],
- "datasource": "${DS_MSSQL_TEST}",
+ "datasource": "gdev-mysql-ds-tests",
"fontSize": "100%",
"gridPos": {
"h": 3,
@@ -281,7 +245,7 @@
{
"alias": "Time",
"dateFormat": "YYYY-MM-DD HH:mm:ss",
- "pattern": "time",
+ "pattern": "time_sec",
"type": "date"
},
{
@@ -303,18 +267,18 @@
{
"alias": "",
"format": "table",
- "rawSql": "SELECT GETDATE() as time",
+ "rawSql": "SELECT cast(NOW() as datetime) as time_sec",
"refId": "A",
"target": ""
}
],
- "title": "GETDATE() as time",
+ "title": "cast()NOW() as datetime) as time",
"transform": "table",
"type": "table"
},
{
"columns": [],
- "datasource": "${DS_MSSQL_TEST}",
+ "datasource": "gdev-mysql-ds-tests",
"fontSize": "100%",
"gridPos": {
"h": 3,
@@ -357,12 +321,12 @@
{
"alias": "",
"format": "table",
- "rawSql": "SELECT GETUTCDATE() as time",
+ "rawSql": "SELECT NOW() as time",
"refId": "A",
"target": ""
}
],
- "title": "GETUTCDATE() as time",
+ "title": "NOW() as time",
"transform": "table",
"type": "table"
},
@@ -371,11 +335,11 @@
"bars": false,
"dashLength": 10,
"dashes": false,
- "datasource": "${DS_MSSQL_TEST}",
+ "datasource": "gdev-mysql-ds-tests",
"fill": 2,
"gridPos": {
- "h": 9,
- "w": 8,
+ "h": 6,
+ "w": 6,
"x": 0,
"y": 7
},
@@ -405,7 +369,7 @@
{
"alias": "",
"format": "time_series",
- "rawSql": "SELECT $__timeGroup(time, '5m') AS time, avg(value) as value FROM metric WHERE $__timeFilter(time) GROUP BY $__timeGroup(time, '5m') ORDER BY 1",
+ "rawSql": "SELECT $__timeGroupAlias(time, '5m'), avg(value) as value FROM metric WHERE $__timeFilter(time) GROUP BY 1 ORDER BY 1",
"refId": "A"
}
],
@@ -454,12 +418,12 @@
"bars": false,
"dashLength": 10,
"dashes": false,
- "datasource": "${DS_MSSQL_TEST}",
+ "datasource": "gdev-mysql-ds-tests",
"fill": 2,
"gridPos": {
- "h": 9,
- "w": 8,
- "x": 8,
+ "h": 6,
+ "w": 6,
+ "x": 6,
"y": 7
},
"id": 9,
@@ -488,7 +452,7 @@
{
"alias": "",
"format": "time_series",
- "rawSql": "SELECT $__timeGroup(time, '5m', NULL) AS time, avg(value) as value FROM metric WHERE $__timeFilter(time) GROUP BY $__timeGroup(time, '5m') ORDER BY 1",
+ "rawSql": "SELECT $__timeGroupAlias(time, '5m', NULL), avg(value) as value FROM metric WHERE $__timeFilter(time) GROUP BY 1 ORDER BY 1",
"refId": "A"
}
],
@@ -537,12 +501,12 @@
"bars": false,
"dashLength": 10,
"dashes": false,
- "datasource": "${DS_MSSQL_TEST}",
+ "datasource": "gdev-mysql-ds-tests",
"fill": 2,
"gridPos": {
- "h": 9,
- "w": 8,
- "x": 16,
+ "h": 6,
+ "w": 6,
+ "x": 12,
"y": 7
},
"id": 10,
@@ -571,7 +535,7 @@
{
"alias": "",
"format": "time_series",
- "rawSql": "SELECT $__timeGroup(time, '5m', 10.0) AS time, avg(value) as value FROM metric WHERE $__timeFilter(time) GROUP BY $__timeGroup(time, '5m') ORDER BY 1",
+ "rawSql": "SELECT $__timeGroupAlias(time, '5m', 10.0), avg(value) as value FROM metric WHERE $__timeFilter(time) GROUP BY 1 ORDER BY 1",
"refId": "A"
}
],
@@ -615,18 +579,101 @@
"alignLevel": null
}
},
+ {
+ "aliasColors": {},
+ "bars": false,
+ "dashLength": 10,
+ "dashes": false,
+ "datasource": "gdev-mysql-ds-tests",
+ "fill": 2,
+ "gridPos": {
+ "h": 6,
+ "w": 6,
+ "x": 18,
+ "y": 7
+ },
+ "id": 36,
+ "legend": {
+ "avg": false,
+ "current": false,
+ "max": false,
+ "min": false,
+ "show": true,
+ "total": false,
+ "values": false
+ },
+ "lines": true,
+ "linewidth": 2,
+ "links": [],
+ "nullPointMode": "null",
+ "percentage": false,
+ "pointradius": 3,
+ "points": true,
+ "renderer": "flot",
+ "seriesOverrides": [],
+ "spaceLength": 10,
+ "stack": false,
+ "steppedLine": true,
+ "targets": [
+ {
+ "alias": "",
+ "format": "time_series",
+ "rawSql": "SELECT $__timeGroupAlias(time, '5m', previous), avg(value) as value FROM metric WHERE $__timeFilter(time) GROUP BY 1 ORDER BY 1",
+ "refId": "A"
+ }
+ ],
+ "thresholds": [],
+ "timeFrom": null,
+ "timeShift": null,
+ "title": "timeGroup macro 5m with fill(previous)",
+ "tooltip": {
+ "shared": true,
+ "sort": 0,
+ "value_type": "individual"
+ },
+ "type": "graph",
+ "xaxis": {
+ "buckets": null,
+ "mode": "time",
+ "name": null,
+ "show": true,
+ "values": []
+ },
+ "yaxes": [
+ {
+ "format": "short",
+ "label": null,
+ "logBase": 1,
+ "max": null,
+ "min": "0",
+ "show": true
+ },
+ {
+ "format": "short",
+ "label": null,
+ "logBase": 1,
+ "max": null,
+ "min": null,
+ "show": true
+ }
+ ],
+ "yaxis": {
+ "align": false,
+ "alignLevel": null
+ }
+ },
{
"aliasColors": {},
"bars": true,
"dashLength": 10,
"dashes": false,
- "datasource": "${DS_MSSQL_TEST}",
+ "datasource": "gdev-mysql-ds-tests",
"fill": 2,
"gridPos": {
- "h": 9,
- "w": 8,
+ "h": 6,
+ "w": 6,
"x": 0,
- "y": 16
+ "y": 13
},
"id": 16,
"legend": {
@@ -654,7 +701,7 @@
{
"alias": "",
"format": "time_series",
- "rawSql": "SELECT $__timeGroup(time, '$summarize') AS time, avg(value) as value FROM metric WHERE $__timeFilter(time) GROUP BY $__timeGroup(time, '$summarize') ORDER BY 1",
+ "rawSql": "SELECT $__timeGroupAlias(time, '$summarize'), avg(value) as value FROM metric WHERE $__timeFilter(time) GROUP BY 1 ORDER BY 1",
"refId": "A"
}
],
@@ -703,13 +750,13 @@
"bars": true,
"dashLength": 10,
"dashes": false,
- "datasource": "${DS_MSSQL_TEST}",
+ "datasource": "gdev-mysql-ds-tests",
"fill": 2,
"gridPos": {
- "h": 9,
- "w": 8,
- "x": 8,
- "y": 16
+ "h": 6,
+ "w": 6,
+ "x": 6,
+ "y": 13
},
"id": 12,
"legend": {
@@ -737,7 +784,7 @@
{
"alias": "",
"format": "time_series",
- "rawSql": "SELECT $__timeGroup(time, '$summarize', NULL) AS time, sum(value) as value FROM metric WHERE $__timeFilter(time) GROUP BY $__timeGroup(time, '$summarize') ORDER BY 1",
+ "rawSql": "SELECT $__timeGroupAlias(time, '$summarize', NULL), sum(value) as value FROM metric WHERE $__timeFilter(time) GROUP BY 1 ORDER BY 1",
"refId": "A"
}
],
@@ -786,13 +833,13 @@
"bars": true,
"dashLength": 10,
"dashes": false,
- "datasource": "${DS_MSSQL_TEST}",
+ "datasource": "gdev-mysql-ds-tests",
"fill": 2,
"gridPos": {
- "h": 9,
- "w": 8,
- "x": 16,
- "y": 16
+ "h": 6,
+ "w": 6,
+ "x": 12,
+ "y": 13
},
"id": 13,
"legend": {
@@ -820,7 +867,7 @@
{
"alias": "",
"format": "time_series",
- "rawSql": "SELECT $__timeGroup(time, '$summarize', 100.0) AS time, sum(value) as value FROM metric WHERE $__timeFilter(time) GROUP BY $__timeGroup(time, '$summarize') ORDER BY 1",
+ "rawSql": "SELECT $__timeGroupAlias(time, '$summarize', 100.0), sum(value) as value FROM metric WHERE $__timeFilter(time) GROUP BY 1 ORDER BY 1",
"refId": "A"
}
],
@@ -864,18 +911,101 @@
"alignLevel": null
}
},
+ {
+ "aliasColors": {},
+ "bars": true,
+ "dashLength": 10,
+ "dashes": false,
+ "datasource": "gdev-mysql-ds-tests",
+ "fill": 2,
+ "gridPos": {
+ "h": 6,
+ "w": 6,
+ "x": 18,
+ "y": 13
+ },
+ "id": 37,
+ "legend": {
+ "avg": false,
+ "current": false,
+ "max": false,
+ "min": false,
+ "show": true,
+ "total": false,
+ "values": false
+ },
+ "lines": false,
+ "linewidth": 2,
+ "links": [],
+ "nullPointMode": "null",
+ "percentage": false,
+ "pointradius": 3,
+ "points": false,
+ "renderer": "flot",
+ "seriesOverrides": [],
+ "spaceLength": 10,
+ "stack": false,
+ "steppedLine": true,
+ "targets": [
+ {
+ "alias": "",
+ "format": "time_series",
+ "rawSql": "SELECT $__timeGroupAlias(time, '$summarize', previous), sum(value) as value FROM metric WHERE $__timeFilter(time) GROUP BY 1 ORDER BY 1",
+ "refId": "A"
+ }
+ ],
+ "thresholds": [],
+ "timeFrom": null,
+ "timeShift": null,
+ "title": "Metrics - timeGroup macro $summarize with fill(previous)",
+ "tooltip": {
+ "shared": true,
+ "sort": 0,
+ "value_type": "individual"
+ },
+ "type": "graph",
+ "xaxis": {
+ "buckets": null,
+ "mode": "time",
+ "name": null,
+ "show": true,
+ "values": []
+ },
+ "yaxes": [
+ {
+ "format": "short",
+ "label": null,
+ "logBase": 1,
+ "max": null,
+ "min": null,
+ "show": true
+ },
+ {
+ "format": "short",
+ "label": null,
+ "logBase": 1,
+ "max": null,
+ "min": null,
+ "show": true
+ }
+ ],
+ "yaxis": {
+ "align": false,
+ "alignLevel": null
+ }
+ },
{
"aliasColors": {},
"bars": false,
"dashLength": 10,
"dashes": false,
- "datasource": "${DS_MSSQL_TEST}",
+ "datasource": "gdev-mysql-ds-tests",
"fill": 2,
"gridPos": {
"h": 8,
"w": 12,
"x": 0,
- "y": 25
+ "y": 19
},
"id": 27,
"legend": {
@@ -907,14 +1037,8 @@
{
"alias": "",
"format": "time_series",
- "rawSql": "SELECT \n $__timeGroup(time, '$summarize') as time, \n measurement + ' - value one' as metric, \n avg(valueOne) as valueOne\nFROM\n metric_values \nWHERE\n $__timeFilter(time) AND\n ($metric = 'ALL' OR measurement = $metric)\nGROUP BY \n $__timeGroup(time, '$summarize'), \n measurement \nORDER BY 1",
+ "rawSql": "SELECT \n $__timeGroupAlias(time, '$summarize'), \n measurement as metric, \n avg(valueOne) as valueOne,\n avg(valueTwo) as valueTwo\nFROM\n metric_values \nWHERE\n $__timeFilter(time) AND\n measurement IN($metric)\nGROUP BY 1, 2\nORDER BY 1",
"refId": "A"
- },
- {
- "alias": "",
- "format": "time_series",
- "rawSql": "SELECT \n $__timeGroup(time, '$summarize') as time, \n measurement + ' - value two' as metric, \n avg(valueTwo) as valueTwo \nFROM\n metric_values\nWHERE\n $__timeFilter(time) AND\n ($metric = 'ALL' OR measurement = $metric)\nGROUP BY \n $__timeGroup(time, '$summarize'), \n measurement \nORDER BY 1",
- "refId": "B"
}
],
"thresholds": [],
@@ -962,13 +1086,13 @@
"bars": false,
"dashLength": 10,
"dashes": false,
- "datasource": "${DS_MSSQL_TEST}",
+ "datasource": "gdev-mysql-ds-tests",
"fill": 2,
"gridPos": {
"h": 8,
"w": 12,
"x": 12,
- "y": 25
+ "y": 19
},
"id": 5,
"legend": {
@@ -1010,14 +1134,8 @@
{
"alias": "",
"format": "time_series",
- "rawSql": "SELECT \n $__timeGroup(time, '$summarize') as time, \n avg(valueOne) as valueOne, \n avg(valueTwo) as valueTwo \nFROM\n metric_values \nWHERE \n $__timeFilter(time) AND \n ($metric = 'ALL' OR measurement = $metric)\nGROUP BY \n $__timeGroup(time, '$summarize')\nORDER BY 1",
+ "rawSql": "SELECT \n $__timeGroupAlias(time, '$summarize'), \n avg(valueOne) as valueOne, \n avg(valueTwo) as valueTwo \nFROM\n metric_values \nWHERE \n $__timeFilter(time) AND \n measurement IN($metric)\nGROUP BY 1\nORDER BY 1",
"refId": "A"
- },
- {
- "alias": "",
- "format": "time_series",
- "rawSql": "SELECT \n time,\n avg(valueOne) OVER (ORDER BY time ROWS BETWEEN 6 PRECEDING AND 6 FOLLOWING) as MovingAverageValueOne,\n avg(valueTwo) OVER (ORDER BY time ROWS BETWEEN 6 PRECEDING AND 6 FOLLOWING) as MovingAverageValueTwo\nFROM\n metric_values \nWHERE \n $__timeFilter(time) AND \n ($metric = 'ALL' OR measurement = $metric)\nORDER BY 1",
- "refId": "B"
}
],
"thresholds": [],
@@ -1065,13 +1183,197 @@
"bars": false,
"dashLength": 10,
"dashes": false,
- "datasource": "${DS_MSSQL_TEST}",
+ "datasource": "gdev-mysql-ds-tests",
"fill": 2,
"gridPos": {
"h": 8,
"w": 12,
"x": 0,
- "y": 33
+ "y": 27
+ },
+ "id": 38,
+ "legend": {
+ "alignAsTable": true,
+ "avg": true,
+ "current": true,
+ "hideEmpty": false,
+ "hideZero": false,
+ "max": true,
+ "min": true,
+ "rightSide": true,
+ "show": true,
+ "total": true,
+ "values": true
+ },
+ "lines": true,
+ "linewidth": 2,
+ "links": [],
+ "nullPointMode": "null",
+ "percentage": false,
+ "pointradius": 3,
+ "points": false,
+ "renderer": "flot",
+ "seriesOverrides": [],
+ "spaceLength": 10,
+ "stack": false,
+ "steppedLine": false,
+ "targets": [
+ {
+ "alias": "",
+ "format": "time_series",
+ "rawSql": "SELECT \n $__unixEpochGroupAlias(timeInt32, '$summarize'), \n measurement, \n avg(valueOne) as valueOne,\n avg(valueTwo) as valueTwo\nFROM\n metric_values \nWHERE\n $__unixEpochFilter(timeInt32) AND\n measurement in($metric)\nGROUP BY 1, 2\nORDER BY 1, 2",
+ "refId": "A"
+ }
+ ],
+ "thresholds": [],
+ "timeFrom": null,
+ "timeShift": null,
+ "title": "Multiple series with metric column using unixEpochGroup macro ($summarize)",
+ "tooltip": {
+ "shared": true,
+ "sort": 0,
+ "value_type": "individual"
+ },
+ "type": "graph",
+ "xaxis": {
+ "buckets": null,
+ "mode": "time",
+ "name": null,
+ "show": true,
+ "values": []
+ },
+ "yaxes": [
+ {
+ "format": "short",
+ "label": null,
+ "logBase": 1,
+ "max": null,
+ "min": "0",
+ "show": true
+ },
+ {
+ "format": "short",
+ "label": null,
+ "logBase": 1,
+ "max": null,
+ "min": null,
+ "show": true
+ }
+ ],
+ "yaxis": {
+ "align": false,
+ "alignLevel": null
+ }
+ },
+ {
+ "aliasColors": {},
+ "bars": false,
+ "dashLength": 10,
+ "dashes": false,
+ "datasource": "gdev-mysql-ds-tests",
+ "fill": 2,
+ "gridPos": {
+ "h": 8,
+ "w": 12,
+ "x": 12,
+ "y": 27
+ },
+ "id": 39,
+ "legend": {
+ "alignAsTable": true,
+ "avg": true,
+ "current": true,
+ "max": true,
+ "min": true,
+ "rightSide": true,
+ "show": true,
+ "total": true,
+ "values": true
+ },
+ "lines": true,
+ "linewidth": 2,
+ "links": [],
+ "nullPointMode": "null",
+ "percentage": false,
+ "pointradius": 3,
+ "points": false,
+ "renderer": "flot",
+ "seriesOverrides": [
+ {
+ "alias": "MovingAverageValueOne",
+ "dashes": true,
+ "lines": false
+ },
+ {
+ "alias": "MovingAverageValueTwo",
+ "dashes": true,
+ "lines": false,
+ "yaxis": 1
+ }
+ ],
+ "spaceLength": 10,
+ "stack": false,
+ "steppedLine": false,
+ "targets": [
+ {
+ "alias": "",
+ "format": "time_series",
+ "rawSql": "SELECT \n $__unixEpochGroupAlias(timeInt32, '$summarize'), \n avg(valueOne) as valueOne,\n avg(valueTwo) as valueTwo\nFROM\n metric_values \nWHERE\n $__unixEpochFilter(timeInt32) AND\n measurement in($metric)\nGROUP BY 1\nORDER BY 1",
+ "refId": "A"
+ }
+ ],
+ "thresholds": [],
+ "timeFrom": null,
+ "timeShift": null,
+ "title": "Multiple series without metric column using unixEpochGroup macro ($summarize)",
+ "tooltip": {
+ "shared": true,
+ "sort": 0,
+ "value_type": "individual"
+ },
+ "type": "graph",
+ "xaxis": {
+ "buckets": null,
+ "mode": "time",
+ "name": null,
+ "show": true,
+ "values": []
+ },
+ "yaxes": [
+ {
+ "format": "short",
+ "label": null,
+ "logBase": 1,
+ "max": null,
+ "min": "0",
+ "show": true
+ },
+ {
+ "format": "short",
+ "label": null,
+ "logBase": 1,
+ "max": null,
+ "min": null,
+ "show": true
+ }
+ ],
+ "yaxis": {
+ "align": false,
+ "alignLevel": null
+ }
+ },
+ {
+ "aliasColors": {},
+ "bars": false,
+ "dashLength": 10,
+ "dashes": false,
+ "datasource": "gdev-mysql-ds-tests",
+ "fill": 2,
+ "gridPos": {
+ "h": 8,
+ "w": 12,
+ "x": 0,
+ "y": 35
},
"id": 4,
"legend": {
@@ -1103,14 +1405,8 @@
{
"alias": "",
"format": "time_series",
- "rawSql": "SELECT $__timeEpoch(time), measurement + ' - value one' as metric, valueOne FROM metric_values WHERE $__timeFilter(time) AND ($metric = 'ALL' OR measurement = $metric) ORDER BY 1",
+ "rawSql": "SELECT $__time(time), measurement as metric, valueOne, valueTwo FROM metric_values WHERE $__timeFilter(time) AND measurement IN($metric) ORDER BY 1",
"refId": "A"
- },
- {
- "alias": "",
- "format": "time_series",
- "rawSql": "SELECT $__timeEpoch(time), measurement + ' - value two' as metric, valueTwo FROM metric_values WHERE $__timeFilter(time) AND ($metric = 'ALL' OR measurement = $metric) ORDER BY 1",
- "refId": "B"
}
],
"thresholds": [],
@@ -1158,13 +1454,13 @@
"bars": false,
"dashLength": 10,
"dashes": false,
- "datasource": "${DS_MSSQL_TEST}",
+ "datasource": "gdev-mysql-ds-tests",
"fill": 2,
"gridPos": {
"h": 8,
"w": 12,
"x": 12,
- "y": 33
+ "y": 35
},
"id": 28,
"legend": {
@@ -1194,7 +1490,7 @@
{
"alias": "",
"format": "time_series",
- "rawSql": "SELECT $__timeEpoch(time), valueOne, valueTwo FROM metric_values ORDER BY 1",
+ "rawSql": "SELECT $__time(time), valueOne, valueTwo FROM metric_values ORDER BY 1",
"refId": "A"
}
],
@@ -1243,13 +1539,13 @@
"bars": false,
"dashLength": 10,
"dashes": false,
- "datasource": "${DS_MSSQL_TEST}",
+ "datasource": "gdev-mysql-ds-tests",
"fill": 2,
"gridPos": {
"h": 8,
"w": 12,
"x": 0,
- "y": 41
+ "y": 43
},
"id": 19,
"legend": {
@@ -1281,14 +1577,8 @@
{
"alias": "",
"format": "time_series",
- "rawSql": "SELECT $__timeEpoch(time), measurement + ' - value one' as metric, valueOne FROM metric_values WHERE $__timeFilter(time) AND ($metric = 'ALL' OR measurement = $metric) ORDER BY 1",
+ "rawSql": "SELECT $__time(time), measurement as metric, valueOne, valueTwo FROM metric_values WHERE $__timeFilter(time) AND measurement IN($metric) ORDER BY 1",
"refId": "A"
- },
- {
- "alias": "",
- "format": "time_series",
- "rawSql": "SELECT $__timeEpoch(time), measurement + ' - value two' as metric, valueTwo FROM metric_values WHERE $__timeFilter(time) AND ($metric = 'ALL' OR measurement = $metric) ORDER BY 1",
- "refId": "B"
}
],
"thresholds": [],
@@ -1336,13 +1626,13 @@
"bars": false,
"dashLength": 10,
"dashes": false,
- "datasource": "${DS_MSSQL_TEST}",
+ "datasource": "gdev-mysql-ds-tests",
"fill": 2,
"gridPos": {
"h": 8,
"w": 12,
"x": 12,
- "y": 41
+ "y": 43
},
"id": 18,
"legend": {
@@ -1372,7 +1662,7 @@
{
"alias": "",
"format": "time_series",
- "rawSql": "SELECT $__timeEpoch(time), valueOne, valueTwo FROM metric_values\nWHERE $__timeFilter(time) AND ($metric = 'ALL' OR measurement = $metric) ORDER BY 1",
+ "rawSql": "SELECT $__timeEpoch(time), valueOne, valueTwo FROM metric_values\nWHERE $__timeFilter(time) AND measurement IN($metric) ORDER BY 1",
"refId": "A"
}
],
@@ -1421,13 +1711,13 @@
"bars": false,
"dashLength": 10,
"dashes": false,
- "datasource": "${DS_MSSQL_TEST}",
+ "datasource": "gdev-mysql-ds-tests",
"fill": 2,
"gridPos": {
"h": 8,
"w": 12,
"x": 0,
- "y": 49
+ "y": 51
},
"id": 17,
"legend": {
@@ -1459,14 +1749,8 @@
{
"alias": "",
"format": "time_series",
- "rawSql": "SELECT $__timeEpoch(time), measurement + ' - value one' as metric, valueOne FROM metric_values WHERE $__timeFilter(time) AND ($metric = 'ALL' OR measurement = $metric) ORDER BY 1",
+ "rawSql": "SELECT $__time(time), measurement as metric, valueOne, valueTwo FROM metric_values WHERE $__timeFilter(time) AND measurement IN($metric) ORDER BY 1",
"refId": "A"
- },
- {
- "alias": "",
- "format": "time_series",
- "rawSql": "SELECT $__timeEpoch(time), measurement + ' - value two' as metric, valueTwo FROM metric_values WHERE $__timeFilter(time) AND ($metric = 'ALL' OR measurement = $metric) ORDER BY 1",
- "refId": "B"
}
],
"thresholds": [],
@@ -1514,13 +1798,13 @@
"bars": false,
"dashLength": 10,
"dashes": false,
- "datasource": "${DS_MSSQL_TEST}",
+ "datasource": "gdev-mysql-ds-tests",
"fill": 2,
"gridPos": {
"h": 8,
"w": 12,
"x": 12,
- "y": 49
+ "y": 51
},
"id": 20,
"legend": {
@@ -1550,7 +1834,7 @@
{
"alias": "",
"format": "time_series",
- "rawSql": "SELECT $__timeEpoch(time), valueOne, valueTwo FROM metric_values\nWHERE $__timeFilter(time) AND ($metric = 'ALL' OR measurement = $metric) ORDER BY 1",
+ "rawSql": "SELECT $__timeEpoch(time), valueOne, valueTwo FROM metric_values\nWHERE $__timeFilter(time) AND measurement IN($metric) ORDER BY 1",
"refId": "A"
}
],
@@ -1594,192 +1878,18 @@
"alignLevel": null
}
},
- {
- "aliasColors": {},
- "bars": false,
- "dashLength": 10,
- "dashes": false,
- "datasource": "${DS_MSSQL_TEST}",
- "fill": 2,
- "gridPos": {
- "h": 8,
- "w": 12,
- "x": 0,
- "y": 57
- },
- "id": 29,
- "legend": {
- "alignAsTable": true,
- "avg": true,
- "current": true,
- "hideEmpty": false,
- "hideZero": false,
- "max": true,
- "min": true,
- "rightSide": true,
- "show": true,
- "total": true,
- "values": true
- },
- "lines": true,
- "linewidth": 2,
- "links": [],
- "nullPointMode": "null",
- "percentage": false,
- "pointradius": 3,
- "points": false,
- "renderer": "flot",
- "seriesOverrides": [],
- "spaceLength": 10,
- "stack": false,
- "steppedLine": false,
- "targets": [
- {
- "alias": "",
- "format": "time_series",
- "rawSql": "DECLARE\n @from int = $__unixEpochFrom(), \n @to int = $__unixEpochTo(), \n @interval nvarchar(50) = '$summarize', \n @metric nvarchar(200) = $metric\n \nEXEC dbo.sp_test_epoch @from, @to, @interval, @metric",
- "refId": "A"
- }
- ],
- "thresholds": [],
- "timeFrom": null,
- "timeShift": null,
- "title": "Stored procedure support using epoch",
- "tooltip": {
- "shared": true,
- "sort": 0,
- "value_type": "individual"
- },
- "type": "graph",
- "xaxis": {
- "buckets": null,
- "mode": "time",
- "name": null,
- "show": true,
- "values": []
- },
- "yaxes": [
- {
- "format": "short",
- "label": null,
- "logBase": 1,
- "max": null,
- "min": "0",
- "show": true
- },
- {
- "format": "short",
- "label": null,
- "logBase": 1,
- "max": null,
- "min": null,
- "show": true
- }
- ],
- "yaxis": {
- "align": false,
- "alignLevel": null
- }
- },
- {
- "aliasColors": {},
- "bars": false,
- "dashLength": 10,
- "dashes": false,
- "datasource": "${DS_MSSQL_TEST}",
- "fill": 2,
- "gridPos": {
- "h": 8,
- "w": 12,
- "x": 12,
- "y": 57
- },
- "id": 30,
- "legend": {
- "alignAsTable": true,
- "avg": true,
- "current": true,
- "hideEmpty": false,
- "hideZero": false,
- "max": true,
- "min": true,
- "rightSide": true,
- "show": true,
- "total": true,
- "values": true
- },
- "lines": true,
- "linewidth": 2,
- "links": [],
- "nullPointMode": "null",
- "percentage": false,
- "pointradius": 3,
- "points": false,
- "renderer": "flot",
- "seriesOverrides": [],
- "spaceLength": 10,
- "stack": false,
- "steppedLine": false,
- "targets": [
- {
- "alias": "",
- "format": "time_series",
- "rawSql": "DECLARE\n @from datetime = $__timeFrom(), \n @to datetime = $__timeTo(), \n @interval nvarchar(50) = '$summarize', \n @metric nvarchar(200) = $metric\n \nEXEC dbo.sp_test_datetime @from, @to, @interval, @metric",
- "refId": "A"
- }
- ],
- "thresholds": [],
- "timeFrom": null,
- "timeShift": null,
- "title": "Stored procedure support using datetime",
- "tooltip": {
- "shared": true,
- "sort": 0,
- "value_type": "individual"
- },
- "type": "graph",
- "xaxis": {
- "buckets": null,
- "mode": "time",
- "name": null,
- "show": true,
- "values": []
- },
- "yaxes": [
- {
- "format": "short",
- "label": null,
- "logBase": 1,
- "max": null,
- "min": "0",
- "show": true
- },
- {
- "format": "short",
- "label": null,
- "logBase": 1,
- "max": null,
- "min": null,
- "show": true
- }
- ],
- "yaxis": {
- "align": false,
- "alignLevel": null
- }
- },
{
"aliasColors": {},
"bars": true,
"dashLength": 10,
"dashes": false,
- "datasource": "${DS_MSSQL_TEST}",
+ "datasource": "gdev-mysql-ds-tests",
"fill": 1,
"gridPos": {
"h": 8,
"w": 12,
"x": 0,
- "y": 65
+ "y": 59
},
"id": 14,
"legend": {
@@ -1809,14 +1919,8 @@
{
"alias": "",
"format": "time_series",
- "rawSql": "SELECT $__timeEpoch(time), measurement + ' - value one' as metric, valueOne FROM metric_values \nWHERE $__timeFilter(time) AND ($metric = 'ALL' OR measurement = $metric) ORDER BY 1",
+ "rawSql": "SELECT $__time(time), measurement as metric, valueOne, valueTwo FROM metric_values WHERE $__timeFilter(time) AND measurement IN($metric) ORDER BY 1",
"refId": "A"
- },
- {
- "alias": "",
- "format": "time_series",
- "rawSql": "SELECT $__timeEpoch(time), measurement + ' - value two' as metric, valueTwo FROM metric_values \nWHERE $__timeFilter(time) AND ($metric = 'ALL' OR measurement = $metric) ORDER BY 1",
- "refId": "B"
}
],
"thresholds": [],
@@ -1867,13 +1971,13 @@
"bars": true,
"dashLength": 10,
"dashes": false,
- "datasource": "${DS_MSSQL_TEST}",
+ "datasource": "gdev-mysql-ds-tests",
"fill": 1,
"gridPos": {
"h": 8,
"w": 12,
"x": 12,
- "y": 65
+ "y": 59
},
"id": 15,
"legend": {
@@ -1903,7 +2007,7 @@
{
"alias": "",
"format": "time_series",
- "rawSql": "SELECT $__timeEpoch(time), valueOne, valueTwo FROM metric_values\nWHERE $__timeFilter(time) AND ($metric = 'ALL' OR measurement = $metric) ORDER BY 1",
+ "rawSql": "SELECT $__timeEpoch(time), valueOne, valueTwo FROM metric_values\nWHERE $__timeFilter(time) AND measurement IN($metric) ORDER BY 1",
"refId": "A"
}
],
@@ -1954,13 +2058,13 @@
"bars": true,
"dashLength": 10,
"dashes": false,
- "datasource": "${DS_MSSQL_TEST}",
+ "datasource": "gdev-mysql-ds-tests",
"fill": 1,
"gridPos": {
"h": 8,
"w": 12,
"x": 0,
- "y": 73
+ "y": 67
},
"id": 25,
"legend": {
@@ -1990,14 +2094,8 @@
{
"alias": "",
"format": "time_series",
- "rawSql": "SELECT $__timeEpoch(time), measurement + ' - value one' as metric, valueOne FROM metric_values \nWHERE $__timeFilter(time) AND ($metric = 'ALL' OR measurement = $metric) ORDER BY 1",
+ "rawSql": "SELECT $__time(time), measurement as metric, valueOne, valueTwo FROM metric_values WHERE $__timeFilter(time) AND measurement IN($metric) ORDER BY 1",
"refId": "A"
- },
- {
- "alias": "",
- "format": "time_series",
- "rawSql": "SELECT $__timeEpoch(time), measurement + ' - value two' as metric, valueTwo FROM metric_values \nWHERE $__timeFilter(time) AND ($metric = 'ALL' OR measurement = $metric) ORDER BY 1",
- "refId": "B"
}
],
"thresholds": [],
@@ -2048,13 +2146,13 @@
"bars": true,
"dashLength": 10,
"dashes": false,
- "datasource": "${DS_MSSQL_TEST}",
+ "datasource": "gdev-mysql-ds-tests",
"fill": 1,
"gridPos": {
"h": 8,
"w": 12,
"x": 12,
- "y": 73
+ "y": 67
},
"id": 22,
"legend": {
@@ -2084,7 +2182,7 @@
{
"alias": "",
"format": "time_series",
- "rawSql": "SELECT $__timeEpoch(time), valueOne, valueTwo FROM metric_values\nWHERE $__timeFilter(time) AND ($metric = 'ALL' OR measurement = $metric) ORDER BY 1",
+ "rawSql": "SELECT $__timeEpoch(time), valueOne, valueTwo FROM metric_values\nWHERE $__timeFilter(time) AND measurement IN($metric) ORDER BY 1",
"refId": "A"
}
],
@@ -2135,13 +2233,13 @@
"bars": true,
"dashLength": 10,
"dashes": false,
- "datasource": "${DS_MSSQL_TEST}",
+ "datasource": "gdev-mysql-ds-tests",
"fill": 1,
"gridPos": {
"h": 8,
"w": 12,
"x": 0,
- "y": 81
+ "y": 75
},
"id": 21,
"legend": {
@@ -2171,14 +2269,8 @@
{
"alias": "",
"format": "time_series",
- "rawSql": "SELECT $__timeEpoch(time), measurement + ' - value one' as metric, valueOne FROM metric_values \nWHERE $__timeFilter(time) AND ($metric = 'ALL' OR measurement = $metric) ORDER BY 1",
+ "rawSql": "SELECT $__time(time), measurement as metric, valueOne, valueTwo FROM metric_values WHERE $__timeFilter(time) AND measurement IN($metric) ORDER BY 1",
"refId": "A"
- },
- {
- "alias": "",
- "format": "time_series",
- "rawSql": "SELECT $__timeEpoch(time), measurement + ' - value two' as metric, valueTwo FROM metric_values \nWHERE $__timeFilter(time) AND ($metric = 'ALL' OR measurement = $metric) ORDER BY 1",
- "refId": "B"
}
],
"thresholds": [],
@@ -2229,13 +2321,13 @@
"bars": true,
"dashLength": 10,
"dashes": false,
- "datasource": "${DS_MSSQL_TEST}",
+ "datasource": "gdev-mysql-ds-tests",
"fill": 1,
"gridPos": {
"h": 8,
"w": 12,
"x": 12,
- "y": 81
+ "y": 75
},
"id": 26,
"legend": {
@@ -2265,7 +2357,7 @@
{
"alias": "",
"format": "time_series",
- "rawSql": "SELECT $__timeEpoch(time), valueOne, valueTwo FROM metric_values \nWHERE $__timeFilter(time) AND ($metric = 'ALL' OR measurement = $metric) ORDER BY 1",
+ "rawSql": "SELECT $__timeEpoch(time), valueOne, valueTwo FROM metric_values\nWHERE $__timeFilter(time) AND measurement IN($metric) ORDER BY 1",
"refId": "A"
}
],
@@ -2316,13 +2408,13 @@
"bars": true,
"dashLength": 10,
"dashes": false,
- "datasource": "${DS_MSSQL_TEST}",
+ "datasource": "gdev-mysql-ds-tests",
"fill": 1,
"gridPos": {
"h": 8,
"w": 12,
"x": 0,
- "y": 89
+ "y": 83
},
"id": 23,
"legend": {
@@ -2352,14 +2444,8 @@
{
"alias": "",
"format": "time_series",
- "rawSql": "SELECT $__timeEpoch(time), measurement + ' - value one' as metric, valueOne FROM metric_values\nWHERE $__timeFilter(time) AND ($metric = 'ALL' OR measurement = $metric) ORDER BY 1",
+ "rawSql": "SELECT $__time(time), measurement as metric, valueOne, valueTwo FROM metric_values WHERE $__timeFilter(time) AND measurement IN($metric) ORDER BY 1",
"refId": "A"
- },
- {
- "alias": "",
- "format": "time_series",
- "rawSql": "SELECT $__timeEpoch(time), measurement + ' - value two' as metric, valueTwo FROM metric_values \nWHERE $__timeFilter(time) AND ($metric = 'ALL' OR measurement = $metric) ORDER BY 1",
- "refId": "B"
}
],
"thresholds": [],
@@ -2410,13 +2496,13 @@
"bars": true,
"dashLength": 10,
"dashes": false,
- "datasource": "${DS_MSSQL_TEST}",
+ "datasource": "gdev-mysql-ds-tests",
"fill": 1,
"gridPos": {
"h": 8,
"w": 12,
"x": 12,
- "y": 89
+ "y": 83
},
"id": 24,
"legend": {
@@ -2446,7 +2532,7 @@
{
"alias": "",
"format": "time_series",
- "rawSql": "SELECT $__timeEpoch(time), valueOne, valueTwo FROM metric_values \nWHERE $__timeFilter(time) AND ($metric = 'ALL' OR measurement = $metric) ORDER BY 1",
+ "rawSql": "SELECT $__timeEpoch(time), valueOne, valueTwo FROM metric_values\nWHERE $__timeFilter(time) AND measurement IN($metric) ORDER BY 1",
"refId": "A"
}
],
@@ -2496,22 +2582,45 @@
"refresh": false,
"schemaVersion": 16,
"style": "dark",
- "tags": [],
+ "tags": [
+ "gdev",
+ "mysql"
+ ],
"templating": {
"list": [
{
- "allValue": "'ALL'",
- "current": {},
- "datasource": "${DS_MSSQL_TEST}",
+ "allValue": "",
+ "current": {
+ "text": "All",
+ "value": "$__all"
+ },
+ "datasource": "gdev-mysql-ds-tests",
"hide": 0,
"includeAll": true,
"label": "Metric",
- "multi": false,
+ "multi": true,
"name": "metric",
- "options": [],
+ "options": [
+ {
+ "selected": true,
+ "text": "All",
+ "value": "$__all"
+ },
+ {
+ "selected": false,
+ "text": "Metric A",
+ "value": "Metric A"
+ },
+ {
+ "selected": false,
+ "text": "Metric B",
+ "value": "Metric B"
+ }
+ ],
"query": "SELECT DISTINCT measurement FROM metric_values",
- "refresh": 1,
+ "refresh": 0,
"regex": "",
+ "skipUrlSync": false,
"sort": 0,
"tagValuesQuery": "",
"tags": [],
@@ -2564,6 +2673,7 @@
],
"query": "1s,10s,30s,1m,5m,10m",
"refresh": 2,
+ "skipUrlSync": false,
"type": "interval"
}
]
@@ -2598,7 +2708,7 @@
]
},
"timezone": "",
- "title": "Microsoft SQL Server Data Source Test",
- "uid": "GlAqcPgmz",
- "version": 58
+ "title": "Datasource tests - MySQL (unittest)",
+ "uid": "Hmf8FDkmz",
+ "version": 2
}
\ No newline at end of file
diff --git a/docker/blocks/postgres/dashboard.json b/devenv/dev-dashboards/datasource_tests_postgres_fakedata.json
similarity index 91%
rename from docker/blocks/postgres/dashboard.json
rename to devenv/dev-dashboards/datasource_tests_postgres_fakedata.json
index 77b0ceac624..508cae86bc3 100644
--- a/docker/blocks/postgres/dashboard.json
+++ b/devenv/dev-dashboards/datasource_tests_postgres_fakedata.json
@@ -1,40 +1,4 @@
{
- "__inputs": [
- {
- "name": "DS_POSTGRESQL",
- "label": "PostgreSQL",
- "description": "",
- "type": "datasource",
- "pluginId": "postgres",
- "pluginName": "PostgreSQL"
- }
- ],
- "__requires": [
- {
- "type": "grafana",
- "id": "grafana",
- "name": "Grafana",
- "version": "5.0.0"
- },
- {
- "type": "panel",
- "id": "graph",
- "name": "Graph",
- "version": ""
- },
- {
- "type": "datasource",
- "id": "postgres",
- "name": "PostgreSQL",
- "version": "1.0.0"
- },
- {
- "type": "panel",
- "id": "table",
- "name": "Table",
- "version": ""
- }
- ],
"annotations": {
"list": [
{
@@ -52,8 +16,7 @@
"editable": true,
"gnetId": null,
"graphTooltip": 0,
- "id": null,
- "iteration": 1518601837383,
+ "iteration": 1532620601931,
"links": [],
"panels": [
{
@@ -63,7 +26,7 @@
"bars": false,
"dashLength": 10,
"dashes": false,
- "datasource": "${DS_POSTGRESQL}",
+ "datasource": "gdev-postgres",
"fill": 2,
"gridPos": {
"h": 9,
@@ -150,14 +113,18 @@
"min": null,
"show": true
}
- ]
+ ],
+ "yaxis": {
+ "align": false,
+ "alignLevel": null
+ }
},
{
"aliasColors": {},
"bars": false,
"dashLength": 10,
"dashes": false,
- "datasource": "${DS_POSTGRESQL}",
+ "datasource": "gdev-postgres",
"fill": 2,
"gridPos": {
"h": 18,
@@ -236,14 +203,18 @@
"min": null,
"show": true
}
- ]
+ ],
+ "yaxis": {
+ "align": false,
+ "alignLevel": null
+ }
},
{
"aliasColors": {},
"bars": false,
"dashLength": 10,
"dashes": false,
- "datasource": "${DS_POSTGRESQL}",
+ "datasource": "gdev-postgres",
"fill": 2,
"gridPos": {
"h": 9,
@@ -316,11 +287,15 @@
"min": null,
"show": true
}
- ]
+ ],
+ "yaxis": {
+ "align": false,
+ "alignLevel": null
+ }
},
{
"columns": [],
- "datasource": "${DS_POSTGRESQL}",
+ "datasource": "gdev-postgres",
"fontSize": "100%",
"gridPos": {
"h": 9,
@@ -377,6 +352,7 @@
"schemaVersion": 16,
"style": "dark",
"tags": [
+ "gdev",
"fake-data-gen",
"postgres"
],
@@ -384,8 +360,11 @@
"list": [
{
"allValue": null,
- "current": {},
- "datasource": "${DS_POSTGRESQL}",
+ "current": {
+ "text": "America",
+ "value": "America"
+ },
+ "datasource": "gdev-postgres",
"hide": 0,
"includeAll": false,
"label": "Datacenter",
@@ -395,6 +374,7 @@
"query": "SELECT DISTINCT datacenter FROM grafana_metric",
"refresh": 1,
"regex": "",
+ "skipUrlSync": false,
"sort": 1,
"tagValuesQuery": "",
"tags": [],
@@ -404,8 +384,11 @@
},
{
"allValue": null,
- "current": {},
- "datasource": "${DS_POSTGRESQL}",
+ "current": {
+ "text": "All",
+ "value": "$__all"
+ },
+ "datasource": "gdev-postgres",
"hide": 0,
"includeAll": true,
"label": "Hostname",
@@ -415,6 +398,7 @@
"query": "SELECT DISTINCT hostname FROM grafana_metric WHERE datacenter='$datacenter'",
"refresh": 1,
"regex": "",
+ "skipUrlSync": false,
"sort": 1,
"tagValuesQuery": "",
"tags": [],
@@ -507,6 +491,7 @@
],
"query": "1s,10s,30s,1m,5m,10m,30m,1h,6h,12h,1d,7d,14d,30d",
"refresh": 2,
+ "skipUrlSync": false,
"type": "interval"
}
]
@@ -541,7 +526,7 @@
]
},
"timezone": "",
- "title": "Grafana Fake Data Gen - PostgreSQL",
+ "title": "Datasource tests - Postgres",
"uid": "JYola5qzz",
- "version": 1
+ "version": 4
}
\ No newline at end of file
diff --git a/docker/blocks/postgres_tests/dashboard.json b/devenv/dev-dashboards/datasource_tests_postgres_unittest.json
similarity index 78%
rename from docker/blocks/postgres_tests/dashboard.json
rename to devenv/dev-dashboards/datasource_tests_postgres_unittest.json
index 9efbe90bdfe..3c56868e9ff 100644
--- a/docker/blocks/postgres_tests/dashboard.json
+++ b/devenv/dev-dashboards/datasource_tests_postgres_unittest.json
@@ -1,40 +1,4 @@
{
- "__inputs": [
- {
- "name": "DS_POSTGRES_TEST",
- "label": "Postgres TEST",
- "description": "",
- "type": "datasource",
- "pluginId": "postgres",
- "pluginName": "PostgreSQL"
- }
- ],
- "__requires": [
- {
- "type": "grafana",
- "id": "grafana",
- "name": "Grafana",
- "version": "5.0.0"
- },
- {
- "type": "panel",
- "id": "graph",
- "name": "Graph",
- "version": "5.0.0"
- },
- {
- "type": "datasource",
- "id": "postgres",
- "name": "PostgreSQL",
- "version": "5.0.0"
- },
- {
- "type": "panel",
- "id": "table",
- "name": "Table",
- "version": "5.0.0"
- }
- ],
"annotations": {
"list": [
{
@@ -47,7 +11,7 @@
"type": "dashboard"
},
{
- "datasource": "${DS_POSTGRES_TEST}",
+ "datasource": "gdev-postgres-ds-tests",
"enable": false,
"hide": false,
"iconColor": "#6ed0e0",
@@ -59,7 +23,7 @@
"type": "tags"
},
{
- "datasource": "${DS_POSTGRES_TEST}",
+ "datasource": "gdev-postgres-ds-tests",
"enable": false,
"hide": false,
"iconColor": "rgba(255, 96, 96, 1)",
@@ -71,7 +35,7 @@
"type": "tags"
},
{
- "datasource": "${DS_POSTGRES_TEST}",
+ "datasource": "gdev-postgres-ds-tests",
"enable": false,
"hide": false,
"iconColor": "#7eb26d",
@@ -83,7 +47,7 @@
"type": "tags"
},
{
- "datasource": "${DS_POSTGRES_TEST}",
+ "datasource": "gdev-postgres-ds-tests",
"enable": false,
"hide": false,
"iconColor": "#1f78c1",
@@ -96,16 +60,16 @@
}
]
},
+ "description": "Run the postgres unit tests to generate the data backing this dashboard",
"editable": true,
"gnetId": null,
"graphTooltip": 0,
- "id": null,
- "iteration": 1523320929325,
+ "iteration": 1534507993194,
"links": [],
"panels": [
{
"columns": [],
- "datasource": "${DS_POSTGRES_TEST}",
+ "datasource": "gdev-postgres-ds-tests",
"fontSize": "100%",
"gridPos": {
"h": 4,
@@ -152,7 +116,7 @@
},
{
"columns": [],
- "datasource": "${DS_POSTGRES_TEST}",
+ "datasource": "gdev-postgres-ds-tests",
"fontSize": "100%",
"gridPos": {
"h": 3,
@@ -206,7 +170,7 @@
},
{
"columns": [],
- "datasource": "${DS_POSTGRES_TEST}",
+ "datasource": "gdev-postgres-ds-tests",
"fontSize": "100%",
"gridPos": {
"h": 3,
@@ -260,7 +224,7 @@
},
{
"columns": [],
- "datasource": "${DS_POSTGRES_TEST}",
+ "datasource": "gdev-postgres-ds-tests",
"fontSize": "100%",
"gridPos": {
"h": 3,
@@ -314,7 +278,7 @@
},
{
"columns": [],
- "datasource": "${DS_POSTGRES_TEST}",
+ "datasource": "gdev-postgres-ds-tests",
"fontSize": "100%",
"gridPos": {
"h": 3,
@@ -371,11 +335,11 @@
"bars": false,
"dashLength": 10,
"dashes": false,
- "datasource": "${DS_POSTGRES_TEST}",
+ "datasource": "gdev-postgres-ds-tests",
"fill": 2,
"gridPos": {
- "h": 9,
- "w": 8,
+ "h": 6,
+ "w": 6,
"x": 0,
"y": 7
},
@@ -405,7 +369,7 @@
{
"alias": "",
"format": "time_series",
- "rawSql": "SELECT $__timeGroup(time, '5m'), avg(value) as value FROM metric WHERE $__timeFilter(time) GROUP BY 1 ORDER BY 1",
+ "rawSql": "SELECT $__timeGroupAlias(time, '5m'), avg(value) as value FROM metric WHERE $__timeFilter(time) GROUP BY 1 ORDER BY 1",
"refId": "A"
}
],
@@ -454,12 +418,12 @@
"bars": false,
"dashLength": 10,
"dashes": false,
- "datasource": "${DS_POSTGRES_TEST}",
+ "datasource": "gdev-postgres-ds-tests",
"fill": 2,
"gridPos": {
- "h": 9,
- "w": 8,
- "x": 8,
+ "h": 6,
+ "w": 6,
+ "x": 6,
"y": 7
},
"id": 9,
@@ -488,7 +452,7 @@
{
"alias": "",
"format": "time_series",
- "rawSql": "SELECT $__timeGroup(time, '5m', NULL), avg(value) as value FROM metric WHERE $__timeFilter(time) GROUP BY 1 ORDER BY 1",
+ "rawSql": "SELECT $__timeGroupAlias(time, '5m', NULL), avg(value) as value FROM metric WHERE $__timeFilter(time) GROUP BY 1 ORDER BY 1",
"refId": "A"
}
],
@@ -537,12 +501,12 @@
"bars": false,
"dashLength": 10,
"dashes": false,
- "datasource": "${DS_POSTGRES_TEST}",
+ "datasource": "gdev-postgres-ds-tests",
"fill": 2,
"gridPos": {
- "h": 9,
- "w": 8,
- "x": 16,
+ "h": 6,
+ "w": 6,
+ "x": 12,
"y": 7
},
"id": 10,
@@ -571,7 +535,7 @@
{
"alias": "",
"format": "time_series",
- "rawSql": "SELECT $__timeGroup(time, '5m', 10.0), avg(value) as value FROM metric WHERE $__timeFilter(time) GROUP BY 1 ORDER BY 1",
+ "rawSql": "SELECT $__timeGroupAlias(time, '5m', 10.0), avg(value) as value FROM metric WHERE $__timeFilter(time) GROUP BY 1 ORDER BY 1",
"refId": "A"
}
],
@@ -615,18 +579,101 @@
"alignLevel": null
}
},
+ {
+ "aliasColors": {},
+ "bars": false,
+ "dashLength": 10,
+ "dashes": false,
+ "datasource": "gdev-postgres-ds-tests",
+ "fill": 2,
+ "gridPos": {
+ "h": 6,
+ "w": 6,
+ "x": 18,
+ "y": 7
+ },
+ "id": 36,
+ "legend": {
+ "avg": false,
+ "current": false,
+ "max": false,
+ "min": false,
+ "show": true,
+ "total": false,
+ "values": false
+ },
+ "lines": true,
+ "linewidth": 2,
+ "links": [],
+ "nullPointMode": "null",
+ "percentage": false,
+ "pointradius": 3,
+ "points": true,
+ "renderer": "flot",
+ "seriesOverrides": [],
+ "spaceLength": 10,
+ "stack": false,
+ "steppedLine": true,
+ "targets": [
+ {
+ "alias": "",
+ "format": "time_series",
+ "rawSql": "SELECT $__timeGroupAlias(time, '5m', previous), avg(value) as value FROM metric WHERE $__timeFilter(time) GROUP BY 1 ORDER BY 1",
+ "refId": "A"
+ }
+ ],
+ "thresholds": [],
+ "timeFrom": null,
+ "timeShift": null,
+ "title": "timeGroup macro 5m with fill(previous)",
+ "tooltip": {
+ "shared": true,
+ "sort": 0,
+ "value_type": "individual"
+ },
+ "type": "graph",
+ "xaxis": {
+ "buckets": null,
+ "mode": "time",
+ "name": null,
+ "show": true,
+ "values": []
+ },
+ "yaxes": [
+ {
+ "format": "short",
+ "label": null,
+ "logBase": 1,
+ "max": null,
+ "min": "0",
+ "show": true
+ },
+ {
+ "format": "short",
+ "label": null,
+ "logBase": 1,
+ "max": null,
+ "min": null,
+ "show": true
+ }
+ ],
+ "yaxis": {
+ "align": false,
+ "alignLevel": null
+ }
+ },
{
"aliasColors": {},
"bars": true,
"dashLength": 10,
"dashes": false,
- "datasource": "${DS_POSTGRES_TEST}",
+ "datasource": "gdev-postgres-ds-tests",
"fill": 2,
"gridPos": {
- "h": 9,
- "w": 8,
+ "h": 6,
+ "w": 6,
"x": 0,
- "y": 16
+ "y": 13
},
"id": 16,
"legend": {
@@ -654,7 +701,7 @@
{
"alias": "",
"format": "time_series",
- "rawSql": "SELECT $__timeGroup(time, '$summarize'), avg(value) as value FROM metric WHERE $__timeFilter(time) GROUP BY 1 ORDER BY 1",
+ "rawSql": "SELECT $__timeGroupAlias(time, '$summarize'), avg(value) as value FROM metric WHERE $__timeFilter(time) GROUP BY 1 ORDER BY 1",
"refId": "A"
}
],
@@ -703,13 +750,13 @@
"bars": true,
"dashLength": 10,
"dashes": false,
- "datasource": "${DS_POSTGRES_TEST}",
+ "datasource": "gdev-postgres-ds-tests",
"fill": 2,
"gridPos": {
- "h": 9,
- "w": 8,
- "x": 8,
- "y": 16
+ "h": 6,
+ "w": 6,
+ "x": 6,
+ "y": 13
},
"id": 12,
"legend": {
@@ -737,7 +784,7 @@
{
"alias": "",
"format": "time_series",
- "rawSql": "SELECT $__timeGroup(time, '$summarize', NULL), sum(value) as value FROM metric WHERE $__timeFilter(time) GROUP BY 1 ORDER BY 1",
+ "rawSql": "SELECT $__timeGroupAlias(time, '$summarize', NULL), sum(value) as value FROM metric WHERE $__timeFilter(time) GROUP BY 1 ORDER BY 1",
"refId": "A"
}
],
@@ -786,13 +833,13 @@
"bars": true,
"dashLength": 10,
"dashes": false,
- "datasource": "${DS_POSTGRES_TEST}",
+ "datasource": "gdev-postgres-ds-tests",
"fill": 2,
"gridPos": {
- "h": 9,
- "w": 8,
- "x": 16,
- "y": 16
+ "h": 6,
+ "w": 6,
+ "x": 12,
+ "y": 13
},
"id": 13,
"legend": {
@@ -820,7 +867,7 @@
{
"alias": "",
"format": "time_series",
- "rawSql": "SELECT $__timeGroup(time, '$summarize', 100.0), sum(value) as value FROM metric WHERE $__timeFilter(time) GROUP BY 1 ORDER BY 1",
+ "rawSql": "SELECT $__timeGroupAlias(time, '$summarize', 100.0), sum(value) as value FROM metric WHERE $__timeFilter(time) GROUP BY 1 ORDER BY 1",
"refId": "A"
}
],
@@ -864,18 +911,101 @@
"alignLevel": null
}
},
+ {
+ "aliasColors": {},
+ "bars": true,
+ "dashLength": 10,
+ "dashes": false,
+ "datasource": "gdev-postgres-ds-tests",
+ "fill": 2,
+ "gridPos": {
+ "h": 6,
+ "w": 6,
+ "x": 18,
+ "y": 13
+ },
+ "id": 37,
+ "legend": {
+ "avg": false,
+ "current": false,
+ "max": false,
+ "min": false,
+ "show": true,
+ "total": false,
+ "values": false
+ },
+ "lines": false,
+ "linewidth": 2,
+ "links": [],
+ "nullPointMode": "null",
+ "percentage": false,
+ "pointradius": 3,
+ "points": false,
+ "renderer": "flot",
+ "seriesOverrides": [],
+ "spaceLength": 10,
+ "stack": false,
+ "steppedLine": true,
+ "targets": [
+ {
+ "alias": "",
+ "format": "time_series",
+ "rawSql": "SELECT $__timeGroupAlias(time, '$summarize', previous), sum(value) as value FROM metric WHERE $__timeFilter(time) GROUP BY 1 ORDER BY 1",
+ "refId": "A"
+ }
+ ],
+ "thresholds": [],
+ "timeFrom": null,
+ "timeShift": null,
+ "title": "Metrics - timeGroup macro $summarize with fill(previous)",
+ "tooltip": {
+ "shared": true,
+ "sort": 0,
+ "value_type": "individual"
+ },
+ "type": "graph",
+ "xaxis": {
+ "buckets": null,
+ "mode": "time",
+ "name": null,
+ "show": true,
+ "values": []
+ },
+ "yaxes": [
+ {
+ "format": "short",
+ "label": null,
+ "logBase": 1,
+ "max": null,
+ "min": null,
+ "show": true
+ },
+ {
+ "format": "short",
+ "label": null,
+ "logBase": 1,
+ "max": null,
+ "min": null,
+ "show": true
+ }
+ ],
+ "yaxis": {
+ "align": false,
+ "alignLevel": null
+ }
+ },
{
"aliasColors": {},
"bars": false,
"dashLength": 10,
"dashes": false,
- "datasource": "${DS_POSTGRES_TEST}",
+ "datasource": "gdev-postgres-ds-tests",
"fill": 2,
"gridPos": {
"h": 8,
"w": 12,
"x": 0,
- "y": 25
+ "y": 19
},
"id": 27,
"legend": {
@@ -907,14 +1037,8 @@
{
"alias": "",
"format": "time_series",
- "rawSql": "SELECT \n $__timeGroup(time, '$summarize'), \n measurement || ' - value one' as metric, \n avg(\"valueOne\") as \"valueOne\"\nFROM\n metric_values \nWHERE\n $__timeFilter(time) AND\n measurement in($metric)\nGROUP BY 1, 2\nORDER BY 1",
+ "rawSql": "SELECT \n $__timeGroupAlias(time, '$summarize'), \n measurement, \n avg(\"valueOne\") as \"valueOne\",\n avg(\"valueTwo\") as \"valueTwo\"\nFROM\n metric_values \nWHERE\n $__timeFilter(time) AND\n measurement in($metric)\nGROUP BY 1, 2\nORDER BY 1",
"refId": "A"
- },
- {
- "alias": "",
- "format": "time_series",
- "rawSql": "SELECT \n $__timeGroup(time, '$summarize'), \n measurement || ' - value two' as metric, \n avg(\"valueTwo\") as \"valueTwo\"\nFROM\n metric_values \nWHERE\n $__timeFilter(time) AND\n measurement in($metric)\nGROUP BY 1, 2\nORDER BY 1",
- "refId": "B"
}
],
"thresholds": [],
@@ -962,13 +1086,13 @@
"bars": false,
"dashLength": 10,
"dashes": false,
- "datasource": "${DS_POSTGRES_TEST}",
+ "datasource": "gdev-postgres-ds-tests",
"fill": 2,
"gridPos": {
"h": 8,
"w": 12,
"x": 12,
- "y": 25
+ "y": 19
},
"id": 5,
"legend": {
@@ -998,7 +1122,7 @@
{
"alias": "",
"format": "time_series",
- "rawSql": "SELECT \n $__timeGroup(time, '$summarize'), \n avg(\"valueOne\") as \"valueOne\", \n avg(\"valueTwo\") as \"valueTwo\" \nFROM\n metric_values \nWHERE\n $__timeFilter(time) AND\n measurement in($metric)\nGROUP BY 1\nORDER BY 1",
+ "rawSql": "SELECT \n $__timeGroupAlias(time, '$summarize'), \n avg(\"valueOne\") as \"valueOne\", \n avg(\"valueTwo\") as \"valueTwo\" \nFROM\n metric_values \nWHERE\n $__timeFilter(time) AND\n measurement in($metric)\nGROUP BY 1\nORDER BY 1",
"refId": "A"
}
],
@@ -1047,13 +1171,185 @@
"bars": false,
"dashLength": 10,
"dashes": false,
- "datasource": "${DS_POSTGRES_TEST}",
+ "datasource": "gdev-postgres-ds-tests",
"fill": 2,
"gridPos": {
"h": 8,
"w": 12,
"x": 0,
- "y": 33
+ "y": 27
+ },
+ "id": 38,
+ "legend": {
+ "alignAsTable": true,
+ "avg": true,
+ "current": true,
+ "hideEmpty": false,
+ "hideZero": false,
+ "max": true,
+ "min": true,
+ "rightSide": true,
+ "show": true,
+ "total": true,
+ "values": true
+ },
+ "lines": true,
+ "linewidth": 2,
+ "links": [],
+ "nullPointMode": "null",
+ "percentage": false,
+ "pointradius": 3,
+ "points": false,
+ "renderer": "flot",
+ "seriesOverrides": [],
+ "spaceLength": 10,
+ "stack": false,
+ "steppedLine": false,
+ "targets": [
+ {
+ "alias": "",
+ "format": "time_series",
+ "rawSql": "SELECT \n $__unixEpochGroupAlias(\"timeInt32\", '$summarize'), \n measurement, \n avg(\"valueOne\") as \"valueOne\",\n avg(\"valueTwo\") as \"valueTwo\"\nFROM\n metric_values \nWHERE\n $__unixEpochFilter(\"timeInt32\") AND\n measurement in($metric)\nGROUP BY 1, 2\nORDER BY 1, 2",
+ "refId": "A"
+ }
+ ],
+ "thresholds": [],
+ "timeFrom": null,
+ "timeShift": null,
+ "title": "Multiple series with metric column using unixEpochGroup macro ($summarize)",
+ "tooltip": {
+ "shared": true,
+ "sort": 0,
+ "value_type": "individual"
+ },
+ "type": "graph",
+ "xaxis": {
+ "buckets": null,
+ "mode": "time",
+ "name": null,
+ "show": true,
+ "values": []
+ },
+ "yaxes": [
+ {
+ "format": "short",
+ "label": null,
+ "logBase": 1,
+ "max": null,
+ "min": "0",
+ "show": true
+ },
+ {
+ "format": "short",
+ "label": null,
+ "logBase": 1,
+ "max": null,
+ "min": null,
+ "show": true
+ }
+ ],
+ "yaxis": {
+ "align": false,
+ "alignLevel": null
+ }
+ },
+ {
+ "aliasColors": {},
+ "bars": false,
+ "dashLength": 10,
+ "dashes": false,
+ "datasource": "gdev-postgres-ds-tests",
+ "fill": 2,
+ "gridPos": {
+ "h": 8,
+ "w": 12,
+ "x": 12,
+ "y": 27
+ },
+ "id": 39,
+ "legend": {
+ "alignAsTable": true,
+ "avg": true,
+ "current": true,
+ "max": true,
+ "min": true,
+ "rightSide": true,
+ "show": true,
+ "total": true,
+ "values": true
+ },
+ "lines": true,
+ "linewidth": 2,
+ "links": [],
+ "nullPointMode": "null",
+ "percentage": false,
+ "pointradius": 3,
+ "points": false,
+ "renderer": "flot",
+ "seriesOverrides": [],
+ "spaceLength": 10,
+ "stack": false,
+ "steppedLine": false,
+ "targets": [
+ {
+ "alias": "",
+ "format": "time_series",
+ "rawSql": "SELECT \n $__unixEpochGroupAlias(\"timeInt32\", '$summarize'), \n avg(\"valueOne\") as \"valueOne\",\n avg(\"valueTwo\") as \"valueTwo\"\nFROM\n metric_values \nWHERE\n $__unixEpochFilter(\"timeInt32\") AND\n measurement in($metric)\nGROUP BY 1\nORDER BY 1",
+ "refId": "A"
+ }
+ ],
+ "thresholds": [],
+ "timeFrom": null,
+ "timeShift": null,
+ "title": "Multiple series without metric column using timeGroup macro ($summarize)",
+ "tooltip": {
+ "shared": true,
+ "sort": 0,
+ "value_type": "individual"
+ },
+ "type": "graph",
+ "xaxis": {
+ "buckets": null,
+ "mode": "time",
+ "name": null,
+ "show": true,
+ "values": []
+ },
+ "yaxes": [
+ {
+ "format": "short",
+ "label": null,
+ "logBase": 1,
+ "max": null,
+ "min": "0",
+ "show": true
+ },
+ {
+ "format": "short",
+ "label": null,
+ "logBase": 1,
+ "max": null,
+ "min": null,
+ "show": true
+ }
+ ],
+ "yaxis": {
+ "align": false,
+ "alignLevel": null
+ }
+ },
+ {
+ "aliasColors": {},
+ "bars": false,
+ "dashLength": 10,
+ "dashes": false,
+ "datasource": "gdev-postgres-ds-tests",
+ "fill": 2,
+ "gridPos": {
+ "h": 8,
+ "w": 12,
+ "x": 0,
+ "y": 35
},
"id": 4,
"legend": {
@@ -1085,14 +1381,8 @@
{
"alias": "",
"format": "time_series",
- "rawSql": "SELECT $__timeEpoch(time), measurement || ' - value one' as metric, \"valueOne\" FROM metric_values \nWHERE $__timeFilter(time) AND measurement in($metric) ORDER BY 1",
+ "rawSql": "SELECT $__timeEpoch(time), measurement, \"valueOne\", \"valueTwo\" FROM metric_values \nWHERE $__timeFilter(time) AND measurement in($metric) ORDER BY 1",
"refId": "A"
- },
- {
- "alias": "",
- "format": "time_series",
- "rawSql": "SELECT $__timeEpoch(time), measurement || ' - value two' as metric, \"valueTwo\" FROM metric_values \nWHERE $__timeFilter(time) AND measurement in($metric) ORDER BY 1",
- "refId": "B"
}
],
"thresholds": [],
@@ -1140,13 +1430,13 @@
"bars": false,
"dashLength": 10,
"dashes": false,
- "datasource": "${DS_POSTGRES_TEST}",
+ "datasource": "gdev-postgres-ds-tests",
"fill": 2,
"gridPos": {
"h": 8,
"w": 12,
"x": 12,
- "y": 33
+ "y": 35
},
"id": 28,
"legend": {
@@ -1225,13 +1515,13 @@
"bars": false,
"dashLength": 10,
"dashes": false,
- "datasource": "${DS_POSTGRES_TEST}",
+ "datasource": "gdev-postgres-ds-tests",
"fill": 2,
"gridPos": {
"h": 8,
"w": 12,
"x": 0,
- "y": 41
+ "y": 43
},
"id": 19,
"legend": {
@@ -1263,14 +1553,8 @@
{
"alias": "",
"format": "time_series",
- "rawSql": "SELECT $__timeEpoch(time), measurement || ' - value one' as metric, \"valueOne\" FROM metric_values \nWHERE $__timeFilter(time) AND measurement in($metric) ORDER BY 1",
+ "rawSql": "SELECT $__timeEpoch(time), measurement, \"valueOne\", \"valueTwo\" FROM metric_values \nWHERE $__timeFilter(time) AND measurement in($metric) ORDER BY 1",
"refId": "A"
- },
- {
- "alias": "",
- "format": "time_series",
- "rawSql": "SELECT $__timeEpoch(time), measurement || ' - value two' as metric, \"valueTwo\" FROM metric_values \nWHERE $__timeFilter(time) AND measurement in($metric) ORDER BY 1",
- "refId": "B"
}
],
"thresholds": [],
@@ -1318,13 +1602,13 @@
"bars": false,
"dashLength": 10,
"dashes": false,
- "datasource": "${DS_POSTGRES_TEST}",
+ "datasource": "gdev-postgres-ds-tests",
"fill": 2,
"gridPos": {
"h": 8,
"w": 12,
"x": 12,
- "y": 41
+ "y": 43
},
"id": 18,
"legend": {
@@ -1403,13 +1687,13 @@
"bars": false,
"dashLength": 10,
"dashes": false,
- "datasource": "${DS_POSTGRES_TEST}",
+ "datasource": "gdev-postgres-ds-tests",
"fill": 2,
"gridPos": {
"h": 8,
"w": 12,
"x": 0,
- "y": 49
+ "y": 51
},
"id": 17,
"legend": {
@@ -1441,14 +1725,8 @@
{
"alias": "",
"format": "time_series",
- "rawSql": "SELECT $__timeEpoch(time), measurement || ' - value one' as metric, \"valueOne\" FROM metric_values \nWHERE $__timeFilter(time) AND measurement in($metric) ORDER BY 1",
+ "rawSql": "SELECT $__timeEpoch(time), measurement, \"valueOne\", \"valueTwo\" FROM metric_values \nWHERE $__timeFilter(time) AND measurement in($metric) ORDER BY 1",
"refId": "A"
- },
- {
- "alias": "",
- "format": "time_series",
- "rawSql": "SELECT $__timeEpoch(time), measurement || ' - value two' as metric, \"valueTwo\" FROM metric_values \nWHERE $__timeFilter(time) AND measurement in($metric) ORDER BY 1",
- "refId": "B"
}
],
"thresholds": [],
@@ -1496,13 +1774,13 @@
"bars": false,
"dashLength": 10,
"dashes": false,
- "datasource": "${DS_POSTGRES_TEST}",
+ "datasource": "gdev-postgres-ds-tests",
"fill": 2,
"gridPos": {
"h": 8,
"w": 12,
"x": 12,
- "y": 49
+ "y": 51
},
"id": 20,
"legend": {
@@ -1581,13 +1859,13 @@
"bars": true,
"dashLength": 10,
"dashes": false,
- "datasource": "${DS_POSTGRES_TEST}",
+ "datasource": "gdev-postgres-ds-tests",
"fill": 1,
"gridPos": {
"h": 8,
"w": 12,
"x": 0,
- "y": 57
+ "y": 59
},
"id": 14,
"legend": {
@@ -1617,14 +1895,8 @@
{
"alias": "",
"format": "time_series",
- "rawSql": "SELECT $__timeEpoch(time), measurement || ' - value one' as metric, \"valueOne\" FROM metric_values \nWHERE $__timeFilter(time) AND measurement in($metric) ORDER BY 1",
+ "rawSql": "SELECT $__timeEpoch(time), measurement, \"valueOne\", \"valueTwo\" FROM metric_values \nWHERE $__timeFilter(time) AND measurement in($metric) ORDER BY 1",
"refId": "A"
- },
- {
- "alias": "",
- "format": "time_series",
- "rawSql": "SELECT $__timeEpoch(time), measurement || ' - value two' as metric, \"valueTwo\" FROM metric_values \nWHERE $__timeFilter(time) AND measurement in($metric) ORDER BY 1",
- "refId": "B"
}
],
"thresholds": [],
@@ -1675,13 +1947,13 @@
"bars": true,
"dashLength": 10,
"dashes": false,
- "datasource": "${DS_POSTGRES_TEST}",
+ "datasource": "gdev-postgres-ds-tests",
"fill": 1,
"gridPos": {
"h": 8,
"w": 12,
"x": 12,
- "y": 57
+ "y": 59
},
"id": 15,
"legend": {
@@ -1762,13 +2034,13 @@
"bars": true,
"dashLength": 10,
"dashes": false,
- "datasource": "${DS_POSTGRES_TEST}",
+ "datasource": "gdev-postgres-ds-tests",
"fill": 1,
"gridPos": {
"h": 8,
"w": 12,
"x": 0,
- "y": 65
+ "y": 67
},
"id": 25,
"legend": {
@@ -1798,14 +2070,8 @@
{
"alias": "",
"format": "time_series",
- "rawSql": "SELECT $__timeEpoch(time), measurement || ' - value one' as metric, \"valueOne\" FROM metric_values \nWHERE $__timeFilter(time) AND measurement in($metric) ORDER BY 1",
+ "rawSql": "SELECT $__timeEpoch(time), measurement, \"valueOne\", \"valueTwo\" FROM metric_values \nWHERE $__timeFilter(time) AND measurement in($metric) ORDER BY 1",
"refId": "A"
- },
- {
- "alias": "",
- "format": "time_series",
- "rawSql": "SELECT $__timeEpoch(time), measurement || ' - value two' as metric, \"valueTwo\" FROM metric_values \nWHERE $__timeFilter(time) AND measurement in($metric) ORDER BY 1",
- "refId": "B"
}
],
"thresholds": [],
@@ -1856,13 +2122,13 @@
"bars": true,
"dashLength": 10,
"dashes": false,
- "datasource": "${DS_POSTGRES_TEST}",
+ "datasource": "gdev-postgres-ds-tests",
"fill": 1,
"gridPos": {
"h": 8,
"w": 12,
"x": 12,
- "y": 65
+ "y": 67
},
"id": 22,
"legend": {
@@ -1943,13 +2209,13 @@
"bars": true,
"dashLength": 10,
"dashes": false,
- "datasource": "${DS_POSTGRES_TEST}",
+ "datasource": "gdev-postgres-ds-tests",
"fill": 1,
"gridPos": {
"h": 8,
"w": 12,
"x": 0,
- "y": 73
+ "y": 75
},
"id": 21,
"legend": {
@@ -1979,14 +2245,8 @@
{
"alias": "",
"format": "time_series",
- "rawSql": "SELECT $__timeEpoch(time), measurement || ' - value one' as metric, \"valueOne\" FROM metric_values \nWHERE $__timeFilter(time) AND measurement in($metric) ORDER BY 1",
+ "rawSql": "SELECT $__timeEpoch(time), measurement, \"valueOne\", \"valueTwo\" FROM metric_values \nWHERE $__timeFilter(time) AND measurement in($metric) ORDER BY 1",
"refId": "A"
- },
- {
- "alias": "",
- "format": "time_series",
- "rawSql": "SELECT $__timeEpoch(time), measurement || ' - value two' as metric, \"valueTwo\" FROM metric_values \nWHERE $__timeFilter(time) AND measurement in($metric) ORDER BY 1",
- "refId": "B"
}
],
"thresholds": [],
@@ -2037,13 +2297,13 @@
"bars": true,
"dashLength": 10,
"dashes": false,
- "datasource": "${DS_POSTGRES_TEST}",
+ "datasource": "gdev-postgres-ds-tests",
"fill": 1,
"gridPos": {
"h": 8,
"w": 12,
"x": 12,
- "y": 73
+ "y": 75
},
"id": 26,
"legend": {
@@ -2124,13 +2384,13 @@
"bars": true,
"dashLength": 10,
"dashes": false,
- "datasource": "${DS_POSTGRES_TEST}",
+ "datasource": "gdev-postgres-ds-tests",
"fill": 1,
"gridPos": {
"h": 8,
"w": 12,
"x": 0,
- "y": 81
+ "y": 83
},
"id": 23,
"legend": {
@@ -2160,14 +2420,8 @@
{
"alias": "",
"format": "time_series",
- "rawSql": "SELECT $__timeEpoch(time), measurement || ' - value one' as metric, \"valueOne\" FROM metric_values \nWHERE $__timeFilter(time) AND measurement in($metric) ORDER BY 1",
+ "rawSql": "SELECT $__timeEpoch(time), measurement, \"valueOne\", \"valueTwo\" FROM metric_values \nWHERE $__timeFilter(time) AND measurement in($metric) ORDER BY 1",
"refId": "A"
- },
- {
- "alias": "",
- "format": "time_series",
- "rawSql": "SELECT $__timeEpoch(time), measurement || ' - value two' as metric, \"valueTwo\" FROM metric_values \nWHERE $__timeFilter(time) AND measurement in($metric) ORDER BY 1",
- "refId": "B"
}
],
"thresholds": [],
@@ -2218,13 +2472,13 @@
"bars": true,
"dashLength": 10,
"dashes": false,
- "datasource": "${DS_POSTGRES_TEST}",
+ "datasource": "gdev-postgres-ds-tests",
"fill": 1,
"gridPos": {
"h": 8,
"w": 12,
"x": 12,
- "y": 81
+ "y": 83
},
"id": 24,
"legend": {
@@ -2304,22 +2558,49 @@
"refresh": false,
"schemaVersion": 16,
"style": "dark",
- "tags": [],
+ "tags": [
+ "gdev",
+ "postgres"
+ ],
"templating": {
"list": [
{
"allValue": null,
- "current": {},
- "datasource": "${DS_POSTGRES_TEST}",
+ "current": {
+ "selected": true,
+ "tags": [],
+ "text": "All",
+ "value": [
+ "$__all"
+ ]
+ },
+ "datasource": "gdev-postgres-ds-tests",
"hide": 0,
"includeAll": true,
"label": "Metric",
"multi": true,
"name": "metric",
- "options": [],
+ "options": [
+ {
+ "selected": true,
+ "text": "All",
+ "value": "$__all"
+ },
+ {
+ "selected": false,
+ "text": "Metric A",
+ "value": "Metric A"
+ },
+ {
+ "selected": false,
+ "text": "Metric B",
+ "value": "Metric B"
+ }
+ ],
"query": "SELECT DISTINCT measurement FROM metric_values",
- "refresh": 1,
+ "refresh": 0,
"regex": "",
+ "skipUrlSync": false,
"sort": 1,
"tagValuesQuery": "",
"tags": [],
@@ -2372,6 +2653,7 @@
],
"query": "1s,10s,30s,1m,5m,10m",
"refresh": 2,
+ "skipUrlSync": false,
"type": "interval"
}
]
@@ -2406,7 +2688,7 @@
]
},
"timezone": "",
- "title": "Postgres Data Source Test",
+ "title": "Datasource tests - Postgres (unittest)",
"uid": "vHQdlVziz",
- "version": 14
+ "version": 1
}
\ No newline at end of file
diff --git a/devenv/dev-dashboards/panel_tests_polystat.json b/devenv/dev-dashboards/panel_tests_polystat.json
new file mode 100644
index 00000000000..51d3085c438
--- /dev/null
+++ b/devenv/dev-dashboards/panel_tests_polystat.json
@@ -0,0 +1,3343 @@
+{
+ "annotations": {
+ "list": [
+ {
+ "builtIn": 1,
+ "datasource": "-- Grafana --",
+ "enable": true,
+ "hide": true,
+ "iconColor": "rgba(0, 211, 255, 1)",
+ "name": "Annotations & Alerts",
+ "type": "dashboard"
+ }
+ ]
+ },
+ "editable": true,
+ "gnetId": null,
+ "graphTooltip": 0,
+ "links": [],
+ "panels": [
+ {
+ "animationModes": [
+ {
+ "text": "Show All",
+ "value": "all"
+ },
+ {
+ "text": "Show Triggered",
+ "value": "triggered"
+ }
+ ],
+ "colors": [
+ "#299c46",
+ "rgba(237, 129, 40, 0.89)",
+ "#d44a3a"
+ ],
+ "d3DivId": "d3_svg_4",
+ "datasource": "gdev-testdata",
+ "decimals": 2,
+ "displayModes": [
+ {
+ "text": "Show All",
+ "value": "all"
+ },
+ {
+ "text": "Show Triggered",
+ "value": "triggered"
+ }
+ ],
+ "fontSizes": [
+ 4,
+ 5,
+ 6,
+ 7,
+ 8,
+ 9,
+ 10,
+ 11,
+ 12,
+ 13,
+ 14,
+ 15,
+ 16,
+ 17,
+ 18,
+ 19,
+ 20,
+ 22,
+ 24,
+ 26,
+ 28,
+ 30,
+ 32,
+ 34,
+ 36,
+ 38,
+ 40,
+ 42,
+ 44,
+ 46,
+ 48,
+ 50,
+ 52,
+ 54,
+ 56,
+ 58,
+ 60,
+ 62,
+ 64,
+ 66,
+ 68,
+ 70
+ ],
+ "fontTypes": [
+ "Open Sans",
+ "Arial",
+ "Avant Garde",
+ "Bookman",
+ "Consolas",
+ "Courier",
+ "Courier New",
+ "Futura",
+ "Garamond",
+ "Helvetica",
+ "Palatino",
+ "Times",
+ "Times New Roman",
+ "Verdana"
+ ],
+ "format": "none",
+ "gridPos": {
+ "h": 9,
+ "w": 12,
+ "x": 0,
+ "y": 0
+ },
+ "id": 4,
+ "links": [],
+ "notcolors": [
+ "rgba(245, 54, 54, 0.9)",
+ "rgba(237, 129, 40, 0.89)",
+ "rgba(50, 172, 45, 0.97)"
+ ],
+ "operatorName": "avg",
+ "operatorOptions": [
+ {
+ "text": "Average",
+ "value": "avg"
+ },
+ {
+ "text": "Count",
+ "value": "count"
+ },
+ {
+ "text": "Current",
+ "value": "current"
+ },
+ {
+ "text": "Delta",
+ "value": "delta"
+ },
+ {
+ "text": "Difference",
+ "value": "diff"
+ },
+ {
+ "text": "First",
+ "value": "first"
+ },
+ {
+ "text": "Log Min",
+ "value": "logmin"
+ },
+ {
+ "text": "Max",
+ "value": "max"
+ },
+ {
+ "text": "Min",
+ "value": "min"
+ },
+ {
+ "text": "Name",
+ "value": "name"
+ },
+ {
+ "text": "Time of Last Point",
+ "value": "last_time"
+ },
+ {
+ "text": "Time Step",
+ "value": "time_step"
+ },
+ {
+ "text": "Total",
+ "value": "total"
+ }
+ ],
+ "polystat": {
+ "animationSpeed": 2500,
+ "columnAutoSize": true,
+ "columns": "",
+ "defaultClickThrough": "",
+ "defaultClickThroughSanitize": true,
+ "displayLimit": 100,
+ "fontAutoScale": true,
+ "fontSize": 12,
+ "globalDisplayMode": "all",
+ "globalOperatorName": "avg",
+ "gradientEnabled": true,
+ "hexagonSortByDirection": "asc",
+ "hexagonSortByField": "name",
+ "maxMetrics": 0,
+ "polygonBorderColor": "black",
+ "polygonBorderSize": 2,
+ "radius": "",
+ "radiusAutoSize": true,
+ "rowAutoSize": true,
+ "rows": "",
+ "shape": "hexagon_pointed_top",
+ "tooltipDisplayMode": "all",
+ "tooltipDisplayTextTriggeredEmpty": "OK",
+ "tooltipFontSize": 12,
+ "tooltipFontType": "Open Sans",
+ "tooltipPrimarySortDirection": "desc",
+ "tooltipPrimarySortField": "thresholdLevel",
+ "tooltipSecondarySortDirection": "desc",
+ "tooltipSecondarySortField": "value",
+ "tooltipTimestampEnabled": true
+ },
+ "savedComposites": [],
+ "savedOverrides": [],
+ "shapes": [
+ {
+ "text": "Hexagon Pointed Top",
+ "value": "hexagon_pointed_top"
+ },
+ {
+ "text": "Hexagon Flat Top",
+ "value": "hexagon_flat_top"
+ },
+ {
+ "text": "Circle",
+ "value": "circle"
+ },
+ {
+ "text": "Cross",
+ "value": "cross"
+ },
+ {
+ "text": "Diamond",
+ "value": "diamond"
+ },
+ {
+ "text": "Square",
+ "value": "square"
+ },
+ {
+ "text": "Star",
+ "value": "star"
+ },
+ {
+ "text": "Triangle",
+ "value": "triangle"
+ },
+ {
+ "text": "Wye",
+ "value": "wye"
+ }
+ ],
+ "sortDirections": [
+ {
+ "text": "Ascending",
+ "value": "asc"
+ },
+ {
+ "text": "Descending",
+ "value": "desc"
+ }
+ ],
+ "sortFields": [
+ {
+ "text": "Name",
+ "value": "name"
+ },
+ {
+ "text": "Threshold Level",
+ "value": "thresholdLevel"
+ },
+ {
+ "text": "Value",
+ "value": "value"
+ }
+ ],
+ "svgContainer": {},
+ "targets": [
+ {
+ "expr": "",
+ "format": "time_series",
+ "intervalFactor": 1,
+ "refId": "A",
+ "scenarioId": "random_walk"
+ },
+ {
+ "expr": "",
+ "format": "time_series",
+ "intervalFactor": 1,
+ "refId": "B",
+ "scenarioId": "random_walk"
+ },
+ {
+ "expr": "",
+ "format": "time_series",
+ "intervalFactor": 1,
+ "refId": "C",
+ "scenarioId": "random_walk"
+ },
+ {
+ "expr": "",
+ "format": "time_series",
+ "intervalFactor": 1,
+ "refId": "D",
+ "scenarioId": "random_walk"
+ },
+ {
+ "expr": "",
+ "format": "time_series",
+ "intervalFactor": 1,
+ "refId": "E",
+ "scenarioId": "random_walk"
+ }
+ ],
+ "thresholdStates": [
+ {
+ "text": "ok",
+ "value": 0
+ },
+ {
+ "text": "warning",
+ "value": 1
+ },
+ {
+ "text": "critical",
+ "value": 2
+ },
+ {
+ "text": "custom",
+ "value": 3
+ }
+ ],
+ "title": "Poor use of space",
+ "type": "grafana-polystat-panel",
+ "unitFormats": [
+ {
+ "submenu": [
+ {
+ "text": "none",
+ "value": "none"
+ },
+ {
+ "text": "short",
+ "value": "short"
+ },
+ {
+ "text": "percent (0-100)",
+ "value": "percent"
+ },
+ {
+ "text": "percent (0.0-1.0)",
+ "value": "percentunit"
+ },
+ {
+ "text": "Humidity (%H)",
+ "value": "humidity"
+ },
+ {
+ "text": "decibel",
+ "value": "dB"
+ },
+ {
+ "text": "hexadecimal (0x)",
+ "value": "hex0x"
+ },
+ {
+ "text": "hexadecimal",
+ "value": "hex"
+ },
+ {
+ "text": "scientific notation",
+ "value": "sci"
+ },
+ {
+ "text": "locale format",
+ "value": "locale"
+ }
+ ],
+ "text": "none"
+ },
+ {
+ "submenu": [
+ {
+ "text": "Dollars ($)",
+ "value": "currencyUSD"
+ },
+ {
+ "text": "Pounds (£)",
+ "value": "currencyGBP"
+ },
+ {
+ "text": "Euro (€)",
+ "value": "currencyEUR"
+ },
+ {
+ "text": "Yen (¥)",
+ "value": "currencyJPY"
+ },
+ {
+ "text": "Rubles (₽)",
+ "value": "currencyRUB"
+ },
+ {
+ "text": "Hryvnias (₴)",
+ "value": "currencyUAH"
+ },
+ {
+ "text": "Real (R$)",
+ "value": "currencyBRL"
+ },
+ {
+ "text": "Danish Krone (kr)",
+ "value": "currencyDKK"
+ },
+ {
+ "text": "Icelandic Króna (kr)",
+ "value": "currencyISK"
+ },
+ {
+ "text": "Norwegian Krone (kr)",
+ "value": "currencyNOK"
+ },
+ {
+ "text": "Swedish Krona (kr)",
+ "value": "currencySEK"
+ },
+ {
+ "text": "Czech koruna (czk)",
+ "value": "currencyCZK"
+ },
+ {
+ "text": "Swiss franc (CHF)",
+ "value": "currencyCHF"
+ },
+ {
+ "text": "Polish Złoty (PLN)",
+ "value": "currencyPLN"
+ },
+ {
+ "text": "Bitcoin (฿)",
+ "value": "currencyBTC"
+ }
+ ],
+ "text": "currency"
+ },
+ {
+ "submenu": [
+ {
+ "text": "Hertz (1/s)",
+ "value": "hertz"
+ },
+ {
+ "text": "nanoseconds (ns)",
+ "value": "ns"
+ },
+ {
+ "text": "microseconds (µs)",
+ "value": "µs"
+ },
+ {
+ "text": "milliseconds (ms)",
+ "value": "ms"
+ },
+ {
+ "text": "seconds (s)",
+ "value": "s"
+ },
+ {
+ "text": "minutes (m)",
+ "value": "m"
+ },
+ {
+ "text": "hours (h)",
+ "value": "h"
+ },
+ {
+ "text": "days (d)",
+ "value": "d"
+ },
+ {
+ "text": "duration (ms)",
+ "value": "dtdurationms"
+ },
+ {
+ "text": "duration (s)",
+ "value": "dtdurations"
+ },
+ {
+ "text": "duration (hh:mm:ss)",
+ "value": "dthms"
+ },
+ {
+ "text": "Timeticks (s/100)",
+ "value": "timeticks"
+ }
+ ],
+ "text": "time"
+ },
+ {
+ "submenu": [
+ {
+ "text": "YYYY-MM-DD HH:mm:ss",
+ "value": "dateTimeAsIso"
+ },
+ {
+ "text": "DD/MM/YYYY h:mm:ss a",
+ "value": "dateTimeAsUS"
+ },
+ {
+ "text": "From Now",
+ "value": "dateTimeFromNow"
+ }
+ ],
+ "text": "date & time"
+ },
+ {
+ "submenu": [
+ {
+ "text": "bits",
+ "value": "bits"
+ },
+ {
+ "text": "bytes",
+ "value": "bytes"
+ },
+ {
+ "text": "kibibytes",
+ "value": "kbytes"
+ },
+ {
+ "text": "mebibytes",
+ "value": "mbytes"
+ },
+ {
+ "text": "gibibytes",
+ "value": "gbytes"
+ }
+ ],
+ "text": "data (IEC)"
+ },
+ {
+ "submenu": [
+ {
+ "text": "bits",
+ "value": "decbits"
+ },
+ {
+ "text": "bytes",
+ "value": "decbytes"
+ },
+ {
+ "text": "kilobytes",
+ "value": "deckbytes"
+ },
+ {
+ "text": "megabytes",
+ "value": "decmbytes"
+ },
+ {
+ "text": "gigabytes",
+ "value": "decgbytes"
+ }
+ ],
+ "text": "data (Metric)"
+ },
+ {
+ "submenu": [
+ {
+ "text": "packets/sec",
+ "value": "pps"
+ },
+ {
+ "text": "bits/sec",
+ "value": "bps"
+ },
+ {
+ "text": "bytes/sec",
+ "value": "Bps"
+ },
+ {
+ "text": "kilobits/sec",
+ "value": "Kbits"
+ },
+ {
+ "text": "kilobytes/sec",
+ "value": "KBs"
+ },
+ {
+ "text": "megabits/sec",
+ "value": "Mbits"
+ },
+ {
+ "text": "megabytes/sec",
+ "value": "MBs"
+ },
+ {
+ "text": "gigabytes/sec",
+ "value": "GBs"
+ },
+ {
+ "text": "gigabits/sec",
+ "value": "Gbits"
+ }
+ ],
+ "text": "data rate"
+ },
+ {
+ "submenu": [
+ {
+ "text": "hashes/sec",
+ "value": "Hs"
+ },
+ {
+ "text": "kilohashes/sec",
+ "value": "KHs"
+ },
+ {
+ "text": "megahashes/sec",
+ "value": "MHs"
+ },
+ {
+ "text": "gigahashes/sec",
+ "value": "GHs"
+ },
+ {
+ "text": "terahashes/sec",
+ "value": "THs"
+ },
+ {
+ "text": "petahashes/sec",
+ "value": "PHs"
+ },
+ {
+ "text": "exahashes/sec",
+ "value": "EHs"
+ }
+ ],
+ "text": "hash rate"
+ },
+ {
+ "submenu": [
+ {
+ "text": "ops/sec (ops)",
+ "value": "ops"
+ },
+ {
+ "text": "requests/sec (rps)",
+ "value": "reqps"
+ },
+ {
+ "text": "reads/sec (rps)",
+ "value": "rps"
+ },
+ {
+ "text": "writes/sec (wps)",
+ "value": "wps"
+ },
+ {
+ "text": "I/O ops/sec (iops)",
+ "value": "iops"
+ },
+ {
+ "text": "ops/min (opm)",
+ "value": "opm"
+ },
+ {
+ "text": "reads/min (rpm)",
+ "value": "rpm"
+ },
+ {
+ "text": "writes/min (wpm)",
+ "value": "wpm"
+ }
+ ],
+ "text": "throughput"
+ },
+ {
+ "submenu": [
+ {
+ "text": "millimetre (mm)",
+ "value": "lengthmm"
+ },
+ {
+ "text": "meter (m)",
+ "value": "lengthm"
+ },
+ {
+ "text": "feet (ft)",
+ "value": "lengthft"
+ },
+ {
+ "text": "kilometer (km)",
+ "value": "lengthkm"
+ },
+ {
+ "text": "mile (mi)",
+ "value": "lengthmi"
+ }
+ ],
+ "text": "length"
+ },
+ {
+ "submenu": [
+ {
+ "text": "Square Meters (m²)",
+ "value": "areaM2"
+ },
+ {
+ "text": "Square Feet (ft²)",
+ "value": "areaF2"
+ },
+ {
+ "text": "Square Miles (mi²)",
+ "value": "areaMI2"
+ }
+ ],
+ "text": "area"
+ },
+ {
+ "submenu": [
+ {
+ "text": "milligram (mg)",
+ "value": "massmg"
+ },
+ {
+ "text": "gram (g)",
+ "value": "massg"
+ },
+ {
+ "text": "kilogram (kg)",
+ "value": "masskg"
+ },
+ {
+ "text": "metric ton (t)",
+ "value": "masst"
+ }
+ ],
+ "text": "mass"
+ },
+ {
+ "submenu": [
+ {
+ "text": "metres/second (m/s)",
+ "value": "velocityms"
+ },
+ {
+ "text": "kilometers/hour (km/h)",
+ "value": "velocitykmh"
+ },
+ {
+ "text": "miles/hour (mph)",
+ "value": "velocitymph"
+ },
+ {
+ "text": "knot (kn)",
+ "value": "velocityknot"
+ }
+ ],
+ "text": "velocity"
+ },
+ {
+ "submenu": [
+ {
+ "text": "millilitre (mL)",
+ "value": "mlitre"
+ },
+ {
+ "text": "litre (L)",
+ "value": "litre"
+ },
+ {
+ "text": "cubic metre",
+ "value": "m3"
+ },
+ {
+ "text": "Normal cubic metre",
+ "value": "Nm3"
+ },
+ {
+ "text": "cubic decimetre",
+ "value": "dm3"
+ },
+ {
+ "text": "gallons",
+ "value": "gallons"
+ }
+ ],
+ "text": "volume"
+ },
+ {
+ "submenu": [
+ {
+ "text": "Watt (W)",
+ "value": "watt"
+ },
+ {
+ "text": "Kilowatt (kW)",
+ "value": "kwatt"
+ },
+ {
+ "text": "Milliwatt (mW)",
+ "value": "mwatt"
+ },
+ {
+ "text": "Watt per square metre (W/m²)",
+ "value": "Wm2"
+ },
+ {
+ "text": "Volt-ampere (VA)",
+ "value": "voltamp"
+ },
+ {
+ "text": "Kilovolt-ampere (kVA)",
+ "value": "kvoltamp"
+ },
+ {
+ "text": "Volt-ampere reactive (var)",
+ "value": "voltampreact"
+ },
+ {
+ "text": "Kilovolt-ampere reactive (kvar)",
+ "value": "kvoltampreact"
+ },
+ {
+ "text": "Watt-hour (Wh)",
+ "value": "watth"
+ },
+ {
+ "text": "Kilowatt-hour (kWh)",
+ "value": "kwatth"
+ },
+ {
+ "text": "Kilowatt-min (kWm)",
+ "value": "kwattm"
+ },
+ {
+ "text": "Joule (J)",
+ "value": "joule"
+ },
+ {
+ "text": "Electron volt (eV)",
+ "value": "ev"
+ },
+ {
+ "text": "Ampere (A)",
+ "value": "amp"
+ },
+ {
+ "text": "Kiloampere (kA)",
+ "value": "kamp"
+ },
+ {
+ "text": "Milliampere (mA)",
+ "value": "mamp"
+ },
+ {
+ "text": "Volt (V)",
+ "value": "volt"
+ },
+ {
+ "text": "Kilovolt (kV)",
+ "value": "kvolt"
+ },
+ {
+ "text": "Millivolt (mV)",
+ "value": "mvolt"
+ },
+ {
+ "text": "Decibel-milliwatt (dBm)",
+ "value": "dBm"
+ },
+ {
+ "text": "Ohm (Ω)",
+ "value": "ohm"
+ },
+ {
+ "text": "Lumens (Lm)",
+ "value": "lumens"
+ }
+ ],
+ "text": "energy"
+ },
+ {
+ "submenu": [
+ {
+ "text": "Celsius (°C)",
+ "value": "celsius"
+ },
+ {
+ "text": "Farenheit (°F)",
+ "value": "farenheit"
+ },
+ {
+ "text": "Kelvin (K)",
+ "value": "kelvin"
+ }
+ ],
+ "text": "temperature"
+ },
+ {
+ "submenu": [
+ {
+ "text": "Millibars",
+ "value": "pressurembar"
+ },
+ {
+ "text": "Bars",
+ "value": "pressurebar"
+ },
+ {
+ "text": "Kilobars",
+ "value": "pressurekbar"
+ },
+ {
+ "text": "Hectopascals",
+ "value": "pressurehpa"
+ },
+ {
+ "text": "Kilopascals",
+ "value": "pressurekpa"
+ },
+ {
+ "text": "Inches of mercury",
+ "value": "pressurehg"
+ },
+ {
+ "text": "PSI",
+ "value": "pressurepsi"
+ }
+ ],
+ "text": "pressure"
+ },
+ {
+ "submenu": [
+ {
+ "text": "Newton-meters (Nm)",
+ "value": "forceNm"
+ },
+ {
+ "text": "Kilonewton-meters (kNm)",
+ "value": "forcekNm"
+ },
+ {
+ "text": "Newtons (N)",
+ "value": "forceN"
+ },
+ {
+ "text": "Kilonewtons (kN)",
+ "value": "forcekN"
+ }
+ ],
+ "text": "force"
+ },
+ {
+ "submenu": [
+ {
+ "text": "Gallons/min (gpm)",
+ "value": "flowgpm"
+ },
+ {
+ "text": "Cubic meters/sec (cms)",
+ "value": "flowcms"
+ },
+ {
+ "text": "Cubic feet/sec (cfs)",
+ "value": "flowcfs"
+ },
+ {
+ "text": "Cubic feet/min (cfm)",
+ "value": "flowcfm"
+ },
+ {
+ "text": "Litre/hour",
+ "value": "litreh"
+ },
+ {
+ "text": "Litre/min (l/min)",
+ "value": "flowlpm"
+ },
+ {
+ "text": "milliLitre/min (mL/min)",
+ "value": "flowmlpm"
+ }
+ ],
+ "text": "flow"
+ },
+ {
+ "submenu": [
+ {
+ "text": "Degrees (°)",
+ "value": "degree"
+ },
+ {
+ "text": "Radians",
+ "value": "radian"
+ },
+ {
+ "text": "Gradian",
+ "value": "grad"
+ }
+ ],
+ "text": "angle"
+ },
+ {
+ "submenu": [
+ {
+ "text": "Meters/sec²",
+ "value": "accMS2"
+ },
+ {
+ "text": "Feet/sec²",
+ "value": "accFS2"
+ },
+ {
+ "text": "G unit",
+ "value": "accG"
+ }
+ ],
+ "text": "acceleration"
+ },
+ {
+ "submenu": [
+ {
+ "text": "Becquerel (Bq)",
+ "value": "radbq"
+ },
+ {
+ "text": "curie (Ci)",
+ "value": "radci"
+ },
+ {
+ "text": "Gray (Gy)",
+ "value": "radgy"
+ },
+ {
+ "text": "rad",
+ "value": "radrad"
+ },
+ {
+ "text": "Sievert (Sv)",
+ "value": "radsv"
+ },
+ {
+ "text": "rem",
+ "value": "radrem"
+ },
+ {
+ "text": "Exposure (C/kg)",
+ "value": "radexpckg"
+ },
+ {
+ "text": "roentgen (R)",
+ "value": "radr"
+ },
+ {
+ "text": "Sievert/hour (Sv/h)",
+ "value": "radsvh"
+ }
+ ],
+ "text": "radiation"
+ },
+ {
+ "submenu": [
+ {
+ "text": "parts-per-million (ppm)",
+ "value": "ppm"
+ },
+ {
+ "text": "parts-per-billion (ppb)",
+ "value": "conppb"
+ },
+ {
+ "text": "nanogram per cubic metre (ng/m³)",
+ "value": "conngm3"
+ },
+ {
+ "text": "nanogram per normal cubic metre (ng/Nm³)",
+ "value": "conngNm3"
+ },
+ {
+ "text": "microgram per cubic metre (μg/m³)",
+ "value": "conμgm3"
+ },
+ {
+ "text": "microgram per normal cubic metre (μg/Nm³)",
+ "value": "conμgNm3"
+ },
+ {
+ "text": "milligram per cubic metre (mg/m³)",
+ "value": "conmgm3"
+ },
+ {
+ "text": "milligram per normal cubic metre (mg/Nm³)",
+ "value": "conmgNm3"
+ },
+ {
+ "text": "gram per cubic metre (g/m³)",
+ "value": "congm3"
+ },
+ {
+ "text": "gram per normal cubic metre (g/Nm³)",
+ "value": "congNm3"
+ }
+ ],
+ "text": "concentration"
+ }
+ ]
+ },
+ {
+ "animationModes": [
+ {
+ "text": "Show All",
+ "value": "all"
+ },
+ {
+ "text": "Show Triggered",
+ "value": "triggered"
+ }
+ ],
+ "colors": [
+ "#299c46",
+ "rgba(237, 129, 40, 0.89)",
+ "#d44a3a"
+ ],
+ "d3DivId": "d3_svg_5",
+ "datasource": "gdev-testdata",
+ "decimals": 2,
+ "displayModes": [
+ {
+ "text": "Show All",
+ "value": "all"
+ },
+ {
+ "text": "Show Triggered",
+ "value": "triggered"
+ }
+ ],
+ "fontSizes": [
+ 4,
+ 5,
+ 6,
+ 7,
+ 8,
+ 9,
+ 10,
+ 11,
+ 12,
+ 13,
+ 14,
+ 15,
+ 16,
+ 17,
+ 18,
+ 19,
+ 20,
+ 22,
+ 24,
+ 26,
+ 28,
+ 30,
+ 32,
+ 34,
+ 36,
+ 38,
+ 40,
+ 42,
+ 44,
+ 46,
+ 48,
+ 50,
+ 52,
+ 54,
+ 56,
+ 58,
+ 60,
+ 62,
+ 64,
+ 66,
+ 68,
+ 70
+ ],
+ "fontTypes": [
+ "Open Sans",
+ "Arial",
+ "Avant Garde",
+ "Bookman",
+ "Consolas",
+ "Courier",
+ "Courier New",
+ "Futura",
+ "Garamond",
+ "Helvetica",
+ "Palatino",
+ "Times",
+ "Times New Roman",
+ "Verdana"
+ ],
+ "format": "none",
+ "gridPos": {
+ "h": 9,
+ "w": 12,
+ "x": 12,
+ "y": 0
+ },
+ "id": 5,
+ "links": [],
+ "notcolors": [
+ "rgba(245, 54, 54, 0.9)",
+ "rgba(237, 129, 40, 0.89)",
+ "rgba(50, 172, 45, 0.97)"
+ ],
+ "operatorName": "avg",
+ "operatorOptions": [
+ {
+ "text": "Average",
+ "value": "avg"
+ },
+ {
+ "text": "Count",
+ "value": "count"
+ },
+ {
+ "text": "Current",
+ "value": "current"
+ },
+ {
+ "text": "Delta",
+ "value": "delta"
+ },
+ {
+ "text": "Difference",
+ "value": "diff"
+ },
+ {
+ "text": "First",
+ "value": "first"
+ },
+ {
+ "text": "Log Min",
+ "value": "logmin"
+ },
+ {
+ "text": "Max",
+ "value": "max"
+ },
+ {
+ "text": "Min",
+ "value": "min"
+ },
+ {
+ "text": "Name",
+ "value": "name"
+ },
+ {
+ "text": "Time of Last Point",
+ "value": "last_time"
+ },
+ {
+ "text": "Time Step",
+ "value": "time_step"
+ },
+ {
+ "text": "Total",
+ "value": "total"
+ }
+ ],
+ "polystat": {
+ "animationSpeed": 2500,
+ "columnAutoSize": true,
+ "columns": "",
+ "defaultClickThrough": "",
+ "defaultClickThroughSanitize": true,
+ "displayLimit": 100,
+ "fontAutoScale": true,
+ "fontSize": 12,
+ "globalDisplayMode": "all",
+ "globalOperatorName": "avg",
+ "gradientEnabled": true,
+ "hexagonSortByDirection": "asc",
+ "hexagonSortByField": "name",
+ "maxMetrics": 0,
+ "polygonBorderColor": "black",
+ "polygonBorderSize": 2,
+ "radius": "",
+ "radiusAutoSize": true,
+ "rowAutoSize": true,
+ "rows": "",
+ "shape": "hexagon_pointed_top",
+ "tooltipDisplayMode": "all",
+ "tooltipDisplayTextTriggeredEmpty": "OK",
+ "tooltipFontSize": 12,
+ "tooltipFontType": "Open Sans",
+ "tooltipPrimarySortDirection": "desc",
+ "tooltipPrimarySortField": "thresholdLevel",
+ "tooltipSecondarySortDirection": "desc",
+ "tooltipSecondarySortField": "value",
+ "tooltipTimestampEnabled": true
+ },
+ "savedComposites": [
+ {
+ "compositeName": "comp",
+ "members": [
+ {
+ "seriesName": "A-series"
+ },
+ {
+ "seriesName": "B-series"
+ }
+ ],
+ "enabled": true,
+ "clickThrough": "",
+ "hideMembers": true,
+ "showName": true,
+ "showValue": true,
+ "animateMode": "all",
+ "thresholdLevel": 0,
+ "sanitizeURLEnabled": true,
+ "sanitizedURL": ""
+ }
+ ],
+ "savedOverrides": [],
+ "shapes": [
+ {
+ "text": "Hexagon Pointed Top",
+ "value": "hexagon_pointed_top"
+ },
+ {
+ "text": "Hexagon Flat Top",
+ "value": "hexagon_flat_top"
+ },
+ {
+ "text": "Circle",
+ "value": "circle"
+ },
+ {
+ "text": "Cross",
+ "value": "cross"
+ },
+ {
+ "text": "Diamond",
+ "value": "diamond"
+ },
+ {
+ "text": "Square",
+ "value": "square"
+ },
+ {
+ "text": "Star",
+ "value": "star"
+ },
+ {
+ "text": "Triangle",
+ "value": "triangle"
+ },
+ {
+ "text": "Wye",
+ "value": "wye"
+ }
+ ],
+ "sortDirections": [
+ {
+ "text": "Ascending",
+ "value": "asc"
+ },
+ {
+ "text": "Descending",
+ "value": "desc"
+ }
+ ],
+ "sortFields": [
+ {
+ "text": "Name",
+ "value": "name"
+ },
+ {
+ "text": "Threshold Level",
+ "value": "thresholdLevel"
+ },
+ {
+ "text": "Value",
+ "value": "value"
+ }
+ ],
+ "svgContainer": {},
+ "targets": [
+ {
+ "expr": "",
+ "format": "time_series",
+ "intervalFactor": 1,
+ "refId": "A",
+ "scenarioId": "random_walk"
+ },
+ {
+ "expr": "",
+ "format": "time_series",
+ "intervalFactor": 1,
+ "refId": "B",
+ "scenarioId": "random_walk"
+ },
+ {
+ "expr": "",
+ "format": "time_series",
+ "intervalFactor": 1,
+ "refId": "C",
+ "scenarioId": "random_walk"
+ },
+ {
+ "expr": "",
+ "format": "time_series",
+ "intervalFactor": 1,
+ "refId": "D",
+ "scenarioId": "random_walk"
+ },
+ {
+ "expr": "",
+ "format": "time_series",
+ "intervalFactor": 1,
+ "refId": "E",
+ "scenarioId": "random_walk"
+ }
+ ],
+ "thresholdStates": [
+ {
+ "text": "ok",
+ "value": 0
+ },
+ {
+ "text": "warning",
+ "value": 1
+ },
+ {
+ "text": "critical",
+ "value": 2
+ },
+ {
+ "text": "custom",
+ "value": 3
+ }
+ ],
+ "title": "Composite crash",
+ "type": "grafana-polystat-panel",
+ "unitFormats": [
+ {
+ "submenu": [
+ {
+ "text": "none",
+ "value": "none"
+ },
+ {
+ "text": "short",
+ "value": "short"
+ },
+ {
+ "text": "percent (0-100)",
+ "value": "percent"
+ },
+ {
+ "text": "percent (0.0-1.0)",
+ "value": "percentunit"
+ },
+ {
+ "text": "Humidity (%H)",
+ "value": "humidity"
+ },
+ {
+ "text": "decibel",
+ "value": "dB"
+ },
+ {
+ "text": "hexadecimal (0x)",
+ "value": "hex0x"
+ },
+ {
+ "text": "hexadecimal",
+ "value": "hex"
+ },
+ {
+ "text": "scientific notation",
+ "value": "sci"
+ },
+ {
+ "text": "locale format",
+ "value": "locale"
+ }
+ ],
+ "text": "none"
+ },
+ {
+ "submenu": [
+ {
+ "text": "Dollars ($)",
+ "value": "currencyUSD"
+ },
+ {
+ "text": "Pounds (£)",
+ "value": "currencyGBP"
+ },
+ {
+ "text": "Euro (€)",
+ "value": "currencyEUR"
+ },
+ {
+ "text": "Yen (¥)",
+ "value": "currencyJPY"
+ },
+ {
+ "text": "Rubles (₽)",
+ "value": "currencyRUB"
+ },
+ {
+ "text": "Hryvnias (₴)",
+ "value": "currencyUAH"
+ },
+ {
+ "text": "Real (R$)",
+ "value": "currencyBRL"
+ },
+ {
+ "text": "Danish Krone (kr)",
+ "value": "currencyDKK"
+ },
+ {
+ "text": "Icelandic Króna (kr)",
+ "value": "currencyISK"
+ },
+ {
+ "text": "Norwegian Krone (kr)",
+ "value": "currencyNOK"
+ },
+ {
+ "text": "Swedish Krona (kr)",
+ "value": "currencySEK"
+ },
+ {
+ "text": "Czech koruna (czk)",
+ "value": "currencyCZK"
+ },
+ {
+ "text": "Swiss franc (CHF)",
+ "value": "currencyCHF"
+ },
+ {
+ "text": "Polish Złoty (PLN)",
+ "value": "currencyPLN"
+ },
+ {
+ "text": "Bitcoin (฿)",
+ "value": "currencyBTC"
+ }
+ ],
+ "text": "currency"
+ },
+ {
+ "submenu": [
+ {
+ "text": "Hertz (1/s)",
+ "value": "hertz"
+ },
+ {
+ "text": "nanoseconds (ns)",
+ "value": "ns"
+ },
+ {
+ "text": "microseconds (µs)",
+ "value": "µs"
+ },
+ {
+ "text": "milliseconds (ms)",
+ "value": "ms"
+ },
+ {
+ "text": "seconds (s)",
+ "value": "s"
+ },
+ {
+ "text": "minutes (m)",
+ "value": "m"
+ },
+ {
+ "text": "hours (h)",
+ "value": "h"
+ },
+ {
+ "text": "days (d)",
+ "value": "d"
+ },
+ {
+ "text": "duration (ms)",
+ "value": "dtdurationms"
+ },
+ {
+ "text": "duration (s)",
+ "value": "dtdurations"
+ },
+ {
+ "text": "duration (hh:mm:ss)",
+ "value": "dthms"
+ },
+ {
+ "text": "Timeticks (s/100)",
+ "value": "timeticks"
+ }
+ ],
+ "text": "time"
+ },
+ {
+ "submenu": [
+ {
+ "text": "YYYY-MM-DD HH:mm:ss",
+ "value": "dateTimeAsIso"
+ },
+ {
+ "text": "DD/MM/YYYY h:mm:ss a",
+ "value": "dateTimeAsUS"
+ },
+ {
+ "text": "From Now",
+ "value": "dateTimeFromNow"
+ }
+ ],
+ "text": "date & time"
+ },
+ {
+ "submenu": [
+ {
+ "text": "bits",
+ "value": "bits"
+ },
+ {
+ "text": "bytes",
+ "value": "bytes"
+ },
+ {
+ "text": "kibibytes",
+ "value": "kbytes"
+ },
+ {
+ "text": "mebibytes",
+ "value": "mbytes"
+ },
+ {
+ "text": "gibibytes",
+ "value": "gbytes"
+ }
+ ],
+ "text": "data (IEC)"
+ },
+ {
+ "submenu": [
+ {
+ "text": "bits",
+ "value": "decbits"
+ },
+ {
+ "text": "bytes",
+ "value": "decbytes"
+ },
+ {
+ "text": "kilobytes",
+ "value": "deckbytes"
+ },
+ {
+ "text": "megabytes",
+ "value": "decmbytes"
+ },
+ {
+ "text": "gigabytes",
+ "value": "decgbytes"
+ }
+ ],
+ "text": "data (Metric)"
+ },
+ {
+ "submenu": [
+ {
+ "text": "packets/sec",
+ "value": "pps"
+ },
+ {
+ "text": "bits/sec",
+ "value": "bps"
+ },
+ {
+ "text": "bytes/sec",
+ "value": "Bps"
+ },
+ {
+ "text": "kilobits/sec",
+ "value": "Kbits"
+ },
+ {
+ "text": "kilobytes/sec",
+ "value": "KBs"
+ },
+ {
+ "text": "megabits/sec",
+ "value": "Mbits"
+ },
+ {
+ "text": "megabytes/sec",
+ "value": "MBs"
+ },
+ {
+ "text": "gigabytes/sec",
+ "value": "GBs"
+ },
+ {
+ "text": "gigabits/sec",
+ "value": "Gbits"
+ }
+ ],
+ "text": "data rate"
+ },
+ {
+ "submenu": [
+ {
+ "text": "hashes/sec",
+ "value": "Hs"
+ },
+ {
+ "text": "kilohashes/sec",
+ "value": "KHs"
+ },
+ {
+ "text": "megahashes/sec",
+ "value": "MHs"
+ },
+ {
+ "text": "gigahashes/sec",
+ "value": "GHs"
+ },
+ {
+ "text": "terahashes/sec",
+ "value": "THs"
+ },
+ {
+ "text": "petahashes/sec",
+ "value": "PHs"
+ },
+ {
+ "text": "exahashes/sec",
+ "value": "EHs"
+ }
+ ],
+ "text": "hash rate"
+ },
+ {
+ "submenu": [
+ {
+ "text": "ops/sec (ops)",
+ "value": "ops"
+ },
+ {
+ "text": "requests/sec (rps)",
+ "value": "reqps"
+ },
+ {
+ "text": "reads/sec (rps)",
+ "value": "rps"
+ },
+ {
+ "text": "writes/sec (wps)",
+ "value": "wps"
+ },
+ {
+ "text": "I/O ops/sec (iops)",
+ "value": "iops"
+ },
+ {
+ "text": "ops/min (opm)",
+ "value": "opm"
+ },
+ {
+ "text": "reads/min (rpm)",
+ "value": "rpm"
+ },
+ {
+ "text": "writes/min (wpm)",
+ "value": "wpm"
+ }
+ ],
+ "text": "throughput"
+ },
+ {
+ "submenu": [
+ {
+ "text": "millimetre (mm)",
+ "value": "lengthmm"
+ },
+ {
+ "text": "meter (m)",
+ "value": "lengthm"
+ },
+ {
+ "text": "feet (ft)",
+ "value": "lengthft"
+ },
+ {
+ "text": "kilometer (km)",
+ "value": "lengthkm"
+ },
+ {
+ "text": "mile (mi)",
+ "value": "lengthmi"
+ }
+ ],
+ "text": "length"
+ },
+ {
+ "submenu": [
+ {
+ "text": "Square Meters (m²)",
+ "value": "areaM2"
+ },
+ {
+ "text": "Square Feet (ft²)",
+ "value": "areaF2"
+ },
+ {
+ "text": "Square Miles (mi²)",
+ "value": "areaMI2"
+ }
+ ],
+ "text": "area"
+ },
+ {
+ "submenu": [
+ {
+ "text": "milligram (mg)",
+ "value": "massmg"
+ },
+ {
+ "text": "gram (g)",
+ "value": "massg"
+ },
+ {
+ "text": "kilogram (kg)",
+ "value": "masskg"
+ },
+ {
+ "text": "metric ton (t)",
+ "value": "masst"
+ }
+ ],
+ "text": "mass"
+ },
+ {
+ "submenu": [
+ {
+ "text": "metres/second (m/s)",
+ "value": "velocityms"
+ },
+ {
+ "text": "kilometers/hour (km/h)",
+ "value": "velocitykmh"
+ },
+ {
+ "text": "miles/hour (mph)",
+ "value": "velocitymph"
+ },
+ {
+ "text": "knot (kn)",
+ "value": "velocityknot"
+ }
+ ],
+ "text": "velocity"
+ },
+ {
+ "submenu": [
+ {
+ "text": "millilitre (mL)",
+ "value": "mlitre"
+ },
+ {
+ "text": "litre (L)",
+ "value": "litre"
+ },
+ {
+ "text": "cubic metre",
+ "value": "m3"
+ },
+ {
+ "text": "Normal cubic metre",
+ "value": "Nm3"
+ },
+ {
+ "text": "cubic decimetre",
+ "value": "dm3"
+ },
+ {
+ "text": "gallons",
+ "value": "gallons"
+ }
+ ],
+ "text": "volume"
+ },
+ {
+ "submenu": [
+ {
+ "text": "Watt (W)",
+ "value": "watt"
+ },
+ {
+ "text": "Kilowatt (kW)",
+ "value": "kwatt"
+ },
+ {
+ "text": "Milliwatt (mW)",
+ "value": "mwatt"
+ },
+ {
+ "text": "Watt per square metre (W/m²)",
+ "value": "Wm2"
+ },
+ {
+ "text": "Volt-ampere (VA)",
+ "value": "voltamp"
+ },
+ {
+ "text": "Kilovolt-ampere (kVA)",
+ "value": "kvoltamp"
+ },
+ {
+ "text": "Volt-ampere reactive (var)",
+ "value": "voltampreact"
+ },
+ {
+ "text": "Kilovolt-ampere reactive (kvar)",
+ "value": "kvoltampreact"
+ },
+ {
+ "text": "Watt-hour (Wh)",
+ "value": "watth"
+ },
+ {
+ "text": "Kilowatt-hour (kWh)",
+ "value": "kwatth"
+ },
+ {
+ "text": "Kilowatt-min (kWm)",
+ "value": "kwattm"
+ },
+ {
+ "text": "Joule (J)",
+ "value": "joule"
+ },
+ {
+ "text": "Electron volt (eV)",
+ "value": "ev"
+ },
+ {
+ "text": "Ampere (A)",
+ "value": "amp"
+ },
+ {
+ "text": "Kiloampere (kA)",
+ "value": "kamp"
+ },
+ {
+ "text": "Milliampere (mA)",
+ "value": "mamp"
+ },
+ {
+ "text": "Volt (V)",
+ "value": "volt"
+ },
+ {
+ "text": "Kilovolt (kV)",
+ "value": "kvolt"
+ },
+ {
+ "text": "Millivolt (mV)",
+ "value": "mvolt"
+ },
+ {
+ "text": "Decibel-milliwatt (dBm)",
+ "value": "dBm"
+ },
+ {
+ "text": "Ohm (Ω)",
+ "value": "ohm"
+ },
+ {
+ "text": "Lumens (Lm)",
+ "value": "lumens"
+ }
+ ],
+ "text": "energy"
+ },
+ {
+ "submenu": [
+ {
+ "text": "Celsius (°C)",
+ "value": "celsius"
+ },
+ {
+ "text": "Farenheit (°F)",
+ "value": "farenheit"
+ },
+ {
+ "text": "Kelvin (K)",
+ "value": "kelvin"
+ }
+ ],
+ "text": "temperature"
+ },
+ {
+ "submenu": [
+ {
+ "text": "Millibars",
+ "value": "pressurembar"
+ },
+ {
+ "text": "Bars",
+ "value": "pressurebar"
+ },
+ {
+ "text": "Kilobars",
+ "value": "pressurekbar"
+ },
+ {
+ "text": "Hectopascals",
+ "value": "pressurehpa"
+ },
+ {
+ "text": "Kilopascals",
+ "value": "pressurekpa"
+ },
+ {
+ "text": "Inches of mercury",
+ "value": "pressurehg"
+ },
+ {
+ "text": "PSI",
+ "value": "pressurepsi"
+ }
+ ],
+ "text": "pressure"
+ },
+ {
+ "submenu": [
+ {
+ "text": "Newton-meters (Nm)",
+ "value": "forceNm"
+ },
+ {
+ "text": "Kilonewton-meters (kNm)",
+ "value": "forcekNm"
+ },
+ {
+ "text": "Newtons (N)",
+ "value": "forceN"
+ },
+ {
+ "text": "Kilonewtons (kN)",
+ "value": "forcekN"
+ }
+ ],
+ "text": "force"
+ },
+ {
+ "submenu": [
+ {
+ "text": "Gallons/min (gpm)",
+ "value": "flowgpm"
+ },
+ {
+ "text": "Cubic meters/sec (cms)",
+ "value": "flowcms"
+ },
+ {
+ "text": "Cubic feet/sec (cfs)",
+ "value": "flowcfs"
+ },
+ {
+ "text": "Cubic feet/min (cfm)",
+ "value": "flowcfm"
+ },
+ {
+ "text": "Litre/hour",
+ "value": "litreh"
+ },
+ {
+ "text": "Litre/min (l/min)",
+ "value": "flowlpm"
+ },
+ {
+ "text": "milliLitre/min (mL/min)",
+ "value": "flowmlpm"
+ }
+ ],
+ "text": "flow"
+ },
+ {
+ "submenu": [
+ {
+ "text": "Degrees (°)",
+ "value": "degree"
+ },
+ {
+ "text": "Radians",
+ "value": "radian"
+ },
+ {
+ "text": "Gradian",
+ "value": "grad"
+ }
+ ],
+ "text": "angle"
+ },
+ {
+ "submenu": [
+ {
+ "text": "Meters/sec²",
+ "value": "accMS2"
+ },
+ {
+ "text": "Feet/sec²",
+ "value": "accFS2"
+ },
+ {
+ "text": "G unit",
+ "value": "accG"
+ }
+ ],
+ "text": "acceleration"
+ },
+ {
+ "submenu": [
+ {
+ "text": "Becquerel (Bq)",
+ "value": "radbq"
+ },
+ {
+ "text": "curie (Ci)",
+ "value": "radci"
+ },
+ {
+ "text": "Gray (Gy)",
+ "value": "radgy"
+ },
+ {
+ "text": "rad",
+ "value": "radrad"
+ },
+ {
+ "text": "Sievert (Sv)",
+ "value": "radsv"
+ },
+ {
+ "text": "rem",
+ "value": "radrem"
+ },
+ {
+ "text": "Exposure (C/kg)",
+ "value": "radexpckg"
+ },
+ {
+ "text": "roentgen (R)",
+ "value": "radr"
+ },
+ {
+ "text": "Sievert/hour (Sv/h)",
+ "value": "radsvh"
+ }
+ ],
+ "text": "radiation"
+ },
+ {
+ "submenu": [
+ {
+ "text": "parts-per-million (ppm)",
+ "value": "ppm"
+ },
+ {
+ "text": "parts-per-billion (ppb)",
+ "value": "conppb"
+ },
+ {
+ "text": "nanogram per cubic metre (ng/m³)",
+ "value": "conngm3"
+ },
+ {
+ "text": "nanogram per normal cubic metre (ng/Nm³)",
+ "value": "conngNm3"
+ },
+ {
+ "text": "microgram per cubic metre (μg/m³)",
+ "value": "conμgm3"
+ },
+ {
+ "text": "microgram per normal cubic metre (μg/Nm³)",
+ "value": "conμgNm3"
+ },
+ {
+ "text": "milligram per cubic metre (mg/m³)",
+ "value": "conmgm3"
+ },
+ {
+ "text": "milligram per normal cubic metre (mg/Nm³)",
+ "value": "conmgNm3"
+ },
+ {
+ "text": "gram per cubic metre (g/m³)",
+ "value": "congm3"
+ },
+ {
+ "text": "gram per normal cubic metre (g/Nm³)",
+ "value": "congNm3"
+ }
+ ],
+ "text": "concentration"
+ }
+ ]
+ },
+ {
+ "animationModes": [
+ {
+ "text": "Show All",
+ "value": "all"
+ },
+ {
+ "text": "Show Triggered",
+ "value": "triggered"
+ }
+ ],
+ "colors": [
+ "#299c46",
+ "rgba(237, 129, 40, 0.89)",
+ "#d44a3a"
+ ],
+ "d3DivId": "d3_svg_2",
+ "datasource": "gdev-testdata",
+ "decimals": 2,
+ "displayModes": [
+ {
+ "text": "Show All",
+ "value": "all"
+ },
+ {
+ "text": "Show Triggered",
+ "value": "triggered"
+ }
+ ],
+ "fontSizes": [
+ 4,
+ 5,
+ 6,
+ 7,
+ 8,
+ 9,
+ 10,
+ 11,
+ 12,
+ 13,
+ 14,
+ 15,
+ 16,
+ 17,
+ 18,
+ 19,
+ 20,
+ 22,
+ 24,
+ 26,
+ 28,
+ 30,
+ 32,
+ 34,
+ 36,
+ 38,
+ 40,
+ 42,
+ 44,
+ 46,
+ 48,
+ 50,
+ 52,
+ 54,
+ 56,
+ 58,
+ 60,
+ 62,
+ 64,
+ 66,
+ 68,
+ 70
+ ],
+ "fontTypes": [
+ "Open Sans",
+ "Arial",
+ "Avant Garde",
+ "Bookman",
+ "Consolas",
+ "Courier",
+ "Courier New",
+ "Futura",
+ "Garamond",
+ "Helvetica",
+ "Palatino",
+ "Times",
+ "Times New Roman",
+ "Verdana"
+ ],
+ "format": "none",
+ "gridPos": {
+ "h": 10,
+ "w": 12,
+ "x": 0,
+ "y": 9
+ },
+ "id": 2,
+ "links": [],
+ "notcolors": [
+ "rgba(245, 54, 54, 0.9)",
+ "rgba(237, 129, 40, 0.89)",
+ "rgba(50, 172, 45, 0.97)"
+ ],
+ "operatorName": "avg",
+ "operatorOptions": [
+ {
+ "text": "Average",
+ "value": "avg"
+ },
+ {
+ "text": "Count",
+ "value": "count"
+ },
+ {
+ "text": "Current",
+ "value": "current"
+ },
+ {
+ "text": "Delta",
+ "value": "delta"
+ },
+ {
+ "text": "Difference",
+ "value": "diff"
+ },
+ {
+ "text": "First",
+ "value": "first"
+ },
+ {
+ "text": "Log Min",
+ "value": "logmin"
+ },
+ {
+ "text": "Max",
+ "value": "max"
+ },
+ {
+ "text": "Min",
+ "value": "min"
+ },
+ {
+ "text": "Name",
+ "value": "name"
+ },
+ {
+ "text": "Time of Last Point",
+ "value": "last_time"
+ },
+ {
+ "text": "Time Step",
+ "value": "time_step"
+ },
+ {
+ "text": "Total",
+ "value": "total"
+ }
+ ],
+ "polystat": {
+ "animationSpeed": 2500,
+ "columnAutoSize": true,
+ "columns": 1,
+ "defaultClickThrough": "",
+ "defaultClickThroughSanitize": true,
+ "displayLimit": 100,
+ "fontAutoScale": true,
+ "fontSize": 12,
+ "globalDisplayMode": "all",
+ "globalOperatorName": "avg",
+ "gradientEnabled": true,
+ "hexagonSortByDirection": "asc",
+ "hexagonSortByField": "name",
+ "maxMetrics": 0,
+ "polygonBorderColor": "black",
+ "polygonBorderSize": 2,
+ "radius": "",
+ "radiusAutoSize": true,
+ "rowAutoSize": true,
+ "rows": 1,
+ "shape": "hexagon_pointed_top",
+ "tooltipDisplayMode": "all",
+ "tooltipDisplayTextTriggeredEmpty": "OK",
+ "tooltipFontSize": 12,
+ "tooltipFontType": "Open Sans",
+ "tooltipPrimarySortDirection": "desc",
+ "tooltipPrimarySortField": "thresholdLevel",
+ "tooltipSecondarySortDirection": "desc",
+ "tooltipSecondarySortField": "value",
+ "tooltipTimestampEnabled": true
+ },
+ "savedComposites": [],
+ "savedOverrides": [],
+ "shapes": [
+ {
+ "text": "Hexagon Pointed Top",
+ "value": "hexagon_pointed_top"
+ },
+ {
+ "text": "Hexagon Flat Top",
+ "value": "hexagon_flat_top"
+ },
+ {
+ "text": "Circle",
+ "value": "circle"
+ },
+ {
+ "text": "Cross",
+ "value": "cross"
+ },
+ {
+ "text": "Diamond",
+ "value": "diamond"
+ },
+ {
+ "text": "Square",
+ "value": "square"
+ },
+ {
+ "text": "Star",
+ "value": "star"
+ },
+ {
+ "text": "Triangle",
+ "value": "triangle"
+ },
+ {
+ "text": "Wye",
+ "value": "wye"
+ }
+ ],
+ "sortDirections": [
+ {
+ "text": "Ascending",
+ "value": "asc"
+ },
+ {
+ "text": "Descending",
+ "value": "desc"
+ }
+ ],
+ "sortFields": [
+ {
+ "text": "Name",
+ "value": "name"
+ },
+ {
+ "text": "Threshold Level",
+ "value": "thresholdLevel"
+ },
+ {
+ "text": "Value",
+ "value": "value"
+ }
+ ],
+ "svgContainer": {},
+ "targets": [
+ {
+ "alias": "Sensor-A",
+ "expr": "",
+ "format": "time_series",
+ "intervalFactor": 1,
+ "refId": "A",
+ "scenarioId": "csv_metric_values",
+ "stringInput": "1,20,90,30,5,0"
+ },
+ {
+ "alias": "Sensor-B",
+ "expr": "",
+ "format": "time_series",
+ "intervalFactor": 1,
+ "refId": "B",
+ "scenarioId": "csv_metric_values",
+ "stringInput": "3433,23432,55"
+ },
+ {
+ "alias": "Sensor-C",
+ "expr": "",
+ "format": "time_series",
+ "intervalFactor": 1,
+ "refId": "C",
+ "scenarioId": "csv_metric_values",
+ "stringInput": "1,2,3,4,5,6"
+ },
+ {
+ "alias": "Sensor-E",
+ "expr": "",
+ "format": "time_series",
+ "intervalFactor": 1,
+ "refId": "D",
+ "scenarioId": "csv_metric_values",
+ "stringInput": "1,20,90,30,5,0"
+ }
+ ],
+ "thresholdStates": [
+ {
+ "text": "ok",
+ "value": 0
+ },
+ {
+ "text": "warning",
+ "value": 1
+ },
+ {
+ "text": "critical",
+ "value": 2
+ },
+ {
+ "text": "custom",
+ "value": 3
+ }
+ ],
+ "title": "No Value in Sensor-C Bug",
+ "type": "grafana-polystat-panel",
+ "unitFormats": [
+ {
+ "submenu": [
+ {
+ "text": "none",
+ "value": "none"
+ },
+ {
+ "text": "short",
+ "value": "short"
+ },
+ {
+ "text": "percent (0-100)",
+ "value": "percent"
+ },
+ {
+ "text": "percent (0.0-1.0)",
+ "value": "percentunit"
+ },
+ {
+ "text": "Humidity (%H)",
+ "value": "humidity"
+ },
+ {
+ "text": "decibel",
+ "value": "dB"
+ },
+ {
+ "text": "hexadecimal (0x)",
+ "value": "hex0x"
+ },
+ {
+ "text": "hexadecimal",
+ "value": "hex"
+ },
+ {
+ "text": "scientific notation",
+ "value": "sci"
+ },
+ {
+ "text": "locale format",
+ "value": "locale"
+ }
+ ],
+ "text": "none"
+ },
+ {
+ "submenu": [
+ {
+ "text": "Dollars ($)",
+ "value": "currencyUSD"
+ },
+ {
+ "text": "Pounds (£)",
+ "value": "currencyGBP"
+ },
+ {
+ "text": "Euro (€)",
+ "value": "currencyEUR"
+ },
+ {
+ "text": "Yen (¥)",
+ "value": "currencyJPY"
+ },
+ {
+ "text": "Rubles (₽)",
+ "value": "currencyRUB"
+ },
+ {
+ "text": "Hryvnias (₴)",
+ "value": "currencyUAH"
+ },
+ {
+ "text": "Real (R$)",
+ "value": "currencyBRL"
+ },
+ {
+ "text": "Danish Krone (kr)",
+ "value": "currencyDKK"
+ },
+ {
+ "text": "Icelandic Króna (kr)",
+ "value": "currencyISK"
+ },
+ {
+ "text": "Norwegian Krone (kr)",
+ "value": "currencyNOK"
+ },
+ {
+ "text": "Swedish Krona (kr)",
+ "value": "currencySEK"
+ },
+ {
+ "text": "Czech koruna (czk)",
+ "value": "currencyCZK"
+ },
+ {
+ "text": "Swiss franc (CHF)",
+ "value": "currencyCHF"
+ },
+ {
+ "text": "Polish Złoty (PLN)",
+ "value": "currencyPLN"
+ },
+ {
+ "text": "Bitcoin (฿)",
+ "value": "currencyBTC"
+ }
+ ],
+ "text": "currency"
+ },
+ {
+ "submenu": [
+ {
+ "text": "Hertz (1/s)",
+ "value": "hertz"
+ },
+ {
+ "text": "nanoseconds (ns)",
+ "value": "ns"
+ },
+ {
+ "text": "microseconds (µs)",
+ "value": "µs"
+ },
+ {
+ "text": "milliseconds (ms)",
+ "value": "ms"
+ },
+ {
+ "text": "seconds (s)",
+ "value": "s"
+ },
+ {
+ "text": "minutes (m)",
+ "value": "m"
+ },
+ {
+ "text": "hours (h)",
+ "value": "h"
+ },
+ {
+ "text": "days (d)",
+ "value": "d"
+ },
+ {
+ "text": "duration (ms)",
+ "value": "dtdurationms"
+ },
+ {
+ "text": "duration (s)",
+ "value": "dtdurations"
+ },
+ {
+ "text": "duration (hh:mm:ss)",
+ "value": "dthms"
+ },
+ {
+ "text": "Timeticks (s/100)",
+ "value": "timeticks"
+ }
+ ],
+ "text": "time"
+ },
+ {
+ "submenu": [
+ {
+ "text": "YYYY-MM-DD HH:mm:ss",
+ "value": "dateTimeAsIso"
+ },
+ {
+ "text": "DD/MM/YYYY h:mm:ss a",
+ "value": "dateTimeAsUS"
+ },
+ {
+ "text": "From Now",
+ "value": "dateTimeFromNow"
+ }
+ ],
+ "text": "date & time"
+ },
+ {
+ "submenu": [
+ {
+ "text": "bits",
+ "value": "bits"
+ },
+ {
+ "text": "bytes",
+ "value": "bytes"
+ },
+ {
+ "text": "kibibytes",
+ "value": "kbytes"
+ },
+ {
+ "text": "mebibytes",
+ "value": "mbytes"
+ },
+ {
+ "text": "gibibytes",
+ "value": "gbytes"
+ }
+ ],
+ "text": "data (IEC)"
+ },
+ {
+ "submenu": [
+ {
+ "text": "bits",
+ "value": "decbits"
+ },
+ {
+ "text": "bytes",
+ "value": "decbytes"
+ },
+ {
+ "text": "kilobytes",
+ "value": "deckbytes"
+ },
+ {
+ "text": "megabytes",
+ "value": "decmbytes"
+ },
+ {
+ "text": "gigabytes",
+ "value": "decgbytes"
+ }
+ ],
+ "text": "data (Metric)"
+ },
+ {
+ "submenu": [
+ {
+ "text": "packets/sec",
+ "value": "pps"
+ },
+ {
+ "text": "bits/sec",
+ "value": "bps"
+ },
+ {
+ "text": "bytes/sec",
+ "value": "Bps"
+ },
+ {
+ "text": "kilobits/sec",
+ "value": "Kbits"
+ },
+ {
+ "text": "kilobytes/sec",
+ "value": "KBs"
+ },
+ {
+ "text": "megabits/sec",
+ "value": "Mbits"
+ },
+ {
+ "text": "megabytes/sec",
+ "value": "MBs"
+ },
+ {
+ "text": "gigabytes/sec",
+ "value": "GBs"
+ },
+ {
+ "text": "gigabits/sec",
+ "value": "Gbits"
+ }
+ ],
+ "text": "data rate"
+ },
+ {
+ "submenu": [
+ {
+ "text": "hashes/sec",
+ "value": "Hs"
+ },
+ {
+ "text": "kilohashes/sec",
+ "value": "KHs"
+ },
+ {
+ "text": "megahashes/sec",
+ "value": "MHs"
+ },
+ {
+ "text": "gigahashes/sec",
+ "value": "GHs"
+ },
+ {
+ "text": "terahashes/sec",
+ "value": "THs"
+ },
+ {
+ "text": "petahashes/sec",
+ "value": "PHs"
+ },
+ {
+ "text": "exahashes/sec",
+ "value": "EHs"
+ }
+ ],
+ "text": "hash rate"
+ },
+ {
+ "submenu": [
+ {
+ "text": "ops/sec (ops)",
+ "value": "ops"
+ },
+ {
+ "text": "requests/sec (rps)",
+ "value": "reqps"
+ },
+ {
+ "text": "reads/sec (rps)",
+ "value": "rps"
+ },
+ {
+ "text": "writes/sec (wps)",
+ "value": "wps"
+ },
+ {
+ "text": "I/O ops/sec (iops)",
+ "value": "iops"
+ },
+ {
+ "text": "ops/min (opm)",
+ "value": "opm"
+ },
+ {
+ "text": "reads/min (rpm)",
+ "value": "rpm"
+ },
+ {
+ "text": "writes/min (wpm)",
+ "value": "wpm"
+ }
+ ],
+ "text": "throughput"
+ },
+ {
+ "submenu": [
+ {
+ "text": "millimetre (mm)",
+ "value": "lengthmm"
+ },
+ {
+ "text": "meter (m)",
+ "value": "lengthm"
+ },
+ {
+ "text": "feet (ft)",
+ "value": "lengthft"
+ },
+ {
+ "text": "kilometer (km)",
+ "value": "lengthkm"
+ },
+ {
+ "text": "mile (mi)",
+ "value": "lengthmi"
+ }
+ ],
+ "text": "length"
+ },
+ {
+ "submenu": [
+ {
+ "text": "Square Meters (m²)",
+ "value": "areaM2"
+ },
+ {
+ "text": "Square Feet (ft²)",
+ "value": "areaF2"
+ },
+ {
+ "text": "Square Miles (mi²)",
+ "value": "areaMI2"
+ }
+ ],
+ "text": "area"
+ },
+ {
+ "submenu": [
+ {
+ "text": "milligram (mg)",
+ "value": "massmg"
+ },
+ {
+ "text": "gram (g)",
+ "value": "massg"
+ },
+ {
+ "text": "kilogram (kg)",
+ "value": "masskg"
+ },
+ {
+ "text": "metric ton (t)",
+ "value": "masst"
+ }
+ ],
+ "text": "mass"
+ },
+ {
+ "submenu": [
+ {
+ "text": "metres/second (m/s)",
+ "value": "velocityms"
+ },
+ {
+ "text": "kilometers/hour (km/h)",
+ "value": "velocitykmh"
+ },
+ {
+ "text": "miles/hour (mph)",
+ "value": "velocitymph"
+ },
+ {
+ "text": "knot (kn)",
+ "value": "velocityknot"
+ }
+ ],
+ "text": "velocity"
+ },
+ {
+ "submenu": [
+ {
+ "text": "millilitre (mL)",
+ "value": "mlitre"
+ },
+ {
+ "text": "litre (L)",
+ "value": "litre"
+ },
+ {
+ "text": "cubic metre",
+ "value": "m3"
+ },
+ {
+ "text": "Normal cubic metre",
+ "value": "Nm3"
+ },
+ {
+ "text": "cubic decimetre",
+ "value": "dm3"
+ },
+ {
+ "text": "gallons",
+ "value": "gallons"
+ }
+ ],
+ "text": "volume"
+ },
+ {
+ "submenu": [
+ {
+ "text": "Watt (W)",
+ "value": "watt"
+ },
+ {
+ "text": "Kilowatt (kW)",
+ "value": "kwatt"
+ },
+ {
+ "text": "Milliwatt (mW)",
+ "value": "mwatt"
+ },
+ {
+ "text": "Watt per square metre (W/m²)",
+ "value": "Wm2"
+ },
+ {
+ "text": "Volt-ampere (VA)",
+ "value": "voltamp"
+ },
+ {
+ "text": "Kilovolt-ampere (kVA)",
+ "value": "kvoltamp"
+ },
+ {
+ "text": "Volt-ampere reactive (var)",
+ "value": "voltampreact"
+ },
+ {
+ "text": "Kilovolt-ampere reactive (kvar)",
+ "value": "kvoltampreact"
+ },
+ {
+ "text": "Watt-hour (Wh)",
+ "value": "watth"
+ },
+ {
+ "text": "Kilowatt-hour (kWh)",
+ "value": "kwatth"
+ },
+ {
+ "text": "Kilowatt-min (kWm)",
+ "value": "kwattm"
+ },
+ {
+ "text": "Joule (J)",
+ "value": "joule"
+ },
+ {
+ "text": "Electron volt (eV)",
+ "value": "ev"
+ },
+ {
+ "text": "Ampere (A)",
+ "value": "amp"
+ },
+ {
+ "text": "Kiloampere (kA)",
+ "value": "kamp"
+ },
+ {
+ "text": "Milliampere (mA)",
+ "value": "mamp"
+ },
+ {
+ "text": "Volt (V)",
+ "value": "volt"
+ },
+ {
+ "text": "Kilovolt (kV)",
+ "value": "kvolt"
+ },
+ {
+ "text": "Millivolt (mV)",
+ "value": "mvolt"
+ },
+ {
+ "text": "Decibel-milliwatt (dBm)",
+ "value": "dBm"
+ },
+ {
+ "text": "Ohm (Ω)",
+ "value": "ohm"
+ },
+ {
+ "text": "Lumens (Lm)",
+ "value": "lumens"
+ }
+ ],
+ "text": "energy"
+ },
+ {
+ "submenu": [
+ {
+ "text": "Celsius (°C)",
+ "value": "celsius"
+ },
+ {
+ "text": "Farenheit (°F)",
+ "value": "farenheit"
+ },
+ {
+ "text": "Kelvin (K)",
+ "value": "kelvin"
+ }
+ ],
+ "text": "temperature"
+ },
+ {
+ "submenu": [
+ {
+ "text": "Millibars",
+ "value": "pressurembar"
+ },
+ {
+ "text": "Bars",
+ "value": "pressurebar"
+ },
+ {
+ "text": "Kilobars",
+ "value": "pressurekbar"
+ },
+ {
+ "text": "Hectopascals",
+ "value": "pressurehpa"
+ },
+ {
+ "text": "Kilopascals",
+ "value": "pressurekpa"
+ },
+ {
+ "text": "Inches of mercury",
+ "value": "pressurehg"
+ },
+ {
+ "text": "PSI",
+ "value": "pressurepsi"
+ }
+ ],
+ "text": "pressure"
+ },
+ {
+ "submenu": [
+ {
+ "text": "Newton-meters (Nm)",
+ "value": "forceNm"
+ },
+ {
+ "text": "Kilonewton-meters (kNm)",
+ "value": "forcekNm"
+ },
+ {
+ "text": "Newtons (N)",
+ "value": "forceN"
+ },
+ {
+ "text": "Kilonewtons (kN)",
+ "value": "forcekN"
+ }
+ ],
+ "text": "force"
+ },
+ {
+ "submenu": [
+ {
+ "text": "Gallons/min (gpm)",
+ "value": "flowgpm"
+ },
+ {
+ "text": "Cubic meters/sec (cms)",
+ "value": "flowcms"
+ },
+ {
+ "text": "Cubic feet/sec (cfs)",
+ "value": "flowcfs"
+ },
+ {
+ "text": "Cubic feet/min (cfm)",
+ "value": "flowcfm"
+ },
+ {
+ "text": "Litre/hour",
+ "value": "litreh"
+ },
+ {
+ "text": "Litre/min (l/min)",
+ "value": "flowlpm"
+ },
+ {
+ "text": "milliLitre/min (mL/min)",
+ "value": "flowmlpm"
+ }
+ ],
+ "text": "flow"
+ },
+ {
+ "submenu": [
+ {
+ "text": "Degrees (°)",
+ "value": "degree"
+ },
+ {
+ "text": "Radians",
+ "value": "radian"
+ },
+ {
+ "text": "Gradian",
+ "value": "grad"
+ }
+ ],
+ "text": "angle"
+ },
+ {
+ "submenu": [
+ {
+ "text": "Meters/sec²",
+ "value": "accMS2"
+ },
+ {
+ "text": "Feet/sec²",
+ "value": "accFS2"
+ },
+ {
+ "text": "G unit",
+ "value": "accG"
+ }
+ ],
+ "text": "acceleration"
+ },
+ {
+ "submenu": [
+ {
+ "text": "Becquerel (Bq)",
+ "value": "radbq"
+ },
+ {
+ "text": "curie (Ci)",
+ "value": "radci"
+ },
+ {
+ "text": "Gray (Gy)",
+ "value": "radgy"
+ },
+ {
+ "text": "rad",
+ "value": "radrad"
+ },
+ {
+ "text": "Sievert (Sv)",
+ "value": "radsv"
+ },
+ {
+ "text": "rem",
+ "value": "radrem"
+ },
+ {
+ "text": "Exposure (C/kg)",
+ "value": "radexpckg"
+ },
+ {
+ "text": "roentgen (R)",
+ "value": "radr"
+ },
+ {
+ "text": "Sievert/hour (Sv/h)",
+ "value": "radsvh"
+ }
+ ],
+ "text": "radiation"
+ },
+ {
+ "submenu": [
+ {
+ "text": "parts-per-million (ppm)",
+ "value": "ppm"
+ },
+ {
+ "text": "parts-per-billion (ppb)",
+ "value": "conppb"
+ },
+ {
+ "text": "nanogram per cubic metre (ng/m³)",
+ "value": "conngm3"
+ },
+ {
+ "text": "nanogram per normal cubic metre (ng/Nm³)",
+ "value": "conngNm3"
+ },
+ {
+ "text": "microgram per cubic metre (μg/m³)",
+ "value": "conμgm3"
+ },
+ {
+ "text": "microgram per normal cubic metre (μg/Nm³)",
+ "value": "conμgNm3"
+ },
+ {
+ "text": "milligram per cubic metre (mg/m³)",
+ "value": "conmgm3"
+ },
+ {
+ "text": "milligram per normal cubic metre (mg/Nm³)",
+ "value": "conmgNm3"
+ },
+ {
+ "text": "gram per cubic metre (g/m³)",
+ "value": "congm3"
+ },
+ {
+ "text": "gram per normal cubic metre (g/Nm³)",
+ "value": "congNm3"
+ }
+ ],
+ "text": "concentration"
+ }
+ ]
+ }
+ ],
+ "schemaVersion": 16,
+ "style": "dark",
+ "tags": [
+ "panel-test",
+ "gdev"
+ ],
+ "templating": {
+ "list": []
+ },
+ "time": {
+ "from": "now-6h",
+ "to": "now"
+ },
+ "timepicker": {
+ "refresh_intervals": [
+ "5s",
+ "10s",
+ "30s",
+ "1m",
+ "5m",
+ "15m",
+ "30m",
+ "1h",
+ "2h",
+ "1d"
+ ],
+ "time_options": [
+ "5m",
+ "15m",
+ "1h",
+ "6h",
+ "12h",
+ "24h",
+ "2d",
+ "7d",
+ "30d"
+ ]
+ },
+ "timezone": "",
+ "title": "Panel Tests - Polystat",
+ "uid": "Kp9Z0hTik",
+ "version": 5
+}
diff --git a/docker/blocks/apache_proxy/Dockerfile b/devenv/docker/blocks/apache_proxy/Dockerfile
similarity index 100%
rename from docker/blocks/apache_proxy/Dockerfile
rename to devenv/docker/blocks/apache_proxy/Dockerfile
diff --git a/docker/blocks/apache_proxy/docker-compose.yaml b/devenv/docker/blocks/apache_proxy/docker-compose.yaml
similarity index 88%
rename from docker/blocks/apache_proxy/docker-compose.yaml
rename to devenv/docker/blocks/apache_proxy/docker-compose.yaml
index 86d4befadd6..3791213f05a 100644
--- a/docker/blocks/apache_proxy/docker-compose.yaml
+++ b/devenv/docker/blocks/apache_proxy/docker-compose.yaml
@@ -5,5 +5,5 @@
# root_url = %(protocol)s://%(domain)s:10081/grafana/
apacheproxy:
- build: blocks/apache_proxy
+ build: docker/blocks/apache_proxy
network_mode: host
diff --git a/docker/blocks/apache_proxy/ports.conf b/devenv/docker/blocks/apache_proxy/ports.conf
similarity index 100%
rename from docker/blocks/apache_proxy/ports.conf
rename to devenv/docker/blocks/apache_proxy/ports.conf
diff --git a/docker/blocks/apache_proxy/proxy.conf b/devenv/docker/blocks/apache_proxy/proxy.conf
similarity index 100%
rename from docker/blocks/apache_proxy/proxy.conf
rename to devenv/docker/blocks/apache_proxy/proxy.conf
diff --git a/docker/blocks/collectd/Dockerfile b/devenv/docker/blocks/collectd/Dockerfile
similarity index 100%
rename from docker/blocks/collectd/Dockerfile
rename to devenv/docker/blocks/collectd/Dockerfile
diff --git a/docker/blocks/collectd/README.md b/devenv/docker/blocks/collectd/README.md
similarity index 100%
rename from docker/blocks/collectd/README.md
rename to devenv/docker/blocks/collectd/README.md
diff --git a/docker/blocks/collectd/collectd.conf.tpl b/devenv/docker/blocks/collectd/collectd.conf.tpl
similarity index 100%
rename from docker/blocks/collectd/collectd.conf.tpl
rename to devenv/docker/blocks/collectd/collectd.conf.tpl
diff --git a/docker/blocks/collectd/docker-compose.yaml b/devenv/docker/blocks/collectd/docker-compose.yaml
similarity index 87%
rename from docker/blocks/collectd/docker-compose.yaml
rename to devenv/docker/blocks/collectd/docker-compose.yaml
index c95827f7928..c5e189b58d8 100644
--- a/docker/blocks/collectd/docker-compose.yaml
+++ b/devenv/docker/blocks/collectd/docker-compose.yaml
@@ -1,5 +1,5 @@
collectd:
- build: blocks/collectd
+ build: docker/blocks/collectd
environment:
HOST_NAME: myserver
GRAPHITE_HOST: graphite
diff --git a/docker/blocks/collectd/etc_mtab b/devenv/docker/blocks/collectd/etc_mtab
similarity index 100%
rename from docker/blocks/collectd/etc_mtab
rename to devenv/docker/blocks/collectd/etc_mtab
diff --git a/docker/blocks/collectd/start_container b/devenv/docker/blocks/collectd/start_container
similarity index 100%
rename from docker/blocks/collectd/start_container
rename to devenv/docker/blocks/collectd/start_container
diff --git a/docker/blocks/elastic/docker-compose.yaml b/devenv/docker/blocks/elastic/docker-compose.yaml
similarity index 100%
rename from docker/blocks/elastic/docker-compose.yaml
rename to devenv/docker/blocks/elastic/docker-compose.yaml
diff --git a/docker/blocks/elastic/elasticsearch.yml b/devenv/docker/blocks/elastic/elasticsearch.yml
similarity index 100%
rename from docker/blocks/elastic/elasticsearch.yml
rename to devenv/docker/blocks/elastic/elasticsearch.yml
diff --git a/docker/blocks/elastic1/docker-compose.yaml b/devenv/docker/blocks/elastic1/docker-compose.yaml
similarity index 100%
rename from docker/blocks/elastic1/docker-compose.yaml
rename to devenv/docker/blocks/elastic1/docker-compose.yaml
diff --git a/docker/blocks/elastic1/elasticsearch.yml b/devenv/docker/blocks/elastic1/elasticsearch.yml
similarity index 100%
rename from docker/blocks/elastic1/elasticsearch.yml
rename to devenv/docker/blocks/elastic1/elasticsearch.yml
diff --git a/docker/blocks/elastic5/docker-compose.yaml b/devenv/docker/blocks/elastic5/docker-compose.yaml
similarity index 100%
rename from docker/blocks/elastic5/docker-compose.yaml
rename to devenv/docker/blocks/elastic5/docker-compose.yaml
diff --git a/docker/blocks/elastic5/elasticsearch.yml b/devenv/docker/blocks/elastic5/elasticsearch.yml
similarity index 100%
rename from docker/blocks/elastic5/elasticsearch.yml
rename to devenv/docker/blocks/elastic5/elasticsearch.yml
diff --git a/docker/blocks/elastic6/docker-compose.yaml b/devenv/docker/blocks/elastic6/docker-compose.yaml
similarity index 100%
rename from docker/blocks/elastic6/docker-compose.yaml
rename to devenv/docker/blocks/elastic6/docker-compose.yaml
diff --git a/docker/blocks/elastic6/elasticsearch.yml b/devenv/docker/blocks/elastic6/elasticsearch.yml
similarity index 100%
rename from docker/blocks/elastic6/elasticsearch.yml
rename to devenv/docker/blocks/elastic6/elasticsearch.yml
diff --git a/docker/blocks/graphite/Dockerfile b/devenv/docker/blocks/graphite/Dockerfile
similarity index 100%
rename from docker/blocks/graphite/Dockerfile
rename to devenv/docker/blocks/graphite/Dockerfile
diff --git a/docker/blocks/graphite/docker-compose.yaml b/devenv/docker/blocks/graphite/docker-compose.yaml
similarity index 89%
rename from docker/blocks/graphite/docker-compose.yaml
rename to devenv/docker/blocks/graphite/docker-compose.yaml
index 606e28638f7..acebd2bd9c0 100644
--- a/docker/blocks/graphite/docker-compose.yaml
+++ b/devenv/docker/blocks/graphite/docker-compose.yaml
@@ -1,5 +1,5 @@
graphite09:
- build: blocks/graphite
+ build: docker/blocks/graphite
ports:
- "8080:80"
- "2003:2003"
diff --git a/docker/blocks/graphite/files/carbon.conf b/devenv/docker/blocks/graphite/files/carbon.conf
similarity index 100%
rename from docker/blocks/graphite/files/carbon.conf
rename to devenv/docker/blocks/graphite/files/carbon.conf
diff --git a/docker/blocks/graphite/files/events_views.py b/devenv/docker/blocks/graphite/files/events_views.py
similarity index 100%
rename from docker/blocks/graphite/files/events_views.py
rename to devenv/docker/blocks/graphite/files/events_views.py
diff --git a/docker/blocks/graphite/files/initial_data.json b/devenv/docker/blocks/graphite/files/initial_data.json
similarity index 100%
rename from docker/blocks/graphite/files/initial_data.json
rename to devenv/docker/blocks/graphite/files/initial_data.json
diff --git a/docker/blocks/graphite/files/local_settings.py b/devenv/docker/blocks/graphite/files/local_settings.py
similarity index 100%
rename from docker/blocks/graphite/files/local_settings.py
rename to devenv/docker/blocks/graphite/files/local_settings.py
diff --git a/docker/blocks/graphite/files/my_htpasswd b/devenv/docker/blocks/graphite/files/my_htpasswd
similarity index 100%
rename from docker/blocks/graphite/files/my_htpasswd
rename to devenv/docker/blocks/graphite/files/my_htpasswd
diff --git a/docker/blocks/graphite/files/nginx.conf b/devenv/docker/blocks/graphite/files/nginx.conf
similarity index 100%
rename from docker/blocks/graphite/files/nginx.conf
rename to devenv/docker/blocks/graphite/files/nginx.conf
diff --git a/docker/blocks/graphite/files/statsd_config.js b/devenv/docker/blocks/graphite/files/statsd_config.js
similarity index 100%
rename from docker/blocks/graphite/files/statsd_config.js
rename to devenv/docker/blocks/graphite/files/statsd_config.js
diff --git a/docker/blocks/graphite/files/storage-aggregation.conf b/devenv/docker/blocks/graphite/files/storage-aggregation.conf
similarity index 100%
rename from docker/blocks/graphite/files/storage-aggregation.conf
rename to devenv/docker/blocks/graphite/files/storage-aggregation.conf
diff --git a/docker/blocks/graphite/files/storage-schemas.conf b/devenv/docker/blocks/graphite/files/storage-schemas.conf
similarity index 100%
rename from docker/blocks/graphite/files/storage-schemas.conf
rename to devenv/docker/blocks/graphite/files/storage-schemas.conf
diff --git a/docker/blocks/graphite/files/supervisord.conf b/devenv/docker/blocks/graphite/files/supervisord.conf
similarity index 100%
rename from docker/blocks/graphite/files/supervisord.conf
rename to devenv/docker/blocks/graphite/files/supervisord.conf
diff --git a/docker/blocks/graphite1/Dockerfile b/devenv/docker/blocks/graphite1/Dockerfile
similarity index 100%
rename from docker/blocks/graphite1/Dockerfile
rename to devenv/docker/blocks/graphite1/Dockerfile
diff --git a/docker/blocks/graphite1/big-dashboard.json b/devenv/docker/blocks/graphite1/big-dashboard.json
similarity index 100%
rename from docker/blocks/graphite1/big-dashboard.json
rename to devenv/docker/blocks/graphite1/big-dashboard.json
diff --git a/docker/blocks/graphite1/conf/etc/logrotate.d/graphite-statsd b/devenv/docker/blocks/graphite1/conf/etc/logrotate.d/graphite-statsd
similarity index 100%
rename from docker/blocks/graphite1/conf/etc/logrotate.d/graphite-statsd
rename to devenv/docker/blocks/graphite1/conf/etc/logrotate.d/graphite-statsd
diff --git a/docker/blocks/graphite1/conf/etc/my_init.d/01_conf_init.sh b/devenv/docker/blocks/graphite1/conf/etc/my_init.d/01_conf_init.sh
similarity index 100%
rename from docker/blocks/graphite1/conf/etc/my_init.d/01_conf_init.sh
rename to devenv/docker/blocks/graphite1/conf/etc/my_init.d/01_conf_init.sh
diff --git a/docker/blocks/graphite1/conf/etc/nginx/nginx.conf b/devenv/docker/blocks/graphite1/conf/etc/nginx/nginx.conf
similarity index 100%
rename from docker/blocks/graphite1/conf/etc/nginx/nginx.conf
rename to devenv/docker/blocks/graphite1/conf/etc/nginx/nginx.conf
diff --git a/docker/blocks/graphite1/conf/etc/nginx/sites-enabled/graphite-statsd.conf b/devenv/docker/blocks/graphite1/conf/etc/nginx/sites-enabled/graphite-statsd.conf
similarity index 100%
rename from docker/blocks/graphite1/conf/etc/nginx/sites-enabled/graphite-statsd.conf
rename to devenv/docker/blocks/graphite1/conf/etc/nginx/sites-enabled/graphite-statsd.conf
diff --git a/docker/blocks/graphite1/conf/etc/service/carbon-aggregator/run b/devenv/docker/blocks/graphite1/conf/etc/service/carbon-aggregator/run
similarity index 100%
rename from docker/blocks/graphite1/conf/etc/service/carbon-aggregator/run
rename to devenv/docker/blocks/graphite1/conf/etc/service/carbon-aggregator/run
diff --git a/docker/blocks/graphite1/conf/etc/service/carbon/run b/devenv/docker/blocks/graphite1/conf/etc/service/carbon/run
similarity index 100%
rename from docker/blocks/graphite1/conf/etc/service/carbon/run
rename to devenv/docker/blocks/graphite1/conf/etc/service/carbon/run
diff --git a/docker/blocks/graphite1/conf/etc/service/graphite/run b/devenv/docker/blocks/graphite1/conf/etc/service/graphite/run
similarity index 100%
rename from docker/blocks/graphite1/conf/etc/service/graphite/run
rename to devenv/docker/blocks/graphite1/conf/etc/service/graphite/run
diff --git a/docker/blocks/graphite1/conf/etc/service/nginx/run b/devenv/docker/blocks/graphite1/conf/etc/service/nginx/run
similarity index 100%
rename from docker/blocks/graphite1/conf/etc/service/nginx/run
rename to devenv/docker/blocks/graphite1/conf/etc/service/nginx/run
diff --git a/docker/blocks/graphite1/conf/etc/service/statsd/run b/devenv/docker/blocks/graphite1/conf/etc/service/statsd/run
similarity index 100%
rename from docker/blocks/graphite1/conf/etc/service/statsd/run
rename to devenv/docker/blocks/graphite1/conf/etc/service/statsd/run
diff --git a/docker/blocks/graphite1/conf/opt/graphite/conf/aggregation-rules.conf b/devenv/docker/blocks/graphite1/conf/opt/graphite/conf/aggregation-rules.conf
similarity index 100%
rename from docker/blocks/graphite1/conf/opt/graphite/conf/aggregation-rules.conf
rename to devenv/docker/blocks/graphite1/conf/opt/graphite/conf/aggregation-rules.conf
diff --git a/docker/blocks/graphite1/conf/opt/graphite/conf/blacklist.conf b/devenv/docker/blocks/graphite1/conf/opt/graphite/conf/blacklist.conf
similarity index 100%
rename from docker/blocks/graphite1/conf/opt/graphite/conf/blacklist.conf
rename to devenv/docker/blocks/graphite1/conf/opt/graphite/conf/blacklist.conf
diff --git a/docker/blocks/graphite1/conf/opt/graphite/conf/carbon.amqp.conf b/devenv/docker/blocks/graphite1/conf/opt/graphite/conf/carbon.amqp.conf
similarity index 100%
rename from docker/blocks/graphite1/conf/opt/graphite/conf/carbon.amqp.conf
rename to devenv/docker/blocks/graphite1/conf/opt/graphite/conf/carbon.amqp.conf
diff --git a/docker/blocks/graphite1/conf/opt/graphite/conf/carbon.conf b/devenv/docker/blocks/graphite1/conf/opt/graphite/conf/carbon.conf
similarity index 100%
rename from docker/blocks/graphite1/conf/opt/graphite/conf/carbon.conf
rename to devenv/docker/blocks/graphite1/conf/opt/graphite/conf/carbon.conf
diff --git a/docker/blocks/graphite1/conf/opt/graphite/conf/dashboard.conf b/devenv/docker/blocks/graphite1/conf/opt/graphite/conf/dashboard.conf
similarity index 100%
rename from docker/blocks/graphite1/conf/opt/graphite/conf/dashboard.conf
rename to devenv/docker/blocks/graphite1/conf/opt/graphite/conf/dashboard.conf
diff --git a/docker/blocks/graphite1/conf/opt/graphite/conf/graphTemplates.conf b/devenv/docker/blocks/graphite1/conf/opt/graphite/conf/graphTemplates.conf
similarity index 100%
rename from docker/blocks/graphite1/conf/opt/graphite/conf/graphTemplates.conf
rename to devenv/docker/blocks/graphite1/conf/opt/graphite/conf/graphTemplates.conf
diff --git a/docker/blocks/graphite1/conf/opt/graphite/conf/relay-rules.conf b/devenv/docker/blocks/graphite1/conf/opt/graphite/conf/relay-rules.conf
similarity index 100%
rename from docker/blocks/graphite1/conf/opt/graphite/conf/relay-rules.conf
rename to devenv/docker/blocks/graphite1/conf/opt/graphite/conf/relay-rules.conf
diff --git a/docker/blocks/graphite1/conf/opt/graphite/conf/rewrite-rules.conf b/devenv/docker/blocks/graphite1/conf/opt/graphite/conf/rewrite-rules.conf
similarity index 100%
rename from docker/blocks/graphite1/conf/opt/graphite/conf/rewrite-rules.conf
rename to devenv/docker/blocks/graphite1/conf/opt/graphite/conf/rewrite-rules.conf
diff --git a/docker/blocks/graphite1/conf/opt/graphite/conf/storage-aggregation.conf b/devenv/docker/blocks/graphite1/conf/opt/graphite/conf/storage-aggregation.conf
similarity index 100%
rename from docker/blocks/graphite1/conf/opt/graphite/conf/storage-aggregation.conf
rename to devenv/docker/blocks/graphite1/conf/opt/graphite/conf/storage-aggregation.conf
diff --git a/docker/blocks/graphite1/conf/opt/graphite/conf/storage-schemas.conf b/devenv/docker/blocks/graphite1/conf/opt/graphite/conf/storage-schemas.conf
similarity index 100%
rename from docker/blocks/graphite1/conf/opt/graphite/conf/storage-schemas.conf
rename to devenv/docker/blocks/graphite1/conf/opt/graphite/conf/storage-schemas.conf
diff --git a/docker/blocks/graphite1/conf/opt/graphite/conf/whitelist.conf b/devenv/docker/blocks/graphite1/conf/opt/graphite/conf/whitelist.conf
similarity index 100%
rename from docker/blocks/graphite1/conf/opt/graphite/conf/whitelist.conf
rename to devenv/docker/blocks/graphite1/conf/opt/graphite/conf/whitelist.conf
diff --git a/docker/blocks/graphite1/conf/opt/graphite/webapp/graphite/app_settings.py b/devenv/docker/blocks/graphite1/conf/opt/graphite/webapp/graphite/app_settings.py
similarity index 100%
rename from docker/blocks/graphite1/conf/opt/graphite/webapp/graphite/app_settings.py
rename to devenv/docker/blocks/graphite1/conf/opt/graphite/webapp/graphite/app_settings.py
diff --git a/docker/blocks/graphite1/conf/opt/graphite/webapp/graphite/local_settings.py b/devenv/docker/blocks/graphite1/conf/opt/graphite/webapp/graphite/local_settings.py
similarity index 100%
rename from docker/blocks/graphite1/conf/opt/graphite/webapp/graphite/local_settings.py
rename to devenv/docker/blocks/graphite1/conf/opt/graphite/webapp/graphite/local_settings.py
diff --git a/docker/blocks/graphite1/conf/opt/statsd/config.js b/devenv/docker/blocks/graphite1/conf/opt/statsd/config.js
similarity index 100%
rename from docker/blocks/graphite1/conf/opt/statsd/config.js
rename to devenv/docker/blocks/graphite1/conf/opt/statsd/config.js
diff --git a/docker/blocks/graphite1/conf/usr/local/bin/django_admin_init.exp b/devenv/docker/blocks/graphite1/conf/usr/local/bin/django_admin_init.exp
similarity index 100%
rename from docker/blocks/graphite1/conf/usr/local/bin/django_admin_init.exp
rename to devenv/docker/blocks/graphite1/conf/usr/local/bin/django_admin_init.exp
diff --git a/docker/blocks/graphite1/conf/usr/local/bin/manage.sh b/devenv/docker/blocks/graphite1/conf/usr/local/bin/manage.sh
similarity index 100%
rename from docker/blocks/graphite1/conf/usr/local/bin/manage.sh
rename to devenv/docker/blocks/graphite1/conf/usr/local/bin/manage.sh
diff --git a/docker/blocks/graphite1/docker-compose.yaml b/devenv/docker/blocks/graphite1/docker-compose.yaml
similarity index 90%
rename from docker/blocks/graphite1/docker-compose.yaml
rename to devenv/docker/blocks/graphite1/docker-compose.yaml
index cd10593f423..1fa3e738ba8 100644
--- a/docker/blocks/graphite1/docker-compose.yaml
+++ b/devenv/docker/blocks/graphite1/docker-compose.yaml
@@ -1,6 +1,6 @@
graphite:
build:
- context: blocks/graphite1
+ context: docker/blocks/graphite1
args:
version: master
ports:
diff --git a/docker/blocks/graphite11/big-dashboard.json b/devenv/docker/blocks/graphite11/big-dashboard.json
similarity index 100%
rename from docker/blocks/graphite11/big-dashboard.json
rename to devenv/docker/blocks/graphite11/big-dashboard.json
diff --git a/docker/blocks/graphite11/docker-compose.yaml b/devenv/docker/blocks/graphite11/docker-compose.yaml
similarity index 100%
rename from docker/blocks/graphite11/docker-compose.yaml
rename to devenv/docker/blocks/graphite11/docker-compose.yaml
diff --git a/docker/blocks/influxdb/docker-compose.yaml b/devenv/docker/blocks/influxdb/docker-compose.yaml
similarity index 100%
rename from docker/blocks/influxdb/docker-compose.yaml
rename to devenv/docker/blocks/influxdb/docker-compose.yaml
diff --git a/docker/blocks/influxdb/influxdb.conf b/devenv/docker/blocks/influxdb/influxdb.conf
similarity index 100%
rename from docker/blocks/influxdb/influxdb.conf
rename to devenv/docker/blocks/influxdb/influxdb.conf
diff --git a/docker/blocks/jaeger/docker-compose.yaml b/devenv/docker/blocks/jaeger/docker-compose.yaml
similarity index 100%
rename from docker/blocks/jaeger/docker-compose.yaml
rename to devenv/docker/blocks/jaeger/docker-compose.yaml
diff --git a/docker/blocks/memcached/docker-compose.yaml b/devenv/docker/blocks/memcached/docker-compose.yaml
similarity index 100%
rename from docker/blocks/memcached/docker-compose.yaml
rename to devenv/docker/blocks/memcached/docker-compose.yaml
diff --git a/docker/blocks/mssql/build/Dockerfile b/devenv/docker/blocks/mssql/build/Dockerfile
similarity index 100%
rename from docker/blocks/mssql/build/Dockerfile
rename to devenv/docker/blocks/mssql/build/Dockerfile
diff --git a/docker/blocks/mssql/build/entrypoint.sh b/devenv/docker/blocks/mssql/build/entrypoint.sh
similarity index 100%
rename from docker/blocks/mssql/build/entrypoint.sh
rename to devenv/docker/blocks/mssql/build/entrypoint.sh
diff --git a/docker/blocks/mssql/build/setup.sh b/devenv/docker/blocks/mssql/build/setup.sh
similarity index 100%
rename from docker/blocks/mssql/build/setup.sh
rename to devenv/docker/blocks/mssql/build/setup.sh
diff --git a/docker/blocks/mssql/build/setup.sql.template b/devenv/docker/blocks/mssql/build/setup.sql.template
similarity index 100%
rename from docker/blocks/mssql/build/setup.sql.template
rename to devenv/docker/blocks/mssql/build/setup.sql.template
diff --git a/docker/blocks/mssql/docker-compose.yaml b/devenv/docker/blocks/mssql/docker-compose.yaml
similarity index 90%
rename from docker/blocks/mssql/docker-compose.yaml
rename to devenv/docker/blocks/mssql/docker-compose.yaml
index a346fb791f7..05a93629e73 100644
--- a/docker/blocks/mssql/docker-compose.yaml
+++ b/devenv/docker/blocks/mssql/docker-compose.yaml
@@ -1,6 +1,6 @@
mssql:
build:
- context: blocks/mssql/build
+ context: docker/blocks/mssql/build
environment:
ACCEPT_EULA: Y
MSSQL_SA_PASSWORD: Password!
diff --git a/docker/blocks/mssql_tests/docker-compose.yaml b/devenv/docker/blocks/mssql_tests/docker-compose.yaml
similarity index 85%
rename from docker/blocks/mssql_tests/docker-compose.yaml
rename to devenv/docker/blocks/mssql_tests/docker-compose.yaml
index 5da6aad82af..eea4d1e3561 100644
--- a/docker/blocks/mssql_tests/docker-compose.yaml
+++ b/devenv/docker/blocks/mssql_tests/docker-compose.yaml
@@ -1,6 +1,6 @@
mssqltests:
build:
- context: blocks/mssql/build
+ context: docker/blocks/mssql/build
environment:
ACCEPT_EULA: Y
MSSQL_SA_PASSWORD: Password!
diff --git a/docker/blocks/mysql/config b/devenv/docker/blocks/mysql/config
similarity index 100%
rename from docker/blocks/mysql/config
rename to devenv/docker/blocks/mysql/config
diff --git a/docker/blocks/mysql/docker-compose.yaml b/devenv/docker/blocks/mysql/docker-compose.yaml
similarity index 100%
rename from docker/blocks/mysql/docker-compose.yaml
rename to devenv/docker/blocks/mysql/docker-compose.yaml
diff --git a/docker/blocks/mysql_opendata/Dockerfile b/devenv/docker/blocks/mysql_opendata/Dockerfile
similarity index 100%
rename from docker/blocks/mysql_opendata/Dockerfile
rename to devenv/docker/blocks/mysql_opendata/Dockerfile
diff --git a/docker/blocks/mysql_opendata/docker-compose.yaml b/devenv/docker/blocks/mysql_opendata/docker-compose.yaml
similarity index 82%
rename from docker/blocks/mysql_opendata/docker-compose.yaml
rename to devenv/docker/blocks/mysql_opendata/docker-compose.yaml
index 594eeed284a..4d478ee0860 100644
--- a/docker/blocks/mysql_opendata/docker-compose.yaml
+++ b/devenv/docker/blocks/mysql_opendata/docker-compose.yaml
@@ -1,5 +1,5 @@
mysql_opendata:
- build: blocks/mysql_opendata
+ build: docker/blocks/mysql_opendata
environment:
MYSQL_ROOT_PASSWORD: rootpass
MYSQL_DATABASE: testdata
diff --git a/docker/blocks/mysql_opendata/import_csv.sql b/devenv/docker/blocks/mysql_opendata/import_csv.sql
similarity index 100%
rename from docker/blocks/mysql_opendata/import_csv.sql
rename to devenv/docker/blocks/mysql_opendata/import_csv.sql
diff --git a/docker/blocks/mysql_tests/Dockerfile b/devenv/docker/blocks/mysql_tests/Dockerfile
similarity index 100%
rename from docker/blocks/mysql_tests/Dockerfile
rename to devenv/docker/blocks/mysql_tests/Dockerfile
diff --git a/docker/blocks/mysql_tests/docker-compose.yaml b/devenv/docker/blocks/mysql_tests/docker-compose.yaml
similarity index 84%
rename from docker/blocks/mysql_tests/docker-compose.yaml
rename to devenv/docker/blocks/mysql_tests/docker-compose.yaml
index 035a6167017..a7509d47880 100644
--- a/docker/blocks/mysql_tests/docker-compose.yaml
+++ b/devenv/docker/blocks/mysql_tests/docker-compose.yaml
@@ -1,6 +1,6 @@
mysqltests:
build:
- context: blocks/mysql_tests
+ context: docker/blocks/mysql_tests
environment:
MYSQL_ROOT_PASSWORD: rootpass
MYSQL_DATABASE: grafana_tests
diff --git a/docker/blocks/mysql_tests/setup.sql b/devenv/docker/blocks/mysql_tests/setup.sql
similarity index 100%
rename from docker/blocks/mysql_tests/setup.sql
rename to devenv/docker/blocks/mysql_tests/setup.sql
diff --git a/docker/blocks/nginx_proxy/Dockerfile b/devenv/docker/blocks/nginx_proxy/Dockerfile
similarity index 100%
rename from docker/blocks/nginx_proxy/Dockerfile
rename to devenv/docker/blocks/nginx_proxy/Dockerfile
diff --git a/docker/blocks/nginx_proxy/docker-compose.yaml b/devenv/docker/blocks/nginx_proxy/docker-compose.yaml
similarity index 88%
rename from docker/blocks/nginx_proxy/docker-compose.yaml
rename to devenv/docker/blocks/nginx_proxy/docker-compose.yaml
index a0ceceb83ac..aefd7226f36 100644
--- a/docker/blocks/nginx_proxy/docker-compose.yaml
+++ b/devenv/docker/blocks/nginx_proxy/docker-compose.yaml
@@ -5,5 +5,5 @@
# root_url = %(protocol)s://%(domain)s:10080/grafana/
nginxproxy:
- build: blocks/nginx_proxy
+ build: docker/blocks/nginx_proxy
network_mode: host
diff --git a/docker/blocks/nginx_proxy/htpasswd b/devenv/docker/blocks/nginx_proxy/htpasswd
similarity index 100%
rename from docker/blocks/nginx_proxy/htpasswd
rename to devenv/docker/blocks/nginx_proxy/htpasswd
diff --git a/docker/blocks/nginx_proxy/nginx.conf b/devenv/docker/blocks/nginx_proxy/nginx.conf
similarity index 100%
rename from docker/blocks/nginx_proxy/nginx.conf
rename to devenv/docker/blocks/nginx_proxy/nginx.conf
diff --git a/docker/blocks/openldap/Dockerfile b/devenv/docker/blocks/openldap/Dockerfile
similarity index 100%
rename from docker/blocks/openldap/Dockerfile
rename to devenv/docker/blocks/openldap/Dockerfile
diff --git a/docker/blocks/openldap/docker-compose.yaml b/devenv/docker/blocks/openldap/docker-compose.yaml
similarity index 82%
rename from docker/blocks/openldap/docker-compose.yaml
rename to devenv/docker/blocks/openldap/docker-compose.yaml
index be06524a57d..d11858ccfb9 100644
--- a/docker/blocks/openldap/docker-compose.yaml
+++ b/devenv/docker/blocks/openldap/docker-compose.yaml
@@ -1,5 +1,5 @@
openldap:
- build: blocks/openldap
+ build: docker/blocks/openldap
environment:
SLAPD_PASSWORD: grafana
SLAPD_DOMAIN: grafana.org
diff --git a/docker/blocks/openldap/entrypoint.sh b/devenv/docker/blocks/openldap/entrypoint.sh
similarity index 100%
rename from docker/blocks/openldap/entrypoint.sh
rename to devenv/docker/blocks/openldap/entrypoint.sh
diff --git a/docker/blocks/openldap/ldap_dev.toml b/devenv/docker/blocks/openldap/ldap_dev.toml
similarity index 99%
rename from docker/blocks/openldap/ldap_dev.toml
rename to devenv/docker/blocks/openldap/ldap_dev.toml
index e79771b57de..8767ff3c64a 100644
--- a/docker/blocks/openldap/ldap_dev.toml
+++ b/devenv/docker/blocks/openldap/ldap_dev.toml
@@ -72,6 +72,7 @@ email = "email"
[[servers.group_mappings]]
group_dn = "cn=admins,ou=groups,dc=grafana,dc=org"
org_role = "Admin"
+grafana_admin = true
# The Grafana organization database id, optional, if left out the default org (id 1) will be used
# org_id = 1
diff --git a/docker/blocks/openldap/modules/memberof.ldif b/devenv/docker/blocks/openldap/modules/memberof.ldif
similarity index 100%
rename from docker/blocks/openldap/modules/memberof.ldif
rename to devenv/docker/blocks/openldap/modules/memberof.ldif
diff --git a/docker/blocks/openldap/notes.md b/devenv/docker/blocks/openldap/notes.md
similarity index 100%
rename from docker/blocks/openldap/notes.md
rename to devenv/docker/blocks/openldap/notes.md
diff --git a/docker/blocks/openldap/prepopulate.sh b/devenv/docker/blocks/openldap/prepopulate.sh
similarity index 100%
rename from docker/blocks/openldap/prepopulate.sh
rename to devenv/docker/blocks/openldap/prepopulate.sh
diff --git a/docker/blocks/openldap/prepopulate/1_units.ldif b/devenv/docker/blocks/openldap/prepopulate/1_units.ldif
similarity index 100%
rename from docker/blocks/openldap/prepopulate/1_units.ldif
rename to devenv/docker/blocks/openldap/prepopulate/1_units.ldif
diff --git a/docker/blocks/openldap/prepopulate/2_users.ldif b/devenv/docker/blocks/openldap/prepopulate/2_users.ldif
similarity index 100%
rename from docker/blocks/openldap/prepopulate/2_users.ldif
rename to devenv/docker/blocks/openldap/prepopulate/2_users.ldif
diff --git a/docker/blocks/openldap/prepopulate/3_groups.ldif b/devenv/docker/blocks/openldap/prepopulate/3_groups.ldif
similarity index 100%
rename from docker/blocks/openldap/prepopulate/3_groups.ldif
rename to devenv/docker/blocks/openldap/prepopulate/3_groups.ldif
diff --git a/docker/blocks/opentsdb/docker-compose.yaml b/devenv/docker/blocks/opentsdb/docker-compose.yaml
similarity index 100%
rename from docker/blocks/opentsdb/docker-compose.yaml
rename to devenv/docker/blocks/opentsdb/docker-compose.yaml
diff --git a/docker/blocks/postgres/docker-compose.yaml b/devenv/docker/blocks/postgres/docker-compose.yaml
similarity index 100%
rename from docker/blocks/postgres/docker-compose.yaml
rename to devenv/docker/blocks/postgres/docker-compose.yaml
diff --git a/docker/blocks/postgres_tests/Dockerfile b/devenv/docker/blocks/postgres_tests/Dockerfile
similarity index 100%
rename from docker/blocks/postgres_tests/Dockerfile
rename to devenv/docker/blocks/postgres_tests/Dockerfile
diff --git a/docker/blocks/postgres_tests/docker-compose.yaml b/devenv/docker/blocks/postgres_tests/docker-compose.yaml
similarity index 80%
rename from docker/blocks/postgres_tests/docker-compose.yaml
rename to devenv/docker/blocks/postgres_tests/docker-compose.yaml
index f5ce0a5a3d3..7e6da7d8517 100644
--- a/docker/blocks/postgres_tests/docker-compose.yaml
+++ b/devenv/docker/blocks/postgres_tests/docker-compose.yaml
@@ -1,6 +1,6 @@
postgrestest:
build:
- context: blocks/postgres_tests
+ context: docker/blocks/postgres_tests
environment:
POSTGRES_USER: grafanatest
POSTGRES_PASSWORD: grafanatest
diff --git a/docker/blocks/postgres_tests/setup.sql b/devenv/docker/blocks/postgres_tests/setup.sql
similarity index 100%
rename from docker/blocks/postgres_tests/setup.sql
rename to devenv/docker/blocks/postgres_tests/setup.sql
diff --git a/docker/blocks/prometheus/Dockerfile b/devenv/docker/blocks/prometheus/Dockerfile
similarity index 100%
rename from docker/blocks/prometheus/Dockerfile
rename to devenv/docker/blocks/prometheus/Dockerfile
diff --git a/docker/blocks/prometheus/alert.rules b/devenv/docker/blocks/prometheus/alert.rules
similarity index 100%
rename from docker/blocks/prometheus/alert.rules
rename to devenv/docker/blocks/prometheus/alert.rules
diff --git a/docker/blocks/prometheus2/docker-compose.yaml b/devenv/docker/blocks/prometheus/docker-compose.yaml
similarity index 86%
rename from docker/blocks/prometheus2/docker-compose.yaml
rename to devenv/docker/blocks/prometheus/docker-compose.yaml
index 589df868084..db778060dde 100644
--- a/docker/blocks/prometheus2/docker-compose.yaml
+++ b/devenv/docker/blocks/prometheus/docker-compose.yaml
@@ -1,5 +1,5 @@
prometheus:
- build: blocks/prometheus2
+ build: docker/blocks/prometheus
network_mode: host
ports:
- "9090:9090"
@@ -25,7 +25,7 @@
- "9093:9093"
prometheus-random-data:
- build: blocks/prometheus_random_data
+ build: docker/blocks/prometheus_random_data
network_mode: host
ports:
- "8081:8080"
diff --git a/docker/blocks/prometheus/prometheus.yml b/devenv/docker/blocks/prometheus/prometheus.yml
similarity index 100%
rename from docker/blocks/prometheus/prometheus.yml
rename to devenv/docker/blocks/prometheus/prometheus.yml
diff --git a/docker/blocks/prometheus2/Dockerfile b/devenv/docker/blocks/prometheus2/Dockerfile
similarity index 100%
rename from docker/blocks/prometheus2/Dockerfile
rename to devenv/docker/blocks/prometheus2/Dockerfile
diff --git a/docker/blocks/prometheus2/alert.rules b/devenv/docker/blocks/prometheus2/alert.rules
similarity index 100%
rename from docker/blocks/prometheus2/alert.rules
rename to devenv/docker/blocks/prometheus2/alert.rules
diff --git a/docker/blocks/prometheus/docker-compose.yaml b/devenv/docker/blocks/prometheus2/docker-compose.yaml
similarity index 85%
rename from docker/blocks/prometheus/docker-compose.yaml
rename to devenv/docker/blocks/prometheus2/docker-compose.yaml
index 3c304cc74ad..d586b4b5742 100644
--- a/docker/blocks/prometheus/docker-compose.yaml
+++ b/devenv/docker/blocks/prometheus2/docker-compose.yaml
@@ -1,5 +1,5 @@
prometheus:
- build: blocks/prometheus
+ build: docker/blocks/prometheus2
network_mode: host
ports:
- "9090:9090"
@@ -25,7 +25,7 @@
- "9093:9093"
prometheus-random-data:
- build: blocks/prometheus_random_data
+ build: docker/blocks/prometheus_random_data
network_mode: host
ports:
- "8081:8080"
diff --git a/docker/blocks/prometheus2/prometheus.yml b/devenv/docker/blocks/prometheus2/prometheus.yml
similarity index 100%
rename from docker/blocks/prometheus2/prometheus.yml
rename to devenv/docker/blocks/prometheus2/prometheus.yml
diff --git a/docker/blocks/prometheus_mac/Dockerfile b/devenv/docker/blocks/prometheus_mac/Dockerfile
similarity index 100%
rename from docker/blocks/prometheus_mac/Dockerfile
rename to devenv/docker/blocks/prometheus_mac/Dockerfile
diff --git a/docker/blocks/prometheus_mac/alert.rules b/devenv/docker/blocks/prometheus_mac/alert.rules
similarity index 100%
rename from docker/blocks/prometheus_mac/alert.rules
rename to devenv/docker/blocks/prometheus_mac/alert.rules
diff --git a/docker/blocks/prometheus_mac/docker-compose.yaml b/devenv/docker/blocks/prometheus_mac/docker-compose.yaml
similarity index 82%
rename from docker/blocks/prometheus_mac/docker-compose.yaml
rename to devenv/docker/blocks/prometheus_mac/docker-compose.yaml
index ef53b07418a..b73d278fae2 100644
--- a/docker/blocks/prometheus_mac/docker-compose.yaml
+++ b/devenv/docker/blocks/prometheus_mac/docker-compose.yaml
@@ -1,5 +1,5 @@
prometheus:
- build: blocks/prometheus_mac
+ build: docker/blocks/prometheus_mac
ports:
- "9090:9090"
@@ -21,6 +21,6 @@
- "9093:9093"
prometheus-random-data:
- build: blocks/prometheus_random_data
+ build: docker/blocks/prometheus_random_data
ports:
- "8081:8080"
diff --git a/docker/blocks/prometheus_mac/prometheus.yml b/devenv/docker/blocks/prometheus_mac/prometheus.yml
similarity index 100%
rename from docker/blocks/prometheus_mac/prometheus.yml
rename to devenv/docker/blocks/prometheus_mac/prometheus.yml
diff --git a/docker/blocks/prometheus_random_data/Dockerfile b/devenv/docker/blocks/prometheus_random_data/Dockerfile
similarity index 100%
rename from docker/blocks/prometheus_random_data/Dockerfile
rename to devenv/docker/blocks/prometheus_random_data/Dockerfile
diff --git a/docker/blocks/smtp/Dockerfile b/devenv/docker/blocks/smtp/Dockerfile
similarity index 100%
rename from docker/blocks/smtp/Dockerfile
rename to devenv/docker/blocks/smtp/Dockerfile
diff --git a/docker/blocks/smtp/bootstrap.sh b/devenv/docker/blocks/smtp/bootstrap.sh
similarity index 100%
rename from docker/blocks/smtp/bootstrap.sh
rename to devenv/docker/blocks/smtp/bootstrap.sh
diff --git a/docker/blocks/smtp/docker-compose.yaml b/devenv/docker/blocks/smtp/docker-compose.yaml
similarity index 100%
rename from docker/blocks/smtp/docker-compose.yaml
rename to devenv/docker/blocks/smtp/docker-compose.yaml
diff --git a/docker/buildcontainer/Dockerfile b/devenv/docker/buildcontainer/Dockerfile
similarity index 100%
rename from docker/buildcontainer/Dockerfile
rename to devenv/docker/buildcontainer/Dockerfile
diff --git a/docker/buildcontainer/build.sh b/devenv/docker/buildcontainer/build.sh
similarity index 100%
rename from docker/buildcontainer/build.sh
rename to devenv/docker/buildcontainer/build.sh
diff --git a/docker/buildcontainer/build_circle.sh b/devenv/docker/buildcontainer/build_circle.sh
similarity index 100%
rename from docker/buildcontainer/build_circle.sh
rename to devenv/docker/buildcontainer/build_circle.sh
diff --git a/docker/buildcontainer/run_circle.sh b/devenv/docker/buildcontainer/run_circle.sh
similarity index 100%
rename from docker/buildcontainer/run_circle.sh
rename to devenv/docker/buildcontainer/run_circle.sh
diff --git a/docker/compose_header.yml b/devenv/docker/compose_header.yml
similarity index 100%
rename from docker/compose_header.yml
rename to devenv/docker/compose_header.yml
diff --git a/docker/debtest/Dockerfile b/devenv/docker/debtest/Dockerfile
similarity index 100%
rename from docker/debtest/Dockerfile
rename to devenv/docker/debtest/Dockerfile
diff --git a/docker/debtest/build.sh b/devenv/docker/debtest/build.sh
similarity index 100%
rename from docker/debtest/build.sh
rename to devenv/docker/debtest/build.sh
diff --git a/docker/rpmtest/build.sh b/devenv/docker/rpmtest/build.sh
similarity index 100%
rename from docker/rpmtest/build.sh
rename to devenv/docker/rpmtest/build.sh
diff --git a/tests/api/clearState.test.ts b/devenv/e2e-api-tests/clearState.test.ts
similarity index 100%
rename from tests/api/clearState.test.ts
rename to devenv/e2e-api-tests/clearState.test.ts
diff --git a/tests/api/client.ts b/devenv/e2e-api-tests/client.ts
similarity index 100%
rename from tests/api/client.ts
rename to devenv/e2e-api-tests/client.ts
diff --git a/tests/api/dashboard.test.ts b/devenv/e2e-api-tests/dashboard.test.ts
similarity index 100%
rename from tests/api/dashboard.test.ts
rename to devenv/e2e-api-tests/dashboard.test.ts
diff --git a/tests/api/folder.test.ts b/devenv/e2e-api-tests/folder.test.ts
similarity index 100%
rename from tests/api/folder.test.ts
rename to devenv/e2e-api-tests/folder.test.ts
diff --git a/tests/api/jest.js b/devenv/e2e-api-tests/jest.js
similarity index 100%
rename from tests/api/jest.js
rename to devenv/e2e-api-tests/jest.js
diff --git a/tests/api/search.test.ts b/devenv/e2e-api-tests/search.test.ts
similarity index 100%
rename from tests/api/search.test.ts
rename to devenv/e2e-api-tests/search.test.ts
diff --git a/tests/api/setup.ts b/devenv/e2e-api-tests/setup.ts
similarity index 100%
rename from tests/api/setup.ts
rename to devenv/e2e-api-tests/setup.ts
diff --git a/tests/api/tsconfig.json b/devenv/e2e-api-tests/tsconfig.json
similarity index 100%
rename from tests/api/tsconfig.json
rename to devenv/e2e-api-tests/tsconfig.json
diff --git a/tests/api/user.test.ts b/devenv/e2e-api-tests/user.test.ts
similarity index 100%
rename from tests/api/user.test.ts
rename to devenv/e2e-api-tests/user.test.ts
diff --git a/devenv/setup.sh b/devenv/setup.sh
index 6412bbc98ea..cc71ecc71bf 100755
--- a/devenv/setup.sh
+++ b/devenv/setup.sh
@@ -7,11 +7,11 @@ bulkDashboard() {
COUNTER=0
MAX=400
while [ $COUNTER -lt $MAX ]; do
- jsonnet -o "dashboards/bulk-testing/dashboard${COUNTER}.json" -e "local bulkDash = import 'dashboards/bulk-testing/bulkdash.jsonnet'; bulkDash + { uid: 'uid-${COUNTER}', title: 'title-${COUNTER}' }"
+ jsonnet -o "bulk-dashboards/dashboard${COUNTER}.json" -e "local bulkDash = import 'bulk-dashboards/bulkdash.jsonnet'; bulkDash + { uid: 'uid-${COUNTER}', title: 'title-${COUNTER}' }"
let COUNTER=COUNTER+1
done
- ln -s -f -r ./dashboards/bulk-testing/bulk-dashboards.yaml ../conf/provisioning/dashboards/custom.yaml
+ ln -s -f -r ./bulk-dashboards/bulk-dashboards.yaml ../conf/provisioning/dashboards/custom.yaml
}
requiresJsonnet() {
diff --git a/docs/sources/administration/permissions.md b/docs/sources/administration/permissions.md
index e7b84a417c0..1d1a70607c8 100644
--- a/docs/sources/administration/permissions.md
+++ b/docs/sources/administration/permissions.md
@@ -52,8 +52,6 @@ This admin flag makes a user a `Super Admin`. This means they can access the `Se
### Dashboard & Folder Permissions
-> Introduced in Grafana v5.0
-
{{< docs-imagebox img="/img/docs/v50/folder_permissions.png" max-width="500px" class="docs-image--right" >}}
For dashboards and dashboard folders there is a **Permissions** page that make it possible to
diff --git a/docs/sources/administration/provisioning.md b/docs/sources/administration/provisioning.md
index 7fff41fb805..a026d1ec0cd 100644
--- a/docs/sources/administration/provisioning.md
+++ b/docs/sources/administration/provisioning.md
@@ -71,6 +71,7 @@ Puppet | [https://forge.puppet.com/puppet/grafana](https://forge.puppet.com/pupp
Ansible | [https://github.com/cloudalchemy/ansible-grafana](https://github.com/cloudalchemy/ansible-grafana)
Chef | [https://github.com/JonathanTron/chef-grafana](https://github.com/JonathanTron/chef-grafana)
Saltstack | [https://github.com/salt-formulas/salt-formula-grafana](https://github.com/salt-formulas/salt-formula-grafana)
+Jsonnet | [https://github.com/grafana/grafonnet-lib/](https://github.com/grafana/grafonnet-lib/)
## Datasources
@@ -154,8 +155,8 @@ Since not all datasources have the same configuration settings we only have the
| tlsAuthWithCACert | boolean | *All* | Enable TLS authentication using CA cert |
| tlsSkipVerify | boolean | *All* | Controls whether a client verifies the server's certificate chain and host name. |
| graphiteVersion | string | Graphite | Graphite version |
-| timeInterval | string | Elastic, InfluxDB & Prometheus | Lowest interval/step value that should be used for this data source |
-| esVersion | string | Elastic | Elasticsearch version as an number (2/5/56) |
+| timeInterval | string | Prometheus, Elasticsearch, InfluxDB, MySQL, PostgreSQL & MSSQL | Lowest interval/step value that should be used for this data source |
+| esVersion | number | Elastic | Elasticsearch version as a number (2/5/56) |
| timeField | string | Elastic | Which field that should be used as timestamp |
| interval | string | Elastic | Index date time format |
| authType | string | Cloudwatch | Auth provider. keys/credentials/arn |
@@ -165,6 +166,8 @@ Since not all datasources have the same configuration settings we only have the
| tsdbVersion | string | OpenTSDB | Version |
| tsdbResolution | string | OpenTSDB | Resolution |
| sslmode | string | PostgreSQL | SSLmode. 'disable', 'require', 'verify-ca' or 'verify-full' |
+| postgresVersion | number | PostgreSQL | Postgres version as a number (903/904/905/906/1000) meaning v9.3, v9.4, ..., v10 |
+| timescaledb | boolean | PostgreSQL | Enable usage of TimescaleDB extension |
#### Secure Json Data
diff --git a/docs/sources/alerting/notifications.md b/docs/sources/alerting/notifications.md
index b3b4305a748..a5b7f4264e0 100644
--- a/docs/sources/alerting/notifications.md
+++ b/docs/sources/alerting/notifications.md
@@ -16,12 +16,11 @@ weight = 2
When an alert changes state, it sends out notifications. Each alert rule can have
multiple notifications. In order to add a notification to an alert rule you first need
-to add and configure a `notification` channel (can be email, PagerDuty or other integration). This is done from the Notification Channels page.
+to add and configure a `notification` channel (can be email, PagerDuty or other integration).
+This is done from the Notification Channels page.
## Notification Channel Setup
-{{< imgbox max-width="30%" img="/img/docs/v50/alerts_notifications_menu.png" caption="Alerting Notification Channels" >}}
-
On the Notification Channels page hit the `New Channel` button to go the page where you
can configure and setup a new Notification Channel.
@@ -30,7 +29,31 @@ sure it's setup correctly.
### Send on all alerts
-When checked, this option will nofity for all alert rules - existing and new.
+When checked, this option will notify for all alert rules - existing and new.
+
+### Send reminders
+
+> Only available in Grafana v5.3 and above.
+
+{{< docs-imagebox max-width="600px" img="/img/docs/v53/alerting_notification_reminders.png" class="docs-image--right" caption="Alerting notification reminders setup" >}}
+
+When this option is checked additional notifications (reminders) will be sent for triggered alerts. You can specify how often reminders
+should be sent using number of seconds (s), minutes (m) or hours (h), for example `30s`, `3m`, `5m` or `1h` etc.
+
+**Important:** Alert reminders are sent after rules are evaluated. Therefore a reminder can never be sent more frequently than a configured [alert rule evaluation interval](/alerting/rules/#name-evaluation-interval).
+
+These examples show how often and when reminders are sent for a triggered alert.
+
+Alert rule evaluation interval | Send reminders every | Reminder sent every (after last alert notification)
+---------- | ----------- | -----------
+`30s` | `15s` | ~30 seconds
+`1m` | `5m` | ~5 minutes
+`5m` | `15m` | ~15 minutes
+`6m` | `20m` | ~24 minutes
+`1h` | `15m` | ~1 hour
+`1h` | `2h` | ~2 hours
+
+
## Supported Notification Types
@@ -130,24 +153,25 @@ There are a couple of configuration options which need to be set up in Grafana U
Once these two properties are set, you can send the alerts to Kafka for further processing or throttling.
-### All supported notifier
+### All supported notifiers
-Name | Type |Support images
------|------------ | ------
-Slack | `slack` | yes
-Pagerduty | `pagerduty` | yes
-Email | `email` | yes
-Webhook | `webhook` | link
-Kafka | `kafka` | no
-Hipchat | `hipchat` | yes
-VictorOps | `victorops` | yes
-Sensu | `sensu` | yes
-OpsGenie | `opsgenie` | yes
-Threema | `threema` | yes
-Pushover | `pushover` | no
-Telegram | `telegram` | no
-Line | `line` | no
-Prometheus Alertmanager | `prometheus-alertmanager` | no
+Name | Type |Support images | Support reminders
+-----|------------ | ------ | ------ |
+Slack | `slack` | yes | yes
+Pagerduty | `pagerduty` | yes | yes
+Email | `email` | yes | yes
+Webhook | `webhook` | link | yes
+Kafka | `kafka` | no | yes
+Hipchat | `hipchat` | yes | yes
+VictorOps | `victorops` | yes | yes
+Sensu | `sensu` | yes | yes
+OpsGenie | `opsgenie` | yes | yes
+Threema | `threema` | yes | yes
+Pushover | `pushover` | no | yes
+Telegram | `telegram` | no | yes
+Line | `line` | no | yes
+Microsoft Teams | `teams` | yes | yes
+Prometheus Alertmanager | `prometheus-alertmanager` | no | no
diff --git a/docs/sources/alerting/rules.md b/docs/sources/alerting/rules.md
index fa7332e7145..488619055e2 100644
--- a/docs/sources/alerting/rules.md
+++ b/docs/sources/alerting/rules.md
@@ -88,6 +88,11 @@ So as you can see from the above scenario Grafana will not send out notification
to fire if the rule already is in state `Alerting`. To improve support for queries that return multiple series
we plan to track state **per series** in a future release.
+> Starting with Grafana v5.3 you can configure reminders to be sent for triggered alerts. This will send additional notifications
+> when an alert continues to fire. If other series (like server2 in the example above) also cause the alert rule to fire they will
+> be included in the reminder notification. Depending on what notification channel you're using you may be able to take advantage
+> of this feature for identifying new/existing series causing alert to fire. [Read more about notification reminders here](/alerting/notifications/#send-reminders).
+
### No Data / Null values
Below your conditions you can configure how the rule evaluation engine should handle queries that return no data or only null values.
diff --git a/docs/sources/tutorials/authproxy.md b/docs/sources/auth/auth-proxy.md
similarity index 67%
rename from docs/sources/tutorials/authproxy.md
rename to docs/sources/auth/auth-proxy.md
index 6f13de85c18..e066eed9190 100644
--- a/docs/sources/tutorials/authproxy.md
+++ b/docs/sources/auth/auth-proxy.md
@@ -1,42 +1,43 @@
+++
-title = "Grafana Authproxy"
+title = "Auth Proxy"
+description = "Grafana Auth Proxy Guide "
+keywords = ["grafana", "configuration", "documentation", "proxy"]
type = "docs"
-keywords = ["grafana", "tutorials", "authproxy"]
+aliases = ["/tutorials/authproxy/"]
[menu.docs]
-parent = "tutorials"
-weight = 10
+name = "Auth Proxy"
+identifier = "auth-proxy"
+parent = "authentication"
+weight = 2
+++
-# Grafana Authproxy
+# Auth Proxy Authentication
-AuthProxy allows you to offload the authentication of users to a web server (there are many reasons why you’d want to run a web server in front of a production version of Grafana, especially if it’s exposed to the Internet).
+You can configure Grafana to let a http reverse proxy handling authentication. Popular web servers have a very
+extensive list of pluggable authentication modules, and any of them can be used with the AuthProxy feature.
+Below we detail the configuration options for auth proxy.
-Popular web servers have a very extensive list of pluggable authentication modules, and any of them can be used with the AuthProxy feature.
-
-The Grafana AuthProxy feature is very simple in design, but it is this simplicity that makes it so powerful.
-
-## Interacting with Grafana’s AuthProxy via curl
-
-The AuthProxy feature can be configured through the Grafana configuration file with the following options:
-
-```js
+```bash
[auth.proxy]
+# Defaults to false, but set to true to enable this feature
enabled = true
+# HTTP Header name that will contain the username or email
header_name = X-WEBAUTH-USER
+# HTTP Header property, defaults to `username` but can also be `email`
header_property = username
+# Set to `true` to enable auto sign up of users who do not exist in Grafana DB. Defaults to `true`.
auto_sign_up = true
+# If combined with Grafana LDAP integration define sync interval
ldap_sync_ttl = 60
+# Limit where auth proxy requests come from by configuring a list of IP addresses.
+# This can be used to prevent users spoofing the X-WEBAUTH-USER header.
whitelist =
+# Optionally define more headers to sync other user attributes
+# Example `headers = Name:X-WEBAUTH-NAME Email:X-WEBAUTH-EMAIL``
+headers =
```
-* **enabled**: this is to toggle the feature on or off
-* **header_name**: this is the HTTP header name that passes the username or email address of the authenticated user to Grafana. Grafana will trust what ever username is contained in this header and automatically log the user in.
-* **header_property**: this tells Grafana whether the value in the header_name is a username or an email address. (In Grafana you can log in using your account username or account email)
-* **auto_sign_up**: If set to true, Grafana will automatically create user accounts in the Grafana DB if one does not exist. If set to false, users who do not exist in the GrafanaDB won’t be able to log in, even though their username and password are valid.
-* **ldap_sync_ttl**: When both auth.proxy and auth.ldap are enabled, user's organisation and role are synchronised from ldap after the http proxy authentication. You can force ldap re-synchronisation after `ldap_sync_ttl` minutes.
-* **whitelist**: Comma separated list of trusted authentication proxies IP.
-
-With a fresh install of Grafana, using the above configuration for the authProxy feature, we can send a simple API call to list all users. The only user that will be present is the default “Admin” user that is added the first time Grafana starts up. As you can see all we need to do to authenticate the request is to provide the “X-WEBAUTH-USER” header.
+## Interacting with Grafana’s AuthProxy via curl
```bash
curl -H "X-WEBAUTH-USER: admin" http://localhost:3000/api/users
@@ -71,7 +72,8 @@ I’ll demonstrate how to use Apache for authenticating users. In this example w
### Apache BasicAuth
-In this example we use Apache as a reverseProxy in front of Grafana. Apache handles the Authentication of users before forwarding requests to the Grafana backend service.
+In this example we use Apache as a reverse proxy in front of Grafana. Apache handles the Authentication of users before forwarding requests to the Grafana backend service.
+
#### Apache configuration
@@ -116,38 +118,7 @@ In this example we use Apache as a reverseProxy in front of Grafana. Apache hand
* The last 3 lines are then just standard reverse proxy configuration to direct all authenticated requests to our Grafana server running on port 3000.
-#### Grafana configuration
-
-```bash
-############# Users ################
-[users]
- # disable user signup / registration
-allow_sign_up = false
-
-# Set to true to automatically assign new users to the default organization (id 1)
-auto_assign_org = true
-
-# Default role new users will be automatically assigned (if auto_assign_org above is set to true)
- auto_assign_org_role = Editor
-
-
-############ Auth Proxy ########
-[auth.proxy]
-enabled = true
-
-# the Header name that contains the authenticated user.
-header_name = X-WEBAUTH-USER
-
-# does the user authenticate against the proxy using a 'username' or an 'email'
-header_property = username
-
-# automatically add the user to the system if they don't already exist.
-auto_sign_up = true
-```
-
-#### Full walk through using Docker.
-
-##### Grafana Container
+## Full walk through using Docker.
For this example, we use the official Grafana docker image available at [Docker Hub](https://hub.docker.com/r/grafana/grafana/)
@@ -166,7 +137,8 @@ header_property = username
auto_sign_up = true
```
-* Launch the Grafana container, using our custom grafana.ini to replace `/etc/grafana/grafana.ini`. We don't expose any ports for this container as it will only be connected to by our Apache container.
+Launch the Grafana container, using our custom grafana.ini to replace `/etc/grafana/grafana.ini`. We don't expose
+any ports for this container as it will only be connected to by our Apache container.
```bash
docker run -i -v $(pwd)/grafana.ini:/etc/grafana/grafana.ini --name grafana grafana/grafana
diff --git a/docs/sources/auth/generic-oauth.md b/docs/sources/auth/generic-oauth.md
new file mode 100644
index 00000000000..f89595d3b11
--- /dev/null
+++ b/docs/sources/auth/generic-oauth.md
@@ -0,0 +1,211 @@
++++
+title = "OAuth authentication"
+description = "Grafana OAuthentication Guide "
+keywords = ["grafana", "configuration", "documentation", "oauth"]
+type = "docs"
+[menu.docs]
+name = "Generic OAuth"
+identifier = "generic_oauth"
+parent = "authentication"
+weight = 3
++++
+
+# Generic OAuth Authentication
+
+You can configure many different oauth2 authentication services with Grafana using the generic oauth2 feature. Below you
+can find examples using Okta, BitBucket, OneLogin and Azure.
+
+This callback URL must match the full HTTP address that you use in your browser to access Grafana, but with the prefix path of `/login/generic_oauth`.
+
+Example config:
+
+```bash
+[auth.generic_oauth]
+enabled = true
+client_id = YOUR_APP_CLIENT_ID
+client_secret = YOUR_APP_CLIENT_SECRET
+scopes =
+auth_url =
+token_url =
+api_url =
+allowed_domains = mycompany.com mycompany.org
+allow_sign_up = true
+```
+
+Set `api_url` to the resource that returns [OpenID UserInfo](https://connect2id.com/products/server/docs/api/userinfo) compatible information.
+
+Grafana will attempt to determine the user's e-mail address by querying the OAuth provider as described below in the following order until an e-mail address is found:
+
+1. Check for the presence of an e-mail address via the `email` field encoded in the OAuth `id_token` parameter.
+2. Check for the presence of an e-mail address in the `attributes` map encoded in the OAuth `id_token` parameter. By default Grafana will perform a lookup into the attributes map using the `email:primary` key, however, this is configurable and can be adjusted by using the `email_attribute_name` configuration option.
+3. Query the `/emails` endpoint of the OAuth provider's API (configured with `api_url`) and check for the presence of an e-mail address marked as a primary address.
+4. If no e-mail address is found in steps (1-3), then the e-mail address of the user is set to the empty string.
+
+## Set up OAuth2 with Okta
+
+First set up Grafana as an OpenId client "webapplication" in Okta. Then set the Base URIs to `https:///` and set the Login redirect URIs to `https:///login/generic_oauth`.
+
+Finally set up the generic oauth module like this:
+```bash
+[auth.generic_oauth]
+name = Okta
+enabled = true
+scopes = openid profile email
+client_id =
+client_secret =
+auth_url = https:///oauth2/v1/authorize
+token_url = https:///oauth2/v1/token
+api_url = https:///oauth2/v1/userinfo
+```
+
+## Set up OAuth2 with Bitbucket
+
+```bash
+[auth.generic_oauth]
+name = BitBucket
+enabled = true
+allow_sign_up = true
+client_id =
+client_secret =
+scopes = account email
+auth_url = https://bitbucket.org/site/oauth2/authorize
+token_url = https://bitbucket.org/site/oauth2/access_token
+api_url = https://api.bitbucket.org/2.0/user
+team_ids =
+allowed_organizations =
+```
+
+## Set up OAuth2 with OneLogin
+
+1. Create a new Custom Connector with the following settings:
+ - Name: Grafana
+ - Sign On Method: OpenID Connect
+ - Redirect URI: `https:///login/generic_oauth`
+ - Signing Algorithm: RS256
+ - Login URL: `https:///login/generic_oauth`
+
+ then:
+2. Add an App to the Grafana Connector:
+ - Display Name: Grafana
+
+ then:
+3. Under the SSO tab on the Grafana App details page you'll find the Client ID and Client Secret.
+
+ Your OneLogin Domain will match the url you use to access OneLogin.
+
+ Configure Grafana as follows:
+
+ ```bash
+ [auth.generic_oauth]
+ name = OneLogin
+ enabled = true
+ allow_sign_up = true
+ client_id =
+ client_secret =
+ scopes = openid email name
+ auth_url = https://.onelogin.com/oidc/auth
+ token_url = https://.onelogin.com/oidc/token
+ api_url = https://.onelogin.com/oidc/me
+ team_ids =
+ allowed_organizations =
+ ```
+
+### Set up OAuth2 with Auth0
+
+1. Create a new Client in Auth0
+ - Name: Grafana
+ - Type: Regular Web Application
+
+2. Go to the Settings tab and set:
+ - Allowed Callback URLs: `https:///login/generic_oauth`
+
+3. Click Save Changes, then use the values at the top of the page to configure Grafana:
+
+ ```bash
+ [auth.generic_oauth]
+ enabled = true
+ allow_sign_up = true
+ team_ids =
+ allowed_organizations =
+ name = Auth0
+ client_id =
+ client_secret =
+ scopes = openid profile email
+ auth_url = https:///authorize
+ token_url = https:///oauth/token
+ api_url = https:///userinfo
+ ```
+
+### Set up OAuth2 with Azure Active Directory
+
+1. Log in to portal.azure.com and click "Azure Active Directory" in the side menu, then click the "Properties" sub-menu item.
+
+2. Copy the "Directory ID", this is needed for setting URLs later
+
+3. Click "App Registrations" and add a new application registration:
+ - Name: Grafana
+ - Application type: Web app / API
+ - Sign-on URL: `https:///login/generic_oauth`
+
+4. Click the name of the new application to open the application details page.
+
+5. Note down the "Application ID", this will be the OAuth client id.
+
+6. Click "Settings", then click "Keys" and add a new entry under Passwords
+ - Key Description: Grafana OAuth
+ - Duration: Never Expires
+
+7. Click Save then copy the key value, this will be the OAuth client secret.
+
+8. Configure Grafana as follows:
+
+ ```bash
+ [auth.generic_oauth]
+ name = Azure AD
+ enabled = true
+ allow_sign_up = true
+ client_id =
+ client_secret =
+ scopes = openid email name
+ auth_url = https://login.microsoftonline.com//oauth2/authorize
+ token_url = https://login.microsoftonline.com//oauth2/token
+ api_url =
+ team_ids =
+ allowed_organizations =
+ ```
+
+Note: It's important to ensure that the SERVER_ROOT_URL in Grafana is set in your Azure Application Return URLs
+
+## Set up OAuth2 with Centrify
+
+1. Create a new Custom OpenID Connect application configuration in the Centrify dashboard.
+
+2. Create a memorable unique Application ID, e.g. "grafana", "grafana_aws", etc.
+
+3. Put in other basic configuration (name, description, logo, category)
+
+4. On the Trust tab, generate a long password and put it into the OpenID Connect Client Secret field.
+
+5. Put the URL to the front page of your Grafana instance into the "Resource Application URL" field.
+
+6. Add an authorized Redirect URI like https://your-grafana-server/login/generic_oauth
+
+7. Set up permissions, policies, etc. just like any other Centrify app
+
+8. Configure Grafana as follows:
+
+ ```bash
+ [auth.generic_oauth]
+ name = Centrify
+ enabled = true
+ allow_sign_up = true
+ client_id =
+ client_secret = .my.centrify.com/OAuth2/Authorize/
+ token_url = https://.my.centrify.com/OAuth2/Token/
+ ```
+
+
+
+
diff --git a/docs/sources/auth/github.md b/docs/sources/auth/github.md
new file mode 100644
index 00000000000..263b3cc5d4d
--- /dev/null
+++ b/docs/sources/auth/github.md
@@ -0,0 +1,98 @@
++++
+title = "Google OAuth2 Authentication"
+description = "Grafana OAuthentication Guide "
+keywords = ["grafana", "configuration", "documentation", "oauth"]
+type = "docs"
+[menu.docs]
+name = "GitHub"
+identifier = "github_oauth2"
+parent = "authentication"
+weight = 4
++++
+
+# GitHub OAuth2 Authentication
+
+To enable the GitHub OAuth2 you must register your application with GitHub. GitHub will generate a client ID and secret key for you to use.
+
+## Configure GitHub OAuth application
+
+You need to create a GitHub OAuth application (you find this under the GitHub
+settings page). When you create the application you will need to specify
+a callback URL. Specify this as callback:
+
+```bash
+http://:/login/github
+```
+
+This callback URL must match the full HTTP address that you use in your
+browser to access Grafana, but with the prefix path of `/login/github`.
+When the GitHub OAuth application is created you will get a Client ID and a
+Client Secret. Specify these in the Grafana configuration file. For
+example:
+
+## Enable GitHub in Grafana
+
+```bash
+[auth.github]
+enabled = true
+allow_sign_up = true
+client_id = YOUR_GITHUB_APP_CLIENT_ID
+client_secret = YOUR_GITHUB_APP_CLIENT_SECRET
+scopes = user:email,read:org
+auth_url = https://github.com/login/oauth/authorize
+token_url = https://github.com/login/oauth/access_token
+api_url = https://api.github.com/user
+team_ids =
+allowed_organizations =
+```
+
+Restart the Grafana back-end. You should now see a GitHub login button
+on the login page. You can now login or sign up with your GitHub
+accounts.
+
+You may allow users to sign-up via GitHub authentication by setting the
+`allow_sign_up` option to `true`. When this option is set to `true`, any
+user successfully authenticating via GitHub authentication will be
+automatically signed up.
+
+### team_ids
+
+Require an active team membership for at least one of the given teams on
+GitHub. If the authenticated user isn't a member of at least one of the
+teams they will not be able to register or authenticate with your
+Grafana instance. For example:
+
+```bash
+[auth.github]
+enabled = true
+client_id = YOUR_GITHUB_APP_CLIENT_ID
+client_secret = YOUR_GITHUB_APP_CLIENT_SECRET
+scopes = user:email,read:org
+team_ids = 150,300
+auth_url = https://github.com/login/oauth/authorize
+token_url = https://github.com/login/oauth/access_token
+api_url = https://api.github.com/user
+allow_sign_up = true
+```
+
+### allowed_organizations
+
+Require an active organization membership for at least one of the given
+organizations on GitHub. If the authenticated user isn't a member of at least
+one of the organizations they will not be able to register or authenticate with
+your Grafana instance. For example
+
+```bash
+[auth.github]
+enabled = true
+client_id = YOUR_GITHUB_APP_CLIENT_ID
+client_secret = YOUR_GITHUB_APP_CLIENT_SECRET
+scopes = user:email,read:org
+auth_url = https://github.com/login/oauth/authorize
+token_url = https://github.com/login/oauth/access_token
+api_url = https://api.github.com/user
+allow_sign_up = true
+# space-delimited organization names
+allowed_organizations = github google
+```
+
diff --git a/docs/sources/auth/gitlab.md b/docs/sources/auth/gitlab.md
new file mode 100644
index 00000000000..32910167f16
--- /dev/null
+++ b/docs/sources/auth/gitlab.md
@@ -0,0 +1,115 @@
++++
+title = "Google OAuth2 Authentication"
+description = "Grafana OAuthentication Guide "
+keywords = ["grafana", "configuration", "documentation", "oauth"]
+type = "docs"
+[menu.docs]
+name = "GitLab"
+identifier = "gitlab_oauth"
+parent = "authentication"
+weight = 5
++++
+
+# GitLab OAuth2 Authentication
+
+To enable the GitLab OAuth2 you must register an application in GitLab. GitLab will generate a client ID and secret key for you to use.
+
+## Create GitLab OAuth keys
+
+You need to [create a GitLab OAuth application](https://docs.gitlab.com/ce/integration/oauth_provider.html).
+Choose a descriptive *Name*, and use the following *Redirect URI*:
+
+```
+https://grafana.example.com/login/gitlab
+```
+
+where `https://grafana.example.com` is the URL you use to connect to Grafana.
+Adjust it as needed if you don't use HTTPS or if you use a different port; for
+instance, if you access Grafana at `http://203.0.113.31:3000`, you should use
+
+```
+http://203.0.113.31:3000/login/gitlab
+```
+
+Finally, select *api* as the *Scope* and submit the form. Note that if you're
+not going to use GitLab groups for authorization (i.e. not setting
+`allowed_groups`, see below), you can select *read_user* instead of *api* as
+the *Scope*, thus giving a more restricted access to your GitLab API.
+
+You'll get an *Application Id* and a *Secret* in return; we'll call them
+`GITLAB_APPLICATION_ID` and `GITLAB_SECRET` respectively for the rest of this
+section.
+
+## Enable GitLab in Grafana
+
+Add the following to your Grafana configuration file to enable GitLab
+authentication:
+
+```bash
+[auth.gitlab]
+enabled = false
+allow_sign_up = false
+client_id = GITLAB_APPLICATION_ID
+client_secret = GITLAB_SECRET
+scopes = api
+auth_url = https://gitlab.com/oauth/authorize
+token_url = https://gitlab.com/oauth/token
+api_url = https://gitlab.com/api/v4
+allowed_groups =
+```
+
+Restart the Grafana backend for your changes to take effect.
+
+If you use your own instance of GitLab instead of `gitlab.com`, adjust
+`auth_url`, `token_url` and `api_url` accordingly by replacing the `gitlab.com`
+hostname with your own.
+
+With `allow_sign_up` set to `false`, only existing users will be able to login
+using their GitLab account, but with `allow_sign_up` set to `true`, *any* user
+who can authenticate on GitLab will be able to login on your Grafana instance;
+if you use the public `gitlab.com`, it means anyone in the world would be able
+to login on your Grafana instance.
+
+You can can however limit access to only members of a given group or list of
+groups by setting the `allowed_groups` option.
+
+### allowed_groups
+
+To limit access to authenticated users that are members of one or more [GitLab
+groups](https://docs.gitlab.com/ce/user/group/index.html), set `allowed_groups`
+to a comma- or space-separated list of groups. For instance, if you want to
+only give access to members of the `example` group, set
+
+
+```ini
+allowed_groups = example
+```
+
+If you want to also give access to members of the subgroup `bar`, which is in
+the group `foo`, set
+
+```ini
+allowed_groups = example, foo/bar
+```
+
+Note that in GitLab, the group or subgroup name doesn't always match its
+display name, especially if the display name contains spaces or special
+characters. Make sure you always use the group or subgroup name as it appears
+in the URL of the group or subgroup.
+
+Here's a complete example with `alloed_sign_up` enabled, and access limited to
+the `example` and `foo/bar` groups:
+
+```ini
+[auth.gitlab]
+enabled = false
+allow_sign_up = true
+client_id = GITLAB_APPLICATION_ID
+client_secret = GITLAB_SECRET
+scopes = api
+auth_url = https://gitlab.com/oauth/authorize
+token_url = https://gitlab.com/oauth/token
+api_url = https://gitlab.com/api/v4
+allowed_groups = example, foo/bar
+```
+
diff --git a/docs/sources/auth/google.md b/docs/sources/auth/google.md
new file mode 100644
index 00000000000..eeb78044d3e
--- /dev/null
+++ b/docs/sources/auth/google.md
@@ -0,0 +1,55 @@
++++
+title = "Google OAuth2 Authentication"
+description = "Grafana OAuthentication Guide "
+keywords = ["grafana", "configuration", "documentation", "oauth"]
+type = "docs"
+[menu.docs]
+name = "Google"
+identifier = "ggogle_oauth2"
+parent = "authentication"
+weight = 3
++++
+
+# Google OAuth2 Authentication
+
+To enable the Google OAuth2 you must register your application with Google. Google will generate a client ID and secret key for you to use.
+
+## Create Google OAuth keys
+
+First, you need to create a Google OAuth Client:
+
+1. Go to https://console.developers.google.com/apis/credentials
+2. Click the 'Create Credentials' button, then click 'OAuth Client ID' in the menu that drops down
+3. Enter the following:
+ - Application Type: Web Application
+ - Name: Grafana
+ - Authorized Javascript Origins: https://grafana.mycompany.com
+ - Authorized Redirect URLs: https://grafana.mycompany.com/login/google
+ - Replace https://grafana.mycompany.com with the URL of your Grafana instance.
+4. Click Create
+5. Copy the Client ID and Client Secret from the 'OAuth Client' modal
+
+## Enable Google OAuth in Grafana
+
+Specify the Client ID and Secret in the [Grafana configuration file]({{< relref "installation/configuration.md#config-file-locations" >}}). For example:
+
+```bash
+[auth.google]
+enabled = true
+client_id = CLIENT_ID
+client_secret = CLIENT_SECRET
+scopes = https://www.googleapis.com/auth/userinfo.profile https://www.googleapis.com/auth/userinfo.email
+auth_url = https://accounts.google.com/o/oauth2/auth
+token_url = https://accounts.google.com/o/oauth2/token
+allowed_domains = mycompany.com mycompany.org
+allow_sign_up = true
+```
+
+Restart the Grafana back-end. You should now see a Google login button
+on the login page. You can now login or sign up with your Google
+accounts. The `allowed_domains` option is optional, and domains were separated by space.
+
+You may allow users to sign-up via Google authentication by setting the
+`allow_sign_up` option to `true`. When this option is set to `true`, any
+user successfully authenticating via Google authentication will be
+automatically signed up.
diff --git a/docs/sources/auth/index.md b/docs/sources/auth/index.md
new file mode 100644
index 00000000000..7fdcc082319
--- /dev/null
+++ b/docs/sources/auth/index.md
@@ -0,0 +1,12 @@
++++
+title = "Authentication"
+description = "Authentication"
+type = "docs"
+[menu.docs]
+name = "Authentication"
+identifier = "authentication"
+parent = "admin"
+weight = 3
++++
+
+
diff --git a/docs/sources/auth/ldap.md b/docs/sources/auth/ldap.md
new file mode 100644
index 00000000000..82db8214fb7
--- /dev/null
+++ b/docs/sources/auth/ldap.md
@@ -0,0 +1,260 @@
++++
+title = "LDAP Authentication"
+description = "Grafana LDAP Authentication Guide "
+keywords = ["grafana", "configuration", "documentation", "ldap", "active directory"]
+type = "docs"
+[menu.docs]
+name = "LDAP"
+identifier = "ldap"
+parent = "authentication"
+weight = 2
++++
+
+# LDAP Authentication
+
+The LDAP integration in Grafana allows your Grafana users to login with their LDAP credentials. You can also specify mappings between LDAP
+group memberships and Grafana Organization user roles.
+
+## Supported LDAP Servers
+
+Grafana uses a [third-party LDAP library](https://github.com/go-ldap/ldap) under the hood that supports basic LDAP v3 functionality.
+This means that you should be able to configure LDAP integration using any compliant LDAPv3 server, for example [OpenLDAP](#openldap) or
+[Active Directory](#active-directory) among [others](https://en.wikipedia.org/wiki/Directory_service#LDAP_implementations).
+
+## Enable LDAP
+
+In order to use LDAP integration you'll first need to enable LDAP in the [main config file]({{< relref "installation/configuration.md" >}}) as well as specify the path to the LDAP
+specific configuration file (default: `/etc/grafana/ldap.toml`).
+
+```bash
+[auth.ldap]
+# Set to `true` to enable LDAP integration (default: `false`)
+enabled = true
+
+# Path to the LDAP specific configuration file (default: `/etc/grafana/ldap.toml`)
+config_file = /etc/grafana/ldap.toml
+
+# Allow sign up should almost always be true (default) to allow new Grafana users to be created (if ldap authentication is ok). If set to
+# false only pre-existing Grafana users will be able to login (if ldap authentication is ok).
+allow_sign_up = true
+```
+
+## Grafana LDAP Configuration
+
+Depending on which LDAP server you're using and how that's configured your Grafana LDAP configuration may vary.
+See [configuration examples](#configuration-examples) for more information.
+
+**LDAP specific configuration file (ldap.toml) example:**
+```bash
+[[servers]]
+# Ldap server host (specify multiple hosts space separated)
+host = "127.0.0.1"
+# Default port is 389 or 636 if use_ssl = true
+port = 389
+# Set to true if ldap server supports TLS
+use_ssl = false
+# Set to true if connect ldap server with STARTTLS pattern (create connection in insecure, then upgrade to secure connection with TLS)
+start_tls = false
+# set to true if you want to skip ssl cert validation
+ssl_skip_verify = false
+# set to the path to your root CA certificate or leave unset to use system defaults
+# root_ca_cert = "/path/to/certificate.crt"
+# Authentication against LDAP servers requiring client certificates
+# client_cert = "/path/to/client.crt"
+# client_key = "/path/to/client.key"
+
+# Search user bind dn
+bind_dn = "cn=admin,dc=grafana,dc=org"
+# Search user bind password
+# If the password contains # or ; you have to wrap it with triple quotes. Ex """#password;"""
+bind_password = 'grafana'
+
+# User search filter, for example "(cn=%s)" or "(sAMAccountName=%s)" or "(uid=%s)"
+# Allow login from email or username, example "(|(sAMAccountName=%s)(userPrincipalName=%s))"
+search_filter = "(cn=%s)"
+
+# An array of base dns to search through
+search_base_dns = ["dc=grafana,dc=org"]
+
+# group_search_filter = "(&(objectClass=posixGroup)(memberUid=%s))"
+# group_search_filter_user_attribute = "distinguishedName"
+# group_search_base_dns = ["ou=groups,dc=grafana,dc=org"]
+
+# Specify names of the ldap attributes your ldap uses
+[servers.attributes]
+name = "givenName"
+surname = "sn"
+username = "cn"
+member_of = "memberOf"
+email = "email"
+```
+
+### Bind
+
+#### Bind & Bind Password
+
+By default the configuration expects you to specify a bind DN and bind password. This should be a read only user that can perform LDAP searches.
+When the user DN is found a second bind is performed with the user provided username & password (in the normal Grafana login form).
+
+```bash
+bind_dn = "cn=admin,dc=grafana,dc=org"
+bind_password = "grafana"
+```
+
+#### Single Bind Example
+
+If you can provide a single bind expression that matches all possible users, you can skip the second bind and bind against the user DN directly.
+This allows you to not specify a bind_password in the configuration file.
+
+```bash
+bind_dn = "cn=%s,o=users,dc=grafana,dc=org"
+```
+
+In this case you skip providing a `bind_password` and instead provide a `bind_dn` value with a `%s` somewhere. This will be replaced with the username entered in on the Grafana login page.
+The search filter and search bases settings are still needed to perform the LDAP search to retrieve the other LDAP information (like LDAP groups and email).
+
+### POSIX schema
+If your ldap server does not support the memberOf attribute add these options:
+
+```bash
+## Group search filter, to retrieve the groups of which the user is a member (only set if memberOf attribute is not available)
+group_search_filter = "(&(objectClass=posixGroup)(memberUid=%s))"
+## An array of the base DNs to search through for groups. Typically uses ou=groups
+group_search_base_dns = ["ou=groups,dc=grafana,dc=org"]
+## the %s in the search filter will be replaced with the attribute defined below
+group_search_filter_user_attribute = "uid"
+```
+
+Also set `member_of = "dn"` in the `[servers.attributes]` section.
+
+### Group Mappings
+
+In `[[servers.group_mappings]]` you can map an LDAP group to a Grafana organization and role. These will be synced every time the user logs in, with LDAP being
+the authoritative source. So, if you change a user's role in the Grafana Org. Users page, this change will be reset the next time the user logs in. If you
+change the LDAP groups of a user, the change will take effect the next time the user logs in.
+
+The first group mapping that an LDAP user is matched to will be used for the sync. If you have LDAP users that fit multiple mappings, the topmost mapping in the
+TOML config will be used.
+
+**LDAP specific configuration file (ldap.toml) example:**
+```bash
+[[servers]]
+# other settings omitted for clarity
+
+[[servers.group_mappings]]
+group_dn = "cn=superadmins,dc=grafana,dc=org"
+org_role = "Admin"
+grafana_admin = true # Available in Grafana v5.3 and above
+
+[[servers.group_mappings]]
+group_dn = "cn=admins,dc=grafana,dc=org"
+org_role = "Admin"
+
+[[servers.group_mappings]]
+group_dn = "cn=users,dc=grafana,dc=org"
+org_role = "Editor"
+
+[[servers.group_mappings]]
+group_dn = "*"
+org_role = "Viewer"
+```
+
+Setting | Required | Description | Default
+------------ | ------------ | ------------- | -------------
+`group_dn` | Yes | LDAP distinguished name (DN) of LDAP group. If you want to match all (or no LDAP groups) then you can use wildcard (`"*"`) |
+`org_role` | Yes | Assign users of `group_dn` the organisation role `"Admin"`, `"Editor"` or `"Viewer"` |
+`org_id` | No | The Grafana organization database id. Setting this allows for multiple group_dn's to be assigned to the same `org_role` provided the `org_id` differs | `1` (default org id)
+`grafana_admin` | No | When `true` makes user of `group_dn` Grafana server admin. A Grafana server admin has admin access over all organisations and users. Available in Grafana v5.3 and above | `false`
+
+### Nested/recursive group membership
+
+Users with nested/recursive group membership must have an LDAP server that supports `LDAP_MATCHING_RULE_IN_CHAIN`
+and configure `group_search_filter` in a way that it returns the groups the submitted username is a member of.
+
+**Active Directory example:**
+
+Active Directory groups store the Distinguished Names (DNs) of members, so your filter will need to know the DN for the user based only on the submitted username.
+Multiple DN templates can be searched by combining filters with the LDAP OR-operator. Examples:
+
+```bash
+group_search_filter = "(member:1.2.840.113556.1.4.1941:=CN=%s,[user container/OU])"
+group_search_filter = "(|(member:1.2.840.113556.1.4.1941:=CN=%s,[user container/OU])(member:1.2.840.113556.1.4.1941:=CN=%s,[another user container/OU]))"
+group_search_filter_user_attribute = "cn"
+```
+
+For troubleshooting, by changing `member_of` in `[servers.attributes]` to "dn" it will show you more accurate group memberships when [debug is enabled](#troubleshooting).
+
+## Configuration examples
+
+### OpenLDAP
+
+[OpenLDAP](http://www.openldap.org/) is an open source directory service.
+
+**LDAP specific configuration file (ldap.toml):**
+```bash
+[[servers]]
+host = "127.0.0.1"
+port = 389
+use_ssl = false
+start_tls = false
+ssl_skip_verify = false
+bind_dn = "cn=admin,dc=grafana,dc=org"
+bind_password = 'grafana'
+search_filter = "(cn=%s)"
+search_base_dns = ["dc=grafana,dc=org"]
+
+[servers.attributes]
+name = "givenName"
+surname = "sn"
+username = "cn"
+member_of = "memberOf"
+email = "email"
+
+# [[servers.group_mappings]] omitted for clarity
+```
+
+### Active Directory
+
+[Active Directory](https://technet.microsoft.com/en-us/library/hh831484(v=ws.11).aspx) is a directory service which is commonly used in Windows environments.
+
+Assuming the following Active Directory server setup:
+
+* IP address: `10.0.0.1`
+* Domain: `CORP`
+* DNS name: `corp.local`
+
+**LDAP specific configuration file (ldap.toml):**
+```bash
+[[servers]]
+host = "10.0.0.1"
+port = 3269
+use_ssl = true
+start_tls = false
+ssl_skip_verify = true
+bind_dn = "CORP\\%s"
+search_filter = "(sAMAccountName=%s)"
+search_base_dns = ["dc=corp,dc=local"]
+
+[servers.attributes]
+name = "givenName"
+surname = "sn"
+username = "sAMAccountName"
+member_of = "memberOf"
+email = "mail"
+
+# [[servers.group_mappings]] omitted for clarity
+```
+
+#### Port requirements
+
+In above example SSL is enabled and an encrypted port have been configured. If your Active Directory don't support SSL please change `enable_ssl = false` and `port = 389`.
+Please inspect your Active Directory configuration and documentation to find the correct settings. For more information about Active Directory and port requirements see [link](https://technet.microsoft.com/en-us/library/dd772723(v=ws.10)).
+
+## Troubleshooting
+
+To troubleshoot and get more log info enable ldap debug logging in the [main config file]({{< relref "installation/configuration.md" >}}).
+
+```bash
+[log]
+filters = ldap:debug
+```
diff --git a/docs/sources/auth/overview.md b/docs/sources/auth/overview.md
new file mode 100644
index 00000000000..20010a9ac09
--- /dev/null
+++ b/docs/sources/auth/overview.md
@@ -0,0 +1,86 @@
++++
+title = "Overview"
+description = "Overview for auth"
+type = "docs"
+[menu.docs]
+name = "Overview"
+identifier = "overview-auth"
+parent = "authentication"
+weight = 1
++++
+
+# User Authentication Overview
+
+Grafana provides many ways to authenticate users. Some authentication integrations also enable syncing user
+permissions and org memberships.
+
+## OAuth Integrations
+
+- [Google OAuth]({{< relref "auth/google.md" >}})
+- [GitHub OAuth]({{< relref "auth/github.md" >}})
+- [Gitlab OAuth]({{< relref "auth/gitlab.md" >}})
+- [Generic OAuth]({{< relref "auth/generic-oauth.md" >}}) (Okta2, BitBucket, Azure, OneLogin, Auth0)
+
+## LDAP integrations
+
+- [LDAP Authentication]({{< relref "auth/ldap.md" >}}) (OpenLDAP, ActiveDirectory, etc)
+
+## Auth proxy
+
+- [Auth Proxy]({{< relref "auth/auth-proxy.md" >}}) If you want to handle authentication outside Grafana using a reverse
+ proxy.
+
+## Grafana Auth
+
+Grafana of course has a built in user authentication system with password authentication enabled by default. You can
+disable authentication by enabling anonymous access. You can also hide login form and only allow login through an auth
+provider (listed above). There is also options for allowing self sign up.
+
+### Anonymous authentication
+
+You can make Grafana accessible without any login required by enabling anonymous access in the configuration file.
+
+Example:
+
+```bash
+[auth.anonymous]
+enabled = true
+
+# Organization name that should be used for unauthenticated users
+org_name = Main Org.
+
+# Role for unauthenticated users, other valid values are `Editor` and `Admin`
+org_role = Viewer
+```
+
+If you change your organization name in the Grafana UI this setting needs to be updated to match the new name.
+
+### Basic authentication
+
+Basic auth is enabled by default and works with the built in Grafana user password authentication system and LDAP
+authenticaten integration.
+
+To disable basic auth:
+
+```bash
+[auth.basic]
+enabled = false
+```
+
+### Disable login form
+
+You can hide the Grafana login form using the below configuration settings.
+
+```bash
+[auth]
+disable_login_form ⁼ true
+```
+
+### Hide sign-out menu
+
+Set to the option detailed below to true to hide sign-out menu link. Useful if you use an auth proxy.
+
+```bash
+[auth]
+disable_signout_menu = true
+```
diff --git a/docs/sources/features/datasources/cloudwatch.md b/docs/sources/features/datasources/cloudwatch.md
index d178c176602..7adc6ebe4fb 100644
--- a/docs/sources/features/datasources/cloudwatch.md
+++ b/docs/sources/features/datasources/cloudwatch.md
@@ -115,6 +115,8 @@ and `dimension keys/values`.
In place of `region` you can specify `default` to use the default region configured in the datasource for the query,
e.g. `metrics(AWS/DynamoDB, default)` or `dimension_values(default, ..., ..., ...)`.
+Read more about the available dimensions in the [CloudWatch Metrics and Dimensions Reference](https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/CW_Support_For_AWS.html).
+
Name | Description
------- | --------
*regions()* | Returns a list of regions AWS provides their service.
diff --git a/docs/sources/features/datasources/elasticsearch.md b/docs/sources/features/datasources/elasticsearch.md
index 31ce78f0bfe..80a2f9a828a 100644
--- a/docs/sources/features/datasources/elasticsearch.md
+++ b/docs/sources/features/datasources/elasticsearch.md
@@ -58,8 +58,8 @@ a time pattern for the index name or a wildcard.
### Elasticsearch version
-Be sure to specify your Elasticsearch version in the version selection dropdown. This is very important as there are differences how queries are composed. Currently only 2.x and 5.x
-are supported.
+Be sure to specify your Elasticsearch version in the version selection dropdown. This is very important as there are differences how queries are composed.
+Currently the versions available is 2.x, 5.x and 5.6+ where 5.6+ means a version of 5.6 or higher, 6.3.2 for example.
### Min time interval
A lower limit for the auto group by time interval. Recommended to be set to write frequency, for example `1m` if your data is written every minute.
@@ -115,7 +115,7 @@ The Elasticsearch data source supports two types of queries you can use in the *
Query | Description
------------ | -------------
-*{"find": "fields", "type": "keyword"} | Returns a list of field names with the index type `keyword`.
+*{"find": "fields", "type": "keyword"}* | Returns a list of field names with the index type `keyword`.
*{"find": "terms", "field": "@hostname", "size": 1000}* | Returns a list of values for a field using term aggregation. Query will user current dashboard time range as time range for query.
*{"find": "terms", "field": "@hostname", "query": ''}* | Returns a list of values for a field using term aggregation & and a specified lucene query filter. Query will use current dashboard time range as time range for query.
diff --git a/docs/sources/features/datasources/mssql.md b/docs/sources/features/datasources/mssql.md
index d4d5cc6d73e..debf771ffb0 100644
--- a/docs/sources/features/datasources/mssql.md
+++ b/docs/sources/features/datasources/mssql.md
@@ -6,7 +6,7 @@ type = "docs"
[menu.docs]
name = "Microsoft SQL Server"
parent = "datasources"
-weight = 7
+weight = 8
+++
# Using Microsoft SQL Server in Grafana
@@ -33,6 +33,24 @@ Name | Description
*User* | Database user's login/username
*Password* | Database user's password
+### Min time interval
+
+A lower limit for the [$__interval](/reference/templating/#the-interval-variable) and [$__interval_ms](/reference/templating/#the-interval-ms-variable) variables.
+Recommended to be set to write frequency, for example `1m` if your data is written every minute.
+This option can also be overridden/configured in a dashboard panel under data source options. It's important to note that this value **needs** to be formatted as a
+number followed by a valid time identifier, e.g. `1m` (1 minute) or `30s` (30 seconds). The following time identifiers are supported:
+
+Identifier | Description
+------------ | -------------
+`y` | year
+`M` | month
+`w` | week
+`d` | day
+`h` | hour
+`m` | minute
+`s` | second
+`ms` | millisecond
+
### Database User Permissions (Important!)
The database user you specify when you add the data source should only be granted SELECT permissions on
@@ -81,10 +99,15 @@ Macro example | Description
*$__timeFrom()* | Will be replaced by the start of the currently active time selection. For example, *'2017-04-21T05:01:17Z'*
*$__timeTo()* | Will be replaced by the end of the currently active time selection. For example, *'2017-04-21T05:06:17Z'*
*$__timeGroup(dateColumn,'5m'[, fillvalue])* | Will be replaced by an expression usable in GROUP BY clause. Providing a *fillValue* of *NULL* or *floating value* will automatically fill empty series in timerange with that value.
For example, *CAST(ROUND(DATEDIFF(second, '1970-01-01', time_column)/300.0, 0) as bigint)\*300*.
-*$__timeGroup(dateColumn,'5m', 0)* | Same as above but with a fill parameter so all null values will be converted to the fill value (all null values would be set to zero using this example).
+*$__timeGroup(dateColumn,'5m', 0)* | Same as above but with a fill parameter so missing points in that series will be added by grafana and 0 will be used as value.
+*$__timeGroup(dateColumn,'5m', NULL)* | Same as above but NULL will be used as value for missing points.
+*$__timeGroup(dateColumn,'5m', previous)* | Same as above but the previous value in that series will be used as fill value if no value has been seen yet NULL will be used (only available in Grafana 5.3+).
+*$__timeGroupAlias(dateColumn,'5m')* | Will be replaced identical to $__timeGroup but with an added column alias (only available in Grafana 5.3+).
*$__unixEpochFilter(dateColumn)* | Will be replaced by a time range filter using the specified column name with times represented as unix timestamp. For example, *dateColumn > 1494410783 AND dateColumn < 1494497183*
*$__unixEpochFrom()* | Will be replaced by the start of the currently active time selection as unix timestamp. For example, *1494410783*
*$__unixEpochTo()* | Will be replaced by the end of the currently active time selection as unix timestamp. For example, *1494497183*
+*$__unixEpochGroup(dateColumn,'5m', [fillmode])* | Same as $__timeGroup but for times stored as unix timestamp (only available in Grafana 5.3+).
+*$__unixEpochGroupAlias(dateColumn,'5m', [fillmode])* | Same as above but also adds a column alias (only available in Grafana 5.3+).
We plan to add many more macros. If you have suggestions for what macros you would like to see, please [open an issue](https://github.com/grafana/grafana) in our GitHub repo.
@@ -148,7 +171,10 @@ The resulting table panel:
## Time series queries
-If you set `Format as` to `Time series`, for use in Graph panel for example, then the query must must have a column named `time` that returns either a sql datetime or any numeric datatype representing unix epoch in seconds. You may return a column named `metric` that is used as metric name for the value column. Any column except `time` and `metric` is treated as a value column. If you omit the `metric` column, tha name of the value column will be the metric name. You may select multiple value columns, each will have its name as metric.
+If you set `Format as` to `Time series`, for use in Graph panel for example, then the query must must have a column named `time` that returns either a sql datetime or any numeric datatype representing unix epoch in seconds. You may return a column named `metric` that is used as metric name for the value column. Any column except `time` and `metric` is treated as a value column. If you omit the `metric` column, the name of the value column will be the metric name. You may select multiple value columns, each will have its name as metric.
+If you return multiple value columns and a column named `metric` then this column is used as prefix for the series name (only available in Grafana 5.3+).
+
+Resultsets of time series queries need to be sorted by time.
**Example database table:**
diff --git a/docs/sources/features/datasources/mysql.md b/docs/sources/features/datasources/mysql.md
index ce50053c7ea..d713a4b42b7 100644
--- a/docs/sources/features/datasources/mysql.md
+++ b/docs/sources/features/datasources/mysql.md
@@ -36,6 +36,24 @@ Name | Description
*User* | Database user's login/username
*Password* | Database user's password
+### Min time interval
+
+A lower limit for the [$__interval](/reference/templating/#the-interval-variable) and [$__interval_ms](/reference/templating/#the-interval-ms-variable) variables.
+Recommended to be set to write frequency, for example `1m` if your data is written every minute.
+This option can also be overridden/configured in a dashboard panel under data source options. It's important to note that this value **needs** to be formatted as a
+number followed by a valid time identifier, e.g. `1m` (1 minute) or `30s` (30 seconds). The following time identifiers are supported:
+
+Identifier | Description
+------------ | -------------
+`y` | year
+`M` | month
+`w` | week
+`d` | day
+`h` | hour
+`m` | minute
+`s` | second
+`ms` | millisecond
+
### Database User Permissions (Important!)
The database user you specify when you add the data source should only be granted SELECT permissions on
@@ -64,10 +82,15 @@ Macro example | Description
*$__timeFrom()* | Will be replaced by the start of the currently active time selection. For example, *'2017-04-21T05:01:17Z'*
*$__timeTo()* | Will be replaced by the end of the currently active time selection. For example, *'2017-04-21T05:06:17Z'*
*$__timeGroup(dateColumn,'5m')* | Will be replaced by an expression usable in GROUP BY clause. For example, *cast(cast(UNIX_TIMESTAMP(dateColumn)/(300) as signed)*300 as signed),*
-*$__timeGroup(dateColumn,'5m',0)* | Same as above but with a fill parameter so all null values will be converted to the fill value (all null values would be set to zero using this example).
+*$__timeGroup(dateColumn,'5m', 0)* | Same as above but with a fill parameter so missing points in that series will be added by grafana and 0 will be used as value.
+*$__timeGroup(dateColumn,'5m', NULL)* | Same as above but NULL will be used as value for missing points.
+*$__timeGroup(dateColumn,'5m', previous)* | Same as above but the previous value in that series will be used as fill value if no value has been seen yet NULL will be used (only available in Grafana 5.3+).
+*$__timeGroupAlias(dateColumn,'5m')* | Will be replaced identical to $__timeGroup but with an added column alias (only available in Grafana 5.3+).
*$__unixEpochFilter(dateColumn)* | Will be replaced by a time range filter using the specified column name with times represented as unix timestamp. For example, *dateColumn > 1494410783 AND dateColumn < 1494497183*
*$__unixEpochFrom()* | Will be replaced by the start of the currently active time selection as unix timestamp. For example, *1494410783*
*$__unixEpochTo()* | Will be replaced by the end of the currently active time selection as unix timestamp. For example, *1494497183*
+*$__unixEpochGroup(dateColumn,'5m', [fillmode])* | Same as $__timeGroup but for times stored as unix timestamp (only available in Grafana 5.3+).
+*$__unixEpochGroupAlias(dateColumn,'5m', [fillmode])* | Same as above but also adds a column alias (only available in Grafana 5.3+).
We plan to add many more macros. If you have suggestions for what macros you would like to see, please [open an issue](https://github.com/grafana/grafana) in our GitHub repo.
@@ -104,6 +127,9 @@ The resulting table panel:
If you set `Format as` to `Time series`, for use in Graph panel for example, then the query must return a column named `time` that returns either a sql datetime or any numeric datatype representing unix epoch.
Any column except `time` and `metric` is treated as a value column.
You may return a column named `metric` that is used as metric name for the value column.
+If you return multiple value columns and a column named `metric` then this column is used as prefix for the series name (only available in Grafana 5.3+).
+
+Resultsets of time series queries need to be sorted by time.
**Example with `metric` column:**
diff --git a/docs/sources/features/datasources/postgres.md b/docs/sources/features/datasources/postgres.md
index f9af60a2efc..7076ff033b3 100644
--- a/docs/sources/features/datasources/postgres.md
+++ b/docs/sources/features/datasources/postgres.md
@@ -16,7 +16,7 @@ Grafana ships with a built-in PostgreSQL data source plugin that allows you to q
## Adding the data source
1. Open the side menu by clicking the Grafana icon in the top header.
-2. In the side menu under the `Dashboards` link you should find a link named `Data Sources`.
+2. In the side menu under the `Configuration` icon you should find a link named `Data Sources`.
3. Click the `+ Add data source` button in the top header.
4. Select *PostgreSQL* from the *Type* dropdown.
@@ -31,13 +31,33 @@ Name | Description
*User* | Database user's login/username
*Password* | Database user's password
*SSL Mode* | This option determines whether or with what priority a secure SSL TCP/IP connection will be negotiated with the server.
+*Version* | This option determines which functions are available in the query builder (only available in Grafana 5.3+).
+*TimescaleDB* | TimescaleDB is a time-series database built as a PostgreSQL extension. If enabled, Grafana will use `time_bucket` in the `$__timeGroup` macro and display TimescaleDB specific aggregate functions in the query builder (only available in Grafana 5.3+).
+
+### Min time interval
+
+A lower limit for the [$__interval](/reference/templating/#the-interval-variable) and [$__interval_ms](/reference/templating/#the-interval-ms-variable) variables.
+Recommended to be set to write frequency, for example `1m` if your data is written every minute.
+This option can also be overridden/configured in a dashboard panel under data source options. It's important to note that this value **needs** to be formatted as a
+number followed by a valid time identifier, e.g. `1m` (1 minute) or `30s` (30 seconds). The following time identifiers are supported:
+
+Identifier | Description
+------------ | -------------
+`y` | year
+`M` | month
+`w` | week
+`d` | day
+`h` | hour
+`m` | minute
+`s` | second
+`ms` | millisecond
### Database User Permissions (Important!)
The database user you specify when you add the data source should only be granted SELECT permissions on
the specified database & tables you want to query. Grafana does not validate that the query is safe. The query
could include any SQL statement. For example, statements like `DELETE FROM user;` and `DROP TABLE user;` would be
-executed. To protect against this we **Highly** recommmend you create a specific postgresql user with restricted permissions.
+executed. To protect against this we **highly** recommend you create a specific PostgreSQL user with restricted permissions.
Example:
@@ -49,9 +69,72 @@ Example:
Make sure the user does not get any unwanted privileges from the public role.
+## Query Editor
+
+> Only available in Grafana v5.3+.
+
+{{< docs-imagebox img="/img/docs/v53/postgres_query_still.png" class="docs-image--no-shadow" animated-gif="/img/docs/v53/postgres_query.gif" >}}
+
+You find the PostgreSQL query editor in the metrics tab in Graph or Singlestat panel's edit mode. You enter edit mode by clicking the
+panel title, then edit.
+
+The query editor has a link named `Generated SQL` that shows up after a query has been executed, while in panel edit mode. Click on it and it will expand and show the raw interpolated SQL string that was executed.
+
+### Select table, time column and metric column (FROM)
+
+When you enter edit mode for the first time or add a new query Grafana will try to prefill the query builder with the first table that has a timestamp column and a numeric column.
+
+In the FROM field, Grafana will suggest tables that are in the `search_path` of the database user. To select a table or view not in your `search_path`
+you can manually enter a fully qualified name (schema.table) like `public.metrics`.
+
+The Time column field refers to the name of the column holding your time values. Selecting a value for the Metric column field is optional. If a value is selected, the Metric column field will be used as the series name.
+
+The metric column suggestions will only contain columns with a text datatype (char,varchar,text).
+If you want to use a column with a different datatype as metric column you may enter the column name with a cast: `ip::text`.
+You may also enter arbitrary SQL expressions in the metric column field that evaluate to a text datatype like
+`hostname || ' ' || container_name`.
+
+### Columns, Window and Aggregation functions (SELECT)
+
+In the `SELECT` row you can specify what columns and functions you want to use.
+In the column field you may write arbitrary expressions instead of a column name like `column1 * column2 / column3`.
+
+The available functions in the query editor depend on the PostgreSQL version you selected when configuring the datasource.
+If you use aggregate functions you need to group your resultset. The editor will automatically add a `GROUP BY time` if you add an aggregate function.
+
+The editor tries to simplify and unify this part of the query. For example:
+
+
+The above will generate the following PostgreSQL `SELECT` clause:
+
+```sql
+avg(tx_bytes) OVER (ORDER BY "time" ROWS 5 PRECEDING) AS "tx_bytes"
+```
+
+You may add further value columns by clicking the plus button and selecting `Column` from the menu. Multiple value columns will be plotted as separate series in the graph panel.
+
+### Filter data (WHERE)
+To add a filter click the plus icon to the right of the `WHERE` condition. You can remove filters by clicking on
+the filter and selecting `Remove`. A filter for the current selected timerange is automatically added to new queries.
+
+### Group By
+To group by time or any other columns click the plus icon at the end of the GROUP BY row. The suggestion dropdown will only show text columns of your currently selected table but you may manually enter any column.
+You can remove the group by clicking on the item and then selecting `Remove`.
+
+If you add any grouping, all selected columns need to have an aggregate function applied. The query builder will automatically add aggregate functions to all columns without aggregate functions when you add groupings.
+
+#### Gap Filling
+
+Grafana can fill in missing values when you group by time. The time function accepts two arguments. The first argument is the time window that you would like to group by, and the second argument is the value you want Grafana to fill missing items with.
+
+### Text Editor Mode (RAW)
+You can switch to the raw query editor mode by clicking the hamburger icon and selecting `Switch editor mode` or by clicking `Edit SQL` below the query.
+
+> If you use the raw query editor, be sure your query at minimum has `ORDER BY time` and a filter on the returned time range.
+
## Macros
-To simplify syntax and to allow for dynamic parts, like date range filters, the query can contain macros.
+Macros can be used within a query to simplify syntax and allow for dynamic parts.
Macro example | Description
------------ | -------------
@@ -60,16 +143,19 @@ Macro example | Description
*$__timeFilter(dateColumn)* | Will be replaced by a time range filter using the specified column name. For example, *dateColumn BETWEEN '2017-04-21T05:01:17Z' AND '2017-04-21T05:06:17Z'*
*$__timeFrom()* | Will be replaced by the start of the currently active time selection. For example, *'2017-04-21T05:01:17Z'*
*$__timeTo()* | Will be replaced by the end of the currently active time selection. For example, *'2017-04-21T05:06:17Z'*
-*$__timeGroup(dateColumn,'5m')* | Will be replaced by an expression usable in GROUP BY clause. For example, *(extract(epoch from dateColumn)/300)::bigint*300 AS time*
-*$__timeGroup(dateColumn,'5m', 0)* | Same as above but with a fill parameter so all null values will be converted to the fill value (all null values would be set to zero using this example).
-*$__unixEpochFilter(dateColumn)* | Will be replaced by a time range filter using the specified column name with times represented as unix timestamp. For example, *dateColumn >= 1494410783 AND dateColumn <= 1494497183*
+*$__timeGroup(dateColumn,'5m')* | Will be replaced by an expression usable in a GROUP BY clause. For example, *(extract(epoch from dateColumn)/300)::bigint*300*
+*$__timeGroup(dateColumn,'5m', 0)* | Same as above but with a fill parameter so missing points in that series will be added by Grafana and 0 will be used as the value.
+*$__timeGroup(dateColumn,'5m', NULL)* | Same as above but NULL will be used as value for missing points.
+*$__timeGroup(dateColumn,'5m', previous)* | Same as above but the previous value in that series will be used as fill value. If no value has been seen yet, NULL will be used (only available in Grafana 5.3+).
+*$__timeGroupAlias(dateColumn,'5m')* | Will be replaced with an expression identical to $__timeGroup, but with an added column alias (only available in Grafana 5.3+).
+*$__unixEpochFilter(dateColumn)* | Will be replaced by a time range filter using the specified column name with times represented as unix timestamps. For example, *dateColumn >= 1494410783 AND dateColumn <= 1494497183*
*$__unixEpochFrom()* | Will be replaced by the start of the currently active time selection as unix timestamp. For example, *1494410783*
*$__unixEpochTo()* | Will be replaced by the end of the currently active time selection as unix timestamp. For example, *1494497183*
+*$__unixEpochGroup(dateColumn,'5m', [fillmode])* | Same as $__timeGroup, but for times stored as unix timestamp (only available in Grafana 5.3+).
+*$__unixEpochGroupAlias(dateColumn,'5m', [fillmode])* | Same as above, but also adds a column alias (only available in Grafana 5.3+).
We plan to add many more macros. If you have suggestions for what macros you would like to see, please [open an issue](https://github.com/grafana/grafana) in our GitHub repo.
-The query editor has a link named `Generated SQL` that shows up after a query as been executed, while in panel edit mode. Click on it and it will expand and show the raw interpolated SQL string that was executed.
-
## Table queries
If the `Format as` query option is set to `Table` then you can basically do any type of SQL query. The table panel will automatically show the results of whatever columns & rows your query returns.
@@ -99,9 +185,12 @@ The resulting table panel:
## Time series queries
-If you set `Format as` to `Time series`, for use in Graph panel for example, then the query must return a column named `time` that returns either a sql datetime or any numeric datatype representing unix epoch.
-Any column except `time` and `metric` is treated as a value column.
+If you set `Format as` to `Time series`, for use in Graph panel for example, then the query must return a column named `time` that returns either a SQL datetime or any numeric datatype representing unix epoch.
+Any column except `time` and `metric` are treated as a value column.
You may return a column named `metric` that is used as metric name for the value column.
+If you return multiple value columns and a column named `metric` then this column is used as prefix for the series name (only available in Grafana 5.3+).
+
+Resultsets of time series queries need to be sorted by time.
**Example with `metric` column:**
@@ -178,7 +267,7 @@ Another option is a query that can create a key/value variable. The query should
SELECT hostname AS __text, id AS __value FROM host
```
-You can also create nested variables. For example if you had another variable named `region`. Then you could have
+You can also create nested variables. Using a variable named `region`, you could have
the hosts variable only show hosts from the current selected region with a query like this (if `region` is a multi-value variable then use the `IN` comparison operator rather than `=` to match against multiple values):
```sql
@@ -187,7 +276,7 @@ SELECT hostname FROM host WHERE region IN($region)
### Using Variables in Queries
-From Grafana 4.3.0 to 4.6.0, template variables are always quoted automatically so if it is a string value do not wrap them in quotes in where clauses.
+From Grafana 4.3.0 to 4.6.0, template variables are always quoted automatically. If your template variables are strings, do not wrap them in quotes in where clauses.
From Grafana 4.7.0, template variable values are only quoted when the template variable is a `multi-value`.
@@ -219,7 +308,7 @@ ORDER BY atimestamp ASC
#### Disabling Quoting for Multi-value Variables
-Grafana automatically creates a quoted, comma-separated string for multi-value variables. For example: if `server01` and `server02` are selected then it will be formatted as: `'server01', 'server02'`. Do disable quoting, use the csv formatting option for variables:
+Grafana automatically creates a quoted, comma-separated string for multi-value variables. For example: if `server01` and `server02` are selected then it will be formatted as: `'server01', 'server02'`. To disable quoting, use the csv formatting option for variables:
`${servers:csv}`
@@ -263,7 +352,7 @@ tags | Optional field name to use for event tags as a comma separated string.
## Alerting
-Time series queries should work in alerting conditions. Table formatted queries is not yet supported in alert rule
+Time series queries should work in alerting conditions. Table formatted queries are not yet supported in alert rule
conditions.
## Configure the Datasource with Provisioning
@@ -285,4 +374,6 @@ datasources:
password: "Password!"
jsonData:
sslmode: "disable" # disable/require/verify-ca/verify-full
+ postgresVersion: 903 # 903=9.3, 904=9.4, 905=9.5, 906=9.6, 1000=10
+ timescaledb: false
```
diff --git a/docs/sources/features/datasources/prometheus.md b/docs/sources/features/datasources/prometheus.md
index 4ff0baee108..611a3b4d9e2 100644
--- a/docs/sources/features/datasources/prometheus.md
+++ b/docs/sources/features/datasources/prometheus.md
@@ -75,6 +75,32 @@ Name | Description
For details of *metric names*, *label names* and *label values* are please refer to the [Prometheus documentation](http://prometheus.io/docs/concepts/data_model/#metric-names-and-labels).
+
+#### Using interval and range variables
+
+> Support for `$__range`, `$__range_s` and `$__range_ms` only available from Grafana v5.3
+
+It's possible to use some global built-in variables in query variables; `$__interval`, `$__interval_ms`, `$__range`, `$__range_s` and `$__range_ms`, see [Global built-in variables](/reference/templating/#global-built-in-variables) for more information. These can be convenient to use in conjunction with the `query_result` function when you need to filter variable queries since
+`label_values` function doesn't support queries.
+
+Make sure to set the variable's `refresh` trigger to be `On Time Range Change` to get the correct instances when changing the time range on the dashboard.
+
+**Example usage:**
+
+Populate a variable with the the busiest 5 request instances based on average QPS over the time range shown in the dashboard:
+
+```
+Query: query_result(topk(5, sum(rate(http_requests_total[$__range])) by (instance)))
+Regex: /"([^"]+)"/
+```
+
+Populate a variable with the instances having a certain state over the time range shown in the dashboard, using the more precise `$__range_s`:
+
+```
+Query: query_result(max_over_time([${__range_s}s]) != )
+Regex:
+```
+
### Using variables in queries
There are two syntaxes:
diff --git a/docs/sources/guides/basic_concepts.md b/docs/sources/guides/basic_concepts.md
index b710a227a79..d3f8dd0ba63 100644
--- a/docs/sources/guides/basic_concepts.md
+++ b/docs/sources/guides/basic_concepts.md
@@ -54,7 +54,7 @@ We utilize a unit abstraction so that Grafana looks great on all screens both sm
> Note: With MaxDataPoint functionality, Grafana can show you the perfect amount of datapoints no matter your resolution or time-range.
-Utilize the [Repeating Row functionality](/reference/templating/#utilizing-template-variables-with-repeating-panels-and-repeating-rows) to dynamically create or remove entire Rows (that can be filled with Panels), based on the Template variables selected.
+Utilize the [Repeating Rows functionality](/reference/templating/#repeating-rows) to dynamically create or remove entire Rows (that can be filled with Panels), based on the Template variables selected.
Rows can be collapsed by clicking on the Row Title. If you save a Dashboard with a Row collapsed, it will save in that state and will not preload those graphs until the row is expanded.
@@ -72,7 +72,7 @@ Panels like the [Graph](/reference/graph/) panel allow you to graph as many metr
Panels can be made more dynamic by utilizing [Dashboard Templating](/reference/templating/) variable strings within the panel configuration (including queries to your Data Source configured via the Query Editor).
-Utilize the [Repeating Panel](/reference/templating/#utilizing-template-variables-with-repeating-panels-and-repeating-rows) functionality to dynamically create or remove Panels based on the [Templating Variables](/reference/templating/#utilizing-template-variables-with-repeating-panels-and-repeating-rows) selected.
+Utilize the [Repeating Panel](/reference/templating/#repeating-panels) functionality to dynamically create or remove Panels based on the [Templating Variables](/reference/templating/#repeating-panels) selected.
The time range on Panels is normally what is set in the [Dashboard time picker](/reference/timerange/) but this can be overridden by utilizes [Panel specific time overrides](/reference/timerange/#panel-time-overrides-timeshift).
diff --git a/docs/sources/guides/getting_started.md b/docs/sources/guides/getting_started.md
index f724504156f..27957990265 100644
--- a/docs/sources/guides/getting_started.md
+++ b/docs/sources/guides/getting_started.md
@@ -13,7 +13,35 @@ weight = 1
# Getting started
-This guide will help you get started and acquainted with Grafana. It assumes you have a working Grafana server up and running and have added at least one [Data Source](/features/datasources/).
+This guide will help you get started and acquainted with Grafana. It assumes you have a working Grafana server up and running. If not please read the [installation guide](/installation/).
+
+## Logging in for the first time
+
+To run Grafana open your browser and go to http://localhost:3000/. 3000 is the default http port that Grafana listens to if you haven't [configured a different port](/installation/configuration/#http-port).
+
+There you will see the login page. Default username is admin and default password is admin. When you log in for the first time you will be asked to change your password. We strongly encourage you to
+follow Grafana’s best practices and change the default administrator password. You can later go to user preferences and change your user name.
+
+
+## How to add a data source
+
+{{< docs-imagebox img="/img/docs/v52/sidemenu-datasource.png" max-width="250px" class="docs-image--right docs-image--no-shadow">}}
+
+Before you create your first dashboard you need to add your data source.
+
+First move your cursor to the cog on the side menu which will show you the configuration menu. If the side menu is not visible click the Grafana icon in the upper left corner. The first item on the configuration menu is data sources, click on that and you'll be taken to the data sources page where you can add and edit data sources. You can also simply click the cog.
+
+
+Click Add data source and you will come to the settings page of your new data source.
+
+{{< docs-imagebox img="/img/docs/v52/add-datasource.png" max-width="700px" class="docs-image--no-shadow">}}
+
+First, give the data source a Name and then select which Type of data source you'll want to create, see [Supported data sources](/features/datasources/#supported-data-sources/) for more information and how to configure your data source.
+
+
+{{< docs-imagebox img="/img/docs/v52/datasource-settings.png" max-width="700px" class="docs-image--no-shadow">}}
+
+After you have configuered your data source you are ready to save and test.
## Beginner guides
@@ -41,7 +69,7 @@ The image above shows you the top header for a Dashboard.
## Dashboards, Panels, the building blocks of Grafana...
-Dashboards are at the core of what Grafana is all about. Dashboards are composed of individual Panels arranged on a grid. Grafana ships with a variety of Panels. Grafana makes it easy to construct the right queries, and customize the display properties so that you can create the perfect Dashboard for your need. Each Panel can interact with data from any configured Grafana Data Source (currently InfluxDB, Graphite, OpenTSDB, Prometheus and Cloudwatch). The [Basic Concepts](/guides/basic_concepts) guide explores these key ideas in detail.
+Dashboards are at the core of what Grafana is all about. Dashboards are composed of individual Panels arranged on a grid. Grafana ships with a variety of Panels. Grafana makes it easy to construct the right queries, and customize the display properties so that you can create the perfect Dashboard for your need. Each Panel can interact with data from any configured Grafana Data Source (currently Graphite, Prometheus, Elasticsearch, InfluxDB, OpenTSDB, MySQL, PostgreSQL, Microsoft SQL Server and AWS Cloudwatch). The [Basic Concepts](/guides/basic_concepts) guide explores these key ideas in detail.
diff --git a/docs/sources/guides/whats-new-in-v5-3.md b/docs/sources/guides/whats-new-in-v5-3.md
new file mode 100644
index 00000000000..4a2674c9b39
--- /dev/null
+++ b/docs/sources/guides/whats-new-in-v5-3.md
@@ -0,0 +1,18 @@
++++
+title = "What's New in Grafana v5.3"
+description = "Feature & improvement highlights for Grafana v5.3"
+keywords = ["grafana", "new", "documentation", "5.3"]
+type = "docs"
+[menu.docs]
+name = "Version 5.3"
+identifier = "v5.3"
+parent = "whatsnew"
+weight = -9
++++
+
+# What's New in Grafana v5.3
+
+## Changelog
+
+Checkout the [CHANGELOG.md](https://github.com/grafana/grafana/blob/master/CHANGELOG.md) file for a complete list
+of new features, changes, and bug fixes.
diff --git a/docs/sources/http_api/alerting.md b/docs/sources/http_api/alerting.md
index e4fe0dad3ff..032fd508dd0 100644
--- a/docs/sources/http_api/alerting.md
+++ b/docs/sources/http_api/alerting.md
@@ -50,6 +50,7 @@ Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
```http
HTTP/1.1 200
Content-Type: application/json
+
[
{
"id": 1,
@@ -59,7 +60,6 @@ Content-Type: application/json
"panelId": 1,
"name": "fire place sensor",
"state": "alerting",
- "message": "Someone is trying to break in through the fire place",
"newStateDate": "2018-05-14T05:55:20+02:00",
"evalDate": "0001-01-01T00:00:00Z",
"evalData": null,
@@ -87,6 +87,7 @@ Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
```http
HTTP/1.1 200
Content-Type: application/json
+
{
"id": 1,
"dashboardId": 1,
@@ -147,6 +148,7 @@ JSON Body Schema:
```http
HTTP/1.1 200
Content-Type: application/json
+
{
"alertId": 1,
"state": "Paused",
@@ -178,6 +180,7 @@ JSON Body Schema:
```http
HTTP/1.1 200
Content-Type: application/json
+
{
"state": "Paused",
"message": "alert paused",
@@ -205,14 +208,21 @@ Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
HTTP/1.1 200
Content-Type: application/json
-{
- "id": 1,
- "name": "Team A",
- "type": "email",
- "isDefault": true,
- "created": "2017-01-01 12:45",
- "updated": "2017-01-01 12:45"
-}
+[
+ {
+ "id": 1,
+ "name": "Team A",
+ "type": "email",
+ "isDefault": false,
+ "sendReminder": false,
+ "settings": {
+ "addresses": "carl@grafana.com;dev@grafana.com"
+ },
+ "created": "2018-04-23T14:44:09+02:00",
+ "updated": "2018-08-20T15:47:49+02:00"
+ }
+]
+
```
## Create alert notification
@@ -233,6 +243,7 @@ Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
"name": "new alert notification", //Required
"type": "email", //Required
"isDefault": false,
+ "sendReminder": false,
"settings": {
"addresses": "carl@grafana.com;dev@grafana.com"
}
@@ -244,14 +255,18 @@ Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
```http
HTTP/1.1 200
Content-Type: application/json
+
{
"id": 1,
"name": "new alert notification",
"type": "email",
"isDefault": false,
- "settings": { addresses: "carl@grafana.com;dev@grafana.com"} }
- "created": "2017-01-01 12:34",
- "updated": "2017-01-01 12:34"
+ "sendReminder": false,
+ "settings": {
+ "addresses": "carl@grafana.com;dev@grafana.com"
+ },
+ "created": "2018-04-23T14:44:09+02:00",
+ "updated": "2018-08-20T15:47:49+02:00"
}
```
@@ -272,6 +287,8 @@ Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
"name": "new alert notification", //Required
"type": "email", //Required
"isDefault": false,
+ "sendReminder": true,
+ "frequency": "15m",
"settings": {
"addresses: "carl@grafana.com;dev@grafana.com"
}
@@ -283,12 +300,17 @@ Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
```http
HTTP/1.1 200
Content-Type: application/json
+
{
"id": 1,
"name": "new alert notification",
"type": "email",
"isDefault": false,
- "settings": { addresses: "carl@grafana.com;dev@grafana.com"} }
+ "sendReminder": true,
+ "frequency": "15m",
+ "settings": {
+ "addresses": "carl@grafana.com;dev@grafana.com"
+ },
"created": "2017-01-01 12:34",
"updated": "2017-01-01 12:34"
}
@@ -312,6 +334,7 @@ Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
```http
HTTP/1.1 200
Content-Type: application/json
+
{
"message": "Notification deleted"
}
diff --git a/docs/sources/http_api/auth.md b/docs/sources/http_api/auth.md
index 8ff40b5ef04..e87d3571322 100644
--- a/docs/sources/http_api/auth.md
+++ b/docs/sources/http_api/auth.md
@@ -5,7 +5,7 @@ keywords = ["grafana", "http", "documentation", "api", "authentication"]
aliases = ["/http_api/authentication/"]
type = "docs"
[menu.docs]
-name = "Authentication"
+name = "Authentication HTTP API"
parent = "http_api"
+++
diff --git a/docs/sources/http_api/dashboard.md b/docs/sources/http_api/dashboard.md
index ea1bd7f2ef7..3df36894901 100644
--- a/docs/sources/http_api/dashboard.md
+++ b/docs/sources/http_api/dashboard.md
@@ -85,7 +85,7 @@ Status Codes:
- **403** – Access denied
- **412** – Precondition failed
-The **412** status code is used for explaing that you cannot create the dashboard and why.
+The **412** status code is used for explaining that you cannot create the dashboard and why.
There can be different reasons for this:
- The dashboard has been changed by someone else, `status=version-mismatch`
diff --git a/docs/sources/http_api/folder.md b/docs/sources/http_api/folder.md
index fb318ecf58e..e8845c3b125 100644
--- a/docs/sources/http_api/folder.md
+++ b/docs/sources/http_api/folder.md
@@ -223,7 +223,7 @@ Status Codes:
- **404** – Folder not found
- **412** – Precondition failed
-The **412** status code is used for explaing that you cannot update the folder and why.
+The **412** status code is used for explaining that you cannot update the folder and why.
There can be different reasons for this:
- The folder has been changed by someone else, `status=version-mismatch`
diff --git a/docs/sources/http_api/user.md b/docs/sources/http_api/user.md
index 134c1842851..b9047187b2d 100644
--- a/docs/sources/http_api/user.md
+++ b/docs/sources/http_api/user.md
@@ -363,6 +363,39 @@ Content-Type: application/json
]
```
+## Teams that the actual User is member of
+
+`GET /api/user/teams`
+
+Return a list of all teams that the current user is member of.
+
+**Example Request**:
+
+```http
+GET /api/user/teams HTTP/1.1
+Accept: application/json
+Content-Type: application/json
+Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
+```
+
+**Example Response**:
+
+```http
+HTTP/1.1 200
+Content-Type: application/json
+
+[
+ {
+ "id": 1,
+ "orgId": 1,
+ "name": "MyTestTeam",
+ "email": "",
+ "avatarUrl": "\/avatar\/3f49c15916554246daa714b9bd0ee398",
+ "memberCount": 1
+ }
+]
+```
+
## Star a dashboard
`POST /api/user/stars/dashboard/:dashboardId`
diff --git a/docs/sources/installation/configuration.md b/docs/sources/installation/configuration.md
index 8eee32bd616..2bf4789257d 100644
--- a/docs/sources/installation/configuration.md
+++ b/docs/sources/installation/configuration.md
@@ -84,7 +84,7 @@ command line in the init.d script or the systemd service file.
### temp_data_lifetime
-How long temporary images in `data` directory should be kept. Defaults to: `24h`. Supported modifiers: `h` (hours),
+How long temporary images in `data` directory should be kept. Defaults to: `24h`. Supported modifiers: `h` (hours),
`m` (minutes), for example: `168h`, `30m`, `10h30m`. Use `0` to never clean up temporary files.
### logs
@@ -181,7 +181,7 @@ embedded database (included in the main Grafana binary).
### url
-Use either URL or or the other fields below to configure the database
+Use either URL or the other fields below to configure the database
Example: `mysql://user:secret@host:port/database`
### type
@@ -195,9 +195,9 @@ will be stored.
### host
-Only applicable to MySQL or Postgres. Includes IP or hostname and port.
+Only applicable to MySQL or Postgres. Includes IP or hostname and port or in case of unix sockets the path to it.
For example, for MySQL running on the same host as Grafana: `host =
-127.0.0.1:3306`
+127.0.0.1:3306` or with unix sockets: `host = /var/run/mysqld/mysqld.sock`
### name
@@ -266,7 +266,8 @@ The number of days the keep me logged in / remember me cookie lasts.
### secret_key
-Used for signing keep me logged in / remember me cookies.
+Used for signing some datasource settings like secrets and passwords. Cannot be changed without requiring an update
+to datasource settings to re-encode them.
### disable_gravatar
@@ -321,368 +322,17 @@ Defaults to `false`.
## [auth]
-### disable_login_form
-
-Set to true to disable (hide) the login form, useful if you use OAuth, defaults to false.
-
-### disable_signout_menu
-
-Set to true to disable the signout link in the side menu. useful if you use auth.proxy, defaults to false.
-
-
-
-## [auth.anonymous]
-
-### enabled
-
-Set to `true` to enable anonymous access. Defaults to `false`
-
-### org_name
-
-Set the organization name that should be used for anonymous users. If
-you change your organization name in the Grafana UI this setting needs
-to be updated to match the new name.
-
-### org_role
-
-Specify role for anonymous users. Defaults to `Viewer`, other valid
-options are `Editor` and `Admin`.
-
-## [auth.github]
-
-You need to create a GitHub OAuth application (you find this under the GitHub
-settings page). When you create the application you will need to specify
-a callback URL. Specify this as callback:
-
-```bash
-http://:/login/github
-```
-
-This callback URL must match the full HTTP address that you use in your
-browser to access Grafana, but with the prefix path of `/login/github`.
-When the GitHub OAuth application is created you will get a Client ID and a
-Client Secret. Specify these in the Grafana configuration file. For
-example:
-
-```bash
-[auth.github]
-enabled = true
-allow_sign_up = true
-client_id = YOUR_GITHUB_APP_CLIENT_ID
-client_secret = YOUR_GITHUB_APP_CLIENT_SECRET
-scopes = user:email,read:org
-auth_url = https://github.com/login/oauth/authorize
-token_url = https://github.com/login/oauth/access_token
-api_url = https://api.github.com/user
-team_ids =
-allowed_organizations =
-```
-
-Restart the Grafana back-end. You should now see a GitHub login button
-on the login page. You can now login or sign up with your GitHub
-accounts.
-
-You may allow users to sign-up via GitHub authentication by setting the
-`allow_sign_up` option to `true`. When this option is set to `true`, any
-user successfully authenticating via GitHub authentication will be
-automatically signed up.
-
-### team_ids
-
-Require an active team membership for at least one of the given teams on
-GitHub. If the authenticated user isn't a member of at least one of the
-teams they will not be able to register or authenticate with your
-Grafana instance. For example:
-
-```bash
-[auth.github]
-enabled = true
-client_id = YOUR_GITHUB_APP_CLIENT_ID
-client_secret = YOUR_GITHUB_APP_CLIENT_SECRET
-scopes = user:email,read:org
-team_ids = 150,300
-auth_url = https://github.com/login/oauth/authorize
-token_url = https://github.com/login/oauth/access_token
-api_url = https://api.github.com/user
-allow_sign_up = true
-```
-
-### allowed_organizations
-
-Require an active organization membership for at least one of the given
-organizations on GitHub. If the authenticated user isn't a member of at least
-one of the organizations they will not be able to register or authenticate with
-your Grafana instance. For example
-
-```bash
-[auth.github]
-enabled = true
-client_id = YOUR_GITHUB_APP_CLIENT_ID
-client_secret = YOUR_GITHUB_APP_CLIENT_SECRET
-scopes = user:email,read:org
-auth_url = https://github.com/login/oauth/authorize
-token_url = https://github.com/login/oauth/access_token
-api_url = https://api.github.com/user
-allow_sign_up = true
-# space-delimited organization names
-allowed_organizations = github google
-```
-
-
-
-## [auth.google]
-
-First, you need to create a Google OAuth Client:
-
-1. Go to https://console.developers.google.com/apis/credentials
-
-2. Click the 'Create Credentials' button, then click 'OAuth Client ID' in the
-menu that drops down
-
-3. Enter the following:
-
- - Application Type: Web Application
- - Name: Grafana
- - Authorized Javascript Origins: https://grafana.mycompany.com
- - Authorized Redirect URLs: https://grafana.mycompany.com/login/google
-
- Replace https://grafana.mycompany.com with the URL of your Grafana instance.
-
-4. Click Create
-
-5. Copy the Client ID and Client Secret from the 'OAuth Client' modal
-
-Specify the Client ID and Secret in the Grafana configuration file. For example:
-
-```bash
-[auth.google]
-enabled = true
-client_id = CLIENT_ID
-client_secret = CLIENT_SECRET
-scopes = https://www.googleapis.com/auth/userinfo.profile https://www.googleapis.com/auth/userinfo.email
-auth_url = https://accounts.google.com/o/oauth2/auth
-token_url = https://accounts.google.com/o/oauth2/token
-allowed_domains = mycompany.com mycompany.org
-allow_sign_up = true
-```
-
-Restart the Grafana back-end. You should now see a Google login button
-on the login page. You can now login or sign up with your Google
-accounts. The `allowed_domains` option is optional, and domains were separated by space.
-
-You may allow users to sign-up via Google authentication by setting the
-`allow_sign_up` option to `true`. When this option is set to `true`, any
-user successfully authenticating via Google authentication will be
-automatically signed up.
-
-## [auth.generic_oauth]
-
-This option could be used if have your own oauth service.
-
-This callback URL must match the full HTTP address that you use in your
-browser to access Grafana, but with the prefix path of `/login/generic_oauth`.
-
-```bash
-[auth.generic_oauth]
-enabled = true
-client_id = YOUR_APP_CLIENT_ID
-client_secret = YOUR_APP_CLIENT_SECRET
-scopes =
-auth_url =
-token_url =
-api_url =
-allowed_domains = mycompany.com mycompany.org
-allow_sign_up = true
-```
-
-Set api_url to the resource that returns [OpenID UserInfo](https://connect2id.com/products/server/docs/api/userinfo) compatible information.
-
-### Set up oauth2 with Okta
-
-First set up Grafana as an OpenId client "webapplication" in Okta. Then set the Base URIs to `https:///` and set the Login redirect URIs to `https:///login/generic_oauth`.
-
-Finally set up the generic oauth module like this:
-```bash
-[auth.generic_oauth]
-name = Okta
-enabled = true
-scopes = openid profile email
-client_id =
-client_secret =
-auth_url = https:///oauth2/v1/authorize
-token_url = https:///oauth2/v1/token
-api_url = https:///oauth2/v1/userinfo
-```
-
-### Set up oauth2 with Bitbucket
-
-```bash
-[auth.generic_oauth]
-name = BitBucket
-enabled = true
-allow_sign_up = true
-client_id =
-client_secret =
-scopes = account email
-auth_url = https://bitbucket.org/site/oauth2/authorize
-token_url = https://bitbucket.org/site/oauth2/access_token
-api_url = https://api.bitbucket.org/2.0/user
-team_ids =
-allowed_organizations =
-```
-
-### Set up oauth2 with OneLogin
-
-1. Create a new Custom Connector with the following settings:
- - Name: Grafana
- - Sign On Method: OpenID Connect
- - Redirect URI: `https:///login/generic_oauth`
- - Signing Algorithm: RS256
- - Login URL: `https:///login/generic_oauth`
-
- then:
-2. Add an App to the Grafana Connector:
- - Display Name: Grafana
-
- then:
-3. Under the SSO tab on the Grafana App details page you'll find the Client ID and Client Secret.
-
- Your OneLogin Domain will match the url you use to access OneLogin.
-
- Configure Grafana as follows:
-
- ```bash
- [auth.generic_oauth]
- name = OneLogin
- enabled = true
- allow_sign_up = true
- client_id =
- client_secret =
- scopes = openid email name
- auth_url = https://.onelogin.com/oidc/auth
- token_url = https://.onelogin.com/oidc/token
- api_url = https://.onelogin.com/oidc/me
- team_ids =
- allowed_organizations =
- ```
-
-### Set up oauth2 with Auth0
-
-1. Create a new Client in Auth0
- - Name: Grafana
- - Type: Regular Web Application
-
-2. Go to the Settings tab and set:
- - Allowed Callback URLs: `https:///login/generic_oauth`
-
-3. Click Save Changes, then use the values at the top of the page to configure Grafana:
-
- ```bash
- [auth.generic_oauth]
- enabled = true
- allow_sign_up = true
- team_ids =
- allowed_organizations =
- name = Auth0
- client_id =
- client_secret =
- scopes = openid profile email
- auth_url = https:///authorize
- token_url = https:///oauth/token
- api_url = https:///userinfo
- ```
-
-### Set up oauth2 with Azure Active Directory
-
-1. Log in to portal.azure.com and click "Azure Active Directory" in the side menu, then click the "Properties" sub-menu item.
-
-2. Copy the "Directory ID", this is needed for setting URLs later
-
-3. Click "App Registrations" and add a new application registration:
- - Name: Grafana
- - Application type: Web app / API
- - Sign-on URL: `https:///login/generic_oauth`
-
-4. Click the name of the new application to open the application details page.
-
-5. Note down the "Application ID", this will be the OAuth client id.
-
-6. Click "Settings", then click "Keys" and add a new entry under Passwords
- - Key Description: Grafana OAuth
- - Duration: Never Expires
-
-7. Click Save then copy the key value, this will be the OAuth client secret.
-
-8. Configure Grafana as follows:
-
- ```bash
- [auth.generic_oauth]
- name = Azure AD
- enabled = true
- allow_sign_up = true
- client_id =
- client_secret =
- scopes = openid email name
- auth_url = https://login.microsoftonline.com//oauth2/authorize
- token_url = https://login.microsoftonline.com//oauth2/token
- api_url =
- team_ids =
- allowed_organizations =
- ```
-Note: It's important to ensure that the SERVER_ROOT_URL in Grafana is set in your Azure Application Return URLs
-
-
-## [auth.basic]
-### enabled
-When enabled is `true` (default) the http api will accept basic authentication.
-
-
-
-## [auth.ldap]
-### enabled
-Set to `true` to enable LDAP integration (default: `false`)
-
-### config_file
-Path to the LDAP specific configuration file (default: `/etc/grafana/ldap.toml`)
-
-### allow_sign_up
-
-Allow sign up should almost always be true (default) to allow new Grafana users to be created (if ldap authentication is ok). If set to
-false only pre-existing Grafana users will be able to login (if ldap authentication is ok).
-
-> For details on LDAP Configuration, go to the [LDAP Integration]({{< relref "ldap.md" >}}) page.
-
-
-
-## [auth.proxy]
-
-This feature allows you to handle authentication in a http reverse proxy.
-
-### enabled
-
-Defaults to `false`
-
-### header_name
-
-Defaults to X-WEBAUTH-USER
-
-#### header_property
-
-Defaults to username but can also be set to email
-
-### auto_sign_up
-
-Set to `true` to enable auto sign up of users who do not exist in Grafana DB. Defaults to `true`.
-
-### whitelist
-
-Limit where auth proxy requests come from by configuring a list of IP addresses. This can be used to prevent users spoofing the X-WEBAUTH-USER header.
-
-### headers
-
-Used to define additional headers for `Name`, `Email` and/or `Login`, for example if the user's name is sent in the X-WEBAUTH-NAME header and their email address in the X-WEBAUTH-EMAIL header, set `headers = Name:X-WEBAUTH-NAME Email:X-WEBAUTH-EMAIL`.
-
-
+Grafana provides many ways to authenticate users. The docs for authentication has been split in to many different pages
+below.
+
+- [Authentication Overview]({{< relref "auth/overview.md" >}}) (anonymous access options, hide login and more)
+- [Google OAuth]({{< relref "auth/google.md" >}}) (auth.google)
+- [GitHub OAuth]({{< relref "auth/github.md" >}}) (auth.github)
+- [Gitlab OAuth]({{< relref "auth/gitlab.md" >}}) (auth.gitlab)
+- [Generic OAuth]({{< relref "auth/generic-oauth.md" >}}) (auth.generic_oauth, okta2, auth0, bitbucket, azure)
+- [Basic Authentication]({{< relref "auth/overview.md" >}}) (auth.basic)
+- [LDAP Authentication]({{< relref "auth/ldap.md" >}}) (auth.ldap)
+- [Auth Proxy]({{< relref "auth/auth-proxy.md" >}}) (auth.proxy)
## [session]
@@ -697,9 +347,9 @@ session provider you have configured.
- **file:** session file path, e.g. `data/sessions`
- **mysql:** go-sql-driver/mysql dsn config string, e.g. `user:password@tcp(127.0.0.1:3306)/database_name`
-- **postgres:** ex: user=a password=b host=localhost port=5432 dbname=c sslmode=verify-full
-- **memcache:** ex: 127.0.0.1:11211
-- **redis:** ex: `addr=127.0.0.1:6379,pool_size=100,prefix=grafana`
+- **postgres:** ex: `user=a password=b host=localhost port=5432 dbname=c sslmode=verify-full`
+- **memcache:** ex: `127.0.0.1:11211`
+- **redis:** ex: `addr=127.0.0.1:6379,pool_size=100,prefix=grafana`. For unix socket, use for example: `network=unix,addr=/var/run/redis/redis.sock,pool_size=100,db=grafana`
Postgres valid `sslmode` are `disable`, `require`, `verify-ca`, and `verify-full` (default).
@@ -906,3 +556,13 @@ Defaults to true. Set to false to disable alerting engine and hide Alerting from
### execute_alerts
Makes it possible to turn off alert rule execution.
+
+### error_or_timeout
+> Available in 5.3 and above
+
+Default setting for new alert rules. Defaults to categorize error and timeouts as alerting. (alerting, keep_state)
+
+### nodata_or_nullvalues
+> Available in 5.3 and above
+
+Default setting for how Grafana handles nodata or null values in alerting. (alerting, no_data, keep_state, ok)
diff --git a/docs/sources/installation/debian.md b/docs/sources/installation/debian.md
index 4bb245a586e..13fa3440170 100644
--- a/docs/sources/installation/debian.md
+++ b/docs/sources/installation/debian.md
@@ -166,3 +166,8 @@ To configure Grafana add a configuration file named `custom.ini` to the
Start Grafana by executing `./bin/grafana-server web`. The `grafana-server`
binary needs the working directory to be the root install directory (where the
binary and the `public` folder is located).
+
+## Logging in for the first time
+
+To run Grafana open your browser and go to http://localhost:3000/. 3000 is the default http port that Grafana listens to if you haven't [configured a different port](/installation/configuration/#http-port).
+Then follow the instructions [here](/guides/getting_started/).
\ No newline at end of file
diff --git a/docs/sources/installation/docker.md b/docs/sources/installation/docker.md
index 1f755625699..ba0d6199ba4 100644
--- a/docs/sources/installation/docker.md
+++ b/docs/sources/installation/docker.md
@@ -20,7 +20,7 @@ $ docker run -d -p 3000:3000 grafana/grafana
## Configuration
-All options defined in conf/grafana.ini can be overridden using environment
+All options defined in `conf/grafana.ini` can be overridden using environment
variables by using the syntax `GF__`.
For example:
@@ -38,6 +38,21 @@ The back-end web server has a number of configuration options. Go to the
[Configuration]({{< relref "configuration.md" >}}) page for details on all
those options.
+> For any changes to `conf/grafana.ini` (or corresponding environment variables) to take effect you need to restart Grafana by restarting the Docker container.
+
+### Default Paths
+
+The following settings are hard-coded when launching the Grafana Docker container and can only be overridden using environment variables, not in `conf/grafana.ini`.
+
+Setting | Default value
+----------------------|---------------------------
+GF_PATHS_CONFIG | /etc/grafana/grafana.ini
+GF_PATHS_DATA | /var/lib/grafana
+GF_PATHS_HOME | /usr/share/grafana
+GF_PATHS_LOGS | /var/log/grafana
+GF_PATHS_PLUGINS | /var/lib/grafana/plugins
+GF_PATHS_PROVISIONING | /etc/grafana/provisioning
+
## Running a Specific Version of Grafana
```bash
@@ -49,10 +64,13 @@ $ docker run \
grafana/grafana:5.1.0
```
-## Running of the master branch
+## Running the master branch
-For every successful commit we publish a Grafana container to [`grafana/grafana`](https://hub.docker.com/r/grafana/grafana/tags/) and [`grafana/grafana-dev`](https://hub.docker.com/r/grafana/grafana-dev/tags/). In `grafana/grafana` container we will always overwrite the `master` tag with the latest version. In `grafana/grafana-dev` we will include
-the git commit in the tag. If you run Grafana master in production we **strongly** recommend that you use the later since different machines might run different version of grafana if they pull the master tag at different times.
+For every successful build of the master branch we update the `grafana/grafana:master` tag and create a new tag `grafana/grafana-dev:master-` with the hash of the git commit that was built. This means you can always get the latest version of Grafana.
+
+When running Grafana master in production we **strongly** recommend that you use the `grafana/grafana-dev:master-` tag as that will guarantee that you use a specific version of Grafana instead of whatever was the most recent commit at the time.
+
+For a list of available tags, check out [grafana/grafana](https://hub.docker.com/r/grafana/grafana/tags/) and [grafana/grafana-dev](https://hub.docker.com/r/grafana/grafana-dev/tags/).
## Installing Plugins for Grafana
@@ -212,3 +230,8 @@ chown -R root:root /etc/grafana && \
chown -R grafana:grafana /var/lib/grafana && \
chown -R grafana:grafana /usr/share/grafana
```
+
+## Logging in for the first time
+
+To run Grafana open your browser and go to http://localhost:3000/. 3000 is the default http port that Grafana listens to if you haven't [configured a different port](/installation/configuration/#http-port).
+Then follow the instructions [here](/guides/getting_started/).
\ No newline at end of file
diff --git a/docs/sources/installation/ldap.md b/docs/sources/installation/ldap.md
deleted file mode 100644
index 9a381b9e467..00000000000
--- a/docs/sources/installation/ldap.md
+++ /dev/null
@@ -1,146 +0,0 @@
-+++
-title = "LDAP Authentication"
-description = "Grafana LDAP Authentication Guide "
-keywords = ["grafana", "configuration", "documentation", "ldap"]
-type = "docs"
-[menu.docs]
-name = "LDAP Authentication"
-identifier = "ldap"
-parent = "admin"
-weight = 2
-+++
-
-# LDAP Authentication
-
-Grafana (2.1 and newer) ships with a strong LDAP integration feature. The LDAP integration in Grafana allows your
-Grafana users to login with their LDAP credentials. You can also specify mappings between LDAP
-group memberships and Grafana Organization user roles.
-
-## Configuration
-You turn on LDAP in the [main config file]({{< relref "configuration.md#auth-ldap" >}}) as well as specify the path to the LDAP
-specific configuration file (default: `/etc/grafana/ldap.toml`).
-
-### Example config
-
-```toml
-# To troubleshoot and get more log info enable ldap debug logging in grafana.ini
-# [log]
-# filters = ldap:debug
-
-[[servers]]
-# Ldap server host (specify multiple hosts space separated)
-host = "127.0.0.1"
-# Default port is 389 or 636 if use_ssl = true
-port = 389
-# Set to true if ldap server supports TLS
-use_ssl = false
-# Set to true if connect ldap server with STARTTLS pattern (create connection in insecure, then upgrade to secure connection with TLS)
-start_tls = false
-# set to true if you want to skip ssl cert validation
-ssl_skip_verify = false
-# set to the path to your root CA certificate or leave unset to use system defaults
-# root_ca_cert = "/path/to/certificate.crt"
-
-# Search user bind dn
-bind_dn = "cn=admin,dc=grafana,dc=org"
-# Search user bind password
-# If the password contains # or ; you have to wrap it with triple quotes. Ex """#password;"""
-bind_password = 'grafana'
-
-# User search filter, for example "(cn=%s)" or "(sAMAccountName=%s)" or "(uid=%s)"
-search_filter = "(cn=%s)"
-
-# An array of base dns to search through
-search_base_dns = ["dc=grafana,dc=org"]
-
-# In POSIX LDAP schemas, without memberOf attribute a secondary query must be made for groups.
-# This is done by enabling group_search_filter below. You must also set member_of= "cn"
-# in [servers.attributes] below.
-
-## Group search filter, to retrieve the groups of which the user is a member (only set if memberOf attribute is not available)
-# group_search_filter = "(&(objectClass=posixGroup)(memberUid=%s))"
-## An array of the base DNs to search through for groups. Typically uses ou=groups
-# group_search_base_dns = ["ou=groups,dc=grafana,dc=org"]
-
-# Specify names of the ldap attributes your ldap uses
-[servers.attributes]
-name = "givenName"
-surname = "sn"
-username = "cn"
-member_of = "memberOf"
-email = "email"
-
-# Map ldap groups to grafana org roles
-[[servers.group_mappings]]
-group_dn = "cn=admins,dc=grafana,dc=org"
-org_role = "Admin"
-# To make user an instance admin (Grafana Admin) uncomment line below
-# grafana_admin = true
-# The Grafana organization database id, optional, if left out the default org (id 1) will be used. Setting this allows for multiple group_dn's to be assigned to the same org_role provided the org_id differs
-# org_id = 1
-
-[[servers.group_mappings]]
-group_dn = "cn=users,dc=grafana,dc=org"
-org_role = "Editor"
-
-[[servers.group_mappings]]
-# If you want to match all (or no ldap groups) then you can use wildcard
-group_dn = "*"
-org_role = "Viewer"
-
-```
-
-## Bind & Bind Password
-
-By default the configuration expects you to specify a bind DN and bind password. This should be a read only user that can perform LDAP searches.
-When the user DN is found a second bind is performed with the user provided username & password (in the normal Grafana login form).
-
-```bash
-bind_dn = "cn=admin,dc=grafana,dc=org"
-bind_password = "grafana"
-```
-
-### Single Bind Example
-
-If you can provide a single bind expression that matches all possible users, you can skip the second bind and bind against the user DN directly.
-This allows you to not specify a bind_password in the configuration file.
-
-```bash
-bind_dn = "cn=%s,o=users,dc=grafana,dc=org"
-```
-
-In this case you skip providing a `bind_password` and instead provide a `bind_dn` value with a `%s` somewhere. This will be replaced with the username entered in on the Grafana login page.
-The search filter and search bases settings are still needed to perform the LDAP search to retrieve the other LDAP information (like LDAP groups and email).
-
-## POSIX schema (no memberOf attribute)
-If your ldap server does not support the memberOf attribute add these options:
-
-```toml
-## Group search filter, to retrieve the groups of which the user is a member (only set if memberOf attribute is not available)
-group_search_filter = "(&(objectClass=posixGroup)(memberUid=%s))"
-## An array of the base DNs to search through for groups. Typically uses ou=groups
-group_search_base_dns = ["ou=groups,dc=grafana,dc=org"]
-```
-
-Also change set `member_of = "cn"` in the `[servers.attributes]` section.
-
-
-## LDAP to Grafana Org Role Sync
-
-### Mappings
-In `[[servers.group_mappings]]` you can map an LDAP group to a Grafana organization
-and role. These will be synced every time the user logs in, with LDAP being
-the authoritative source. So, if you change a user's role in the Grafana Org.
-Users page, this change will be reset the next time the user logs in. If you
-change the LDAP groups of a user, the change will take effect the next
-time the user logs in.
-
-### Grafana Admin
-with a servers.group_mappings section you can set grafana_admin = true or false to sync Grafana Admin permission. A Grafana server admin has admin access over all orgs &
-users.
-
-### Priority
-The first group mapping that an LDAP user is matched to will be used for the sync. If you have LDAP users that fit multiple mappings, the topmost mapping in the TOML config will be used.
-
-
-
diff --git a/docs/sources/installation/mac.md b/docs/sources/installation/mac.md
index 12ff4adaab9..fbc00c01737 100644
--- a/docs/sources/installation/mac.md
+++ b/docs/sources/installation/mac.md
@@ -92,3 +92,7 @@ Start Grafana by executing `./bin/grafana-server web`. The `grafana-server`
binary needs the working directory to be the root install directory (where the
binary and the `public` folder is located).
+## Logging in for the first time
+
+To run Grafana open your browser and go to http://localhost:3000/. 3000 is the default http port that Grafana listens to if you haven't [configured a different port](/installation/configuration/#http-port).
+Then follow the instructions [here](/guides/getting_started/).
\ No newline at end of file
diff --git a/docs/sources/installation/rpm.md b/docs/sources/installation/rpm.md
index 13597b9d921..24c301c5763 100644
--- a/docs/sources/installation/rpm.md
+++ b/docs/sources/installation/rpm.md
@@ -193,3 +193,7 @@ Start Grafana by executing `./bin/grafana-server web`. The `grafana-server`
binary needs the working directory to be the root install directory (where the
binary and the `public` folder is located).
+## Logging in for the first time
+
+To run Grafana open your browser and go to http://localhost:3000/. 3000 is the default http port that Grafana listens to if you haven't [configured a different port](/installation/configuration/#http-port).
+Then follow the instructions [here](/guides/getting_started/).
\ No newline at end of file
diff --git a/docs/sources/installation/upgrading.md b/docs/sources/installation/upgrading.md
index c72bb4c0921..a476a38c3c5 100644
--- a/docs/sources/installation/upgrading.md
+++ b/docs/sources/installation/upgrading.md
@@ -109,3 +109,11 @@ positioning system when you load them in v5. Dashboards saved in v5 will not wor
external panel plugins might need to be updated to work properly.
For more details on the new panel positioning system, [click here]({{< relref "reference/dashboard.md#panel-size-position" >}})
+
+## Upgrading to v5.2
+
+One of the database migrations included in this release will update all annotation timestamps from second to millisecond precision. If you have a large amount of annotations the database migration may take a long time to complete which may cause problems if you use systemd to run Grafana.
+
+We've got one report where using systemd, PostgreSQL and a large amount of annotations (table size 1645mb) took 8-20 minutes for the database migration to complete. However, the grafana-server process was killed after 90 seconds by systemd. Any database migration queries in progress when systemd kills the grafana-server process continues to execute in database until finished.
+
+If you're using systemd and have a large amount of annotations consider temporary adjusting the systemd `TimeoutStartSec` setting to something high like `30m` before upgrading.
diff --git a/docs/sources/installation/windows.md b/docs/sources/installation/windows.md
index 5dc87984512..572081a1c54 100644
--- a/docs/sources/installation/windows.md
+++ b/docs/sources/installation/windows.md
@@ -38,6 +38,11 @@ service using that tool.
Read more about the [configuration options]({{< relref "configuration.md" >}}).
+## Logging in for the first time
+
+To run Grafana open your browser and go to the port you configured above, e.g. http://localhost:8080/.
+Then follow the instructions [here](/guides/getting_started/).
+
## Building on Windows
The Grafana backend includes Sqlite3 which requires GCC to compile. So
diff --git a/docs/sources/project/building_from_source.md b/docs/sources/project/building_from_source.md
index a0b553594ce..ea75b9797e8 100644
--- a/docs/sources/project/building_from_source.md
+++ b/docs/sources/project/building_from_source.md
@@ -13,7 +13,7 @@ dev environment. Grafana ships with its own required backend server; also comple
## Dependencies
-- [Go 1.10](https://golang.org/dl/)
+- [Go 1.11](https://golang.org/dl/)
- [Git](https://git-scm.com/downloads)
- [NodeJS LTS](https://nodejs.org/download/)
- node-gyp is the Node.js native addon build tool and it requires extra dependencies: python 2.7, make and GCC. These are already installed for most Linux distros and MacOS. See the Building On Windows section or the [node-gyp installation instructions](https://github.com/nodejs/node-gyp#installation) for more details.
@@ -57,7 +57,7 @@ For this you need nodejs (v.6+).
```bash
npm install -g yarn
yarn install --pure-lockfile
-npm run watch
+yarn watch
```
## Running Grafana Locally
@@ -83,21 +83,18 @@ go get github.com/Unknwon/bra
bra run
```
-You'll also need to run `npm run watch` to watch for changes to the front-end (typescript, html, sass)
+You'll also need to run `yarn watch` to watch for changes to the front-end (typescript, html, sass)
### Running tests
-- You can run backend Golang tests using "go test ./pkg/...".
-- Execute all frontend tests with "npm run test"
+- You can run backend Golang tests using `go test ./pkg/...`.
+- Execute all frontend tests with `yarn test`
-Writing & watching frontend tests (we have two test runners)
+Writing & watching frontend tests
+
+- Start watcher: `yarn jest`
+- Jest will run all test files that end with the name ".test.ts"
-- jest for all new tests that do not require browser context (React+more)
- - Start watcher: `npm run jest`
- - Jest will run all test files that end with the name ".jest.ts"
-- karma + mocha is used for testing angularjs components. We do want to migrate these test to jest over time (if possible).
- - Start watcher: `npm run karma`
- - Karma+Mocha runs all files that end with the name "_specs.ts".
## Creating optimized release packages
@@ -144,3 +141,8 @@ Please contribute to the Grafana project and submit a pull request! Build new fe
**Problem**: On Windows, getting errors about a tool not being installed even though you just installed that tool.
**Solution**: It is usually because it got added to the path and you have to restart your command prompt to use it.
+
+## Logging in for the first time
+
+To run Grafana open your browser and go to the default port http://localhost:3000 or the port you have configured.
+Then follow the instructions [here](/guides/getting_started/).
\ No newline at end of file
diff --git a/docs/sources/reference/annotations.md b/docs/sources/reference/annotations.md
index bfc104ef522..3bb50f4badf 100644
--- a/docs/sources/reference/annotations.md
+++ b/docs/sources/reference/annotations.md
@@ -45,8 +45,9 @@ can still show them if you add a new **Annotation Query** and filter by tags. Bu
### Query by tag
You can create new annotation queries that fetch annotations from the native annotation store via the `-- Grafana --` data source and by setting *Filter by* to `Tags`. Specify at least
-one tag. For example create an annotation query name `outages` and specify a tag named `outage`. This query will show all annotations you create (from any dashboard or via API) that
-have the `outage` tag.
+one tag. For example create an annotation query name `outages` and specify a tag named `outage`. This query will show all annotations you create (from any dashboard or via API) that have the `outage` tag. By default, if you add multiple tags in the annotation query, Grafana will only show annotations that have all the tags you supplied. You can invert the behavior by enabling `Match any` which means that Grafana will show annotations that contains at least one of the tags you supplied.
+
+In 5.4+ it's possible to use template variables in the tag query. So if you have a dashboard showing stats for different services and an template variable that dictates which services to show, you can now use the same template variable in your annotation query to only show annotations for those services.
## Querying other data sources
diff --git a/docs/sources/reference/templating.md b/docs/sources/reference/templating.md
index efe9db61e3d..31251fd6389 100644
--- a/docs/sources/reference/templating.md
+++ b/docs/sources/reference/templating.md
@@ -245,7 +245,7 @@ Grafana has global built-in variables that can be used in expressions in the que
### The $__interval Variable
-This $__interval variable is similar to the `auto` interval variable that is described above. It can be used as a parameter to group by time (for InfluxDB), Date histogram interval (for Elasticsearch) or as a *summarize* function parameter (for Graphite).
+This $__interval variable is similar to the `auto` interval variable that is described above. It can be used as a parameter to group by time (for InfluxDB, MySQL, Postgres, MSSQL), Date histogram interval (for Elasticsearch) or as a *summarize* function parameter (for Graphite).
Grafana automatically calculates an interval that can be used to group by time in queries. When there are more data points than can be shown on a graph then queries can be made more efficient by grouping by a larger interval. It is more efficient to group by 1 day than by 10s when looking at 3 months of data and the graph will look the same and the query will be faster. The `$__interval` is calculated using the time range and the width of the graph (the number of pixels).
@@ -273,29 +273,49 @@ The `$__timeFilter` is used in the MySQL data source.
This variable is only available in the Singlestat panel and can be used in the prefix or suffix fields on the Options tab. The variable will be replaced with the series name or alias.
+### The $__range Variable
+
+> Only available in Grafana v5.3+
+
+Currently only supported for Prometheus data sources. This variable represents the range for the current dashboard. It is calculated by `to - from`. It has a millisecond and a second representation called `$__range_ms` and `$__range_s`.
+
## Repeating Panels
Template variables can be very useful to dynamically change your queries across a whole dashboard. If you want
Grafana to dynamically create new panels or rows based on what values you have selected you can use the *Repeat* feature.
-If you have a variable with `Multi-value` or `Include all value` options enabled you can choose one panel or one row and have Grafana repeat that row
-for every selected value. You find this option under the General tab in panel edit mode. Select the variable to repeat by, and a `min span`.
-The `min span` controls how small Grafana will make the panels (if you have many values selected). Grafana will automatically adjust the width of
-each repeated panel so that the whole row is filled. Currently, you cannot mix other panels on a row with a repeated panel.
+If you have a variable with `Multi-value` or `Include all value` options enabled you can choose one panel and have Grafana repeat that panel
+for every selected value. You find the *Repeat* feature under the *General tab* in panel edit mode.
+
+The `direction` controls how the panels will be arranged.
+
+By choosing `horizontal` the panels will be arranged side-by-side. Grafana will automatically adjust the width
+of each repeated panel so that the whole row is filled. Currently, you cannot mix other panels on a row with a repeated
+panel. Each panel will never be smaller that the provided `Min width` if you have many selected values.
+
+By choosing `vertical` the panels will be arranged from top to bottom in a column. The `Min width` doesn't have any effect in this case. The width of the repeated panels will be the same as of the first panel (the original template) being repeated.
Only make changes to the first panel (the original template). To have the changes take effect on all panels you need to trigger a dynamic dashboard re-build.
You can do this by either changing the variable value (that is the basis for the repeat) or reload the dashboard.
## Repeating Rows
-This option requires you to open the row options view. Hover over the row left side to trigger the row menu, in this menu click `Row Options`. This
-opens the row options view. Here you find a *Repeat* dropdown where you can select the variable to repeat by.
+As seen above with the *Panels* you can also repeat *Rows* if you have variables set with `Multi-value` or
+`Include all value` selection option.
-### URL state
+To enable this feature you need to first add a new *Row* using the *Add Panel* menu. Then by hovering the row title and
+clicking on the cog button, you will access the `Row Options` configuration panel. You can then select the variable
+you want to repeat the row for.
+
+It may be a good idea to use a variable in the row title as well.
+
+Example: [Repeated Rows Dashboard](http://play.grafana.org/dashboard/db/repeated-rows)
+
+## URL state
Variable values are always synced to the URL using the syntax `var-=value`.
-### Examples
+## Examples
- [Graphite Templated Dashboard](http://play.grafana.org/dashboard/db/graphite-templated-nested)
- [Elasticsearch Templated Dashboard](http://play.grafana.org/dashboard/db/elasticsearch-templated)
diff --git a/docs/sources/tutorials/ha_setup.md b/docs/sources/tutorials/ha_setup.md
index 9ae2989f6e6..0f138b20a17 100644
--- a/docs/sources/tutorials/ha_setup.md
+++ b/docs/sources/tutorials/ha_setup.md
@@ -27,7 +27,7 @@ Grafana will now persist all long term data in the database. How to configure th
## User sessions
The second thing to consider is how to deal with user sessions and how to configure your load balancer infront of Grafana.
-Grafana support two says of storing session data locally on disk or in a database/cache-server.
+Grafana supports two ways of storing session data: locally on disk or in a database/cache-server.
If you want to store sessions on disk you can use `sticky sessions` in your load balanacer. If you prefer to store session data in a database/cache-server
you can use any stateless routing strategy in your load balancer (ex round robin or least connections).
diff --git a/examples/README.md b/examples/README.md
deleted file mode 100644
index 75f1f9a9a86..00000000000
--- a/examples/README.md
+++ /dev/null
@@ -1,5 +0,0 @@
-## Example plugin implementations
-
-datasource:[simple-json-datasource](https://github.com/grafana/simple-json-datasource)
-app: [example-app](https://github.com/grafana/example-app)
-panel: [grafana-piechart-panel](https://github.com/grafana/piechart-panel)
diff --git a/examples/alerting-dashboard.json b/examples/alerting-dashboard.json
deleted file mode 100644
index 744460d7847..00000000000
--- a/examples/alerting-dashboard.json
+++ /dev/null
@@ -1,800 +0,0 @@
-{
- "__inputs": [
- {
- "name": "DS_GRAPHITE",
- "label": "graphite",
- "description": "",
- "type": "datasource",
- "pluginId": "graphite",
- "pluginName": "Graphite"
- }
- ],
- "__requires": [
- {
- "type": "panel",
- "id": "graph",
- "name": "Graph",
- "version": ""
- },
- {
- "type": "grafana",
- "id": "grafana",
- "name": "Grafana",
- "version": "3.1.0"
- },
- {
- "type": "datasource",
- "id": "graphite",
- "name": "Graphite",
- "version": "1.0.0"
- }
- ],
- "id": null,
- "title": "Alerting example",
- "tags": [],
- "style": "dark",
- "timezone": "browser",
- "editable": true,
- "hideControls": false,
- "sharedCrosshair": false,
- "rows": [
- {
- "collapse": false,
- "editable": true,
- "height": "250px",
- "panels": [
- {
- "alert": {
- "conditions": [
- {
- "evaluator": {
- "params": [
- 355
- ],
- "type": "gt"
- },
- "query": {
- "params": [
- "A",
- "5m",
- "now"
- ]
- },
- "reducer": {
- "params": [],
- "type": "avg"
- },
- "type": "query"
- }
- ],
- "enabled": true,
- "frequency": "60s",
- "handler": 1,
- "name": "Critical alert panel",
- "notifications": [],
- "severity": "critical"
- },
- "alerting": {},
- "aliasColors": {},
- "bars": false,
- "datasource": "${DS_GRAPHITE}",
- "editable": true,
- "error": false,
- "fill": 1,
- "grid": {},
- "id": 1,
- "isNew": true,
- "legend": {
- "avg": false,
- "current": false,
- "max": false,
- "min": false,
- "show": true,
- "total": false,
- "values": false
- },
- "lines": true,
- "linewidth": 2,
- "links": [],
- "nullPointMode": "connected",
- "percentage": false,
- "pointradius": 5,
- "points": false,
- "renderer": "flot",
- "seriesOverrides": [],
- "span": 4,
- "stack": false,
- "steppedLine": false,
- "targets": [
- {
- "refId": "A",
- "target": "aliasByNode(statsd.fakesite.counters.session_start.*.count, 4)"
- },
- {
- "refId": "B",
- "target": "aliasByNode(scale(statsd.$apa.counters.session_start.*.count, 10), 4)"
- }
- ],
- "thresholds": [
- {
- "colorMode": "critical",
- "fill": true,
- "line": true,
- "op": "gt",
- "value": 355
- }
- ],
- "timeFrom": null,
- "timeShift": null,
- "title": "Critical panel",
- "tooltip": {
- "msResolution": false,
- "shared": true,
- "sort": 0,
- "value_type": "cumulative"
- },
- "type": "graph",
- "xaxis": {
- "show": true
- },
- "yaxes": [
- {
- "format": "short",
- "label": null,
- "logBase": 1,
- "max": null,
- "min": null,
- "show": true
- },
- {
- "format": "short",
- "label": null,
- "logBase": 1,
- "max": null,
- "min": null,
- "show": true
- }
- ]
- },
- {
- "alert": {
- "conditions": [
- {
- "evaluator": {
- "params": [
- 20
- ],
- "type": "gt"
- },
- "query": {
- "params": [
- "A",
- "5m",
- "now"
- ]
- },
- "reducer": {
- "params": [],
- "type": "avg"
- },
- "type": "query"
- }
- ],
- "enabled": true,
- "frequency": "60s",
- "handler": 1,
- "name": "Warning panel alert",
- "notifications": [],
- "severity": "warning"
- },
- "alerting": {},
- "aliasColors": {},
- "bars": false,
- "datasource": "${DS_GRAPHITE}",
- "editable": true,
- "error": false,
- "fill": 1,
- "id": 2,
- "isNew": true,
- "legend": {
- "avg": false,
- "current": false,
- "max": false,
- "min": false,
- "show": true,
- "total": false,
- "values": false
- },
- "lines": true,
- "linewidth": 2,
- "links": [],
- "nullPointMode": "connected",
- "percentage": false,
- "pointradius": 5,
- "points": false,
- "renderer": "flot",
- "seriesOverrides": [],
- "span": 4,
- "stack": false,
- "steppedLine": false,
- "targets": [
- {
- "refId": "A",
- "target": "aliasByNode(statsd.fakesite.counters.session_start.*.count, 4)"
- }
- ],
- "thresholds": [
- {
- "colorMode": "warning",
- "fill": true,
- "fillColor": "rgba(235, 138, 14, 0.12)",
- "line": true,
- "lineColor": "rgba(247, 149, 32, 0.60)",
- "op": "gt",
- "value": 20
- }
- ],
- "timeFrom": null,
- "timeShift": null,
- "title": "Warning panel",
- "tooltip": {
- "msResolution": false,
- "shared": true,
- "sort": 0,
- "value_type": "cumulative"
- },
- "type": "graph",
- "xaxis": {
- "show": true
- },
- "yaxes": [
- {
- "format": "short",
- "label": null,
- "logBase": 1,
- "max": null,
- "min": null,
- "show": true
- },
- {
- "format": "short",
- "label": null,
- "logBase": 1,
- "max": null,
- "min": null,
- "show": true
- }
- ]
- },
- {
- "alert": {
- "conditions": [
- {
- "evaluator": {
- "params": [
- 1
- ],
- "type": "lt"
- },
- "query": {
- "params": [
- "A",
- "5m",
- "now"
- ]
- },
- "reducer": {
- "params": [],
- "type": "count"
- },
- "type": "query"
- }
- ],
- "enabled": true,
- "frequency": "60s",
- "handler": 1,
- "name": "No datapoints",
- "notifications": [],
- "severity": "critical"
- },
- "alerting": {},
- "aliasColors": {},
- "bars": false,
- "datasource": "${DS_GRAPHITE}",
- "editable": true,
- "error": false,
- "fill": 1,
- "id": 20,
- "isNew": true,
- "legend": {
- "avg": false,
- "current": false,
- "max": false,
- "min": false,
- "show": true,
- "total": false,
- "values": false
- },
- "lines": true,
- "linewidth": 2,
- "links": [],
- "nullPointMode": "connected",
- "percentage": false,
- "pointradius": 5,
- "points": false,
- "renderer": "flot",
- "seriesOverrides": [],
- "span": 4,
- "stack": false,
- "steppedLine": false,
- "targets": [
- {
- "refId": "A",
- "target": "aliasByNode(statsd.fakesite.counters.session_start.*.count, 4)"
- }
- ],
- "thresholds": [
- {
- "value": 1,
- "op": "lt",
- "fill": true,
- "line": true,
- "colorMode": "critical"
- }
- ],
- "timeFrom": null,
- "timeShift": null,
- "title": "Count datapoints",
- "tooltip": {
- "msResolution": false,
- "shared": true,
- "sort": 0,
- "value_type": "cumulative"
- },
- "type": "graph",
- "xaxis": {
- "show": true
- },
- "yaxes": [
- {
- "format": "short",
- "label": null,
- "logBase": 1,
- "max": null,
- "min": null,
- "show": true
- },
- {
- "format": "short",
- "label": null,
- "logBase": 1,
- "max": null,
- "min": null,
- "show": true
- }
- ]
- }
- ],
- "title": "Row"
- },
- {
- "collapse": false,
- "editable": true,
- "height": "250px",
- "panels": [
- {
- "alert": {
- "conditions": [
- {
- "evaluator": {
- "params": [
- 20
- ],
- "type": "lt"
- },
- "query": {
- "params": [
- "A",
- "5m",
- "now"
- ]
- },
- "reducer": {
- "params": [],
- "type": "avg"
- },
- "type": "query"
- }
- ],
- "enabled": true,
- "frequency": "60s",
- "handler": 1,
- "name": "Alert below value",
- "notifications": [],
- "severity": "critical"
- },
- "alerting": {},
- "aliasColors": {},
- "bars": false,
- "datasource": "${DS_GRAPHITE}",
- "editable": true,
- "error": false,
- "fill": 1,
- "id": 17,
- "isNew": true,
- "legend": {
- "avg": false,
- "current": false,
- "max": false,
- "min": false,
- "show": true,
- "total": false,
- "values": false
- },
- "lines": true,
- "linewidth": 2,
- "links": [],
- "nullPointMode": "connected",
- "percentage": false,
- "pointradius": 5,
- "points": false,
- "renderer": "flot",
- "seriesOverrides": [],
- "span": 3,
- "stack": false,
- "steppedLine": false,
- "targets": [
- {
- "refId": "A",
- "target": "aliasByNode(statsd.fakesite.counters.session_start.*.count, 4)"
- }
- ],
- "thresholds": [
- {
- "colorMode": "critical",
- "fill": true,
- "fillColor": "rgba(234, 112, 112, 0.12)",
- "line": true,
- "lineColor": "rgba(237, 46, 24, 0.60)",
- "op": "lt",
- "value": 20
- }
- ],
- "timeFrom": null,
- "timeShift": null,
- "title": "Alert below value",
- "tooltip": {
- "msResolution": false,
- "shared": true,
- "sort": 0,
- "value_type": "cumulative"
- },
- "type": "graph",
- "xaxis": {
- "show": true
- },
- "yaxes": [
- {
- "format": "short",
- "label": null,
- "logBase": 1,
- "max": null,
- "min": null,
- "show": true
- },
- {
- "format": "short",
- "label": null,
- "logBase": 1,
- "max": null,
- "min": null,
- "show": true
- }
- ]
- },
- {
- "alert": {
- "conditions": [
- {
- "evaluator": {
- "params": [
- 10,
- 80
- ],
- "type": "outside_range"
- },
- "query": {
- "params": [
- "A",
- "5m",
- "now"
- ]
- },
- "reducer": {
- "params": [],
- "type": "avg"
- },
- "type": "query"
- }
- ],
- "enabled": true,
- "frequency": "10s",
- "handler": 1,
- "name": "Alert is outside range",
- "notifications": [],
- "severity": "critical"
- },
- "alerting": {},
- "aliasColors": {},
- "bars": false,
- "datasource": "${DS_GRAPHITE}",
- "editable": true,
- "error": false,
- "fill": 1,
- "id": 18,
- "isNew": true,
- "legend": {
- "avg": false,
- "current": false,
- "max": false,
- "min": false,
- "show": true,
- "total": false,
- "values": false
- },
- "lines": true,
- "linewidth": 2,
- "links": [],
- "nullPointMode": "connected",
- "percentage": false,
- "pointradius": 5,
- "points": false,
- "renderer": "flot",
- "seriesOverrides": [],
- "span": 3,
- "stack": false,
- "steppedLine": false,
- "targets": [
- {
- "refId": "A",
- "target": "aliasByNode(statsd.fakesite.counters.session_start.*.count, 4)"
- }
- ],
- "thresholds": [
- {
- "colorMode": "critical",
- "fill": true,
- "fillColor": "rgba(234, 112, 112, 0.12)",
- "line": true,
- "lineColor": "rgba(237, 46, 24, 0.60)",
- "op": "lt",
- "value": 10
- },
- {
- "colorMode": "critical",
- "fill": true,
- "fillColor": "rgba(234, 112, 112, 0.12)",
- "line": true,
- "lineColor": "rgba(237, 46, 24, 0.60)",
- "op": "gt",
- "value": 80
- }
- ],
- "timeFrom": null,
- "timeShift": null,
- "title": "Alert is outside range",
- "tooltip": {
- "msResolution": false,
- "shared": true,
- "sort": 0,
- "value_type": "cumulative"
- },
- "type": "graph",
- "xaxis": {
- "show": true
- },
- "yaxes": [
- {
- "format": "short",
- "label": null,
- "logBase": 1,
- "max": null,
- "min": null,
- "show": true
- },
- {
- "format": "short",
- "label": null,
- "logBase": 1,
- "max": null,
- "min": null,
- "show": true
- }
- ]
- },
- {
- "alert": {
- "conditions": [
- {
- "evaluator": {
- "params": [
- 60,
- 80
- ],
- "type": "within_range"
- },
- "query": {
- "params": [
- "A",
- "5m",
- "now"
- ]
- },
- "reducer": {
- "params": [],
- "type": "avg"
- },
- "type": "query"
- }
- ],
- "enabled": true,
- "frequency": "10s",
- "handler": 1,
- "name": "Alert is within range",
- "notifications": [],
- "severity": "critical"
- },
- "alerting": {},
- "aliasColors": {},
- "bars": false,
- "datasource": "${DS_GRAPHITE}",
- "editable": true,
- "error": false,
- "fill": 1,
- "id": 19,
- "isNew": true,
- "legend": {
- "avg": false,
- "current": false,
- "max": false,
- "min": false,
- "show": true,
- "total": false,
- "values": false
- },
- "lines": true,
- "linewidth": 2,
- "links": [],
- "nullPointMode": "connected",
- "percentage": false,
- "pointradius": 5,
- "points": false,
- "renderer": "flot",
- "seriesOverrides": [],
- "span": 3,
- "stack": false,
- "steppedLine": false,
- "targets": [
- {
- "refId": "A",
- "target": "aliasByNode(statsd.fakesite.counters.session_start.*.count, 4)"
- }
- ],
- "thresholds": [
- {
- "colorMode": "critical",
- "fill": true,
- "fillColor": "rgba(234, 112, 112, 0.12)",
- "line": true,
- "lineColor": "rgba(237, 46, 24, 0.60)",
- "op": "gt",
- "value": 60
- },
- {
- "colorMode": "critical",
- "fill": true,
- "fillColor": "rgba(234, 112, 112, 0.12)",
- "line": true,
- "lineColor": "rgba(237, 46, 24, 0.60)",
- "op": "lt",
- "value": 80
- }
- ],
- "timeFrom": null,
- "timeShift": null,
- "title": "Alert is within range",
- "tooltip": {
- "msResolution": false,
- "shared": true,
- "sort": 0,
- "value_type": "cumulative"
- },
- "type": "graph",
- "xaxis": {
- "show": true
- },
- "yaxes": [
- {
- "format": "short",
- "label": null,
- "logBase": 1,
- "max": null,
- "min": null,
- "show": true
- },
- {
- "format": "short",
- "label": null,
- "logBase": 1,
- "max": null,
- "min": null,
- "show": true
- }
- ]
- }
- ],
- "title": "New row"
- }
- ],
- "time": {
- "from": "now-6h",
- "to": "now"
- },
- "timepicker": {
- "refresh_intervals": [
- "5s",
- "10s",
- "30s",
- "1m",
- "5m",
- "15m",
- "30m",
- "1h",
- "2h",
- "1d"
- ],
- "time_options": [
- "5m",
- "15m",
- "1h",
- "6h",
- "12h",
- "24h",
- "2d",
- "7d",
- "30d"
- ]
- },
- "templating": {
- "list": [
- {
- "current": {
- "text": "fakesite",
- "value": "fakesite"
- },
- "datasource": null,
- "hide": 0,
- "includeAll": false,
- "multi": false,
- "name": "apa",
- "options": [
- {
- "selected": true,
- "text": "fakesite",
- "value": "fakesite"
- }
- ],
- "query": "fakesite",
- "refresh": 0,
- "type": "custom"
- }
- ]
- },
- "annotations": {
- "list": []
- },
- "schemaVersion": 13,
- "version": 15,
- "links": [],
- "gnetId": null
-}
\ No newline at end of file
diff --git a/examples/alerting-multiple-alerts.json b/examples/alerting-multiple-alerts.json
deleted file mode 100644
index e6e729ecc06..00000000000
--- a/examples/alerting-multiple-alerts.json
+++ /dev/null
@@ -1,2216 +0,0 @@
-{
- "__inputs": [
- {
- "name": "DS_GRAPHITE",
- "label": "graphite",
- "description": "",
- "type": "datasource",
- "pluginId": "graphite",
- "pluginName": "Graphite"
- }
- ],
- "__requires": [
- {
- "type": "panel",
- "id": "graph",
- "name": "Graph",
- "version": ""
- },
- {
- "type": "grafana",
- "id": "grafana",
- "name": "Grafana",
- "version": "3.1.0"
- },
- {
- "type": "datasource",
- "id": "graphite",
- "name": "Graphite",
- "version": "1.0.0"
- }
- ],
- "id": null,
- "title": "Dashboard with many alerts",
- "tags": [],
- "style": "dark",
- "timezone": "browser",
- "editable": true,
- "hideControls": false,
- "sharedCrosshair": false,
- "rows": [
- {
- "collapse": false,
- "editable": true,
- "height": "250px",
- "panels": [
- {
- "alert": {
- "conditions": [
- {
- "evaluator": {
- "params": [
- 30
- ],
- "type": "gt"
- },
- "query": {
- "params": [
- "A",
- "5m",
- "now"
- ]
- },
- "reducer": {
- "params": [],
- "type": "sum"
- },
- "type": "query"
- }
- ],
- "enabled": true,
- "frequency": "60s",
- "handler": 1,
- "name": "Critical alert panel",
- "notifications": [],
- "severity": "critical"
- },
- "aliasColors": {},
- "bars": false,
- "datasource": "${DS_GRAPHITE}",
- "editable": true,
- "error": false,
- "fill": 1,
- "grid": {},
- "id": 1,
- "isNew": true,
- "legend": {
- "avg": false,
- "current": false,
- "max": false,
- "min": false,
- "show": true,
- "total": false,
- "values": false
- },
- "lines": true,
- "linewidth": 2,
- "links": [],
- "nullPointMode": "connected",
- "percentage": false,
- "pointradius": 5,
- "points": false,
- "renderer": "flot",
- "seriesOverrides": [],
- "span": 3,
- "stack": false,
- "steppedLine": false,
- "targets": [
- {
- "refId": "A",
- "target": "aliasByNode(statsd.fakesite.counters.session_start.*.count, 4)"
- }
- ],
- "thresholds": [
- {
- "colorMode": "critical",
- "fill": true,
- "fillColor": "rgba(234, 112, 112, 0.12)",
- "line": true,
- "lineColor": "rgba(237, 46, 24, 0.60)",
- "op": "gt",
- "value": 30
- }
- ],
- "timeFrom": null,
- "timeShift": null,
- "title": "Critical panel",
- "tooltip": {
- "msResolution": false,
- "shared": true,
- "sort": 0,
- "value_type": "cumulative"
- },
- "type": "graph",
- "xaxis": {
- "show": true
- },
- "yaxes": [
- {
- "format": "short",
- "label": null,
- "logBase": 1,
- "max": null,
- "min": null,
- "show": true
- },
- {
- "format": "short",
- "label": null,
- "logBase": 1,
- "max": null,
- "min": null,
- "show": true
- }
- ]
- },
- {
- "alert": {
- "conditions": [
- {
- "evaluator": {
- "params": [
- 30
- ],
- "type": "gt"
- },
- "query": {
- "params": [
- "A",
- "5m",
- "now"
- ]
- },
- "reducer": {
- "params": [],
- "type": "sum"
- },
- "type": "query"
- }
- ],
- "enabled": true,
- "frequency": "60s",
- "handler": 1,
- "name": "Critical alert panel",
- "notifications": [],
- "severity": "critical"
- },
- "aliasColors": {},
- "bars": false,
- "datasource": "${DS_GRAPHITE}",
- "editable": true,
- "error": false,
- "fill": 1,
- "grid": {},
- "id": 5,
- "isNew": true,
- "legend": {
- "avg": false,
- "current": false,
- "max": false,
- "min": false,
- "show": true,
- "total": false,
- "values": false
- },
- "lines": true,
- "linewidth": 2,
- "links": [],
- "nullPointMode": "connected",
- "percentage": false,
- "pointradius": 5,
- "points": false,
- "renderer": "flot",
- "seriesOverrides": [],
- "span": 3,
- "stack": false,
- "steppedLine": false,
- "targets": [
- {
- "refId": "A",
- "target": "aliasByNode(statsd.fakesite.counters.session_start.*.count, 4)"
- }
- ],
- "thresholds": [
- {
- "colorMode": "critical",
- "fill": true,
- "fillColor": "rgba(234, 112, 112, 0.12)",
- "line": true,
- "lineColor": "rgba(237, 46, 24, 0.60)",
- "op": "gt",
- "value": 30
- }
- ],
- "timeFrom": null,
- "timeShift": null,
- "title": "Critical panel",
- "tooltip": {
- "msResolution": false,
- "shared": true,
- "sort": 0,
- "value_type": "cumulative"
- },
- "type": "graph",
- "xaxis": {
- "show": true
- },
- "yaxes": [
- {
- "format": "short",
- "label": null,
- "logBase": 1,
- "max": null,
- "min": null,
- "show": true
- },
- {
- "format": "short",
- "label": null,
- "logBase": 1,
- "max": null,
- "min": null,
- "show": true
- }
- ]
- },
- {
- "alert": {
- "conditions": [
- {
- "evaluator": {
- "params": [
- 30
- ],
- "type": "gt"
- },
- "query": {
- "params": [
- "A",
- "5m",
- "now"
- ]
- },
- "reducer": {
- "params": [],
- "type": "sum"
- },
- "type": "query"
- }
- ],
- "enabled": true,
- "frequency": "60s",
- "handler": 1,
- "name": "Critical alert panel",
- "notifications": [],
- "severity": "critical"
- },
- "aliasColors": {},
- "bars": false,
- "datasource": "${DS_GRAPHITE}",
- "editable": true,
- "error": false,
- "fill": 1,
- "grid": {},
- "id": 6,
- "isNew": true,
- "legend": {
- "avg": false,
- "current": false,
- "max": false,
- "min": false,
- "show": true,
- "total": false,
- "values": false
- },
- "lines": true,
- "linewidth": 2,
- "links": [],
- "nullPointMode": "connected",
- "percentage": false,
- "pointradius": 5,
- "points": false,
- "renderer": "flot",
- "seriesOverrides": [],
- "span": 3,
- "stack": false,
- "steppedLine": false,
- "targets": [
- {
- "refId": "A",
- "target": "aliasByNode(statsd.fakesite.counters.session_start.*.count, 4)"
- }
- ],
- "thresholds": [
- {
- "colorMode": "critical",
- "fill": true,
- "fillColor": "rgba(234, 112, 112, 0.12)",
- "line": true,
- "lineColor": "rgba(237, 46, 24, 0.60)",
- "op": "gt",
- "value": 30
- }
- ],
- "timeFrom": null,
- "timeShift": null,
- "title": "Critical panel",
- "tooltip": {
- "msResolution": false,
- "shared": true,
- "sort": 0,
- "value_type": "cumulative"
- },
- "type": "graph",
- "xaxis": {
- "show": true
- },
- "yaxes": [
- {
- "format": "short",
- "label": null,
- "logBase": 1,
- "max": null,
- "min": null,
- "show": true
- },
- {
- "format": "short",
- "label": null,
- "logBase": 1,
- "max": null,
- "min": null,
- "show": true
- }
- ]
- },
- {
- "alert": {
- "conditions": [
- {
- "evaluator": {
- "params": [
- 30
- ],
- "type": "gt"
- },
- "query": {
- "params": [
- "A",
- "5m",
- "now"
- ]
- },
- "reducer": {
- "params": [],
- "type": "sum"
- },
- "type": "query"
- }
- ],
- "enabled": true,
- "frequency": "60s",
- "handler": 1,
- "name": "Critical alert panel",
- "notifications": [],
- "severity": "critical"
- },
- "aliasColors": {},
- "bars": false,
- "datasource": "${DS_GRAPHITE}",
- "editable": true,
- "error": false,
- "fill": 1,
- "grid": {},
- "id": 8,
- "isNew": true,
- "legend": {
- "avg": false,
- "current": false,
- "max": false,
- "min": false,
- "show": true,
- "total": false,
- "values": false
- },
- "lines": true,
- "linewidth": 2,
- "links": [],
- "nullPointMode": "connected",
- "percentage": false,
- "pointradius": 5,
- "points": false,
- "renderer": "flot",
- "seriesOverrides": [],
- "span": 3,
- "stack": false,
- "steppedLine": false,
- "targets": [
- {
- "refId": "A",
- "target": "aliasByNode(statsd.fakesite.counters.session_start.*.count, 4)"
- }
- ],
- "thresholds": [
- {
- "colorMode": "critical",
- "fill": true,
- "fillColor": "rgba(234, 112, 112, 0.12)",
- "line": true,
- "lineColor": "rgba(237, 46, 24, 0.60)",
- "op": "gt",
- "value": 30
- }
- ],
- "timeFrom": null,
- "timeShift": null,
- "title": "Critical panel",
- "tooltip": {
- "msResolution": false,
- "shared": true,
- "sort": 0,
- "value_type": "cumulative"
- },
- "type": "graph",
- "xaxis": {
- "show": true
- },
- "yaxes": [
- {
- "format": "short",
- "label": null,
- "logBase": 1,
- "max": null,
- "min": null,
- "show": true
- },
- {
- "format": "short",
- "label": null,
- "logBase": 1,
- "max": null,
- "min": null,
- "show": true
- }
- ]
- }
- ],
- "title": "Row"
- },
- {
- "collapse": false,
- "editable": true,
- "height": "250px",
- "panels": [
- {
- "alert": {
- "conditions": [
- {
- "evaluator": {
- "params": [
- 20
- ],
- "type": "gt"
- },
- "query": {
- "params": [
- "A",
- "5m",
- "now"
- ]
- },
- "reducer": {
- "params": [],
- "type": "avg"
- },
- "type": "query"
- }
- ],
- "enabled": true,
- "frequency": "60s",
- "handler": 1,
- "name": "Warning panel alert",
- "notifications": [],
- "severity": "warning"
- },
- "aliasColors": {},
- "bars": false,
- "datasource": "${DS_GRAPHITE}",
- "editable": true,
- "error": false,
- "fill": 1,
- "id": 2,
- "isNew": true,
- "legend": {
- "avg": false,
- "current": false,
- "max": false,
- "min": false,
- "show": true,
- "total": false,
- "values": false
- },
- "lines": true,
- "linewidth": 2,
- "links": [],
- "nullPointMode": "connected",
- "percentage": false,
- "pointradius": 5,
- "points": false,
- "renderer": "flot",
- "seriesOverrides": [],
- "span": 3,
- "stack": false,
- "steppedLine": false,
- "targets": [
- {
- "refId": "A",
- "target": "aliasByNode(statsd.fakesite.counters.session_start.*.count, 4)"
- }
- ],
- "thresholds": [
- {
- "colorMode": "warning",
- "fill": true,
- "fillColor": "rgba(235, 138, 14, 0.12)",
- "line": true,
- "lineColor": "rgba(247, 149, 32, 0.60)",
- "op": "gt",
- "value": 20
- }
- ],
- "timeFrom": null,
- "timeShift": null,
- "title": "Warning panel",
- "tooltip": {
- "msResolution": false,
- "shared": true,
- "sort": 0,
- "value_type": "cumulative"
- },
- "type": "graph",
- "xaxis": {
- "show": true
- },
- "yaxes": [
- {
- "format": "short",
- "label": null,
- "logBase": 1,
- "max": null,
- "min": null,
- "show": true
- },
- {
- "format": "short",
- "label": null,
- "logBase": 1,
- "max": null,
- "min": null,
- "show": true
- }
- ]
- },
- {
- "alert": {
- "conditions": [
- {
- "evaluator": {
- "params": [
- 20
- ],
- "type": "gt"
- },
- "query": {
- "params": [
- "A",
- "5m",
- "now"
- ]
- },
- "reducer": {
- "params": [],
- "type": "avg"
- },
- "type": "query"
- }
- ],
- "enabled": true,
- "frequency": "60s",
- "handler": 1,
- "name": "Warning panel alert",
- "notifications": [],
- "severity": "warning"
- },
- "aliasColors": {},
- "bars": false,
- "datasource": "${DS_GRAPHITE}",
- "editable": true,
- "error": false,
- "fill": 1,
- "id": 3,
- "isNew": true,
- "legend": {
- "avg": false,
- "current": false,
- "max": false,
- "min": false,
- "show": true,
- "total": false,
- "values": false
- },
- "lines": true,
- "linewidth": 2,
- "links": [],
- "nullPointMode": "connected",
- "percentage": false,
- "pointradius": 5,
- "points": false,
- "renderer": "flot",
- "seriesOverrides": [],
- "span": 3,
- "stack": false,
- "steppedLine": false,
- "targets": [
- {
- "refId": "A",
- "target": "aliasByNode(statsd.fakesite.counters.session_start.*.count, 4)"
- }
- ],
- "thresholds": [
- {
- "colorMode": "warning",
- "fill": true,
- "fillColor": "rgba(235, 138, 14, 0.12)",
- "line": true,
- "lineColor": "rgba(247, 149, 32, 0.60)",
- "op": "gt",
- "value": 20
- }
- ],
- "timeFrom": null,
- "timeShift": null,
- "title": "Warning panel",
- "tooltip": {
- "msResolution": false,
- "shared": true,
- "sort": 0,
- "value_type": "cumulative"
- },
- "type": "graph",
- "xaxis": {
- "show": true
- },
- "yaxes": [
- {
- "format": "short",
- "label": null,
- "logBase": 1,
- "max": null,
- "min": null,
- "show": true
- },
- {
- "format": "short",
- "label": null,
- "logBase": 1,
- "max": null,
- "min": null,
- "show": true
- }
- ]
- },
- {
- "alert": {
- "conditions": [
- {
- "evaluator": {
- "params": [
- 20
- ],
- "type": "gt"
- },
- "query": {
- "params": [
- "A",
- "5m",
- "now"
- ]
- },
- "reducer": {
- "params": [],
- "type": "avg"
- },
- "type": "query"
- }
- ],
- "enabled": true,
- "frequency": "60s",
- "handler": 1,
- "name": "Warning panel alert",
- "notifications": [],
- "severity": "warning"
- },
- "aliasColors": {},
- "bars": false,
- "datasource": "${DS_GRAPHITE}",
- "editable": true,
- "error": false,
- "fill": 1,
- "id": 4,
- "isNew": true,
- "legend": {
- "avg": false,
- "current": false,
- "max": false,
- "min": false,
- "show": true,
- "total": false,
- "values": false
- },
- "lines": true,
- "linewidth": 2,
- "links": [],
- "nullPointMode": "connected",
- "percentage": false,
- "pointradius": 5,
- "points": false,
- "renderer": "flot",
- "seriesOverrides": [],
- "span": 3,
- "stack": false,
- "steppedLine": false,
- "targets": [
- {
- "refId": "A",
- "target": "aliasByNode(statsd.fakesite.counters.session_start.*.count, 4)"
- }
- ],
- "thresholds": [
- {
- "colorMode": "warning",
- "fill": true,
- "fillColor": "rgba(235, 138, 14, 0.12)",
- "line": true,
- "lineColor": "rgba(247, 149, 32, 0.60)",
- "op": "gt",
- "value": 20
- }
- ],
- "timeFrom": null,
- "timeShift": null,
- "title": "Warning panel",
- "tooltip": {
- "msResolution": false,
- "shared": true,
- "sort": 0,
- "value_type": "cumulative"
- },
- "type": "graph",
- "xaxis": {
- "show": true
- },
- "yaxes": [
- {
- "format": "short",
- "label": null,
- "logBase": 1,
- "max": null,
- "min": null,
- "show": true
- },
- {
- "format": "short",
- "label": null,
- "logBase": 1,
- "max": null,
- "min": null,
- "show": true
- }
- ]
- },
- {
- "alert": {
- "conditions": [
- {
- "evaluator": {
- "params": [
- 20
- ],
- "type": "gt"
- },
- "query": {
- "params": [
- "A",
- "5m",
- "now"
- ]
- },
- "reducer": {
- "params": [],
- "type": "avg"
- },
- "type": "query"
- }
- ],
- "enabled": true,
- "frequency": "60s",
- "handler": 1,
- "name": "Warning panel alert",
- "notifications": [],
- "severity": "warning"
- },
- "aliasColors": {},
- "bars": false,
- "datasource": "${DS_GRAPHITE}",
- "editable": true,
- "error": false,
- "fill": 1,
- "id": 7,
- "isNew": true,
- "legend": {
- "avg": false,
- "current": false,
- "max": false,
- "min": false,
- "show": true,
- "total": false,
- "values": false
- },
- "lines": true,
- "linewidth": 2,
- "links": [],
- "nullPointMode": "connected",
- "percentage": false,
- "pointradius": 5,
- "points": false,
- "renderer": "flot",
- "seriesOverrides": [],
- "span": 3,
- "stack": false,
- "steppedLine": false,
- "targets": [
- {
- "refId": "A",
- "target": "aliasByNode(statsd.fakesite.counters.session_start.*.count, 4)"
- }
- ],
- "thresholds": [
- {
- "colorMode": "warning",
- "fill": true,
- "fillColor": "rgba(235, 138, 14, 0.12)",
- "line": true,
- "lineColor": "rgba(247, 149, 32, 0.60)",
- "op": "gt",
- "value": 20
- }
- ],
- "timeFrom": null,
- "timeShift": null,
- "title": "Warning panel",
- "tooltip": {
- "msResolution": false,
- "shared": true,
- "sort": 0,
- "value_type": "cumulative"
- },
- "type": "graph",
- "xaxis": {
- "show": true
- },
- "yaxes": [
- {
- "format": "short",
- "label": null,
- "logBase": 1,
- "max": null,
- "min": null,
- "show": true
- },
- {
- "format": "short",
- "label": null,
- "logBase": 1,
- "max": null,
- "min": null,
- "show": true
- }
- ]
- }
- ],
- "title": "New row"
- },
- {
- "collapse": false,
- "editable": true,
- "height": "250px",
- "panels": [
- {
- "alert": {
- "conditions": [
- {
- "evaluator": {
- "params": [
- 50
- ],
- "type": "gt"
- },
- "query": {
- "params": [
- "A",
- "5m",
- "now"
- ]
- },
- "reducer": {
- "params": [],
- "type": "avg"
- },
- "type": "query"
- }
- ],
- "enabled": true,
- "frequency": "10s",
- "handler": 1,
- "name": "Fast Critical panel alert",
- "notifications": [],
- "severity": "critical"
- },
- "aliasColors": {},
- "bars": false,
- "datasource": "${DS_GRAPHITE}",
- "editable": true,
- "error": false,
- "fill": 1,
- "id": 9,
- "isNew": true,
- "legend": {
- "avg": false,
- "current": false,
- "max": false,
- "min": false,
- "show": true,
- "total": false,
- "values": false
- },
- "lines": true,
- "linewidth": 2,
- "links": [],
- "nullPointMode": "connected",
- "percentage": false,
- "pointradius": 5,
- "points": false,
- "renderer": "flot",
- "seriesOverrides": [],
- "span": 3,
- "stack": false,
- "steppedLine": false,
- "targets": [
- {
- "refId": "A",
- "target": "aliasByNode(statsd.fakesite.counters.session_start.*.count, 4)"
- }
- ],
- "thresholds": [
- {
- "colorMode": "critical",
- "fill": true,
- "fillColor": "rgba(234, 112, 112, 0.12)",
- "line": true,
- "lineColor": "rgba(237, 46, 24, 0.60)",
- "op": "gt",
- "value": 50
- }
- ],
- "timeFrom": null,
- "timeShift": null,
- "title": "Fast Critical panel",
- "tooltip": {
- "msResolution": false,
- "shared": true,
- "sort": 0,
- "value_type": "cumulative"
- },
- "type": "graph",
- "xaxis": {
- "show": true
- },
- "yaxes": [
- {
- "format": "short",
- "label": null,
- "logBase": 1,
- "max": null,
- "min": null,
- "show": true
- },
- {
- "format": "short",
- "label": null,
- "logBase": 1,
- "max": null,
- "min": null,
- "show": true
- }
- ]
- },
- {
- "alert": {
- "conditions": [
- {
- "evaluator": {
- "params": [
- 50
- ],
- "type": "gt"
- },
- "query": {
- "params": [
- "A",
- "5m",
- "now"
- ]
- },
- "reducer": {
- "params": [],
- "type": "avg"
- },
- "type": "query"
- }
- ],
- "enabled": true,
- "frequency": "10s",
- "handler": 1,
- "name": "Fast Critical panel alert",
- "notifications": [],
- "severity": "critical"
- },
- "aliasColors": {},
- "bars": false,
- "datasource": "${DS_GRAPHITE}",
- "editable": true,
- "error": false,
- "fill": 1,
- "id": 10,
- "isNew": true,
- "legend": {
- "avg": false,
- "current": false,
- "max": false,
- "min": false,
- "show": true,
- "total": false,
- "values": false
- },
- "lines": true,
- "linewidth": 2,
- "links": [],
- "nullPointMode": "connected",
- "percentage": false,
- "pointradius": 5,
- "points": false,
- "renderer": "flot",
- "seriesOverrides": [],
- "span": 3,
- "stack": false,
- "steppedLine": false,
- "targets": [
- {
- "refId": "A",
- "target": "aliasByNode(statsd.fakesite.counters.session_start.*.count, 4)"
- }
- ],
- "thresholds": [
- {
- "colorMode": "critical",
- "fill": true,
- "fillColor": "rgba(234, 112, 112, 0.12)",
- "line": true,
- "lineColor": "rgba(237, 46, 24, 0.60)",
- "op": "gt",
- "value": 50
- }
- ],
- "timeFrom": null,
- "timeShift": null,
- "title": "Fast Critical panel",
- "tooltip": {
- "msResolution": false,
- "shared": true,
- "sort": 0,
- "value_type": "cumulative"
- },
- "type": "graph",
- "xaxis": {
- "show": true
- },
- "yaxes": [
- {
- "format": "short",
- "label": null,
- "logBase": 1,
- "max": null,
- "min": null,
- "show": true
- },
- {
- "format": "short",
- "label": null,
- "logBase": 1,
- "max": null,
- "min": null,
- "show": true
- }
- ]
- },
- {
- "alert": {
- "conditions": [
- {
- "evaluator": {
- "params": [
- 50
- ],
- "type": "gt"
- },
- "query": {
- "params": [
- "A",
- "5m",
- "now"
- ]
- },
- "reducer": {
- "params": [],
- "type": "avg"
- },
- "type": "query"
- }
- ],
- "enabled": true,
- "frequency": "10s",
- "handler": 1,
- "name": "Fast Critical panel alert",
- "notifications": [],
- "severity": "critical"
- },
- "aliasColors": {},
- "bars": false,
- "datasource": "${DS_GRAPHITE}",
- "editable": true,
- "error": false,
- "fill": 1,
- "id": 11,
- "isNew": true,
- "legend": {
- "avg": false,
- "current": false,
- "max": false,
- "min": false,
- "show": true,
- "total": false,
- "values": false
- },
- "lines": true,
- "linewidth": 2,
- "links": [],
- "nullPointMode": "connected",
- "percentage": false,
- "pointradius": 5,
- "points": false,
- "renderer": "flot",
- "seriesOverrides": [],
- "span": 3,
- "stack": false,
- "steppedLine": false,
- "targets": [
- {
- "refId": "A",
- "target": "aliasByNode(statsd.fakesite.counters.session_start.*.count, 4)"
- }
- ],
- "thresholds": [
- {
- "colorMode": "critical",
- "fill": true,
- "fillColor": "rgba(234, 112, 112, 0.12)",
- "line": true,
- "lineColor": "rgba(237, 46, 24, 0.60)",
- "op": "gt",
- "value": 50
- }
- ],
- "timeFrom": null,
- "timeShift": null,
- "title": "Fast Critical panel",
- "tooltip": {
- "msResolution": false,
- "shared": true,
- "sort": 0,
- "value_type": "cumulative"
- },
- "type": "graph",
- "xaxis": {
- "show": true
- },
- "yaxes": [
- {
- "format": "short",
- "label": null,
- "logBase": 1,
- "max": null,
- "min": null,
- "show": true
- },
- {
- "format": "short",
- "label": null,
- "logBase": 1,
- "max": null,
- "min": null,
- "show": true
- }
- ]
- },
- {
- "alert": {
- "conditions": [
- {
- "evaluator": {
- "params": [
- 50
- ],
- "type": "gt"
- },
- "query": {
- "params": [
- "A",
- "5m",
- "now"
- ]
- },
- "reducer": {
- "params": [],
- "type": "avg"
- },
- "type": "query"
- }
- ],
- "enabled": true,
- "frequency": "10s",
- "handler": 1,
- "name": "Fast Critical panel alert",
- "notifications": [],
- "severity": "critical"
- },
- "aliasColors": {},
- "bars": false,
- "datasource": "${DS_GRAPHITE}",
- "editable": true,
- "error": false,
- "fill": 1,
- "id": 12,
- "isNew": true,
- "legend": {
- "avg": false,
- "current": false,
- "max": false,
- "min": false,
- "show": true,
- "total": false,
- "values": false
- },
- "lines": true,
- "linewidth": 2,
- "links": [],
- "nullPointMode": "connected",
- "percentage": false,
- "pointradius": 5,
- "points": false,
- "renderer": "flot",
- "seriesOverrides": [],
- "span": 3,
- "stack": false,
- "steppedLine": false,
- "targets": [
- {
- "refId": "A",
- "target": "aliasByNode(statsd.fakesite.counters.session_start.*.count, 4)"
- }
- ],
- "thresholds": [
- {
- "colorMode": "critical",
- "fill": true,
- "fillColor": "rgba(234, 112, 112, 0.12)",
- "line": true,
- "lineColor": "rgba(237, 46, 24, 0.60)",
- "op": "gt",
- "value": 50
- }
- ],
- "timeFrom": null,
- "timeShift": null,
- "title": "Fast Critical panel",
- "tooltip": {
- "msResolution": false,
- "shared": true,
- "sort": 0,
- "value_type": "cumulative"
- },
- "type": "graph",
- "xaxis": {
- "show": true
- },
- "yaxes": [
- {
- "format": "short",
- "label": null,
- "logBase": 1,
- "max": null,
- "min": null,
- "show": true
- },
- {
- "format": "short",
- "label": null,
- "logBase": 1,
- "max": null,
- "min": null,
- "show": true
- }
- ]
- }
- ],
- "title": "New row"
- },
- {
- "collapse": false,
- "editable": true,
- "height": "250px",
- "panels": [
- {
- "alert": {
- "enabled": true,
- "conditions": [
- {
- "type": "query",
- "query": {
- "params": [
- "A",
- "5m",
- "now"
- ]
- },
- "reducer": {
- "type": "avg",
- "params": []
- },
- "evaluator": {
- "type": "gt",
- "params": [
- 10
- ]
- }
- }
- ],
- "severity": "warning",
- "frequency": "1s",
- "handler": 1,
- "notifications": [],
- "name": "Fast Warning panel alert"
- },
- "aliasColors": {},
- "bars": false,
- "datasource": "${DS_GRAPHITE}",
- "editable": true,
- "error": false,
- "fill": 1,
- "id": 13,
- "isNew": true,
- "legend": {
- "avg": false,
- "current": false,
- "max": false,
- "min": false,
- "show": true,
- "total": false,
- "values": false
- },
- "lines": true,
- "linewidth": 2,
- "links": [],
- "nullPointMode": "connected",
- "percentage": false,
- "pointradius": 5,
- "points": false,
- "renderer": "flot",
- "seriesOverrides": [],
- "span": 3,
- "stack": false,
- "steppedLine": false,
- "targets": [
- {
- "refId": "A",
- "target": "aliasByNode(statsd.fakesite.counters.session_start.*.count, 4)"
- }
- ],
- "thresholds": [
- {
- "value": 10,
- "op": "gt",
- "fill": true,
- "line": true,
- "colorMode": "warning",
- "fillColor": "rgba(235, 138, 14, 0.12)",
- "lineColor": "rgba(247, 149, 32, 0.60)"
- }
- ],
- "timeFrom": null,
- "timeShift": null,
- "title": "Fast Warning panel",
- "tooltip": {
- "msResolution": false,
- "shared": true,
- "sort": 0,
- "value_type": "cumulative"
- },
- "type": "graph",
- "xaxis": {
- "show": true
- },
- "yaxes": [
- {
- "format": "short",
- "label": null,
- "logBase": 1,
- "max": null,
- "min": null,
- "show": true
- },
- {
- "format": "short",
- "label": null,
- "logBase": 1,
- "max": null,
- "min": null,
- "show": true
- }
- ]
- },
- {
- "alert": {
- "enabled": true,
- "conditions": [
- {
- "type": "query",
- "query": {
- "params": [
- "A",
- "5m",
- "now"
- ]
- },
- "reducer": {
- "type": "avg",
- "params": []
- },
- "evaluator": {
- "type": "gt",
- "params": [
- 10
- ]
- }
- }
- ],
- "severity": "warning",
- "frequency": "1s",
- "handler": 1,
- "notifications": [],
- "name": "Fast Warning panel alert"
- },
- "aliasColors": {},
- "bars": false,
- "datasource": "${DS_GRAPHITE}",
- "editable": true,
- "error": false,
- "fill": 1,
- "id": 14,
- "isNew": true,
- "legend": {
- "avg": false,
- "current": false,
- "max": false,
- "min": false,
- "show": true,
- "total": false,
- "values": false
- },
- "lines": true,
- "linewidth": 2,
- "links": [],
- "nullPointMode": "connected",
- "percentage": false,
- "pointradius": 5,
- "points": false,
- "renderer": "flot",
- "seriesOverrides": [],
- "span": 3,
- "stack": false,
- "steppedLine": false,
- "targets": [
- {
- "refId": "A",
- "target": "aliasByNode(statsd.fakesite.counters.session_start.*.count, 4)"
- }
- ],
- "thresholds": [
- {
- "value": 10,
- "op": "gt",
- "fill": true,
- "line": true,
- "colorMode": "warning",
- "fillColor": "rgba(235, 138, 14, 0.12)",
- "lineColor": "rgba(247, 149, 32, 0.60)"
- }
- ],
- "timeFrom": null,
- "timeShift": null,
- "title": "Fast Warning panel",
- "tooltip": {
- "msResolution": false,
- "shared": true,
- "sort": 0,
- "value_type": "cumulative"
- },
- "type": "graph",
- "xaxis": {
- "show": true
- },
- "yaxes": [
- {
- "format": "short",
- "label": null,
- "logBase": 1,
- "max": null,
- "min": null,
- "show": true
- },
- {
- "format": "short",
- "label": null,
- "logBase": 1,
- "max": null,
- "min": null,
- "show": true
- }
- ]
- },
- {
- "alert": {
- "enabled": true,
- "conditions": [
- {
- "type": "query",
- "query": {
- "params": [
- "A",
- "5m",
- "now"
- ]
- },
- "reducer": {
- "type": "avg",
- "params": []
- },
- "evaluator": {
- "type": "gt",
- "params": [
- 10
- ]
- }
- }
- ],
- "severity": "warning",
- "frequency": "1s",
- "handler": 1,
- "notifications": [],
- "name": "Fast Warning panel alert"
- },
- "aliasColors": {},
- "bars": false,
- "datasource": "${DS_GRAPHITE}",
- "editable": true,
- "error": false,
- "fill": 1,
- "id": 15,
- "isNew": true,
- "legend": {
- "avg": false,
- "current": false,
- "max": false,
- "min": false,
- "show": true,
- "total": false,
- "values": false
- },
- "lines": true,
- "linewidth": 2,
- "links": [],
- "nullPointMode": "connected",
- "percentage": false,
- "pointradius": 5,
- "points": false,
- "renderer": "flot",
- "seriesOverrides": [],
- "span": 3,
- "stack": false,
- "steppedLine": false,
- "targets": [
- {
- "refId": "A",
- "target": "aliasByNode(statsd.fakesite.counters.session_start.*.count, 4)"
- }
- ],
- "thresholds": [
- {
- "value": 10,
- "op": "gt",
- "fill": true,
- "line": true,
- "colorMode": "warning",
- "fillColor": "rgba(235, 138, 14, 0.12)",
- "lineColor": "rgba(247, 149, 32, 0.60)"
- }
- ],
- "timeFrom": null,
- "timeShift": null,
- "title": "Fast Warning panel",
- "tooltip": {
- "msResolution": false,
- "shared": true,
- "sort": 0,
- "value_type": "cumulative"
- },
- "type": "graph",
- "xaxis": {
- "show": true
- },
- "yaxes": [
- {
- "format": "short",
- "label": null,
- "logBase": 1,
- "max": null,
- "min": null,
- "show": true
- },
- {
- "format": "short",
- "label": null,
- "logBase": 1,
- "max": null,
- "min": null,
- "show": true
- }
- ]
- },
- {
- "alert": {
- "enabled": true,
- "conditions": [
- {
- "type": "query",
- "query": {
- "params": [
- "A",
- "5m",
- "now"
- ]
- },
- "reducer": {
- "type": "avg",
- "params": []
- },
- "evaluator": {
- "type": "gt",
- "params": [
- 10
- ]
- }
- }
- ],
- "severity": "warning",
- "frequency": "1s",
- "handler": 1,
- "notifications": [],
- "name": "Fast Warning panel alert"
- },
- "aliasColors": {},
- "bars": false,
- "datasource": "${DS_GRAPHITE}",
- "editable": true,
- "error": false,
- "fill": 1,
- "id": 16,
- "isNew": true,
- "legend": {
- "avg": false,
- "current": false,
- "max": false,
- "min": false,
- "show": true,
- "total": false,
- "values": false
- },
- "lines": true,
- "linewidth": 2,
- "links": [],
- "nullPointMode": "connected",
- "percentage": false,
- "pointradius": 5,
- "points": false,
- "renderer": "flot",
- "seriesOverrides": [],
- "span": 3,
- "stack": false,
- "steppedLine": false,
- "targets": [
- {
- "refId": "A",
- "target": "aliasByNode(statsd.fakesite.counters.session_start.*.count, 4)"
- }
- ],
- "thresholds": [
- {
- "value": 10,
- "op": "gt",
- "fill": true,
- "line": true,
- "colorMode": "warning",
- "fillColor": "rgba(235, 138, 14, 0.12)",
- "lineColor": "rgba(247, 149, 32, 0.60)"
- }
- ],
- "timeFrom": null,
- "timeShift": null,
- "title": "Fast Warning panel",
- "tooltip": {
- "msResolution": false,
- "shared": true,
- "sort": 0,
- "value_type": "cumulative"
- },
- "type": "graph",
- "xaxis": {
- "show": true
- },
- "yaxes": [
- {
- "format": "short",
- "label": null,
- "logBase": 1,
- "max": null,
- "min": null,
- "show": true
- },
- {
- "format": "short",
- "label": null,
- "logBase": 1,
- "max": null,
- "min": null,
- "show": true
- }
- ]
- }
- ],
- "title": "New row"
- },
- {
- "title": "New row",
- "height": "250px",
- "editable": true,
- "collapse": false,
- "panels": [
- {
- "title": "Alert below value",
- "error": false,
- "span": 3,
- "editable": true,
- "type": "graph",
- "isNew": true,
- "id": 17,
- "targets": [
- {
- "target": "aliasByNode(statsd.fakesite.counters.session_start.*.count, 4)",
- "refId": "A"
- }
- ],
- "datasource": "${DS_GRAPHITE}",
- "renderer": "flot",
- "yaxes": [
- {
- "label": null,
- "show": true,
- "logBase": 1,
- "min": null,
- "max": null,
- "format": "short"
- },
- {
- "label": null,
- "show": true,
- "logBase": 1,
- "min": null,
- "max": null,
- "format": "short"
- }
- ],
- "xaxis": {
- "show": true
- },
- "alert": {
- "conditions": [
- {
- "type": "query",
- "query": {
- "params": [
- "A",
- "5m",
- "now"
- ]
- },
- "reducer": {
- "type": "avg",
- "params": []
- },
- "evaluator": {
- "type": "lt",
- "params": [
- 20
- ]
- }
- }
- ],
- "severity": "critical",
- "frequency": "60s",
- "handler": 1,
- "notifications": [],
- "name": "Alert below value",
- "enabled": true
- },
- "lines": true,
- "fill": 1,
- "linewidth": 2,
- "points": false,
- "pointradius": 5,
- "bars": false,
- "stack": false,
- "percentage": false,
- "legend": {
- "show": true,
- "values": false,
- "min": false,
- "max": false,
- "current": false,
- "total": false,
- "avg": false
- },
- "nullPointMode": "connected",
- "steppedLine": false,
- "tooltip": {
- "value_type": "cumulative",
- "shared": true,
- "sort": 0,
- "msResolution": false
- },
- "timeFrom": null,
- "timeShift": null,
- "aliasColors": {},
- "seriesOverrides": [],
- "thresholds": [
- {
- "value": 20,
- "op": "lt",
- "fill": true,
- "line": true,
- "colorMode": "critical",
- "fillColor": "rgba(234, 112, 112, 0.12)",
- "lineColor": "rgba(237, 46, 24, 0.60)"
- }
- ],
- "links": []
- },
- {
- "title": "Alert is outside range",
- "error": false,
- "span": 3,
- "editable": true,
- "type": "graph",
- "isNew": true,
- "id": 18,
- "targets": [
- {
- "target": "aliasByNode(statsd.fakesite.counters.session_start.*.count, 4)",
- "refId": "A"
- }
- ],
- "datasource": "${DS_GRAPHITE}",
- "renderer": "flot",
- "yaxes": [
- {
- "label": null,
- "show": true,
- "logBase": 1,
- "min": null,
- "max": null,
- "format": "short"
- },
- {
- "label": null,
- "show": true,
- "logBase": 1,
- "min": null,
- "max": null,
- "format": "short"
- }
- ],
- "xaxis": {
- "show": true
- },
- "alert": {
- "conditions": [
- {
- "type": "query",
- "query": {
- "params": [
- "A",
- "5m",
- "now"
- ]
- },
- "reducer": {
- "type": "avg",
- "params": []
- },
- "evaluator": {
- "type": "outside_range",
- "params": [
- 10,
- 80
- ]
- }
- }
- ],
- "severity": "critical",
- "frequency": "10s",
- "handler": 1,
- "notifications": [],
- "name": "Alert is outside range",
- "enabled": true
- },
- "lines": true,
- "fill": 1,
- "linewidth": 2,
- "points": false,
- "pointradius": 5,
- "bars": false,
- "stack": false,
- "percentage": false,
- "legend": {
- "show": true,
- "values": false,
- "min": false,
- "max": false,
- "current": false,
- "total": false,
- "avg": false
- },
- "nullPointMode": "connected",
- "steppedLine": false,
- "tooltip": {
- "value_type": "cumulative",
- "shared": true,
- "sort": 0,
- "msResolution": false
- },
- "timeFrom": null,
- "timeShift": null,
- "aliasColors": {},
- "seriesOverrides": [],
- "thresholds": [
- {
- "value": 10,
- "op": "lt",
- "fill": true,
- "line": true,
- "colorMode": "critical",
- "fillColor": "rgba(234, 112, 112, 0.12)",
- "lineColor": "rgba(237, 46, 24, 0.60)"
- },
- {
- "value": 80,
- "op": "gt",
- "fill": true,
- "line": true,
- "colorMode": "critical",
- "fillColor": "rgba(234, 112, 112, 0.12)",
- "lineColor": "rgba(237, 46, 24, 0.60)"
- }
- ],
- "links": []
- },
- {
- "title": "Alert is within range",
- "error": false,
- "span": 3,
- "editable": true,
- "type": "graph",
- "isNew": true,
- "id": 19,
- "targets": [
- {
- "target": "aliasByNode(statsd.fakesite.counters.session_start.*.count, 4)",
- "refId": "A"
- }
- ],
- "datasource": "${DS_GRAPHITE}",
- "renderer": "flot",
- "yaxes": [
- {
- "label": null,
- "show": true,
- "logBase": 1,
- "min": null,
- "max": null,
- "format": "short"
- },
- {
- "label": null,
- "show": true,
- "logBase": 1,
- "min": null,
- "max": null,
- "format": "short"
- }
- ],
- "xaxis": {
- "show": true
- },
- "alert": {
- "conditions": [
- {
- "type": "query",
- "query": {
- "params": [
- "A",
- "5m",
- "now"
- ]
- },
- "reducer": {
- "type": "avg",
- "params": []
- },
- "evaluator": {
- "type": "within_range",
- "params": [
- 60,
- 80
- ]
- }
- }
- ],
- "severity": "critical",
- "frequency": "10s",
- "handler": 1,
- "notifications": [],
- "name": "Alert is within range",
- "enabled": true
- },
- "lines": true,
- "fill": 1,
- "linewidth": 2,
- "points": false,
- "pointradius": 5,
- "bars": false,
- "stack": false,
- "percentage": false,
- "legend": {
- "show": true,
- "values": false,
- "min": false,
- "max": false,
- "current": false,
- "total": false,
- "avg": false
- },
- "nullPointMode": "connected",
- "steppedLine": false,
- "tooltip": {
- "value_type": "cumulative",
- "shared": true,
- "sort": 0,
- "msResolution": false
- },
- "timeFrom": null,
- "timeShift": null,
- "aliasColors": {},
- "seriesOverrides": [],
- "thresholds": [
- {
- "value": 60,
- "op": "gt",
- "fill": true,
- "line": true,
- "colorMode": "critical",
- "fillColor": "rgba(234, 112, 112, 0.12)",
- "lineColor": "rgba(237, 46, 24, 0.60)"
- },
- {
- "value": 80,
- "op": "lt",
- "fill": true,
- "line": true,
- "colorMode": "critical",
- "fillColor": "rgba(234, 112, 112, 0.12)",
- "lineColor": "rgba(237, 46, 24, 0.60)"
- }
- ],
- "links": []
- }
- ]
- }
- ],
- "time": {
- "from": "now-6h",
- "to": "now"
- },
- "timepicker": {
- "refresh_intervals": [
- "5s",
- "10s",
- "30s",
- "1m",
- "5m",
- "15m",
- "30m",
- "1h",
- "2h",
- "1d"
- ],
- "time_options": [
- "5m",
- "15m",
- "1h",
- "6h",
- "12h",
- "24h",
- "2d",
- "7d",
- "30d"
- ]
- },
- "templating": {
- "list": []
- },
- "annotations": {
- "list": []
- },
- "schemaVersion": 13,
- "version": 50,
- "links": [],
- "gnetId": null
-}
\ No newline at end of file
diff --git a/jest.config.js b/jest.config.js
index 606465c9840..cac634fbf10 100644
--- a/jest.config.js
+++ b/jest.config.js
@@ -1,19 +1,14 @@
module.exports = {
verbose: false,
- "globals": {
- "ts-jest": {
- "tsConfigFile": "tsconfig.json"
- }
- },
"transform": {
- "^.+\\.tsx?$": "/node_modules/ts-jest/preprocessor.js"
+ "^.+\\.(ts|tsx)$": "ts-jest"
},
"moduleDirectories": ["node_modules", "public"],
"roots": [
"/public"
],
- "testRegex": "(\\.|/)(jest)\\.(jsx?|tsx?)$",
+ "testRegex": "(\\.|/)(test)\\.(jsx?|tsx?)$",
"moduleFileExtensions": [
"ts",
"tsx",
diff --git a/karma.conf.js b/karma.conf.js
deleted file mode 100644
index 352e8e4e027..00000000000
--- a/karma.conf.js
+++ /dev/null
@@ -1,40 +0,0 @@
-var webpack = require('webpack');
-var path = require('path');
-var webpackTestConfig = require('./scripts/webpack/webpack.test.js');
-
-module.exports = function(config) {
-
- 'use strict';
-
- config.set({
- frameworks: ['mocha', 'expect', 'sinon'],
-
- // list of files / patterns to load in the browser
- files: [
- { pattern: 'public/test/index.ts', watched: false }
- ],
-
- preprocessors: {
- 'public/test/index.ts': ['webpack', 'sourcemap'],
- },
-
- webpack: webpackTestConfig,
- webpackMiddleware: {
- stats: 'minimal',
- },
-
- // list of files to exclude
- exclude: [],
- reporters: ['dots'],
- port: 9876,
- colors: true,
- logLevel: config.LOG_INFO,
- autoWatch: true,
- browsers: ['PhantomJS'],
- captureTimeout: 20000,
- singleRun: true,
- // autoWatchBatchDelay: 1000,
- // browserNoActivityTimeout: 60000,
- });
-
-};
diff --git a/latest.json b/latest.json
index 8e26289c856..bce09c3283b 100644
--- a/latest.json
+++ b/latest.json
@@ -1,4 +1,4 @@
{
- "stable": "5.2.0",
- "testing": "5.2.0"
+ "stable": "5.2.4",
+ "testing": "5.2.4"
}
diff --git a/package.json b/package.json
index c26438230cc..1e7ed02c87b 100644
--- a/package.json
+++ b/package.json
@@ -11,11 +11,12 @@
},
"devDependencies": {
"@types/d3": "^4.10.1",
- "@types/enzyme": "^2.8.9",
+ "@types/enzyme": "^3.1.13",
"@types/jest": "^21.1.4",
"@types/node": "^8.0.31",
- "@types/react": "^16.0.25",
- "@types/react-dom": "^16.0.3",
+ "@types/react": "^16.4.14",
+ "@types/react-custom-scrollbars": "^4.0.5",
+ "@types/react-dom": "^16.0.7",
"angular-mocks": "1.6.6",
"autoprefixer": "^6.4.0",
"axios": "^0.17.1",
@@ -25,16 +26,15 @@
"babel-preset-es2015": "^6.24.1",
"clean-webpack-plugin": "^0.1.19",
"css-loader": "^0.28.7",
- "enzyme": "^3.1.0",
- "enzyme-adapter-react-16": "^1.0.1",
- "enzyme-to-json": "^3.3.0",
+ "enzyme": "^3.6.0",
+ "enzyme-adapter-react-16": "^1.5.0",
+ "enzyme-to-json": "^3.3.4",
"es6-promise": "^3.0.2",
"es6-shim": "^0.35.3",
"expect.js": "~0.2.0",
"expose-loader": "^0.7.3",
- "extract-text-webpack-plugin": "^4.0.0-beta.0",
"file-loader": "^1.1.11",
- "fork-ts-checker-webpack-plugin": "^0.4.1",
+ "fork-ts-checker-webpack-plugin": "^0.4.9",
"gaze": "^1.1.2",
"glob": "~7.0.0",
"grunt": "1.0.1",
@@ -45,10 +45,7 @@
"grunt-contrib-concat": "^1.0.1",
"grunt-contrib-copy": "~1.0.0",
"grunt-contrib-cssmin": "~1.0.2",
- "grunt-contrib-jshint": "~1.1.0",
"grunt-exec": "^1.0.1",
- "grunt-jscs": "3.0.1",
- "grunt-karma": "~2.0.0",
"grunt-notify": "^0.4.5",
"grunt-postcss": "^0.8.0",
"grunt-sass": "^2.0.0",
@@ -59,46 +56,39 @@
"html-webpack-harddisk-plugin": "^0.2.0",
"html-webpack-plugin": "^3.2.0",
"husky": "^0.14.3",
- "jest": "^22.0.4",
- "jshint-stylish": "~2.2.1",
- "karma": "1.7.0",
- "karma-chrome-launcher": "~2.2.0",
- "karma-expect": "~1.1.3",
- "karma-mocha": "~1.3.0",
- "karma-phantomjs-launcher": "1.0.4",
- "karma-sinon": "^1.0.5",
- "karma-sourcemap-loader": "^0.3.7",
- "karma-webpack": "^3.0.0",
+ "jest": "^23.6.0",
"lint-staged": "^6.0.0",
"load-grunt-tasks": "3.5.2",
- "mobx-react-devtools": "^4.2.15",
+ "mini-css-extract-plugin": "^0.4.0",
"mocha": "^4.0.1",
"ng-annotate-loader": "^0.6.1",
- "ng-annotate-webpack-plugin": "^0.2.1-pre",
+ "ng-annotate-webpack-plugin": "^0.3.0",
"ngtemplate-loader": "^2.0.1",
"npm": "^5.4.2",
+ "optimize-css-assets-webpack-plugin": "^4.0.2",
"phantomjs-prebuilt": "^2.1.15",
"postcss-browser-reporter": "^0.5.0",
"postcss-loader": "^2.0.6",
"postcss-reporter": "^5.0.0",
"prettier": "1.9.2",
- "react-hot-loader": "^4.2.0",
- "react-test-renderer": "^16.0.0",
+ "react-hot-loader": "^4.3.6",
+ "react-test-renderer": "^16.5.0",
"sass-lint": "^1.10.2",
"sass-loader": "^7.0.1",
"sinon": "1.17.6",
"style-loader": "^0.21.0",
"systemjs": "0.20.19",
"systemjs-plugin-css": "^0.1.36",
- "ts-loader": "^4.3.0",
- "ts-jest": "^22.4.6",
+ "ts-jest": "^23.1.4",
+ "ts-loader": "^5.1.0",
+ "tslib": "^1.9.3",
"tslint": "^5.8.0",
"tslint-loader": "^3.5.3",
- "typescript": "^2.6.2",
+ "typescript": "^3.0.3",
+ "uglifyjs-webpack-plugin": "^1.2.7",
"webpack": "^4.8.0",
"webpack-bundle-analyzer": "^2.9.0",
"webpack-cleanup-plugin": "^0.5.1",
- "fork-ts-checker-webpack-plugin": "^0.4.2",
"webpack-cli": "^2.1.4",
"webpack-dev-server": "^3.1.0",
"webpack-merge": "^4.1.0",
@@ -110,9 +100,7 @@
"watch": "webpack --progress --colors --watch --mode development --config scripts/webpack/webpack.dev.js",
"build": "grunt build",
"test": "grunt test",
- "test:coverage": "grunt test --coverage=true",
- "lint": "tslint -c tslint.json --project tsconfig.json --type-check",
- "karma": "grunt karma:dev",
+ "lint": "tslint -c tslint.json --project tsconfig.json",
"jest": "jest --notify --watch",
"api-tests": "jest --notify --watch --config=tests/api/jest.js",
"precommit": "lint-staged && grunt precommit"
@@ -143,6 +131,7 @@
"angular-native-dragdrop": "1.2.2",
"angular-route": "1.6.6",
"angular-sanitize": "1.6.6",
+ "babel-jest": "^23.6.0",
"babel-polyfill": "^6.26.0",
"baron": "^3.0.3",
"brace": "^0.10.0",
@@ -155,34 +144,36 @@
"immutable": "^3.8.2",
"jquery": "^3.2.1",
"lodash": "^4.17.10",
- "mini-css-extract-plugin": "^0.4.0",
- "mobx": "^3.4.1",
- "mobx-react": "^4.3.5",
- "mobx-state-tree": "^1.3.1",
"moment": "^2.22.2",
"mousetrap": "^1.6.0",
"mousetrap-global-bind": "^1.1.0",
- "optimize-css-assets-webpack-plugin": "^4.0.2",
"prismjs": "^1.6.0",
- "prop-types": "^15.6.0",
- "react": "^16.2.0",
- "react-dom": "^16.2.0",
+ "prop-types": "^15.6.2",
+ "rc-cascader": "^0.14.0",
+ "react": "^16.5.0",
+ "react-custom-scrollbars": "^4.2.1",
+ "react-dom": "^16.5.0",
"react-grid-layout": "0.16.6",
"react-highlight-words": "^0.10.0",
"react-popper": "^0.7.5",
+ "react-redux": "^5.0.7",
"react-select": "^1.1.0",
"react-sizeme": "^2.3.6",
"react-transition-group": "^2.2.1",
+ "redux": "^4.0.0",
+ "redux-logger": "^3.0.6",
+ "redux-thunk": "^2.3.0",
"remarkable": "^1.7.1",
"rst2html": "github:thoward/rst2html#990cb89",
"rxjs": "^5.4.3",
"slate": "^0.33.4",
"slate-plain-serializer": "^0.5.10",
+ "slate-prism": "^0.5.0",
"slate-react": "^0.12.4",
"tether": "^1.4.0",
"tether-drop": "https://github.com/torkelo/drop/tarball/master",
"tinycolor2": "^1.4.1",
- "uglifyjs-webpack-plugin": "^1.2.7"
+ "tslint-react": "^3.6.0"
},
"resolutions": {
"caniuse-db": "1.0.30000772"
diff --git a/packaging/docker/Dockerfile b/packaging/docker/Dockerfile
new file mode 100644
index 00000000000..890d6a4fb11
--- /dev/null
+++ b/packaging/docker/Dockerfile
@@ -0,0 +1,52 @@
+FROM debian:stretch-slim
+
+ARG GRAFANA_TGZ="grafana-latest.linux-x64.tar.gz"
+
+RUN apt-get update && apt-get install -qq -y tar && \
+ apt-get autoremove -y && \
+ rm -rf /var/lib/apt/lists/*
+
+COPY ${GRAFANA_TGZ} /tmp/grafana.tar.gz
+
+RUN mkdir /tmp/grafana && tar xfvz /tmp/grafana.tar.gz --strip-components=1 -C /tmp/grafana
+
+FROM debian:stretch-slim
+
+ARG GF_UID="472"
+ARG GF_GID="472"
+
+ENV PATH=/usr/share/grafana/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin \
+ GF_PATHS_CONFIG="/etc/grafana/grafana.ini" \
+ GF_PATHS_DATA="/var/lib/grafana" \
+ GF_PATHS_HOME="/usr/share/grafana" \
+ GF_PATHS_LOGS="/var/log/grafana" \
+ GF_PATHS_PLUGINS="/var/lib/grafana/plugins" \
+ GF_PATHS_PROVISIONING="/etc/grafana/provisioning"
+
+WORKDIR $GF_PATHS_HOME
+
+RUN apt-get update && apt-get install -qq -y libfontconfig ca-certificates && \
+ apt-get autoremove -y && \
+ rm -rf /var/lib/apt/lists/*
+
+COPY --from=0 /tmp/grafana "$GF_PATHS_HOME"
+
+RUN mkdir -p "$GF_PATHS_HOME/.aws" && \
+ groupadd -r -g $GF_GID grafana && \
+ useradd -r -u $GF_UID -g grafana grafana && \
+ mkdir -p "$GF_PATHS_PROVISIONING/datasources" \
+ "$GF_PATHS_PROVISIONING/dashboards" \
+ "$GF_PATHS_LOGS" \
+ "$GF_PATHS_PLUGINS" \
+ "$GF_PATHS_DATA" && \
+ cp "$GF_PATHS_HOME/conf/sample.ini" "$GF_PATHS_CONFIG" && \
+ cp "$GF_PATHS_HOME/conf/ldap.toml" /etc/grafana/ldap.toml && \
+ chown -R grafana:grafana "$GF_PATHS_DATA" "$GF_PATHS_HOME/.aws" "$GF_PATHS_LOGS" "$GF_PATHS_PLUGINS" && \
+ chmod 777 "$GF_PATHS_DATA" "$GF_PATHS_HOME/.aws" "$GF_PATHS_LOGS" "$GF_PATHS_PLUGINS"
+
+EXPOSE 3000
+
+COPY ./run.sh /run.sh
+
+USER grafana
+ENTRYPOINT [ "/run.sh" ]
diff --git a/packaging/docker/README.md b/packaging/docker/README.md
new file mode 100644
index 00000000000..cfb3c7248ef
--- /dev/null
+++ b/packaging/docker/README.md
@@ -0,0 +1,43 @@
+# Grafana Docker image
+
+## Running your Grafana container
+
+Start your container binding the external port `3000`.
+
+```bash
+docker run -d --name=grafana -p 3000:3000 grafana/grafana
+```
+
+Try it out, default admin user is admin/admin.
+
+## How to use the container
+
+Further documentation can be found at http://docs.grafana.org/installation/docker/
+
+## Changelog
+
+### v5.1.5, v5.2.0-beta2
+* Fix: config keys ending with _FILE are not respected [#170](https://github.com/grafana/grafana-docker/issues/170)
+
+### v5.2.0-beta1
+* Support for Docker Secrets
+
+### v5.1.0
+* Major restructuring of the container
+* Usage of `chown` removed
+* File permissions incompatibility with previous versions
+ * user id changed from 104 to 472
+ * group id changed from 107 to 472
+* Runs as the grafana user by default (instead of root)
+* All default volumes removed
+
+### v4.2.0
+* Plugins are now installed into ${GF_PATHS_PLUGINS}
+* Building the container now requires a full url to the deb package instead of just version
+* Fixes bug caused by installing multiple plugins
+
+### v4.0.0-beta2
+* Plugins dir (`/var/lib/grafana/plugins`) is no longer a separate volume
+
+### v3.1.1
+* Make it possible to install specific plugin version https://github.com/grafana/grafana-docker/issues/59#issuecomment-260584026
diff --git a/packaging/docker/build-deploy.sh b/packaging/docker/build-deploy.sh
new file mode 100755
index 00000000000..ac3226a4a61
--- /dev/null
+++ b/packaging/docker/build-deploy.sh
@@ -0,0 +1,13 @@
+#!/bin/sh
+set -e
+
+_grafana_version=$1
+./build.sh "$_grafana_version"
+docker login -u "$DOCKER_USER" -p "$DOCKER_PASS"
+
+./push_to_docker_hub.sh "$_grafana_version"
+
+if echo "$_grafana_version" | grep -q "^master-"; then
+ apk add --no-cache curl
+ ./deploy_to_k8s.sh "grafana/grafana-dev:$_grafana_version"
+fi
diff --git a/packaging/docker/build.sh b/packaging/docker/build.sh
new file mode 100755
index 00000000000..c303c71cd5f
--- /dev/null
+++ b/packaging/docker/build.sh
@@ -0,0 +1,25 @@
+#!/bin/sh
+
+_grafana_tag=$1
+
+# If the tag starts with v, treat this as a official release
+if echo "$_grafana_tag" | grep -q "^v"; then
+ _grafana_version=$(echo "${_grafana_tag}" | cut -d "v" -f 2)
+ _docker_repo=${2:-grafana/grafana}
+else
+ _grafana_version=$_grafana_tag
+ _docker_repo=${2:-grafana/grafana-dev}
+fi
+
+echo "Building ${_docker_repo}:${_grafana_version}"
+
+docker build \
+ --tag "${_docker_repo}:${_grafana_version}" \
+ --no-cache=true .
+
+# Tag as 'latest' for official release; otherwise tag as grafana/grafana:master
+if echo "$_grafana_tag" | grep -q "^v"; then
+ docker tag "${_docker_repo}:${_grafana_version}" "${_docker_repo}:latest"
+else
+ docker tag "${_docker_repo}:${_grafana_version}" "grafana/grafana:master"
+fi
diff --git a/packaging/docker/custom/Dockerfile b/packaging/docker/custom/Dockerfile
new file mode 100644
index 00000000000..79eba5f29e9
--- /dev/null
+++ b/packaging/docker/custom/Dockerfile
@@ -0,0 +1,16 @@
+ARG GRAFANA_VERSION="latest"
+
+FROM grafana/grafana:${GRAFANA_VERSION}
+
+USER grafana
+
+ARG GF_INSTALL_PLUGINS=""
+
+RUN if [ ! -z "${GF_INSTALL_PLUGINS}" ]; then \
+ OLDIFS=$IFS; \
+ IFS=','; \
+ for plugin in ${GF_INSTALL_PLUGINS}; do \
+ IFS=$OLDIFS; \
+ grafana-cli --pluginsDir "$GF_PATHS_PLUGINS" plugins install ${plugin}; \
+ done; \
+fi
diff --git a/packaging/docker/deploy_to_k8s.sh b/packaging/docker/deploy_to_k8s.sh
new file mode 100755
index 00000000000..26cf88ef688
--- /dev/null
+++ b/packaging/docker/deploy_to_k8s.sh
@@ -0,0 +1,6 @@
+#!/bin/sh
+
+curl -s --header "Content-Type: application/json" \
+ --data "{\"build_parameters\": {\"CIRCLE_JOB\": \"deploy\", \"IMAGE_NAMES\": \"$1\"}}" \
+ --request POST \
+ https://circleci.com/api/v1.1/project/github/raintank/deployment_tools/tree/master?circle-token=$CIRCLE_TOKEN
diff --git a/packaging/docker/push_to_docker_hub.sh b/packaging/docker/push_to_docker_hub.sh
new file mode 100755
index 00000000000..526c216f8fa
--- /dev/null
+++ b/packaging/docker/push_to_docker_hub.sh
@@ -0,0 +1,24 @@
+#!/bin/sh
+set -e
+
+_grafana_tag=$1
+
+# If the tag starts with v, treat this as a official release
+if echo "$_grafana_tag" | grep -q "^v"; then
+ _grafana_version=$(echo "${_grafana_tag}" | cut -d "v" -f 2)
+ _docker_repo=${2:-grafana/grafana}
+else
+ _grafana_version=$_grafana_tag
+ _docker_repo=${2:-grafana/grafana-dev}
+fi
+
+echo "pushing ${_docker_repo}:${_grafana_version}"
+docker push "${_docker_repo}:${_grafana_version}"
+
+if echo "$_grafana_tag" | grep -q "^v" && echo "$_grafana_tag" | grep -vq "beta"; then
+ echo "pushing ${_docker_repo}:latest"
+ docker push "${_docker_repo}:latest"
+elif echo "$_grafana_tag" | grep -q "master"; then
+ echo "pushing grafana/grafana:master"
+ docker push grafana/grafana:master
+fi
diff --git a/packaging/docker/run.sh b/packaging/docker/run.sh
new file mode 100755
index 00000000000..bc001bdf90a
--- /dev/null
+++ b/packaging/docker/run.sh
@@ -0,0 +1,88 @@
+#!/bin/bash -e
+
+PERMISSIONS_OK=0
+
+if [ ! -r "$GF_PATHS_CONFIG" ]; then
+ echo "GF_PATHS_CONFIG='$GF_PATHS_CONFIG' is not readable."
+ PERMISSIONS_OK=1
+fi
+
+if [ ! -w "$GF_PATHS_DATA" ]; then
+ echo "GF_PATHS_DATA='$GF_PATHS_DATA' is not writable."
+ PERMISSIONS_OK=1
+fi
+
+if [ ! -r "$GF_PATHS_HOME" ]; then
+ echo "GF_PATHS_HOME='$GF_PATHS_HOME' is not readable."
+ PERMISSIONS_OK=1
+fi
+
+if [ $PERMISSIONS_OK -eq 1 ]; then
+ echo "You may have issues with file permissions, more information here: http://docs.grafana.org/installation/docker/#migration-from-a-previous-version-of-the-docker-container-to-5-1-or-later"
+fi
+
+if [ ! -d "$GF_PATHS_PLUGINS" ]; then
+ mkdir "$GF_PATHS_PLUGINS"
+fi
+
+if [ ! -z ${GF_AWS_PROFILES+x} ]; then
+ > "$GF_PATHS_HOME/.aws/credentials"
+
+ for profile in ${GF_AWS_PROFILES}; do
+ access_key_varname="GF_AWS_${profile}_ACCESS_KEY_ID"
+ secret_key_varname="GF_AWS_${profile}_SECRET_ACCESS_KEY"
+ region_varname="GF_AWS_${profile}_REGION"
+
+ if [ ! -z "${!access_key_varname}" -a ! -z "${!secret_key_varname}" ]; then
+ echo "[${profile}]" >> "$GF_PATHS_HOME/.aws/credentials"
+ echo "aws_access_key_id = ${!access_key_varname}" >> "$GF_PATHS_HOME/.aws/credentials"
+ echo "aws_secret_access_key = ${!secret_key_varname}" >> "$GF_PATHS_HOME/.aws/credentials"
+ if [ ! -z "${!region_varname}" ]; then
+ echo "region = ${!region_varname}" >> "$GF_PATHS_HOME/.aws/credentials"
+ fi
+ fi
+ done
+
+ chmod 600 "$GF_PATHS_HOME/.aws/credentials"
+fi
+
+# Convert all environment variables with names ending in __FILE into the content of
+# the file that they point at and use the name without the trailing __FILE.
+# This can be used to carry in Docker secrets.
+for VAR_NAME in $(env | grep '^GF_[^=]\+__FILE=.\+' | sed -r "s/([^=]*)__FILE=.*/\1/g"); do
+ VAR_NAME_FILE="$VAR_NAME"__FILE
+ if [ "${!VAR_NAME}" ]; then
+ echo >&2 "ERROR: Both $VAR_NAME and $VAR_NAME_FILE are set (but are exclusive)"
+ exit 1
+ fi
+ echo "Getting secret $VAR_NAME from ${!VAR_NAME_FILE}"
+ export "$VAR_NAME"="$(< "${!VAR_NAME_FILE}")"
+ unset "$VAR_NAME_FILE"
+done
+
+export HOME="$GF_PATHS_HOME"
+
+if [ ! -z "${GF_INSTALL_PLUGINS}" ]; then
+ OLDIFS=$IFS
+ IFS=','
+ for plugin in ${GF_INSTALL_PLUGINS}; do
+ IFS=$OLDIFS
+ if [[ $plugin =~ .*\;.* ]]; then
+ pluginUrl=$(echo "$plugin" | cut -d';' -f 1)
+ pluginWithoutUrl=$(echo "$plugin" | cut -d';' -f 2)
+ grafana-cli --pluginUrl "${pluginUrl}" --pluginsDir "${GF_PATHS_PLUGINS}" plugins install ${pluginWithoutUrl}
+ else
+ grafana-cli --pluginsDir "${GF_PATHS_PLUGINS}" plugins install ${plugin}
+ fi
+ done
+fi
+
+exec grafana-server \
+ --homepath="$GF_PATHS_HOME" \
+ --config="$GF_PATHS_CONFIG" \
+ "$@" \
+ cfg:default.log.mode="console" \
+ cfg:default.paths.data="$GF_PATHS_DATA" \
+ cfg:default.paths.logs="$GF_PATHS_LOGS" \
+ cfg:default.paths.plugins="$GF_PATHS_PLUGINS" \
+ cfg:default.paths.provisioning="$GF_PATHS_PROVISIONING"
diff --git a/pkg/api/alerting.go b/pkg/api/alerting.go
index 60013fe2b10..a936d696207 100644
--- a/pkg/api/alerting.go
+++ b/pkg/api/alerting.go
@@ -192,14 +192,7 @@ func GetAlertNotifications(c *m.ReqContext) Response {
result := make([]*dtos.AlertNotification, 0)
for _, notification := range query.Result {
- result = append(result, &dtos.AlertNotification{
- Id: notification.Id,
- Name: notification.Name,
- Type: notification.Type,
- IsDefault: notification.IsDefault,
- Created: notification.Created,
- Updated: notification.Updated,
- })
+ result = append(result, dtos.NewAlertNotification(notification))
}
return JSON(200, result)
@@ -215,7 +208,7 @@ func GetAlertNotificationByID(c *m.ReqContext) Response {
return Error(500, "Failed to get alert notifications", err)
}
- return JSON(200, query.Result)
+ return JSON(200, dtos.NewAlertNotification(query.Result))
}
func CreateAlertNotification(c *m.ReqContext, cmd m.CreateAlertNotificationCommand) Response {
@@ -225,7 +218,7 @@ func CreateAlertNotification(c *m.ReqContext, cmd m.CreateAlertNotificationComma
return Error(500, "Failed to create alert notification", err)
}
- return JSON(200, cmd.Result)
+ return JSON(200, dtos.NewAlertNotification(cmd.Result))
}
func UpdateAlertNotification(c *m.ReqContext, cmd m.UpdateAlertNotificationCommand) Response {
@@ -235,7 +228,7 @@ func UpdateAlertNotification(c *m.ReqContext, cmd m.UpdateAlertNotificationComma
return Error(500, "Failed to update alert notification", err)
}
- return JSON(200, cmd.Result)
+ return JSON(200, dtos.NewAlertNotification(cmd.Result))
}
func DeleteAlertNotification(c *m.ReqContext) Response {
diff --git a/pkg/api/annotations.go b/pkg/api/annotations.go
index 55c9c954940..242b5531f51 100644
--- a/pkg/api/annotations.go
+++ b/pkg/api/annotations.go
@@ -24,6 +24,7 @@ func GetAnnotations(c *m.ReqContext) Response {
Limit: c.QueryInt64("limit"),
Tags: c.QueryStrings("tags"),
Type: c.Query("type"),
+ MatchAny: c.QueryBool("matchAny"),
}
repo := annotations.GetRepository()
diff --git a/pkg/api/api.go b/pkg/api/api.go
index 84425fdae3d..39b332aeb9f 100644
--- a/pkg/api/api.go
+++ b/pkg/api/api.go
@@ -120,6 +120,7 @@ func (hs *HTTPServer) registerRoutes() {
userRoute.Put("/", bind(m.UpdateUserCommand{}), Wrap(UpdateSignedInUser))
userRoute.Post("/using/:id", Wrap(UserSetUsingOrg))
userRoute.Get("/orgs", Wrap(GetSignedInUserOrgList))
+ userRoute.Get("/teams", Wrap(GetSignedInUserTeamList))
userRoute.Post("/stars/dashboard/:id", Wrap(StarDashboard))
userRoute.Delete("/stars/dashboard/:id", Wrap(UnstarDashboard))
@@ -319,7 +320,7 @@ func (hs *HTTPServer) registerRoutes() {
apiRoute.Get("/search/", Search)
// metrics
- apiRoute.Post("/tsdb/query", bind(dtos.MetricRequest{}), Wrap(QueryMetrics))
+ apiRoute.Post("/tsdb/query", bind(dtos.MetricRequest{}), Wrap(hs.QueryMetrics))
apiRoute.Get("/tsdb/testdata/scenarios", Wrap(GetTestDataScenarios))
apiRoute.Get("/tsdb/testdata/gensql", reqGrafanaAdmin, Wrap(GenerateSQLTestData))
apiRoute.Get("/tsdb/testdata/random-walk", Wrap(GetTestDataRandomWalk))
diff --git a/pkg/api/dataproxy.go b/pkg/api/dataproxy.go
index 33839ca985d..f455d3dbd29 100644
--- a/pkg/api/dataproxy.go
+++ b/pkg/api/dataproxy.go
@@ -13,19 +13,20 @@ import (
const HeaderNameNoBackendCache = "X-Grafana-NoCache"
-func (hs *HTTPServer) getDatasourceByID(id int64, orgID int64, nocache bool) (*m.DataSource, error) {
+func (hs *HTTPServer) getDatasourceFromCache(id int64, c *m.ReqContext) (*m.DataSource, error) {
+ nocache := c.Req.Header.Get(HeaderNameNoBackendCache) == "true"
cacheKey := fmt.Sprintf("ds-%d", id)
if !nocache {
if cached, found := hs.cache.Get(cacheKey); found {
ds := cached.(*m.DataSource)
- if ds.OrgId == orgID {
+ if ds.OrgId == c.OrgId {
return ds, nil
}
}
}
- query := m.GetDataSourceByIdQuery{Id: id, OrgId: orgID}
+ query := m.GetDataSourceByIdQuery{Id: id, OrgId: c.OrgId}
if err := bus.Dispatch(&query); err != nil {
return nil, err
}
@@ -37,10 +38,7 @@ func (hs *HTTPServer) getDatasourceByID(id int64, orgID int64, nocache bool) (*m
func (hs *HTTPServer) ProxyDataSourceRequest(c *m.ReqContext) {
c.TimeRequest(metrics.M_DataSource_ProxyReq_Timer)
- nocache := c.Req.Header.Get(HeaderNameNoBackendCache) == "true"
-
- ds, err := hs.getDatasourceByID(c.ParamsInt64(":id"), c.OrgId, nocache)
-
+ ds, err := hs.getDatasourceFromCache(c.ParamsInt64(":id"), c)
if err != nil {
c.JsonApiErr(500, "Unable to load datasource meta data", err)
return
diff --git a/pkg/api/datasources.go b/pkg/api/datasources.go
index 6ffefea991a..23dbb221d71 100644
--- a/pkg/api/datasources.go
+++ b/pkg/api/datasources.go
@@ -158,12 +158,26 @@ func UpdateDataSource(c *m.ReqContext, cmd m.UpdateDataSourceCommand) Response {
}
return Error(500, "Failed to update datasource", err)
}
- ds := convertModelToDtos(cmd.Result)
+
+ query := m.GetDataSourceByIdQuery{
+ Id: cmd.Id,
+ OrgId: c.OrgId,
+ }
+
+ if err := bus.Dispatch(&query); err != nil {
+ if err == m.ErrDataSourceNotFound {
+ return Error(404, "Data source not found", nil)
+ }
+ return Error(500, "Failed to query datasources", err)
+ }
+
+ dtos := convertModelToDtos(query.Result)
+
return JSON(200, util.DynMap{
"message": "Datasource updated",
"id": cmd.Id,
"name": cmd.Name,
- "datasource": ds,
+ "datasource": dtos,
})
}
diff --git a/pkg/api/dtos/alerting.go b/pkg/api/dtos/alerting.go
index d30f2697f3f..697d0a35a08 100644
--- a/pkg/api/dtos/alerting.go
+++ b/pkg/api/dtos/alerting.go
@@ -1,35 +1,76 @@
package dtos
import (
+ "fmt"
"time"
"github.com/grafana/grafana/pkg/components/null"
"github.com/grafana/grafana/pkg/components/simplejson"
- m "github.com/grafana/grafana/pkg/models"
+ "github.com/grafana/grafana/pkg/models"
)
type AlertRule struct {
- Id int64 `json:"id"`
- DashboardId int64 `json:"dashboardId"`
- PanelId int64 `json:"panelId"`
- Name string `json:"name"`
- Message string `json:"message"`
- State m.AlertStateType `json:"state"`
- NewStateDate time.Time `json:"newStateDate"`
- EvalDate time.Time `json:"evalDate"`
- EvalData *simplejson.Json `json:"evalData"`
- ExecutionError string `json:"executionError"`
- Url string `json:"url"`
- CanEdit bool `json:"canEdit"`
+ Id int64 `json:"id"`
+ DashboardId int64 `json:"dashboardId"`
+ PanelId int64 `json:"panelId"`
+ Name string `json:"name"`
+ Message string `json:"message"`
+ State models.AlertStateType `json:"state"`
+ NewStateDate time.Time `json:"newStateDate"`
+ EvalDate time.Time `json:"evalDate"`
+ EvalData *simplejson.Json `json:"evalData"`
+ ExecutionError string `json:"executionError"`
+ Url string `json:"url"`
+ CanEdit bool `json:"canEdit"`
+}
+
+func formatShort(interval time.Duration) string {
+ var result string
+
+ hours := interval / time.Hour
+ if hours > 0 {
+ result += fmt.Sprintf("%dh", hours)
+ }
+
+ remaining := interval - (hours * time.Hour)
+ mins := remaining / time.Minute
+ if mins > 0 {
+ result += fmt.Sprintf("%dm", mins)
+ }
+
+ remaining = remaining - (mins * time.Minute)
+ seconds := remaining / time.Second
+ if seconds > 0 {
+ result += fmt.Sprintf("%ds", seconds)
+ }
+
+ return result
+}
+
+func NewAlertNotification(notification *models.AlertNotification) *AlertNotification {
+ return &AlertNotification{
+ Id: notification.Id,
+ Name: notification.Name,
+ Type: notification.Type,
+ IsDefault: notification.IsDefault,
+ Created: notification.Created,
+ Updated: notification.Updated,
+ Frequency: formatShort(notification.Frequency),
+ SendReminder: notification.SendReminder,
+ Settings: notification.Settings,
+ }
}
type AlertNotification struct {
- Id int64 `json:"id"`
- Name string `json:"name"`
- Type string `json:"type"`
- IsDefault bool `json:"isDefault"`
- Created time.Time `json:"created"`
- Updated time.Time `json:"updated"`
+ Id int64 `json:"id"`
+ Name string `json:"name"`
+ Type string `json:"type"`
+ IsDefault bool `json:"isDefault"`
+ SendReminder bool `json:"sendReminder"`
+ Frequency string `json:"frequency"`
+ Created time.Time `json:"created"`
+ Updated time.Time `json:"updated"`
+ Settings *simplejson.Json `json:"settings"`
}
type AlertTestCommand struct {
@@ -39,7 +80,7 @@ type AlertTestCommand struct {
type AlertTestResult struct {
Firing bool `json:"firing"`
- State m.AlertStateType `json:"state"`
+ State models.AlertStateType `json:"state"`
ConditionEvals string `json:"conditionEvals"`
TimeMs string `json:"timeMs"`
Error string `json:"error,omitempty"`
@@ -59,9 +100,11 @@ type EvalMatch struct {
}
type NotificationTestCommand struct {
- Name string `json:"name"`
- Type string `json:"type"`
- Settings *simplejson.Json `json:"settings"`
+ Name string `json:"name"`
+ Type string `json:"type"`
+ SendReminder bool `json:"sendReminder"`
+ Frequency string `json:"frequency"`
+ Settings *simplejson.Json `json:"settings"`
}
type PauseAlertCommand struct {
diff --git a/pkg/api/dtos/alerting_test.go b/pkg/api/dtos/alerting_test.go
new file mode 100644
index 00000000000..f4c09f202cb
--- /dev/null
+++ b/pkg/api/dtos/alerting_test.go
@@ -0,0 +1,35 @@
+package dtos
+
+import (
+ "testing"
+ "time"
+)
+
+func TestFormatShort(t *testing.T) {
+ tcs := []struct {
+ interval time.Duration
+ expected string
+ }{
+ {interval: time.Hour, expected: "1h"},
+ {interval: time.Hour + time.Minute, expected: "1h1m"},
+ {interval: (time.Hour * 10) + time.Minute, expected: "10h1m"},
+ {interval: (time.Hour * 10) + (time.Minute * 10) + time.Second, expected: "10h10m1s"},
+ {interval: time.Minute * 10, expected: "10m"},
+ }
+
+ for _, tc := range tcs {
+ got := formatShort(tc.interval)
+ if got != tc.expected {
+ t.Errorf("expected %s got %s interval: %v", tc.expected, got, tc.interval)
+ }
+
+ parsed, err := time.ParseDuration(tc.expected)
+ if err != nil {
+ t.Fatalf("could not parse expected duration")
+ }
+
+ if parsed != tc.interval {
+ t.Errorf("expects the parsed duration to equal the interval. Got %v expected: %v", parsed, tc.interval)
+ }
+ }
+}
diff --git a/pkg/api/frontendsettings.go b/pkg/api/frontendsettings.go
index da3c88566c1..a58be38781e 100644
--- a/pkg/api/frontendsettings.go
+++ b/pkg/api/frontendsettings.go
@@ -132,20 +132,22 @@ func getFrontendSettingsMap(c *m.ReqContext) (map[string]interface{}, error) {
}
jsonObj := map[string]interface{}{
- "defaultDatasource": defaultDatasource,
- "datasources": datasources,
- "panels": panels,
- "appSubUrl": setting.AppSubUrl,
- "allowOrgCreate": (setting.AllowUserOrgCreate && c.IsSignedIn) || c.IsGrafanaAdmin,
- "authProxyEnabled": setting.AuthProxyEnabled,
- "ldapEnabled": setting.LdapEnabled,
- "alertingEnabled": setting.AlertingEnabled,
- "exploreEnabled": setting.ExploreEnabled,
- "googleAnalyticsId": setting.GoogleAnalyticsId,
- "disableLoginForm": setting.DisableLoginForm,
- "externalUserMngInfo": setting.ExternalUserMngInfo,
- "externalUserMngLinkUrl": setting.ExternalUserMngLinkUrl,
- "externalUserMngLinkName": setting.ExternalUserMngLinkName,
+ "defaultDatasource": defaultDatasource,
+ "datasources": datasources,
+ "panels": panels,
+ "appSubUrl": setting.AppSubUrl,
+ "allowOrgCreate": (setting.AllowUserOrgCreate && c.IsSignedIn) || c.IsGrafanaAdmin,
+ "authProxyEnabled": setting.AuthProxyEnabled,
+ "ldapEnabled": setting.LdapEnabled,
+ "alertingEnabled": setting.AlertingEnabled,
+ "alertingErrorOrTimeout": setting.AlertingErrorOrTimeout,
+ "alertingNoDataOrNullValues": setting.AlertingNoDataOrNullValues,
+ "exploreEnabled": setting.ExploreEnabled,
+ "googleAnalyticsId": setting.GoogleAnalyticsId,
+ "disableLoginForm": setting.DisableLoginForm,
+ "externalUserMngInfo": setting.ExternalUserMngInfo,
+ "externalUserMngLinkUrl": setting.ExternalUserMngLinkUrl,
+ "externalUserMngLinkName": setting.ExternalUserMngLinkName,
"buildInfo": map[string]interface{}{
"version": setting.BuildVersion,
"commit": setting.BuildCommit,
diff --git a/pkg/api/http_server.go b/pkg/api/http_server.go
index 0de63ce5e08..432d6a18369 100644
--- a/pkg/api/http_server.go
+++ b/pkg/api/http_server.go
@@ -233,6 +233,10 @@ func (hs *HTTPServer) addMiddlewaresAndStaticRoutes() {
}
func (hs *HTTPServer) metricsEndpoint(ctx *macaron.Context) {
+ if !hs.Cfg.MetricsEndpointEnabled {
+ return
+ }
+
if ctx.Req.Method != "GET" || ctx.Req.URL.Path != "/metrics" {
return
}
diff --git a/pkg/api/index.go b/pkg/api/index.go
index ea10940d3ba..b8101a01fc8 100644
--- a/pkg/api/index.go
+++ b/pkg/api/index.go
@@ -91,6 +91,9 @@ func setIndexViewData(c *m.ReqContext) (*dtos.IndexViewData, error) {
if themeURLParam == "light" {
data.User.LightTheme = true
data.Theme = "light"
+ } else if themeURLParam == "dark" {
+ data.User.LightTheme = false
+ data.Theme = "dark"
}
if hasEditPermissionInFoldersQuery.Result {
diff --git a/pkg/api/live/conn.go b/pkg/api/live/conn.go
index f2a041d7631..0fae7f75b73 100644
--- a/pkg/api/live/conn.go
+++ b/pkg/api/live/conn.go
@@ -70,7 +70,7 @@ func (c *connection) readPump() {
func (c *connection) handleMessage(message []byte) {
json, err := simplejson.NewJson(message)
if err != nil {
- log.Error(3, "Unreadable message on websocket channel:", err)
+ log.Error(3, "Unreadable message on websocket channel. error: %v", err)
}
msgType := json.Get("action").MustString()
diff --git a/pkg/api/login.go b/pkg/api/login.go
index 01fa71a6e44..632d04e37f1 100644
--- a/pkg/api/login.go
+++ b/pkg/api/login.go
@@ -78,7 +78,13 @@ func tryLoginUsingRememberCookie(c *m.ReqContext) bool {
user := userQuery.Result
// validate remember me cookie
- if val, _ := c.GetSuperSecureCookie(user.Rands+user.Password, setting.CookieRememberName); val != user.Login {
+ signingKey := user.Rands + user.Password
+ if len(signingKey) < 10 {
+ c.Logger.Error("Invalid user signingKey")
+ return false
+ }
+
+ if val, _ := c.GetSuperSecureCookie(signingKey, setting.CookieRememberName); val != user.Login {
return false
}
diff --git a/pkg/api/metrics.go b/pkg/api/metrics.go
index 00ad25ab8c2..cb80bd346b8 100644
--- a/pkg/api/metrics.go
+++ b/pkg/api/metrics.go
@@ -13,21 +13,21 @@ import (
)
// POST /api/tsdb/query
-func QueryMetrics(c *m.ReqContext, reqDto dtos.MetricRequest) Response {
+func (hs *HTTPServer) QueryMetrics(c *m.ReqContext, reqDto dtos.MetricRequest) Response {
timeRange := tsdb.NewTimeRange(reqDto.From, reqDto.To)
if len(reqDto.Queries) == 0 {
return Error(400, "No queries found in query", nil)
}
- dsID, err := reqDto.Queries[0].Get("datasourceId").Int64()
+ datasourceId, err := reqDto.Queries[0].Get("datasourceId").Int64()
if err != nil {
return Error(400, "Query missing datasourceId", nil)
}
- dsQuery := m.GetDataSourceByIdQuery{Id: dsID, OrgId: c.OrgId}
- if err := bus.Dispatch(&dsQuery); err != nil {
- return Error(500, "failed to fetch data source", err)
+ ds, err := hs.getDatasourceFromCache(datasourceId, c)
+ if err != nil {
+ return Error(500, "Unable to load datasource meta data", err)
}
request := &tsdb.TsdbQuery{TimeRange: timeRange}
@@ -38,11 +38,11 @@ func QueryMetrics(c *m.ReqContext, reqDto dtos.MetricRequest) Response {
MaxDataPoints: query.Get("maxDataPoints").MustInt64(100),
IntervalMs: query.Get("intervalMs").MustInt64(1000),
Model: query,
- DataSource: dsQuery.Result,
+ DataSource: ds,
})
}
- resp, err := tsdb.HandleRequest(context.Background(), dsQuery.Result, request)
+ resp, err := tsdb.HandleRequest(c.Req.Context(), ds, request)
if err != nil {
return Error(500, "Metric request error", err)
}
@@ -99,7 +99,7 @@ func GetTestDataRandomWalk(c *m.ReqContext) Response {
timeRange := tsdb.NewTimeRange(from, to)
request := &tsdb.TsdbQuery{TimeRange: timeRange}
- dsInfo := &m.DataSource{Type: "grafana-testdata-datasource"}
+ dsInfo := &m.DataSource{Type: "testdata"}
request.Queries = append(request.Queries, &tsdb.Query{
RefId: "A",
IntervalMs: intervalMs,
diff --git a/pkg/api/pluginproxy/ds_proxy.go b/pkg/api/pluginproxy/ds_proxy.go
index b420398f9a9..fb2cab9b9b1 100644
--- a/pkg/api/pluginproxy/ds_proxy.go
+++ b/pkg/api/pluginproxy/ds_proxy.go
@@ -203,6 +203,7 @@ func (proxy *DataSourceProxy) getDirector() func(req *http.Request) {
req.Header.Del("X-Forwarded-Host")
req.Header.Del("X-Forwarded-Port")
req.Header.Del("X-Forwarded-Proto")
+ req.Header.Set("User-Agent", fmt.Sprintf("Grafana/%s", setting.BuildVersion))
// set X-Forwarded-For header
if req.RemoteAddr != "" {
@@ -319,9 +320,15 @@ func (proxy *DataSourceProxy) applyRoute(req *http.Request) {
SecureJsonData: proxy.ds.SecureJsonData.Decrypt(),
}
- routeURL, err := url.Parse(proxy.route.Url)
+ interpolatedURL, err := interpolateString(proxy.route.Url, data)
if err != nil {
- logger.Error("Error parsing plugin route url")
+ logger.Error("Error interpolating proxy url", "error", err)
+ return
+ }
+
+ routeURL, err := url.Parse(interpolatedURL)
+ if err != nil {
+ logger.Error("Error parsing plugin route url", "error", err)
return
}
diff --git a/pkg/api/pluginproxy/ds_proxy_test.go b/pkg/api/pluginproxy/ds_proxy_test.go
index bb553b4d075..e6d05872787 100644
--- a/pkg/api/pluginproxy/ds_proxy_test.go
+++ b/pkg/api/pluginproxy/ds_proxy_test.go
@@ -49,6 +49,13 @@ func TestDSRouteRule(t *testing.T) {
{Name: "x-header", Content: "my secret {{.SecureJsonData.key}}"},
},
},
+ {
+ Path: "api/common",
+ Url: "{{.JsonData.dynamicUrl}}",
+ Headers: []plugins.AppPluginRouteHeader{
+ {Name: "x-header", Content: "my secret {{.SecureJsonData.key}}"},
+ },
+ },
},
}
@@ -57,7 +64,8 @@ func TestDSRouteRule(t *testing.T) {
ds := &m.DataSource{
JsonData: simplejson.NewFromAny(map[string]interface{}{
- "clientId": "asd",
+ "clientId": "asd",
+ "dynamicUrl": "https://dynamic.grafana.com",
}),
SecureJsonData: map[string][]byte{
"key": key,
@@ -83,6 +91,17 @@ func TestDSRouteRule(t *testing.T) {
})
})
+ Convey("When matching route path and has dynamic url", func() {
+ proxy := NewDataSourceProxy(ds, plugin, ctx, "api/common/some/method")
+ proxy.route = plugin.Routes[3]
+ proxy.applyRoute(req)
+
+ Convey("should add headers and interpolate the url", func() {
+ So(req.URL.String(), ShouldEqual, "https://dynamic.grafana.com/some/method")
+ So(req.Header.Get("x-header"), ShouldEqual, "my secret 123")
+ })
+ })
+
Convey("Validating request", func() {
Convey("plugin route with valid role", func() {
proxy := NewDataSourceProxy(ds, plugin, ctx, "api/v4/some/method")
@@ -212,20 +231,21 @@ func TestDSRouteRule(t *testing.T) {
})
Convey("When proxying graphite", func() {
+ setting.BuildVersion = "5.3.0"
plugin := &plugins.DataSourcePlugin{}
ds := &m.DataSource{Url: "htttp://graphite:8080", Type: m.DS_GRAPHITE}
ctx := &m.ReqContext{}
proxy := NewDataSourceProxy(ds, plugin, ctx, "/render")
+ req, err := http.NewRequest(http.MethodGet, "http://grafana.com/sub", nil)
+ So(err, ShouldBeNil)
- requestURL, _ := url.Parse("http://grafana.com/sub")
- req := http.Request{URL: requestURL}
-
- proxy.getDirector()(&req)
+ proxy.getDirector()(req)
Convey("Can translate request url and path", func() {
So(req.URL.Host, ShouldEqual, "graphite:8080")
So(req.URL.Path, ShouldEqual, "/render")
+ So(req.Header.Get("User-Agent"), ShouldEqual, "Grafana/5.3.0")
})
})
@@ -243,10 +263,10 @@ func TestDSRouteRule(t *testing.T) {
ctx := &m.ReqContext{}
proxy := NewDataSourceProxy(ds, plugin, ctx, "")
- requestURL, _ := url.Parse("http://grafana.com/sub")
- req := http.Request{URL: requestURL}
+ req, err := http.NewRequest(http.MethodGet, "http://grafana.com/sub", nil)
+ So(err, ShouldBeNil)
- proxy.getDirector()(&req)
+ proxy.getDirector()(req)
Convey("Should add db to url", func() {
So(req.URL.Path, ShouldEqual, "/db/site/")
diff --git a/pkg/api/team_members.go b/pkg/api/team_members.go
index 60a170a8c31..5b5970de6ad 100644
--- a/pkg/api/team_members.go
+++ b/pkg/api/team_members.go
@@ -4,6 +4,7 @@ import (
"github.com/grafana/grafana/pkg/api/dtos"
"github.com/grafana/grafana/pkg/bus"
m "github.com/grafana/grafana/pkg/models"
+ "github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/util"
)
@@ -17,6 +18,11 @@ func GetTeamMembers(c *m.ReqContext) Response {
for _, member := range query.Result {
member.AvatarUrl = dtos.GetGravatarUrl(member.Email)
+ member.Labels = []string{}
+
+ if setting.IsEnterprise && setting.LdapEnabled && member.External {
+ member.Labels = append(member.Labels, "LDAP")
+ }
}
return JSON(200, query.Result)
diff --git a/pkg/api/user.go b/pkg/api/user.go
index 725c623575f..4b916202e65 100644
--- a/pkg/api/user.go
+++ b/pkg/api/user.go
@@ -111,6 +111,21 @@ func GetSignedInUserOrgList(c *m.ReqContext) Response {
return getUserOrgList(c.UserId)
}
+// GET /api/user/teams
+func GetSignedInUserTeamList(c *m.ReqContext) Response {
+ query := m.GetTeamsByUserQuery{OrgId: c.OrgId, UserId: c.UserId}
+
+ if err := bus.Dispatch(&query); err != nil {
+ return Error(500, "Failed to get user teams", err)
+ }
+
+ for _, team := range query.Result {
+ team.AvatarUrl = dtos.GetGravatarUrlWithDefault(team.Email, team.Name)
+ }
+
+ return JSON(200, query.Result)
+}
+
// GET /api/user/:id/orgs
func GetUserOrgList(c *m.ReqContext) Response {
return getUserOrgList(c.ParamsInt64(":id"))
diff --git a/pkg/cmd/grafana-cli/commands/install_command.go b/pkg/cmd/grafana-cli/commands/install_command.go
index 9bdb73a5858..5d4969e06af 100644
--- a/pkg/cmd/grafana-cli/commands/install_command.go
+++ b/pkg/cmd/grafana-cli/commands/install_command.go
@@ -152,7 +152,7 @@ func downloadFile(pluginName, filePath, url string) (err error) {
return err
}
- r, err := zip.NewReader(bytes.NewReader(body), resp.ContentLength)
+ r, err := zip.NewReader(bytes.NewReader(body), int64(len(body)))
if err != nil {
return err
}
diff --git a/pkg/cmd/grafana-cli/commands/upgrade_command.go b/pkg/cmd/grafana-cli/commands/upgrade_command.go
index 355ccab3d1c..396371d3577 100644
--- a/pkg/cmd/grafana-cli/commands/upgrade_command.go
+++ b/pkg/cmd/grafana-cli/commands/upgrade_command.go
@@ -16,7 +16,7 @@ func upgradeCommand(c CommandLine) error {
return err
}
- v, err2 := s.GetPlugin(localPlugin.Id, c.RepoDirectory())
+ v, err2 := s.GetPlugin(pluginName, c.RepoDirectory())
if err2 != nil {
return err2
@@ -24,9 +24,9 @@ func upgradeCommand(c CommandLine) error {
if ShouldUpgrade(localPlugin.Info.Version, v) {
s.RemoveInstalledPlugin(pluginsDir, pluginName)
- return InstallPlugin(localPlugin.Id, "", c)
+ return InstallPlugin(pluginName, "", c)
}
- logger.Infof("%s %s is up to date \n", color.GreenString("✔"), localPlugin.Id)
+ logger.Infof("%s %s is up to date \n", color.GreenString("✔"), pluginName)
return nil
}
diff --git a/pkg/cmd/grafana-cli/services/services.go b/pkg/cmd/grafana-cli/services/services.go
index e743d42022c..338975bc130 100644
--- a/pkg/cmd/grafana-cli/services/services.go
+++ b/pkg/cmd/grafana-cli/services/services.go
@@ -63,7 +63,7 @@ func ListAllPlugins(repoUrl string) (m.PluginRepo, error) {
var data m.PluginRepo
err = json.Unmarshal(body, &data)
if err != nil {
- logger.Info("Failed to unmarshal graphite response error: %v", err)
+ logger.Info("Failed to unmarshal plugin repo response error:", err)
return m.PluginRepo{}, err
}
@@ -140,7 +140,7 @@ func GetPlugin(pluginId, repoUrl string) (m.Plugin, error) {
var data m.Plugin
err = json.Unmarshal(body, &data)
if err != nil {
- logger.Info("Failed to unmarshal graphite response error: %v", err)
+ logger.Info("Failed to unmarshal plugin repo response error:", err)
return m.Plugin{}, err
}
diff --git a/pkg/cmd/grafana-cli/utils/grafana_path.go b/pkg/cmd/grafana-cli/utils/grafana_path.go
index afb622bbb93..5f5c944f52b 100644
--- a/pkg/cmd/grafana-cli/utils/grafana_path.go
+++ b/pkg/cmd/grafana-cli/utils/grafana_path.go
@@ -42,6 +42,8 @@ func returnOsDefault(currentOs string) string {
return "/usr/local/var/lib/grafana/plugins"
case "freebsd":
return "/var/db/grafana/plugins"
+ case "openbsd":
+ return "/var/grafana/plugins"
default: //"linux"
return "/var/lib/grafana/plugins"
}
diff --git a/pkg/cmd/grafana-server/main.go b/pkg/cmd/grafana-server/main.go
index f00e6bba0fd..f1e298671d7 100644
--- a/pkg/cmd/grafana-server/main.go
+++ b/pkg/cmd/grafana-server/main.go
@@ -96,13 +96,17 @@ func main() {
func listenToSystemSignals(server *GrafanaServerImpl) {
signalChan := make(chan os.Signal, 1)
- ignoreChan := make(chan os.Signal, 1)
+ sighupChan := make(chan os.Signal, 1)
- signal.Notify(ignoreChan, syscall.SIGHUP)
+ signal.Notify(sighupChan, syscall.SIGHUP)
signal.Notify(signalChan, os.Interrupt, os.Kill, syscall.SIGTERM)
- select {
- case sig := <-signalChan:
- server.Shutdown(fmt.Sprintf("System signal: %s", sig))
+ for {
+ select {
+ case _ = <-sighupChan:
+ log.Reload()
+ case sig := <-signalChan:
+ server.Shutdown(fmt.Sprintf("System signal: %s", sig))
+ }
}
}
diff --git a/pkg/components/imguploader/azureblobuploader.go b/pkg/components/imguploader/azureblobuploader.go
index 3c0ac5b8884..a902807925b 100644
--- a/pkg/components/imguploader/azureblobuploader.go
+++ b/pkg/components/imguploader/azureblobuploader.go
@@ -52,7 +52,7 @@ func (az *AzureBlobUploader) Upload(ctx context.Context, imageDiskPath string) (
}
randomFileName := util.GetRandomString(30) + ".png"
// upload image
- az.log.Debug("Uploading image to azure_blob", "conatiner_name", az.container_name, "blob_name", randomFileName)
+ az.log.Debug("Uploading image to azure_blob", "container_name", az.container_name, "blob_name", randomFileName)
resp, err := blob.FileUpload(az.container_name, randomFileName, file)
if err != nil {
return "", err
@@ -274,10 +274,10 @@ func (a *Auth) canonicalizedHeaders(req *http.Request) string {
}
}
- splitted := strings.Split(buffer.String(), "\n")
- sort.Strings(splitted)
+ split := strings.Split(buffer.String(), "\n")
+ sort.Strings(split)
- return strings.Join(splitted, "\n")
+ return strings.Join(split, "\n")
}
/*
@@ -313,8 +313,8 @@ func (a *Auth) canonicalizedResource(req *http.Request) string {
buffer.WriteString(fmt.Sprintf("\n%s:%s", key, strings.Join(values, ",")))
}
- splitted := strings.Split(buffer.String(), "\n")
- sort.Strings(splitted)
+ split := strings.Split(buffer.String(), "\n")
+ sort.Strings(split)
- return strings.Join(splitted, "\n")
+ return strings.Join(split, "\n")
}
diff --git a/pkg/components/imguploader/s3uploader.go b/pkg/components/imguploader/s3uploader.go
index 62196357c61..a1e4aed0f47 100644
--- a/pkg/components/imguploader/s3uploader.go
+++ b/pkg/components/imguploader/s3uploader.go
@@ -60,7 +60,7 @@ func (u *S3Uploader) Upload(ctx context.Context, imageDiskPath string) (string,
s3_endpoint, _ := endpoints.DefaultResolver().EndpointFor("s3", u.region)
key := u.path + util.GetRandomString(20) + ".png"
image_url := s3_endpoint.URL + "/" + u.bucket + "/" + key
- log.Debug("Uploading image to s3", "url = ", image_url)
+ log.Debug("Uploading image to s3. url = %s", image_url)
file, err := os.Open(imageDiskPath)
if err != nil {
diff --git a/pkg/components/simplejson/simplejson.go b/pkg/components/simplejson/simplejson.go
index 85e2f955943..35e305eb414 100644
--- a/pkg/components/simplejson/simplejson.go
+++ b/pkg/components/simplejson/simplejson.go
@@ -256,7 +256,7 @@ func (j *Json) StringArray() ([]string, error) {
// MustArray guarantees the return of a `[]interface{}` (with optional default)
//
-// useful when you want to interate over array values in a succinct manner:
+// useful when you want to iterate over array values in a succinct manner:
// for i, v := range js.Get("results").MustArray() {
// fmt.Println(i, v)
// }
@@ -281,7 +281,7 @@ func (j *Json) MustArray(args ...[]interface{}) []interface{} {
// MustMap guarantees the return of a `map[string]interface{}` (with optional default)
//
-// useful when you want to interate over map values in a succinct manner:
+// useful when you want to iterate over map values in a succinct manner:
// for k, v := range js.Get("dictionary").MustMap() {
// fmt.Println(k, v)
// }
@@ -329,7 +329,7 @@ func (j *Json) MustString(args ...string) string {
// MustStringArray guarantees the return of a `[]string` (with optional default)
//
-// useful when you want to interate over array values in a succinct manner:
+// useful when you want to iterate over array values in a succinct manner:
// for i, s := range js.Get("results").MustStringArray() {
// fmt.Println(i, s)
// }
diff --git a/pkg/log/file.go b/pkg/log/file.go
index d137adbf3de..b8430dc6086 100644
--- a/pkg/log/file.go
+++ b/pkg/log/file.go
@@ -236,3 +236,20 @@ func (w *FileLogWriter) Close() {
func (w *FileLogWriter) Flush() {
w.mw.fd.Sync()
}
+
+// Reload file logger
+func (w *FileLogWriter) Reload() {
+ // block Logger's io.Writer
+ w.mw.Lock()
+ defer w.mw.Unlock()
+
+ // Close
+ fd := w.mw.fd
+ fd.Close()
+
+ // Open again
+ err := w.StartLogger()
+ if err != nil {
+ fmt.Fprintf(os.Stderr, "Reload StartLogger: %s\n", err)
+ }
+}
diff --git a/pkg/log/handlers.go b/pkg/log/handlers.go
index 14a96fdcdb4..804d8fcbd70 100644
--- a/pkg/log/handlers.go
+++ b/pkg/log/handlers.go
@@ -3,3 +3,7 @@ package log
type DisposableHandler interface {
Close()
}
+
+type ReloadableHandler interface {
+ Reload()
+}
diff --git a/pkg/log/log.go b/pkg/log/log.go
index 0e6874e1b4b..8f0522748ef 100644
--- a/pkg/log/log.go
+++ b/pkg/log/log.go
@@ -21,10 +21,12 @@ import (
var Root log15.Logger
var loggersToClose []DisposableHandler
+var loggersToReload []ReloadableHandler
var filters map[string]log15.Lvl
func init() {
loggersToClose = make([]DisposableHandler, 0)
+ loggersToReload = make([]ReloadableHandler, 0)
Root = log15.Root()
Root.SetHandler(log15.DiscardHandler())
}
@@ -103,7 +105,7 @@ func Critical(skip int, format string, v ...interface{}) {
}
func Fatal(skip int, format string, v ...interface{}) {
- Root.Crit(fmt.Sprintf(format, v))
+ Root.Crit(fmt.Sprintf(format, v...))
Close()
os.Exit(1)
}
@@ -115,6 +117,12 @@ func Close() {
loggersToClose = make([]DisposableHandler, 0)
}
+func Reload() {
+ for _, logger := range loggersToReload {
+ logger.Reload()
+ }
+}
+
func GetLogLevelFor(name string) Lvl {
if level, ok := filters[name]; ok {
switch level {
@@ -230,6 +238,7 @@ func ReadLoggingConfig(modes []string, logsPath string, cfg *ini.File) {
fileHandler.Init()
loggersToClose = append(loggersToClose, fileHandler)
+ loggersToReload = append(loggersToReload, fileHandler)
handler = fileHandler
case "syslog":
sysLogHandler := NewSyslog(sec, format)
diff --git a/pkg/login/auth.go b/pkg/login/auth.go
index 215a22cde33..991fa72fd54 100644
--- a/pkg/login/auth.go
+++ b/pkg/login/auth.go
@@ -2,7 +2,6 @@ package login
import (
"errors"
-
"github.com/grafana/grafana/pkg/bus"
m "github.com/grafana/grafana/pkg/models"
)
@@ -14,6 +13,7 @@ var (
ErrProviderDeniedRequest = errors.New("Login provider denied login request")
ErrSignUpNotAllowed = errors.New("Signup is not allowed for this adapter")
ErrTooManyLoginAttempts = errors.New("Too many consecutive incorrect login attempts for user. Login for user temporarily blocked")
+ ErrPasswordEmpty = errors.New("No password provided.")
ErrUsersQuotaReached = errors.New("Users quota reached")
ErrGettingUserQuota = errors.New("Error getting user quota")
)
@@ -28,6 +28,10 @@ func AuthenticateUser(query *m.LoginUserQuery) error {
return err
}
+ if err := validatePasswordSet(query.Password); err != nil {
+ return err
+ }
+
err := loginUsingGrafanaDB(query)
if err == nil || (err != m.ErrUserNotFound && err != ErrInvalidCredentials) {
return err
@@ -52,3 +56,10 @@ func AuthenticateUser(query *m.LoginUserQuery) error {
return err
}
+func validatePasswordSet(password string) error {
+ if len(password) == 0 {
+ return ErrPasswordEmpty
+ }
+
+ return nil
+}
diff --git a/pkg/login/auth_test.go b/pkg/login/auth_test.go
index 932125c410e..a4cd8284cdd 100644
--- a/pkg/login/auth_test.go
+++ b/pkg/login/auth_test.go
@@ -10,6 +10,24 @@ import (
func TestAuthenticateUser(t *testing.T) {
Convey("Authenticate user", t, func() {
+ authScenario("When a user authenticates without setting a password", func(sc *authScenarioContext) {
+ mockLoginAttemptValidation(nil, sc)
+ mockLoginUsingGrafanaDB(nil, sc)
+ mockLoginUsingLdap(false, nil, sc)
+
+ loginQuery := m.LoginUserQuery{
+ Username: "user",
+ Password: "",
+ }
+ err := AuthenticateUser(&loginQuery)
+
+ Convey("login should fail", func() {
+ So(sc.grafanaLoginWasCalled, ShouldBeFalse)
+ So(sc.ldapLoginWasCalled, ShouldBeFalse)
+ So(err, ShouldEqual, ErrPasswordEmpty)
+ })
+ })
+
authScenario("When a user authenticates having too many login attempts", func(sc *authScenarioContext) {
mockLoginAttemptValidation(ErrTooManyLoginAttempts, sc)
mockLoginUsingGrafanaDB(nil, sc)
diff --git a/pkg/login/ext_user.go b/pkg/login/ext_user.go
index a421e3ebe0a..1262c1cc44f 100644
--- a/pkg/login/ext_user.go
+++ b/pkg/login/ext_user.go
@@ -35,7 +35,7 @@ func UpsertUser(cmd *m.UpsertUserCommand) error {
limitReached, err := quota.QuotaReached(cmd.ReqContext, "user")
if err != nil {
- log.Warn("Error getting user quota", "err", err)
+ log.Warn("Error getting user quota. error: %v", err)
return ErrGettingUserQuota
}
if limitReached {
@@ -135,7 +135,7 @@ func updateUser(user *m.User, extUser *m.ExternalUserInfo) error {
return nil
}
- log.Debug("Syncing user info", "id", user.Id, "update", updateCmd)
+ log.Debug2("Syncing user info", "id", user.Id, "update", updateCmd)
return bus.Dispatch(updateCmd)
}
diff --git a/pkg/login/ldap.go b/pkg/login/ldap.go
index 9e4918f0290..43f45f900d9 100644
--- a/pkg/login/ldap.go
+++ b/pkg/login/ldap.go
@@ -59,6 +59,13 @@ func (a *ldapAuther) Dial() error {
}
}
}
+ var clientCert tls.Certificate
+ if a.server.ClientCert != "" && a.server.ClientKey != "" {
+ clientCert, err = tls.LoadX509KeyPair(a.server.ClientCert, a.server.ClientKey)
+ if err != nil {
+ return err
+ }
+ }
for _, host := range strings.Split(a.server.Host, " ") {
address := fmt.Sprintf("%s:%d", host, a.server.Port)
if a.server.UseSSL {
@@ -67,6 +74,9 @@ func (a *ldapAuther) Dial() error {
ServerName: host,
RootCAs: certPool,
}
+ if len(clientCert.Certificate) > 0 {
+ tlsCfg.Certificates = append(tlsCfg.Certificates, clientCert)
+ }
if a.server.StartTLS {
a.conn, err = ldap.Dial("tcp", address)
if err == nil {
@@ -316,15 +326,19 @@ func (a *ldapAuther) searchForUser(username string) (*LdapUserInfo, error) {
a.log.Info("Searching for user's groups", "filter", filter)
+ // support old way of reading settings
+ groupIdAttribute := a.server.Attr.MemberOf
+ // but prefer dn attribute if default settings are used
+ if groupIdAttribute == "" || groupIdAttribute == "memberOf" {
+ groupIdAttribute = "dn"
+ }
+
groupSearchReq := ldap.SearchRequest{
BaseDN: groupSearchBase,
Scope: ldap.ScopeWholeSubtree,
DerefAliases: ldap.NeverDerefAliases,
- Attributes: []string{
- // Here MemberOf would be the thing that identifies the group, which is normally 'cn'
- a.server.Attr.MemberOf,
- },
- Filter: filter,
+ Attributes: []string{groupIdAttribute},
+ Filter: filter,
}
groupSearchResult, err = a.conn.Search(&groupSearchReq)
@@ -334,7 +348,7 @@ func (a *ldapAuther) searchForUser(username string) (*LdapUserInfo, error) {
if len(groupSearchResult.Entries) > 0 {
for i := range groupSearchResult.Entries {
- memberOf = append(memberOf, getLdapAttrN(a.server.Attr.MemberOf, groupSearchResult, i))
+ memberOf = append(memberOf, getLdapAttrN(groupIdAttribute, groupSearchResult, i))
}
break
}
diff --git a/pkg/login/ldap_settings.go b/pkg/login/ldap_settings.go
index c4f5982b237..40791a509db 100644
--- a/pkg/login/ldap_settings.go
+++ b/pkg/login/ldap_settings.go
@@ -21,6 +21,8 @@ type LdapServerConf struct {
StartTLS bool `toml:"start_tls"`
SkipVerifySSL bool `toml:"ssl_skip_verify"`
RootCACert string `toml:"root_ca_cert"`
+ ClientCert string `toml:"client_cert"`
+ ClientKey string `toml:"client_key"`
BindDN string `toml:"bind_dn"`
BindPassword string `toml:"bind_password"`
Attr LdapAttributeMap `toml:"attributes"`
@@ -46,7 +48,7 @@ type LdapAttributeMap struct {
type LdapGroupToOrgRole struct {
GroupDN string `toml:"group_dn"`
OrgId int64 `toml:"org_id"`
- IsGrafanaAdmin *bool `toml:"grafana_admin"` // This is a pointer to know if it was set or not (for backwards compatability)
+ IsGrafanaAdmin *bool `toml:"grafana_admin"` // This is a pointer to know if it was set or not (for backwards compatibility)
OrgRole m.RoleType `toml:"org_role"`
}
diff --git a/pkg/metrics/metrics.go b/pkg/metrics/metrics.go
index a8d9f7308fa..9a514fdb6f3 100644
--- a/pkg/metrics/metrics.go
+++ b/pkg/metrics/metrics.go
@@ -61,6 +61,23 @@ var (
M_Grafana_Version *prometheus.GaugeVec
)
+func newCounterVecStartingAtZero(opts prometheus.CounterOpts, labels []string, labelValues ...string) *prometheus.CounterVec {
+ counter := prometheus.NewCounterVec(opts, labels)
+
+ for _, label := range labelValues {
+ counter.WithLabelValues(label).Add(0)
+ }
+
+ return counter
+}
+
+func newCounterStartingAtZero(opts prometheus.CounterOpts, labelValues ...string) prometheus.Counter {
+ counter := prometheus.NewCounter(opts)
+ counter.Add(0)
+
+ return counter
+}
+
func init() {
M_Instance_Start = prometheus.NewCounter(prometheus.CounterOpts{
Name: "instance_start_total",
@@ -68,32 +85,27 @@ func init() {
Namespace: exporterName,
})
- M_Page_Status = prometheus.NewCounterVec(
+ httpStatusCodes := []string{"200", "404", "500", "unknown"}
+ M_Page_Status = newCounterVecStartingAtZero(
prometheus.CounterOpts{
Name: "page_response_status_total",
Help: "page http response status",
Namespace: exporterName,
- },
- []string{"code"},
- )
+ }, []string{"code"}, httpStatusCodes...)
- M_Api_Status = prometheus.NewCounterVec(
+ M_Api_Status = newCounterVecStartingAtZero(
prometheus.CounterOpts{
Name: "api_response_status_total",
Help: "api http response status",
Namespace: exporterName,
- },
- []string{"code"},
- )
+ }, []string{"code"}, httpStatusCodes...)
- M_Proxy_Status = prometheus.NewCounterVec(
+ M_Proxy_Status = newCounterVecStartingAtZero(
prometheus.CounterOpts{
Name: "proxy_response_status_total",
Help: "proxy http response status",
Namespace: exporterName,
- },
- []string{"code"},
- )
+ }, []string{"code"}, httpStatusCodes...)
M_Http_Request_Total = prometheus.NewCounterVec(
prometheus.CounterOpts{
@@ -111,19 +123,19 @@ func init() {
[]string{"handler", "statuscode", "method"},
)
- M_Api_User_SignUpStarted = prometheus.NewCounter(prometheus.CounterOpts{
+ M_Api_User_SignUpStarted = newCounterStartingAtZero(prometheus.CounterOpts{
Name: "api_user_signup_started_total",
Help: "amount of users who started the signup flow",
Namespace: exporterName,
})
- M_Api_User_SignUpCompleted = prometheus.NewCounter(prometheus.CounterOpts{
+ M_Api_User_SignUpCompleted = newCounterStartingAtZero(prometheus.CounterOpts{
Name: "api_user_signup_completed_total",
Help: "amount of users who completed the signup flow",
Namespace: exporterName,
})
- M_Api_User_SignUpInvite = prometheus.NewCounter(prometheus.CounterOpts{
+ M_Api_User_SignUpInvite = newCounterStartingAtZero(prometheus.CounterOpts{
Name: "api_user_signup_invite_total",
Help: "amount of users who have been invited",
Namespace: exporterName,
@@ -147,49 +159,49 @@ func init() {
Namespace: exporterName,
})
- M_Api_Admin_User_Create = prometheus.NewCounter(prometheus.CounterOpts{
+ M_Api_Admin_User_Create = newCounterStartingAtZero(prometheus.CounterOpts{
Name: "api_admin_user_created_total",
Help: "api admin user created counter",
Namespace: exporterName,
})
- M_Api_Login_Post = prometheus.NewCounter(prometheus.CounterOpts{
+ M_Api_Login_Post = newCounterStartingAtZero(prometheus.CounterOpts{
Name: "api_login_post_total",
Help: "api login post counter",
Namespace: exporterName,
})
- M_Api_Login_OAuth = prometheus.NewCounter(prometheus.CounterOpts{
+ M_Api_Login_OAuth = newCounterStartingAtZero(prometheus.CounterOpts{
Name: "api_login_oauth_total",
Help: "api login oauth counter",
Namespace: exporterName,
})
- M_Api_Org_Create = prometheus.NewCounter(prometheus.CounterOpts{
+ M_Api_Org_Create = newCounterStartingAtZero(prometheus.CounterOpts{
Name: "api_org_create_total",
Help: "api org created counter",
Namespace: exporterName,
})
- M_Api_Dashboard_Snapshot_Create = prometheus.NewCounter(prometheus.CounterOpts{
+ M_Api_Dashboard_Snapshot_Create = newCounterStartingAtZero(prometheus.CounterOpts{
Name: "api_dashboard_snapshot_create_total",
Help: "dashboard snapshots created",
Namespace: exporterName,
})
- M_Api_Dashboard_Snapshot_External = prometheus.NewCounter(prometheus.CounterOpts{
+ M_Api_Dashboard_Snapshot_External = newCounterStartingAtZero(prometheus.CounterOpts{
Name: "api_dashboard_snapshot_external_total",
Help: "external dashboard snapshots created",
Namespace: exporterName,
})
- M_Api_Dashboard_Snapshot_Get = prometheus.NewCounter(prometheus.CounterOpts{
+ M_Api_Dashboard_Snapshot_Get = newCounterStartingAtZero(prometheus.CounterOpts{
Name: "api_dashboard_snapshot_get_total",
Help: "loaded dashboards",
Namespace: exporterName,
})
- M_Api_Dashboard_Insert = prometheus.NewCounter(prometheus.CounterOpts{
+ M_Api_Dashboard_Insert = newCounterStartingAtZero(prometheus.CounterOpts{
Name: "api_models_dashboard_insert_total",
Help: "dashboards inserted ",
Namespace: exporterName,
@@ -207,25 +219,25 @@ func init() {
Namespace: exporterName,
}, []string{"type"})
- M_Aws_CloudWatch_GetMetricStatistics = prometheus.NewCounter(prometheus.CounterOpts{
+ M_Aws_CloudWatch_GetMetricStatistics = newCounterStartingAtZero(prometheus.CounterOpts{
Name: "aws_cloudwatch_get_metric_statistics_total",
Help: "counter for getting metric statistics from aws",
Namespace: exporterName,
})
- M_Aws_CloudWatch_ListMetrics = prometheus.NewCounter(prometheus.CounterOpts{
+ M_Aws_CloudWatch_ListMetrics = newCounterStartingAtZero(prometheus.CounterOpts{
Name: "aws_cloudwatch_list_metrics_total",
Help: "counter for getting list of metrics from aws",
Namespace: exporterName,
})
- M_Aws_CloudWatch_GetMetricData = prometheus.NewCounter(prometheus.CounterOpts{
+ M_Aws_CloudWatch_GetMetricData = newCounterStartingAtZero(prometheus.CounterOpts{
Name: "aws_cloudwatch_get_metric_data_total",
Help: "counter for getting metric data time series from aws",
Namespace: exporterName,
})
- M_DB_DataSource_QueryById = prometheus.NewCounter(prometheus.CounterOpts{
+ M_DB_DataSource_QueryById = newCounterStartingAtZero(prometheus.CounterOpts{
Name: "db_datasource_query_by_id_total",
Help: "counter for getting datasource by id",
Namespace: exporterName,
@@ -350,7 +362,7 @@ func getEdition() string {
}
}
-func sendUsageStats() {
+func sendUsageStats(oauthProviders map[string]bool) {
if !setting.ReportingEnabled {
return
}
@@ -440,6 +452,34 @@ func sendUsageStats() {
metrics["stats.ds_access.other."+access+".count"] = count
}
+ anStats := models.GetAlertNotifierUsageStatsQuery{}
+ if err := bus.Dispatch(&anStats); err != nil {
+ metricsLogger.Error("Failed to get alert notification stats", "error", err)
+ return
+ }
+
+ for _, stats := range anStats.Result {
+ metrics["stats.alert_notifiers."+stats.Type+".count"] = stats.Count
+ }
+
+ authTypes := map[string]bool{}
+ authTypes["anonymous"] = setting.AnonymousEnabled
+ authTypes["basic_auth"] = setting.BasicAuthEnabled
+ authTypes["ldap"] = setting.LdapEnabled
+ authTypes["auth_proxy"] = setting.AuthProxyEnabled
+
+ for provider, enabled := range oauthProviders {
+ authTypes["oauth_"+provider] = enabled
+ }
+
+ for authType, enabled := range authTypes {
+ enabledValue := 0
+ if enabled {
+ enabledValue = 1
+ }
+ metrics["stats.auth_enabled."+authType+".count"] = enabledValue
+ }
+
out, _ := json.MarshalIndent(report, "", " ")
data := bytes.NewBuffer(out)
diff --git a/pkg/metrics/metrics_test.go b/pkg/metrics/metrics_test.go
index 8d88e03d106..43739221f1e 100644
--- a/pkg/metrics/metrics_test.go
+++ b/pkg/metrics/metrics_test.go
@@ -115,6 +115,24 @@ func TestMetrics(t *testing.T) {
return nil
})
+ var getAlertNotifierUsageStatsQuery *models.GetAlertNotifierUsageStatsQuery
+ bus.AddHandler("test", func(query *models.GetAlertNotifierUsageStatsQuery) error {
+ query.Result = []*models.NotifierUsageStats{
+ {
+ Type: "slack",
+ Count: 1,
+ },
+ {
+ Type: "webhook",
+ Count: 2,
+ },
+ }
+
+ getAlertNotifierUsageStatsQuery = query
+
+ return nil
+ })
+
var wg sync.WaitGroup
var responseBuffer *bytes.Buffer
var req *http.Request
@@ -129,11 +147,19 @@ func TestMetrics(t *testing.T) {
}))
usageStatsURL = ts.URL
- sendUsageStats()
+ oauthProviders := map[string]bool{
+ "github": true,
+ "gitlab": true,
+ "google": true,
+ "generic_oauth": true,
+ "grafana_com": true,
+ }
+
+ sendUsageStats(oauthProviders)
Convey("Given reporting not enabled and sending usage stats", func() {
setting.ReportingEnabled = false
- sendUsageStats()
+ sendUsageStats(oauthProviders)
Convey("Should not gather stats or call http endpoint", func() {
So(getSystemStatsQuery, ShouldBeNil)
@@ -146,8 +172,13 @@ func TestMetrics(t *testing.T) {
Convey("Given reporting enabled and sending usage stats", func() {
setting.ReportingEnabled = true
setting.BuildVersion = "5.0.0"
+ setting.AnonymousEnabled = true
+ setting.BasicAuthEnabled = true
+ setting.LdapEnabled = true
+ setting.AuthProxyEnabled = true
+
wg.Add(1)
- sendUsageStats()
+ sendUsageStats(oauthProviders)
Convey("Should gather stats and call http endpoint", func() {
if waitTimeout(&wg, 2*time.Second) {
@@ -157,6 +188,7 @@ func TestMetrics(t *testing.T) {
So(getSystemStatsQuery, ShouldNotBeNil)
So(getDataSourceStatsQuery, ShouldNotBeNil)
So(getDataSourceAccessStatsQuery, ShouldNotBeNil)
+ So(getAlertNotifierUsageStatsQuery, ShouldNotBeNil)
So(req, ShouldNotBeNil)
So(req.Method, ShouldEqual, http.MethodPost)
So(req.Header.Get("Content-Type"), ShouldEqual, "application/json")
@@ -198,6 +230,19 @@ func TestMetrics(t *testing.T) {
So(metrics.Get("stats.ds_access."+models.DS_PROMETHEUS+".proxy.count").MustInt(), ShouldEqual, 3)
So(metrics.Get("stats.ds_access.other.direct.count").MustInt(), ShouldEqual, 6+7)
So(metrics.Get("stats.ds_access.other.proxy.count").MustInt(), ShouldEqual, 4+8)
+
+ So(metrics.Get("stats.alert_notifiers.slack.count").MustInt(), ShouldEqual, 1)
+ So(metrics.Get("stats.alert_notifiers.webhook.count").MustInt(), ShouldEqual, 2)
+
+ So(metrics.Get("stats.auth_enabled.anonymous.count").MustInt(), ShouldEqual, 1)
+ So(metrics.Get("stats.auth_enabled.basic_auth.count").MustInt(), ShouldEqual, 1)
+ So(metrics.Get("stats.auth_enabled.ldap.count").MustInt(), ShouldEqual, 1)
+ So(metrics.Get("stats.auth_enabled.auth_proxy.count").MustInt(), ShouldEqual, 1)
+ So(metrics.Get("stats.auth_enabled.oauth_github.count").MustInt(), ShouldEqual, 1)
+ So(metrics.Get("stats.auth_enabled.oauth_gitlab.count").MustInt(), ShouldEqual, 1)
+ So(metrics.Get("stats.auth_enabled.oauth_google.count").MustInt(), ShouldEqual, 1)
+ So(metrics.Get("stats.auth_enabled.oauth_generic_oauth.count").MustInt(), ShouldEqual, 1)
+ So(metrics.Get("stats.auth_enabled.oauth_grafana_com.count").MustInt(), ShouldEqual, 1)
})
})
diff --git a/pkg/metrics/service.go b/pkg/metrics/service.go
index ec38e0acfec..d2c0c815da9 100644
--- a/pkg/metrics/service.go
+++ b/pkg/metrics/service.go
@@ -28,9 +28,9 @@ func init() {
type InternalMetricsService struct {
Cfg *setting.Cfg `inject:""`
- enabled bool
intervalSeconds int64
graphiteCfg *graphitebridge.Config
+ oauthProviders map[string]bool
}
func (im *InternalMetricsService) Init() error {
@@ -61,7 +61,7 @@ func (im *InternalMetricsService) Run(ctx context.Context) error {
for {
select {
case <-onceEveryDayTick.C:
- sendUsageStats()
+ sendUsageStats(im.oauthProviders)
case <-everyMinuteTicker.C:
updateTotalStats()
case <-ctx.Done():
diff --git a/pkg/metrics/settings.go b/pkg/metrics/settings.go
index 58b84a7192f..18b9e78d6ff 100644
--- a/pkg/metrics/settings.go
+++ b/pkg/metrics/settings.go
@@ -5,6 +5,8 @@ import (
"strings"
"time"
+ "github.com/grafana/grafana/pkg/social"
+
"github.com/grafana/grafana/pkg/metrics/graphitebridge"
"github.com/grafana/grafana/pkg/setting"
"github.com/prometheus/client_golang/prometheus"
@@ -16,17 +18,14 @@ func (im *InternalMetricsService) readSettings() error {
return fmt.Errorf("Unable to find metrics config section %v", err)
}
- im.enabled = section.Key("enabled").MustBool(false)
im.intervalSeconds = section.Key("interval_seconds").MustInt64(10)
- if !im.enabled {
- return nil
- }
-
if err := im.parseGraphiteSettings(); err != nil {
return fmt.Errorf("Unable to parse metrics graphite section, %v", err)
}
+ im.oauthProviders = social.GetOAuthProviders(im.Cfg)
+
return nil
}
diff --git a/pkg/middleware/auth_proxy.go b/pkg/middleware/auth_proxy.go
index 144a0ae3a69..29bd305b336 100644
--- a/pkg/middleware/auth_proxy.go
+++ b/pkg/middleware/auth_proxy.go
@@ -36,7 +36,7 @@ func initContextWithAuthProxy(ctx *m.ReqContext, orgID int64) bool {
// initialize session
if err := ctx.Session.Start(ctx.Context); err != nil {
- log.Error(3, "Failed to start session", err)
+ log.Error(3, "Failed to start session. error %v", err)
return false
}
@@ -146,12 +146,12 @@ func initContextWithAuthProxy(ctx *m.ReqContext, orgID int64) bool {
if getRequestUserId(ctx) > 0 && getRequestUserId(ctx) != query.Result.UserId {
// remove session
if err := ctx.Session.Destory(ctx.Context); err != nil {
- log.Error(3, "Failed to destroy session, err")
+ log.Error(3, "Failed to destroy session. error: %v", err)
}
// initialize a new session
if err := ctx.Session.Start(ctx.Context); err != nil {
- log.Error(3, "Failed to start session", err)
+ log.Error(3, "Failed to start session. error: %v", err)
}
}
diff --git a/pkg/models/alert_notifications.go b/pkg/models/alert_notifications.go
index 87b515f370c..42d33d5ed22 100644
--- a/pkg/models/alert_notifications.go
+++ b/pkg/models/alert_notifications.go
@@ -1,38 +1,50 @@
package models
import (
+ "errors"
"time"
"github.com/grafana/grafana/pkg/components/simplejson"
)
+var (
+ ErrNotificationFrequencyNotFound = errors.New("Notification frequency not specified")
+ ErrJournalingNotFound = errors.New("alert notification journaling not found")
+)
+
type AlertNotification struct {
- Id int64 `json:"id"`
- OrgId int64 `json:"-"`
- Name string `json:"name"`
- Type string `json:"type"`
- IsDefault bool `json:"isDefault"`
- Settings *simplejson.Json `json:"settings"`
- Created time.Time `json:"created"`
- Updated time.Time `json:"updated"`
+ Id int64 `json:"id"`
+ OrgId int64 `json:"-"`
+ Name string `json:"name"`
+ Type string `json:"type"`
+ SendReminder bool `json:"sendReminder"`
+ Frequency time.Duration `json:"frequency"`
+ IsDefault bool `json:"isDefault"`
+ Settings *simplejson.Json `json:"settings"`
+ Created time.Time `json:"created"`
+ Updated time.Time `json:"updated"`
}
type CreateAlertNotificationCommand struct {
- Name string `json:"name" binding:"Required"`
- Type string `json:"type" binding:"Required"`
- IsDefault bool `json:"isDefault"`
- Settings *simplejson.Json `json:"settings"`
+ Name string `json:"name" binding:"Required"`
+ Type string `json:"type" binding:"Required"`
+ SendReminder bool `json:"sendReminder"`
+ Frequency string `json:"frequency"`
+ IsDefault bool `json:"isDefault"`
+ Settings *simplejson.Json `json:"settings"`
OrgId int64 `json:"-"`
Result *AlertNotification
}
type UpdateAlertNotificationCommand struct {
- Id int64 `json:"id" binding:"Required"`
- Name string `json:"name" binding:"Required"`
- Type string `json:"type" binding:"Required"`
- IsDefault bool `json:"isDefault"`
- Settings *simplejson.Json `json:"settings" binding:"Required"`
+ Id int64 `json:"id" binding:"Required"`
+ Name string `json:"name" binding:"Required"`
+ Type string `json:"type" binding:"Required"`
+ SendReminder bool `json:"sendReminder"`
+ Frequency string `json:"frequency"`
+ IsDefault bool `json:"isDefault"`
+ Settings *simplejson.Json `json:"settings" binding:"Required"`
OrgId int64 `json:"-"`
Result *AlertNotification
@@ -63,3 +75,34 @@ type GetAllAlertNotificationsQuery struct {
Result []*AlertNotification
}
+
+type AlertNotificationJournal struct {
+ Id int64
+ OrgId int64
+ AlertId int64
+ NotifierId int64
+ SentAt int64
+ Success bool
+}
+
+type RecordNotificationJournalCommand struct {
+ OrgId int64
+ AlertId int64
+ NotifierId int64
+ SentAt int64
+ Success bool
+}
+
+type GetLatestNotificationQuery struct {
+ OrgId int64
+ AlertId int64
+ NotifierId int64
+
+ Result *AlertNotificationJournal
+}
+
+type CleanNotificationJournalCommand struct {
+ OrgId int64
+ AlertId int64
+ NotifierId int64
+}
diff --git a/pkg/models/datasource.go b/pkg/models/datasource.go
index b7e3e3eaa17..cbdd0136f4d 100644
--- a/pkg/models/datasource.go
+++ b/pkg/models/datasource.go
@@ -59,22 +59,22 @@ type DataSource struct {
}
var knownDatasourcePlugins = map[string]bool{
- DS_ES: true,
- DS_GRAPHITE: true,
- DS_INFLUXDB: true,
- DS_INFLUXDB_08: true,
- DS_KAIROSDB: true,
- DS_CLOUDWATCH: true,
- DS_PROMETHEUS: true,
- DS_OPENTSDB: true,
- DS_POSTGRES: true,
- DS_MYSQL: true,
- DS_MSSQL: true,
- "opennms": true,
- "abhisant-druid-datasource": true,
- "dalmatinerdb-datasource": true,
- "gnocci": true,
- "zabbix": true,
+ DS_ES: true,
+ DS_GRAPHITE: true,
+ DS_INFLUXDB: true,
+ DS_INFLUXDB_08: true,
+ DS_KAIROSDB: true,
+ DS_CLOUDWATCH: true,
+ DS_PROMETHEUS: true,
+ DS_OPENTSDB: true,
+ DS_POSTGRES: true,
+ DS_MYSQL: true,
+ DS_MSSQL: true,
+ "opennms": true,
+ "abhisant-druid-datasource": true,
+ "dalmatinerdb-datasource": true,
+ "gnocci": true,
+ "zabbix": true,
"alexanderzobnin-zabbix-datasource": true,
"newrelic-app": true,
"grafana-datadog-datasource": true,
diff --git a/pkg/models/models.go b/pkg/models/models.go
index c2560021ee1..ba894ae591f 100644
--- a/pkg/models/models.go
+++ b/pkg/models/models.go
@@ -8,4 +8,5 @@ const (
TWITTER
GENERIC
GRAFANA_COM
+ GITLAB
)
diff --git a/pkg/models/stats.go b/pkg/models/stats.go
index 4cd50d37463..d3e145dedf4 100644
--- a/pkg/models/stats.go
+++ b/pkg/models/stats.go
@@ -40,6 +40,15 @@ type GetDataSourceAccessStatsQuery struct {
Result []*DataSourceAccessStats
}
+type NotifierUsageStats struct {
+ Type string
+ Count int64
+}
+
+type GetAlertNotifierUsageStatsQuery struct {
+ Result []*NotifierUsageStats
+}
+
type AdminStats struct {
Users int `json:"users"`
Orgs int `json:"orgs"`
diff --git a/pkg/models/team_member.go b/pkg/models/team_member.go
index 9434dad8ecd..dd64787f465 100644
--- a/pkg/models/team_member.go
+++ b/pkg/models/team_member.go
@@ -12,10 +12,11 @@ var (
// TeamMember model
type TeamMember struct {
- Id int64
- OrgId int64
- TeamId int64
- UserId int64
+ Id int64
+ OrgId int64
+ TeamId int64
+ UserId int64
+ External bool
Created time.Time
Updated time.Time
@@ -25,9 +26,10 @@ type TeamMember struct {
// COMMANDS
type AddTeamMemberCommand struct {
- UserId int64 `json:"userId" binding:"Required"`
- OrgId int64 `json:"-"`
- TeamId int64 `json:"-"`
+ UserId int64 `json:"userId" binding:"Required"`
+ OrgId int64 `json:"-"`
+ TeamId int64 `json:"-"`
+ External bool `json:"-"`
}
type RemoveTeamMemberCommand struct {
@@ -40,20 +42,23 @@ type RemoveTeamMemberCommand struct {
// QUERIES
type GetTeamMembersQuery struct {
- OrgId int64
- TeamId int64
- UserId int64
- Result []*TeamMemberDTO
+ OrgId int64
+ TeamId int64
+ UserId int64
+ External bool
+ Result []*TeamMemberDTO
}
// ----------------------
// Projections and DTOs
type TeamMemberDTO struct {
- OrgId int64 `json:"orgId"`
- TeamId int64 `json:"teamId"`
- UserId int64 `json:"userId"`
- Email string `json:"email"`
- Login string `json:"login"`
- AvatarUrl string `json:"avatarUrl"`
+ OrgId int64 `json:"orgId"`
+ TeamId int64 `json:"teamId"`
+ UserId int64 `json:"userId"`
+ External bool `json:"-"`
+ Email string `json:"email"`
+ Login string `json:"login"`
+ AvatarUrl string `json:"avatarUrl"`
+ Labels []string `json:"labels"`
}
diff --git a/pkg/plugins/dashboard_importer_test.go b/pkg/plugins/dashboard_importer_test.go
index 6f31b49f99d..ca8dfcd515c 100644
--- a/pkg/plugins/dashboard_importer_test.go
+++ b/pkg/plugins/dashboard_importer_test.go
@@ -35,7 +35,7 @@ func TestDashboardImport(t *testing.T) {
So(cmd.Result, ShouldNotBeNil)
resultStr, _ := mock.SavedDashboards[0].Dashboard.Data.EncodePretty()
- expectedBytes, _ := ioutil.ReadFile("../../tests/test-app/dashboards/connections_result.json")
+ expectedBytes, _ := ioutil.ReadFile("testdata/test-app/dashboards/connections_result.json")
expectedJson, _ := simplejson.NewJson(expectedBytes)
expectedStr, _ := expectedJson.EncodePretty()
@@ -89,7 +89,7 @@ func pluginScenario(desc string, t *testing.T, fn func()) {
Convey("Given a plugin", t, func() {
setting.Raw = ini.Empty()
sec, _ := setting.Raw.NewSection("plugin.test-app")
- sec.NewKey("path", "../../tests/test-app")
+ sec.NewKey("path", "testdata/test-app")
pm := &PluginManager{}
err := pm.Init()
diff --git a/pkg/plugins/dashboards_test.go b/pkg/plugins/dashboards_test.go
index c422a1431c0..6fc6ace0e00 100644
--- a/pkg/plugins/dashboards_test.go
+++ b/pkg/plugins/dashboards_test.go
@@ -16,7 +16,7 @@ func TestPluginDashboards(t *testing.T) {
Convey("When asking plugin dashboard info", t, func() {
setting.Raw = ini.Empty()
sec, _ := setting.Raw.NewSection("plugin.test-app")
- sec.NewKey("path", "../../tests/test-app")
+ sec.NewKey("path", "testdata/test-app")
pm := &PluginManager{}
err := pm.Init()
diff --git a/pkg/plugins/dashboards_updater.go b/pkg/plugins/dashboards_updater.go
index ebe11ed32d4..616d4541bec 100644
--- a/pkg/plugins/dashboards_updater.go
+++ b/pkg/plugins/dashboards_updater.go
@@ -48,11 +48,7 @@ func autoUpdateAppDashboard(pluginDashInfo *PluginDashboardInfoDTO, orgId int64)
Path: pluginDashInfo.Path,
}
- if err := bus.Dispatch(&updateCmd); err != nil {
- return err
- }
-
- return nil
+ return bus.Dispatch(&updateCmd)
}
func syncPluginDashboards(pluginDef *PluginBase, orgId int64) {
diff --git a/pkg/plugins/plugins_test.go b/pkg/plugins/plugins_test.go
index fa68ae4389d..d16e6abb4c7 100644
--- a/pkg/plugins/plugins_test.go
+++ b/pkg/plugins/plugins_test.go
@@ -30,7 +30,7 @@ func TestPluginScans(t *testing.T) {
Convey("When reading app plugin definition", t, func() {
setting.Raw = ini.Empty()
sec, _ := setting.Raw.NewSection("plugin.nginx-app")
- sec.NewKey("path", "../../tests/test-app")
+ sec.NewKey("path", "testdata/test-app")
pm := &PluginManager{}
err := pm.Init()
diff --git a/tests/datasource-test/module.js b/pkg/plugins/testdata/datasource-test/module.js
similarity index 100%
rename from tests/datasource-test/module.js
rename to pkg/plugins/testdata/datasource-test/module.js
diff --git a/tests/datasource-test/plugin.json b/pkg/plugins/testdata/datasource-test/plugin.json
similarity index 100%
rename from tests/datasource-test/plugin.json
rename to pkg/plugins/testdata/datasource-test/plugin.json
diff --git a/tests/test-app/dashboards/connections.json b/pkg/plugins/testdata/test-app/dashboards/connections.json
similarity index 100%
rename from tests/test-app/dashboards/connections.json
rename to pkg/plugins/testdata/test-app/dashboards/connections.json
diff --git a/tests/test-app/dashboards/connections_result.json b/pkg/plugins/testdata/test-app/dashboards/connections_result.json
similarity index 100%
rename from tests/test-app/dashboards/connections_result.json
rename to pkg/plugins/testdata/test-app/dashboards/connections_result.json
diff --git a/tests/test-app/dashboards/memory.json b/pkg/plugins/testdata/test-app/dashboards/memory.json
similarity index 100%
rename from tests/test-app/dashboards/memory.json
rename to pkg/plugins/testdata/test-app/dashboards/memory.json
diff --git a/tests/test-app/plugin.json b/pkg/plugins/testdata/test-app/plugin.json
similarity index 100%
rename from tests/test-app/plugin.json
rename to pkg/plugins/testdata/test-app/plugin.json
diff --git a/pkg/services/alerting/extractor.go b/pkg/services/alerting/extractor.go
index e1c1bfacb2e..229092e217b 100644
--- a/pkg/services/alerting/extractor.go
+++ b/pkg/services/alerting/extractor.go
@@ -82,12 +82,13 @@ func (e *DashAlertExtractor) getAlertFromPanels(jsonWithPanels *simplejson.Json,
if collapsed && collapsedJSON.MustBool() {
// extract alerts from sub panels for collapsed panels
- als, err := e.getAlertFromPanels(panel, validateAlertFunc)
+ alertSlice, err := e.getAlertFromPanels(panel,
+ validateAlertFunc)
if err != nil {
return nil, err
}
- alerts = append(alerts, als...)
+ alerts = append(alerts, alertSlice...)
continue
}
diff --git a/pkg/services/alerting/interfaces.go b/pkg/services/alerting/interfaces.go
index 18f969ba1b9..46f8b3c769c 100644
--- a/pkg/services/alerting/interfaces.go
+++ b/pkg/services/alerting/interfaces.go
@@ -1,6 +1,9 @@
package alerting
-import "time"
+import (
+ "context"
+ "time"
+)
type EvalHandler interface {
Eval(evalContext *EvalContext)
@@ -15,10 +18,14 @@ type Notifier interface {
Notify(evalContext *EvalContext) error
GetType() string
NeedsImage() bool
- ShouldNotify(evalContext *EvalContext) bool
+
+ // ShouldNotify checks this evaluation should send an alert notification
+ ShouldNotify(ctx context.Context, evalContext *EvalContext) bool
GetNotifierId() int64
GetIsDefault() bool
+ GetSendReminder() bool
+ GetFrequency() time.Duration
}
type NotifierSlice []Notifier
diff --git a/pkg/services/alerting/notifier.go b/pkg/services/alerting/notifier.go
index 07212746f7e..7fbd956f4f9 100644
--- a/pkg/services/alerting/notifier.go
+++ b/pkg/services/alerting/notifier.go
@@ -1,12 +1,11 @@
package alerting
import (
+ "context"
"errors"
"fmt"
"time"
- "golang.org/x/sync/errgroup"
-
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/components/imguploader"
"github.com/grafana/grafana/pkg/log"
@@ -59,17 +58,47 @@ func (n *notificationService) SendIfNeeded(context *EvalContext) error {
return n.sendNotifications(context, notifiers)
}
-func (n *notificationService) sendNotifications(context *EvalContext, notifiers []Notifier) error {
- g, _ := errgroup.WithContext(context.Ctx)
-
+func (n *notificationService) sendNotifications(evalContext *EvalContext, notifiers []Notifier) error {
for _, notifier := range notifiers {
- not := notifier //avoid updating scope variable in go routine
- n.log.Debug("Sending notification", "type", not.GetType(), "id", not.GetNotifierId(), "isDefault", not.GetIsDefault())
- metrics.M_Alerting_Notification_Sent.WithLabelValues(not.GetType()).Inc()
- g.Go(func() error { return not.Notify(context) })
+ not := notifier
+
+ err := bus.InTransaction(evalContext.Ctx, func(ctx context.Context) error {
+ n.log.Debug("trying to send notification", "id", not.GetNotifierId())
+
+ // Verify that we can send the notification again
+ // but this time within the same transaction.
+ if !evalContext.IsTestRun && !not.ShouldNotify(context.Background(), evalContext) {
+ return nil
+ }
+
+ n.log.Debug("Sending notification", "type", not.GetType(), "id", not.GetNotifierId(), "isDefault", not.GetIsDefault())
+ metrics.M_Alerting_Notification_Sent.WithLabelValues(not.GetType()).Inc()
+
+ //send notification
+ success := not.Notify(evalContext) == nil
+
+ if evalContext.IsTestRun {
+ return nil
+ }
+
+ //write result to db.
+ cmd := &m.RecordNotificationJournalCommand{
+ OrgId: evalContext.Rule.OrgId,
+ AlertId: evalContext.Rule.Id,
+ NotifierId: not.GetNotifierId(),
+ SentAt: time.Now().Unix(),
+ Success: success,
+ }
+
+ return bus.DispatchCtx(ctx, cmd)
+ })
+
+ if err != nil {
+ n.log.Error("failed to send notification", "id", not.GetNotifierId())
+ }
}
- return g.Wait()
+ return nil
}
func (n *notificationService) uploadImage(context *EvalContext) (err error) {
@@ -81,7 +110,7 @@ func (n *notificationService) uploadImage(context *EvalContext) (err error) {
renderOpts := rendering.Opts{
Width: 1000,
Height: 500,
- Timeout: time.Second * 30,
+ Timeout: alertTimeout / 2,
OrgId: context.Rule.OrgId,
OrgRole: m.ROLE_ADMIN,
}
@@ -111,7 +140,7 @@ func (n *notificationService) uploadImage(context *EvalContext) (err error) {
return nil
}
-func (n *notificationService) getNeededNotifiers(orgId int64, notificationIds []int64, context *EvalContext) (NotifierSlice, error) {
+func (n *notificationService) getNeededNotifiers(orgId int64, notificationIds []int64, evalContext *EvalContext) (NotifierSlice, error) {
query := &m.GetAlertNotificationsToSendQuery{OrgId: orgId, Ids: notificationIds}
if err := bus.Dispatch(query); err != nil {
@@ -124,7 +153,8 @@ func (n *notificationService) getNeededNotifiers(orgId int64, notificationIds []
if err != nil {
return nil, err
}
- if not.ShouldNotify(context) {
+
+ if not.ShouldNotify(evalContext.Ctx, evalContext) {
result = append(result, not)
}
}
diff --git a/pkg/services/alerting/notifiers/alertmanager.go b/pkg/services/alerting/notifiers/alertmanager.go
index d449167de13..9826dd1dffb 100644
--- a/pkg/services/alerting/notifiers/alertmanager.go
+++ b/pkg/services/alerting/notifiers/alertmanager.go
@@ -1,6 +1,7 @@
package notifiers
import (
+ "context"
"time"
"github.com/grafana/grafana/pkg/bus"
@@ -33,7 +34,7 @@ func NewAlertmanagerNotifier(model *m.AlertNotification) (alerting.Notifier, err
}
return &AlertmanagerNotifier{
- NotifierBase: NewNotifierBase(model.Id, model.IsDefault, model.Name, model.Type, model.Settings),
+ NotifierBase: NewNotifierBase(model),
Url: url,
log: log.New("alerting.notifier.prometheus-alertmanager"),
}, nil
@@ -45,7 +46,7 @@ type AlertmanagerNotifier struct {
log log.Logger
}
-func (this *AlertmanagerNotifier) ShouldNotify(evalContext *alerting.EvalContext) bool {
+func (this *AlertmanagerNotifier) ShouldNotify(ctx context.Context, evalContext *alerting.EvalContext) bool {
this.log.Debug("Should notify", "ruleId", evalContext.Rule.Id, "state", evalContext.Rule.State, "previousState", evalContext.PrevAlertState)
// Do not notify when we become OK for the first time.
diff --git a/pkg/services/alerting/notifiers/base.go b/pkg/services/alerting/notifiers/base.go
index 868db3aec79..ca011356247 100644
--- a/pkg/services/alerting/notifiers/base.go
+++ b/pkg/services/alerting/notifiers/base.go
@@ -1,50 +1,94 @@
package notifiers
import (
- "github.com/grafana/grafana/pkg/components/simplejson"
- m "github.com/grafana/grafana/pkg/models"
+ "context"
+ "time"
+
+ "github.com/grafana/grafana/pkg/bus"
+ "github.com/grafana/grafana/pkg/log"
+ "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/alerting"
)
type NotifierBase struct {
- Name string
- Type string
- Id int64
- IsDeault bool
- UploadImage bool
+ Name string
+ Type string
+ Id int64
+ IsDeault bool
+ UploadImage bool
+ SendReminder bool
+ Frequency time.Duration
+
+ log log.Logger
}
-func NewNotifierBase(id int64, isDefault bool, name, notifierType string, model *simplejson.Json) NotifierBase {
+func NewNotifierBase(model *models.AlertNotification) NotifierBase {
uploadImage := true
- value, exist := model.CheckGet("uploadImage")
+ value, exist := model.Settings.CheckGet("uploadImage")
if exist {
uploadImage = value.MustBool()
}
return NotifierBase{
- Id: id,
- Name: name,
- IsDeault: isDefault,
- Type: notifierType,
- UploadImage: uploadImage,
+ Id: model.Id,
+ Name: model.Name,
+ IsDeault: model.IsDefault,
+ Type: model.Type,
+ UploadImage: uploadImage,
+ SendReminder: model.SendReminder,
+ Frequency: model.Frequency,
+ log: log.New("alerting.notifier." + model.Name),
}
}
-func defaultShouldNotify(context *alerting.EvalContext) bool {
+func defaultShouldNotify(context *alerting.EvalContext, sendReminder bool, frequency time.Duration, lastNotify time.Time) bool {
// Only notify on state change.
- if context.PrevAlertState == context.Rule.State {
+ if context.PrevAlertState == context.Rule.State && !sendReminder {
return false
}
+
+ // Do not notify if interval has not elapsed
+ if sendReminder && !lastNotify.IsZero() && lastNotify.Add(frequency).After(time.Now()) {
+ return false
+ }
+
+ // Do not notify if alert state if OK or pending even on repeated notify
+ if sendReminder && (context.Rule.State == models.AlertStateOK || context.Rule.State == models.AlertStatePending) {
+ return false
+ }
+
// Do not notify when we become OK for the first time.
- if (context.PrevAlertState == m.AlertStatePending) && (context.Rule.State == m.AlertStateOK) {
+ if (context.PrevAlertState == models.AlertStatePending) && (context.Rule.State == models.AlertStateOK) {
return false
}
+
return true
}
-func (n *NotifierBase) ShouldNotify(context *alerting.EvalContext) bool {
- return defaultShouldNotify(context)
+// ShouldNotify checks this evaluation should send an alert notification
+func (n *NotifierBase) ShouldNotify(ctx context.Context, c *alerting.EvalContext) bool {
+ cmd := &models.GetLatestNotificationQuery{
+ OrgId: c.Rule.OrgId,
+ AlertId: c.Rule.Id,
+ NotifierId: n.Id,
+ }
+
+ err := bus.DispatchCtx(ctx, cmd)
+ if err == models.ErrJournalingNotFound {
+ return true
+ }
+
+ if err != nil {
+ n.log.Error("Could not determine last time alert notifier fired", "Alert name", c.Rule.Name, "Error", err)
+ return false
+ }
+
+ if !cmd.Result.Success {
+ return true
+ }
+
+ return defaultShouldNotify(c, n.SendReminder, n.Frequency, time.Unix(cmd.Result.SentAt, 0))
}
func (n *NotifierBase) GetType() string {
@@ -62,3 +106,11 @@ func (n *NotifierBase) GetNotifierId() int64 {
func (n *NotifierBase) GetIsDefault() bool {
return n.IsDeault
}
+
+func (n *NotifierBase) GetSendReminder() bool {
+ return n.SendReminder
+}
+
+func (n *NotifierBase) GetFrequency() time.Duration {
+ return n.Frequency
+}
diff --git a/pkg/services/alerting/notifiers/base_test.go b/pkg/services/alerting/notifiers/base_test.go
index b7142d144cc..57b82f32466 100644
--- a/pkg/services/alerting/notifiers/base_test.go
+++ b/pkg/services/alerting/notifiers/base_test.go
@@ -2,7 +2,11 @@ package notifiers
import (
"context"
+ "errors"
"testing"
+ "time"
+
+ "github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/components/simplejson"
m "github.com/grafana/grafana/pkg/models"
@@ -10,47 +14,129 @@ import (
. "github.com/smartystreets/goconvey/convey"
)
-func TestBaseNotifier(t *testing.T) {
- Convey("Base notifier tests", t, func() {
- Convey("default constructor for notifiers", func() {
- bJson := simplejson.New()
+func TestShouldSendAlertNotification(t *testing.T) {
+ tcs := []struct {
+ name string
+ prevState m.AlertStateType
+ newState m.AlertStateType
+ expected bool
+ sendReminder bool
+ }{
+ {
+ name: "pending -> ok should not trigger an notification",
+ newState: m.AlertStatePending,
+ prevState: m.AlertStateOK,
+ expected: false,
+ },
+ {
+ name: "ok -> alerting should trigger an notification",
+ newState: m.AlertStateOK,
+ prevState: m.AlertStateAlerting,
+ expected: true,
+ },
+ {
+ name: "ok -> pending should not trigger an notification",
+ newState: m.AlertStateOK,
+ prevState: m.AlertStatePending,
+ expected: false,
+ },
+ {
+ name: "ok -> ok should not trigger an notification",
+ newState: m.AlertStateOK,
+ prevState: m.AlertStateOK,
+ expected: false,
+ sendReminder: false,
+ },
+ {
+ name: "ok -> alerting should not trigger an notification",
+ newState: m.AlertStateOK,
+ prevState: m.AlertStateAlerting,
+ expected: true,
+ sendReminder: true,
+ },
+ {
+ name: "ok -> ok with reminder should not trigger an notification",
+ newState: m.AlertStateOK,
+ prevState: m.AlertStateOK,
+ expected: false,
+ sendReminder: true,
+ },
+ }
- Convey("can parse false value", func() {
- bJson.Set("uploadImage", false)
-
- base := NewNotifierBase(1, false, "name", "email", bJson)
- So(base.UploadImage, ShouldBeFalse)
- })
-
- Convey("can parse true value", func() {
- bJson.Set("uploadImage", true)
-
- base := NewNotifierBase(1, false, "name", "email", bJson)
- So(base.UploadImage, ShouldBeTrue)
- })
-
- Convey("default value should be true for backwards compatibility", func() {
- base := NewNotifierBase(1, false, "name", "email", bJson)
- So(base.UploadImage, ShouldBeTrue)
- })
+ for _, tc := range tcs {
+ evalContext := alerting.NewEvalContext(context.TODO(), &alerting.Rule{
+ State: tc.newState,
})
- Convey("should notify", func() {
- Convey("pending -> ok", func() {
- context := alerting.NewEvalContext(context.TODO(), &alerting.Rule{
- State: m.AlertStatePending,
- })
- context.Rule.State = m.AlertStateOK
- So(defaultShouldNotify(context), ShouldBeFalse)
+ evalContext.Rule.State = tc.prevState
+ if defaultShouldNotify(evalContext, true, 0, time.Now()) != tc.expected {
+ t.Errorf("failed %s. expected %+v to return %v", tc.name, tc, tc.expected)
+ }
+ }
+}
+
+func TestShouldNotifyWhenNoJournalingIsFound(t *testing.T) {
+ Convey("base notifier", t, func() {
+ bus.ClearBusHandlers()
+
+ notifier := NewNotifierBase(&m.AlertNotification{
+ Id: 1,
+ Name: "name",
+ Type: "email",
+ Settings: simplejson.New(),
+ })
+ evalContext := alerting.NewEvalContext(context.TODO(), &alerting.Rule{})
+
+ Convey("should notify if no journaling is found", func() {
+ bus.AddHandlerCtx("", func(ctx context.Context, q *m.GetLatestNotificationQuery) error {
+ return m.ErrJournalingNotFound
})
- Convey("ok -> alerting", func() {
- context := alerting.NewEvalContext(context.TODO(), &alerting.Rule{
- State: m.AlertStateOK,
- })
- context.Rule.State = m.AlertStateAlerting
- So(defaultShouldNotify(context), ShouldBeTrue)
+ if !notifier.ShouldNotify(context.Background(), evalContext) {
+ t.Errorf("should send notifications when ErrJournalingNotFound is returned")
+ }
+ })
+
+ Convey("should not notify query returns error", func() {
+ bus.AddHandlerCtx("", func(ctx context.Context, q *m.GetLatestNotificationQuery) error {
+ return errors.New("some kind of error unknown error")
})
+
+ if notifier.ShouldNotify(context.Background(), evalContext) {
+ t.Errorf("should not send notifications when query returns error")
+ }
+ })
+ })
+}
+
+func TestBaseNotifier(t *testing.T) {
+ Convey("default constructor for notifiers", t, func() {
+ bJson := simplejson.New()
+
+ model := &m.AlertNotification{
+ Id: 1,
+ Name: "name",
+ Type: "email",
+ Settings: bJson,
+ }
+
+ Convey("can parse false value", func() {
+ bJson.Set("uploadImage", false)
+
+ base := NewNotifierBase(model)
+ So(base.UploadImage, ShouldBeFalse)
+ })
+
+ Convey("can parse true value", func() {
+ bJson.Set("uploadImage", true)
+
+ base := NewNotifierBase(model)
+ So(base.UploadImage, ShouldBeTrue)
+ })
+
+ Convey("default value should be true for backwards compatibility", func() {
+ base := NewNotifierBase(model)
+ So(base.UploadImage, ShouldBeTrue)
})
})
}
diff --git a/pkg/services/alerting/notifiers/dingding.go b/pkg/services/alerting/notifiers/dingding.go
index 14eacef5831..738e43af2d2 100644
--- a/pkg/services/alerting/notifiers/dingding.go
+++ b/pkg/services/alerting/notifiers/dingding.go
@@ -32,7 +32,7 @@ func NewDingDingNotifier(model *m.AlertNotification) (alerting.Notifier, error)
}
return &DingDingNotifier{
- NotifierBase: NewNotifierBase(model.Id, model.IsDefault, model.Name, model.Type, model.Settings),
+ NotifierBase: NewNotifierBase(model),
Url: url,
log: log.New("alerting.notifier.dingding"),
}, nil
diff --git a/pkg/services/alerting/notifiers/discord.go b/pkg/services/alerting/notifiers/discord.go
index 3ffa7484870..57d9d438fa2 100644
--- a/pkg/services/alerting/notifiers/discord.go
+++ b/pkg/services/alerting/notifiers/discord.go
@@ -39,7 +39,7 @@ func NewDiscordNotifier(model *m.AlertNotification) (alerting.Notifier, error) {
}
return &DiscordNotifier{
- NotifierBase: NewNotifierBase(model.Id, model.IsDefault, model.Name, model.Type, model.Settings),
+ NotifierBase: NewNotifierBase(model),
WebhookURL: url,
log: log.New("alerting.notifier.discord"),
}, nil
diff --git a/pkg/services/alerting/notifiers/email.go b/pkg/services/alerting/notifiers/email.go
index 562ffbe1269..17b88f7d97f 100644
--- a/pkg/services/alerting/notifiers/email.go
+++ b/pkg/services/alerting/notifiers/email.go
@@ -52,7 +52,7 @@ func NewEmailNotifier(model *m.AlertNotification) (alerting.Notifier, error) {
})
return &EmailNotifier{
- NotifierBase: NewNotifierBase(model.Id, model.IsDefault, model.Name, model.Type, model.Settings),
+ NotifierBase: NewNotifierBase(model),
Addresses: addresses,
log: log.New("alerting.notifier.email"),
}, nil
diff --git a/pkg/services/alerting/notifiers/hipchat.go b/pkg/services/alerting/notifiers/hipchat.go
index 58e1b7bd71e..388cec79597 100644
--- a/pkg/services/alerting/notifiers/hipchat.go
+++ b/pkg/services/alerting/notifiers/hipchat.go
@@ -59,7 +59,7 @@ func NewHipChatNotifier(model *models.AlertNotification) (alerting.Notifier, err
roomId := model.Settings.Get("roomid").MustString()
return &HipChatNotifier{
- NotifierBase: NewNotifierBase(model.Id, model.IsDefault, model.Name, model.Type, model.Settings),
+ NotifierBase: NewNotifierBase(model),
Url: url,
ApiKey: apikey,
RoomId: roomId,
@@ -125,7 +125,7 @@ func (this *HipChatNotifier) Notify(evalContext *alerting.EvalContext) error {
case models.AlertStateOK:
color = "green"
case models.AlertStateNoData:
- color = "grey"
+ color = "gray"
case models.AlertStateAlerting:
color = "red"
}
diff --git a/pkg/services/alerting/notifiers/kafka.go b/pkg/services/alerting/notifiers/kafka.go
index 92f6489106b..d8d19fc5dae 100644
--- a/pkg/services/alerting/notifiers/kafka.go
+++ b/pkg/services/alerting/notifiers/kafka.go
@@ -43,7 +43,7 @@ func NewKafkaNotifier(model *m.AlertNotification) (alerting.Notifier, error) {
}
return &KafkaNotifier{
- NotifierBase: NewNotifierBase(model.Id, model.IsDefault, model.Name, model.Type, model.Settings),
+ NotifierBase: NewNotifierBase(model),
Endpoint: endpoint,
Topic: topic,
log: log.New("alerting.notifier.kafka"),
diff --git a/pkg/services/alerting/notifiers/line.go b/pkg/services/alerting/notifiers/line.go
index 4814662f3a9..9e3888b8f95 100644
--- a/pkg/services/alerting/notifiers/line.go
+++ b/pkg/services/alerting/notifiers/line.go
@@ -39,7 +39,7 @@ func NewLINENotifier(model *m.AlertNotification) (alerting.Notifier, error) {
}
return &LineNotifier{
- NotifierBase: NewNotifierBase(model.Id, model.IsDefault, model.Name, model.Type, model.Settings),
+ NotifierBase: NewNotifierBase(model),
Token: token,
log: log.New("alerting.notifier.line"),
}, nil
diff --git a/pkg/services/alerting/notifiers/opsgenie.go b/pkg/services/alerting/notifiers/opsgenie.go
index f0f5142cf05..84148a0d99c 100644
--- a/pkg/services/alerting/notifiers/opsgenie.go
+++ b/pkg/services/alerting/notifiers/opsgenie.go
@@ -56,7 +56,7 @@ func NewOpsGenieNotifier(model *m.AlertNotification) (alerting.Notifier, error)
}
return &OpsGenieNotifier{
- NotifierBase: NewNotifierBase(model.Id, model.IsDefault, model.Name, model.Type, model.Settings),
+ NotifierBase: NewNotifierBase(model),
ApiKey: apiKey,
ApiUrl: apiUrl,
AutoClose: autoClose,
diff --git a/pkg/services/alerting/notifiers/pagerduty.go b/pkg/services/alerting/notifiers/pagerduty.go
index 02219b2203d..bf85466388f 100644
--- a/pkg/services/alerting/notifiers/pagerduty.go
+++ b/pkg/services/alerting/notifiers/pagerduty.go
@@ -51,7 +51,7 @@ func NewPagerdutyNotifier(model *m.AlertNotification) (alerting.Notifier, error)
}
return &PagerdutyNotifier{
- NotifierBase: NewNotifierBase(model.Id, model.IsDefault, model.Name, model.Type, model.Settings),
+ NotifierBase: NewNotifierBase(model),
Key: key,
AutoResolve: autoResolve,
log: log.New("alerting.notifier.pagerduty"),
diff --git a/pkg/services/alerting/notifiers/pushover.go b/pkg/services/alerting/notifiers/pushover.go
index cbe9e16801a..55dc02c5f4a 100644
--- a/pkg/services/alerting/notifiers/pushover.go
+++ b/pkg/services/alerting/notifiers/pushover.go
@@ -99,7 +99,7 @@ func NewPushoverNotifier(model *m.AlertNotification) (alerting.Notifier, error)
return nil, alerting.ValidationError{Reason: "API token not given"}
}
return &PushoverNotifier{
- NotifierBase: NewNotifierBase(model.Id, model.IsDefault, model.Name, model.Type, model.Settings),
+ NotifierBase: NewNotifierBase(model),
UserKey: userKey,
ApiToken: apiToken,
Priority: priority,
diff --git a/pkg/services/alerting/notifiers/sensu.go b/pkg/services/alerting/notifiers/sensu.go
index 9f77801d458..21d5d3d9d9e 100644
--- a/pkg/services/alerting/notifiers/sensu.go
+++ b/pkg/services/alerting/notifiers/sensu.go
@@ -51,7 +51,7 @@ func NewSensuNotifier(model *m.AlertNotification) (alerting.Notifier, error) {
}
return &SensuNotifier{
- NotifierBase: NewNotifierBase(model.Id, model.IsDefault, model.Name, model.Type, model.Settings),
+ NotifierBase: NewNotifierBase(model),
Url: url,
User: model.Settings.Get("username").MustString(),
Source: model.Settings.Get("source").MustString(),
diff --git a/pkg/services/alerting/notifiers/slack.go b/pkg/services/alerting/notifiers/slack.go
index a8139b62726..374b49ea957 100644
--- a/pkg/services/alerting/notifiers/slack.go
+++ b/pkg/services/alerting/notifiers/slack.go
@@ -58,7 +58,7 @@ func init() {
data-placement="right">
- Provide a bot token to use the Slack file.upload API (starts with "xoxb")
+ Provide a bot token to use the Slack file.upload API (starts with "xoxb"). Specify #channel-name or @username in Recipient for this to work
`,
@@ -78,7 +78,7 @@ func NewSlackNotifier(model *m.AlertNotification) (alerting.Notifier, error) {
uploadImage := model.Settings.Get("uploadImage").MustBool(true)
return &SlackNotifier{
- NotifierBase: NewNotifierBase(model.Id, model.IsDefault, model.Name, model.Type, model.Settings),
+ NotifierBase: NewNotifierBase(model),
Url: url,
Recipient: recipient,
Mention: mention,
diff --git a/pkg/services/alerting/notifiers/teams.go b/pkg/services/alerting/notifiers/teams.go
index 4e34e16ab51..2dad11285b4 100644
--- a/pkg/services/alerting/notifiers/teams.go
+++ b/pkg/services/alerting/notifiers/teams.go
@@ -33,7 +33,7 @@ func NewTeamsNotifier(model *m.AlertNotification) (alerting.Notifier, error) {
}
return &TeamsNotifier{
- NotifierBase: NewNotifierBase(model.Id, model.IsDefault, model.Name, model.Type, model.Settings),
+ NotifierBase: NewNotifierBase(model),
Url: url,
log: log.New("alerting.notifier.teams"),
}, nil
@@ -74,7 +74,7 @@ func (this *TeamsNotifier) Notify(evalContext *alerting.EvalContext) error {
}
message := ""
- if evalContext.Rule.State != m.AlertStateOK { //dont add message when going back to alert state ok.
+ if evalContext.Rule.State != m.AlertStateOK { //don't add message when going back to alert state ok.
message = evalContext.Rule.Message
}
@@ -96,14 +96,26 @@ func (this *TeamsNotifier) Notify(evalContext *alerting.EvalContext) error {
},
},
"text": message,
- "potentialAction": []map[string]interface{}{
+ },
+ },
+ "potentialAction": []map[string]interface{}{
+ {
+ "@context": "http://schema.org",
+ "@type": "OpenUri",
+ "name": "View Rule",
+ "targets": []map[string]interface{}{
{
- "@context": "http://schema.org",
- "@type": "ViewAction",
- "name": "View Rule",
- "target": []string{
- ruleUrl,
- },
+ "os": "default", "uri": ruleUrl,
+ },
+ },
+ },
+ {
+ "@context": "http://schema.org",
+ "@type": "OpenUri",
+ "name": "View Graph",
+ "targets": []map[string]interface{}{
+ {
+ "os": "default", "uri": evalContext.ImagePublicUrl,
},
},
},
diff --git a/pkg/services/alerting/notifiers/telegram.go b/pkg/services/alerting/notifiers/telegram.go
index ca24c996914..5492de45d39 100644
--- a/pkg/services/alerting/notifiers/telegram.go
+++ b/pkg/services/alerting/notifiers/telegram.go
@@ -78,7 +78,7 @@ func NewTelegramNotifier(model *m.AlertNotification) (alerting.Notifier, error)
}
return &TelegramNotifier{
- NotifierBase: NewNotifierBase(model.Id, model.IsDefault, model.Name, model.Type, model.Settings),
+ NotifierBase: NewNotifierBase(model),
BotToken: botToken,
ChatID: chatId,
UploadImage: uploadImage,
@@ -216,7 +216,7 @@ func appendIfPossible(message string, extra string, sizeLimit int) string {
if len(extra)+len(message) <= sizeLimit {
return message + extra
}
- log.Debug("Line too long for image caption.", "value", extra)
+ log.Debug("Line too long for image caption. value: %s", extra)
return message
}
diff --git a/pkg/services/alerting/notifiers/threema.go b/pkg/services/alerting/notifiers/threema.go
index e4ffffc9108..28a62fade17 100644
--- a/pkg/services/alerting/notifiers/threema.go
+++ b/pkg/services/alerting/notifiers/threema.go
@@ -106,7 +106,7 @@ func NewThreemaNotifier(model *m.AlertNotification) (alerting.Notifier, error) {
}
return &ThreemaNotifier{
- NotifierBase: NewNotifierBase(model.Id, model.IsDefault, model.Name, model.Type, model.Settings),
+ NotifierBase: NewNotifierBase(model),
GatewayID: gatewayID,
RecipientID: recipientID,
APISecret: apiSecret,
diff --git a/pkg/services/alerting/notifiers/victorops.go b/pkg/services/alerting/notifiers/victorops.go
index a753ca3cbf6..3093aec9957 100644
--- a/pkg/services/alerting/notifiers/victorops.go
+++ b/pkg/services/alerting/notifiers/victorops.go
@@ -51,7 +51,7 @@ func NewVictoropsNotifier(model *models.AlertNotification) (alerting.Notifier, e
}
return &VictoropsNotifier{
- NotifierBase: NewNotifierBase(model.Id, model.IsDefault, model.Name, model.Type, model.Settings),
+ NotifierBase: NewNotifierBase(model),
URL: url,
AutoResolve: autoResolve,
log: log.New("alerting.notifier.victorops"),
diff --git a/pkg/services/alerting/notifiers/webhook.go b/pkg/services/alerting/notifiers/webhook.go
index 4c97ed2b75e..4045e496af9 100644
--- a/pkg/services/alerting/notifiers/webhook.go
+++ b/pkg/services/alerting/notifiers/webhook.go
@@ -47,7 +47,7 @@ func NewWebHookNotifier(model *m.AlertNotification) (alerting.Notifier, error) {
}
return &WebhookNotifier{
- NotifierBase: NewNotifierBase(model.Id, model.IsDefault, model.Name, model.Type, model.Settings),
+ NotifierBase: NewNotifierBase(model),
Url: url,
User: model.Settings.Get("username").MustString(),
Password: model.Settings.Get("password").MustString(),
diff --git a/pkg/services/alerting/result_handler.go b/pkg/services/alerting/result_handler.go
index c57b28c7c3e..363d06d1132 100644
--- a/pkg/services/alerting/result_handler.go
+++ b/pkg/services/alerting/result_handler.go
@@ -88,6 +88,18 @@ func (handler *DefaultResultHandler) Handle(evalContext *EvalContext) error {
}
}
+ if evalContext.Rule.State == m.AlertStateOK && evalContext.PrevAlertState != m.AlertStateOK {
+ for _, notifierId := range evalContext.Rule.Notifications {
+ cmd := &m.CleanNotificationJournalCommand{
+ AlertId: evalContext.Rule.Id,
+ NotifierId: notifierId,
+ OrgId: evalContext.Rule.OrgId,
+ }
+ if err := bus.DispatchCtx(evalContext.Ctx, cmd); err != nil {
+ handler.log.Error("Failed to clean up old notification records", "notifier", notifierId, "alert", evalContext.Rule.Id, "Error", err)
+ }
+ }
+ }
handler.notifier.SendIfNeeded(evalContext)
return nil
diff --git a/pkg/services/annotations/annotations.go b/pkg/services/annotations/annotations.go
index 9b490169d3b..60a92aa897a 100644
--- a/pkg/services/annotations/annotations.go
+++ b/pkg/services/annotations/annotations.go
@@ -21,6 +21,7 @@ type ItemQuery struct {
RegionId int64 `json:"regionId"`
Tags []string `json:"tags"`
Type string `json:"type"`
+ MatchAny bool `json:"matchAny"`
Limit int64 `json:"limit"`
}
diff --git a/pkg/services/provisioning/datasources/config_reader.go b/pkg/services/provisioning/datasources/config_reader.go
index 4b8931f0ed3..b2930c2b679 100644
--- a/pkg/services/provisioning/datasources/config_reader.go
+++ b/pkg/services/provisioning/datasources/config_reader.go
@@ -83,7 +83,7 @@ func (cr *configReader) parseDatasourceConfig(path string, file os.FileInfo) (*D
}
func validateDefaultUniqueness(datasources []*DatasourcesAsConfig) error {
- defaultCount := 0
+ defaultCount := map[int64]int{}
for i := range datasources {
if datasources[i].Datasources == nil {
continue
@@ -95,8 +95,8 @@ func validateDefaultUniqueness(datasources []*DatasourcesAsConfig) error {
}
if ds.IsDefault {
- defaultCount++
- if defaultCount > 1 {
+ defaultCount[ds.OrgId] = defaultCount[ds.OrgId] + 1
+ if defaultCount[ds.OrgId] > 1 {
return ErrInvalidConfigToManyDefault
}
}
diff --git a/pkg/services/provisioning/datasources/config_reader_test.go b/pkg/services/provisioning/datasources/config_reader_test.go
index 2e407dbe4de..07c8d68e75c 100644
--- a/pkg/services/provisioning/datasources/config_reader_test.go
+++ b/pkg/services/provisioning/datasources/config_reader_test.go
@@ -19,6 +19,7 @@ var (
allProperties = "testdata/all-properties"
versionZero = "testdata/version-0"
brokenYaml = "testdata/broken-yaml"
+ multipleOrgsWithDefault = "testdata/multiple-org-default"
fakeRepo *fakeRepository
)
@@ -73,6 +74,19 @@ func TestDatasourceAsConfig(t *testing.T) {
})
})
+ Convey("Multiple datasources in different organizations with isDefault in each organization", func() {
+ dc := newDatasourceProvisioner(logger)
+ err := dc.applyChanges(multipleOrgsWithDefault)
+ Convey("should not raise error", func() {
+ So(err, ShouldBeNil)
+ So(len(fakeRepo.inserted), ShouldEqual, 4)
+ So(fakeRepo.inserted[0].IsDefault, ShouldBeTrue)
+ So(fakeRepo.inserted[0].OrgId, ShouldEqual, 1)
+ So(fakeRepo.inserted[2].IsDefault, ShouldBeTrue)
+ So(fakeRepo.inserted[2].OrgId, ShouldEqual, 2)
+ })
+ })
+
Convey("Two configured datasource and purge others ", func() {
Convey("two other datasources in database", func() {
fakeRepo.loadAll = []*models.DataSource{
diff --git a/pkg/services/provisioning/datasources/datasources.go b/pkg/services/provisioning/datasources/datasources.go
index 1fa0a3b3173..de6c876baad 100644
--- a/pkg/services/provisioning/datasources/datasources.go
+++ b/pkg/services/provisioning/datasources/datasources.go
@@ -11,7 +11,7 @@ import (
)
var (
- ErrInvalidConfigToManyDefault = errors.New("datasource.yaml config is invalid. Only one datasource can be marked as default")
+ ErrInvalidConfigToManyDefault = errors.New("datasource.yaml config is invalid. Only one datasource per organization can be marked as default")
)
func Provision(configDirectory string) error {
diff --git a/pkg/services/provisioning/datasources/testdata/broken-yaml/commented.yaml b/pkg/services/provisioning/datasources/testdata/broken-yaml/commented.yaml
index 1bb9cb53b45..b532c9012ec 100644
--- a/pkg/services/provisioning/datasources/testdata/broken-yaml/commented.yaml
+++ b/pkg/services/provisioning/datasources/testdata/broken-yaml/commented.yaml
@@ -4,7 +4,7 @@
# org_id: 1
# # list of datasources to insert/update depending
-# # whats available in the datbase
+# # what's available in the database
#datasources:
# # name of the datasource. Required
# - name: Graphite
diff --git a/pkg/services/provisioning/datasources/testdata/multiple-org-default/config.yaml b/pkg/services/provisioning/datasources/testdata/multiple-org-default/config.yaml
new file mode 100644
index 00000000000..f185abb6f53
--- /dev/null
+++ b/pkg/services/provisioning/datasources/testdata/multiple-org-default/config.yaml
@@ -0,0 +1,25 @@
+apiVersion: 1
+
+datasources:
+ - orgId: 1
+ name: prometheus
+ type: prometheus
+ isDefault: True
+ access: proxy
+ url: http://prometheus.example.com:9090
+ - name: Graphite
+ type: graphite
+ access: proxy
+ url: http://localhost:8080
+ - orgId: 2
+ name: prometheus
+ type: prometheus
+ isDefault: True
+ access: proxy
+ url: http://prometheus.example.com:9090
+ - orgId: 2
+ name: Graphite
+ type: graphite
+ access: proxy
+ url: http://localhost:8080
+
diff --git a/pkg/services/rendering/http_mode.go b/pkg/services/rendering/http_mode.go
index 9084ca27353..40259c44746 100644
--- a/pkg/services/rendering/http_mode.go
+++ b/pkg/services/rendering/http_mode.go
@@ -2,6 +2,7 @@ package rendering
import (
"context"
+ "fmt"
"io"
"net"
"net/http"
@@ -20,14 +21,13 @@ var netTransport = &http.Transport{
TLSHandshakeTimeout: 5 * time.Second,
}
+var netClient = &http.Client{
+ Transport: netTransport,
+}
+
func (rs *RenderingService) renderViaHttp(ctx context.Context, opts Opts) (*RenderResult, error) {
filePath := rs.getFilePathForNewImage()
- var netClient = &http.Client{
- Timeout: opts.Timeout,
- Transport: netTransport,
- }
-
rendererUrl, err := url.Parse(rs.Cfg.RendererUrl)
if err != nil {
return nil, err
@@ -35,10 +35,10 @@ func (rs *RenderingService) renderViaHttp(ctx context.Context, opts Opts) (*Rend
queryParams := rendererUrl.Query()
queryParams.Add("url", rs.getURL(opts.Path))
- queryParams.Add("renderKey", rs.getRenderKey(opts.UserId, opts.OrgId, opts.OrgRole))
+ queryParams.Add("renderKey", rs.getRenderKey(opts.OrgId, opts.UserId, opts.OrgRole))
queryParams.Add("width", strconv.Itoa(opts.Width))
queryParams.Add("height", strconv.Itoa(opts.Height))
- queryParams.Add("domain", rs.getLocalDomain())
+ queryParams.Add("domain", rs.domain)
queryParams.Add("timezone", isoTimeOffsetToPosixTz(opts.Timezone))
queryParams.Add("encoding", opts.Encoding)
queryParams.Add("timeout", strconv.Itoa(int(opts.Timeout.Seconds())))
@@ -49,20 +49,48 @@ func (rs *RenderingService) renderViaHttp(ctx context.Context, opts Opts) (*Rend
return nil, err
}
+ reqContext, cancel := context.WithTimeout(ctx, opts.Timeout+time.Second*2)
+ defer cancel()
+
+ req = req.WithContext(reqContext)
+
// make request to renderer server
resp, err := netClient.Do(req)
if err != nil {
- return nil, err
+ rs.log.Error("Failed to send request to remote rendering service.", "error", err)
+ return nil, fmt.Errorf("Failed to send request to remote rendering service. %s", err)
}
// save response to file
defer resp.Body.Close()
+
+ // check for timeout first
+ if reqContext.Err() == context.DeadlineExceeded {
+ rs.log.Info("Rendering timed out")
+ return nil, ErrTimeout
+ }
+
+ // if we didn't get a 200 response, something went wrong.
+ if resp.StatusCode != http.StatusOK {
+ rs.log.Error("Remote rendering request failed", "error", resp.Status)
+ return nil, fmt.Errorf("Remote rendering request failed. %d: %s", resp.StatusCode, resp.Status)
+ }
+
out, err := os.Create(filePath)
if err != nil {
return nil, err
}
defer out.Close()
- io.Copy(out, resp.Body)
+ _, err = io.Copy(out, resp.Body)
+ if err != nil {
+ // check that we didn't timeout while receiving the response.
+ if reqContext.Err() == context.DeadlineExceeded {
+ rs.log.Info("Rendering timed out")
+ return nil, ErrTimeout
+ }
+ rs.log.Error("Remote rendering request failed", "error", err)
+ return nil, fmt.Errorf("Remote rendering request failed. %s", err)
+ }
return &RenderResult{FilePath: filePath}, err
}
diff --git a/pkg/services/rendering/phantomjs.go b/pkg/services/rendering/phantomjs.go
index 87ccaf6b5d2..1bd7489c153 100644
--- a/pkg/services/rendering/phantomjs.go
+++ b/pkg/services/rendering/phantomjs.go
@@ -49,7 +49,7 @@ func (rs *RenderingService) renderViaPhantomJS(ctx context.Context, opts Opts) (
fmt.Sprintf("width=%v", opts.Width),
fmt.Sprintf("height=%v", opts.Height),
fmt.Sprintf("png=%v", pngPath),
- fmt.Sprintf("domain=%v", rs.getLocalDomain()),
+ fmt.Sprintf("domain=%v", rs.domain),
fmt.Sprintf("timeout=%v", opts.Timeout.Seconds()),
fmt.Sprintf("renderKey=%v", renderKey),
}
diff --git a/pkg/services/rendering/plugin_mode.go b/pkg/services/rendering/plugin_mode.go
index 550779ad7c3..58fef2b095f 100644
--- a/pkg/services/rendering/plugin_mode.go
+++ b/pkg/services/rendering/plugin_mode.go
@@ -77,10 +77,10 @@ func (rs *RenderingService) renderViaPlugin(ctx context.Context, opts Opts) (*Re
Height: int32(opts.Height),
FilePath: pngPath,
Timeout: int32(opts.Timeout.Seconds()),
- RenderKey: rs.getRenderKey(opts.UserId, opts.OrgId, opts.OrgRole),
+ RenderKey: rs.getRenderKey(opts.OrgId, opts.UserId, opts.OrgRole),
Encoding: opts.Encoding,
Timezone: isoTimeOffsetToPosixTz(opts.Timezone),
- Domain: rs.getLocalDomain(),
+ Domain: rs.domain,
})
if err != nil {
diff --git a/pkg/services/rendering/rendering.go b/pkg/services/rendering/rendering.go
index 799aecc3e88..ecef83d74d9 100644
--- a/pkg/services/rendering/rendering.go
+++ b/pkg/services/rendering/rendering.go
@@ -3,6 +3,8 @@ package rendering
import (
"context"
"fmt"
+ "net/url"
+ "os"
"path/filepath"
plugin "github.com/hashicorp/go-plugin"
@@ -27,12 +29,31 @@ type RenderingService struct {
grpcPlugin pluginModel.RendererPlugin
pluginInfo *plugins.RendererPlugin
renderAction renderFunc
+ domain string
Cfg *setting.Cfg `inject:""`
}
func (rs *RenderingService) Init() error {
rs.log = log.New("rendering")
+
+ // ensure ImagesDir exists
+ err := os.MkdirAll(rs.Cfg.ImagesDir, 0700)
+ if err != nil {
+ return err
+ }
+
+ // set value used for domain attribute of renderKey cookie
+ if rs.Cfg.RendererUrl != "" {
+ // RendererCallbackUrl has already been passed, it won't generate an error.
+ u, _ := url.Parse(rs.Cfg.RendererCallbackUrl)
+ rs.domain = u.Hostname()
+ } else if setting.HttpAddr != setting.DEFAULT_HTTP_ADDR {
+ rs.domain = setting.HttpAddr
+ } else {
+ rs.domain = "localhost"
+ }
+
return nil
}
@@ -82,16 +103,17 @@ func (rs *RenderingService) getFilePathForNewImage() string {
}
func (rs *RenderingService) getURL(path string) string {
- // &render=1 signals to the legacy redirect layer to
- return fmt.Sprintf("%s://%s:%s/%s&render=1", setting.Protocol, rs.getLocalDomain(), setting.HttpPort, path)
-}
+ if rs.Cfg.RendererUrl != "" {
+ // The backend rendering service can potentially be remote.
+ // So we need to use the root_url to ensure the rendering service
+ // can reach this Grafana instance.
+
+ // &render=1 signals to the legacy redirect layer to
+ return fmt.Sprintf("%s%s&render=1", rs.Cfg.RendererCallbackUrl, path)
-func (rs *RenderingService) getLocalDomain() string {
- if setting.HttpAddr != setting.DEFAULT_HTTP_ADDR {
- return setting.HttpAddr
}
-
- return "localhost"
+ // &render=1 signals to the legacy redirect layer to
+ return fmt.Sprintf("%s://%s:%s/%s&render=1", setting.Protocol, rs.domain, setting.HttpPort, path)
}
func (rs *RenderingService) getRenderKey(orgId, userId int64, orgRole models.RoleType) string {
diff --git a/pkg/services/sqlstore/alert.go b/pkg/services/sqlstore/alert.go
index af911dc22e6..ba898769578 100644
--- a/pkg/services/sqlstore/alert.go
+++ b/pkg/services/sqlstore/alert.go
@@ -40,7 +40,7 @@ func GetAlertById(query *m.GetAlertByIdQuery) error {
func GetAllAlertQueryHandler(query *m.GetAllAlertsQuery) error {
var alerts []*m.Alert
- err := x.Sql("select * from alert").Find(&alerts)
+ err := x.SQL("select * from alert").Find(&alerts)
if err != nil {
return err
}
@@ -190,7 +190,7 @@ func updateAlerts(existingAlerts []*m.Alert, cmd *m.SaveAlertsCommand, sess *DBS
alert.Updated = timeNow()
alert.State = alertToUpdate.State
sess.MustCols("message")
- _, err := sess.Id(alert.Id).Update(alert)
+ _, err := sess.ID(alert.Id).Update(alert)
if err != nil {
return err
}
@@ -249,7 +249,7 @@ func SetAlertState(cmd *m.SetAlertStateCommand) error {
return inTransaction(func(sess *DBSession) error {
alert := m.Alert{}
- if has, err := sess.Id(cmd.AlertId).Get(&alert); err != nil {
+ if has, err := sess.ID(cmd.AlertId).Get(&alert); err != nil {
return err
} else if !has {
return fmt.Errorf("Could not find alert")
diff --git a/pkg/services/sqlstore/alert_notification.go b/pkg/services/sqlstore/alert_notification.go
index 651241f7714..31867910ddb 100644
--- a/pkg/services/sqlstore/alert_notification.go
+++ b/pkg/services/sqlstore/alert_notification.go
@@ -2,6 +2,7 @@ package sqlstore
import (
"bytes"
+ "context"
"fmt"
"strings"
"time"
@@ -17,6 +18,9 @@ func init() {
bus.AddHandler("sql", DeleteAlertNotification)
bus.AddHandler("sql", GetAlertNotificationsToSend)
bus.AddHandler("sql", GetAllAlertNotifications)
+ bus.AddHandlerCtx("sql", RecordNotificationJournal)
+ bus.AddHandlerCtx("sql", GetLatestNotification)
+ bus.AddHandlerCtx("sql", CleanNotificationJournal)
}
func DeleteAlertNotification(cmd *m.DeleteAlertNotificationCommand) error {
@@ -53,7 +57,9 @@ func GetAlertNotificationsToSend(query *m.GetAlertNotificationsToSendQuery) erro
alert_notification.created,
alert_notification.updated,
alert_notification.settings,
- alert_notification.is_default
+ alert_notification.is_default,
+ alert_notification.send_reminder,
+ alert_notification.frequency
FROM alert_notification
`)
@@ -91,7 +97,9 @@ func getAlertNotificationInternal(query *m.GetAlertNotificationsQuery, sess *DBS
alert_notification.created,
alert_notification.updated,
alert_notification.settings,
- alert_notification.is_default
+ alert_notification.is_default,
+ alert_notification.send_reminder,
+ alert_notification.frequency
FROM alert_notification
`)
@@ -111,7 +119,7 @@ func getAlertNotificationInternal(query *m.GetAlertNotificationsQuery, sess *DBS
}
results := make([]*m.AlertNotification, 0)
- if err := sess.Sql(sql.String(), params...).Find(&results); err != nil {
+ if err := sess.SQL(sql.String(), params...).Find(&results); err != nil {
return err
}
@@ -137,17 +145,31 @@ func CreateAlertNotificationCommand(cmd *m.CreateAlertNotificationCommand) error
return fmt.Errorf("Alert notification name %s already exists", cmd.Name)
}
- alertNotification := &m.AlertNotification{
- OrgId: cmd.OrgId,
- Name: cmd.Name,
- Type: cmd.Type,
- Settings: cmd.Settings,
- Created: time.Now(),
- Updated: time.Now(),
- IsDefault: cmd.IsDefault,
+ var frequency time.Duration
+ if cmd.SendReminder {
+ if cmd.Frequency == "" {
+ return m.ErrNotificationFrequencyNotFound
+ }
+
+ frequency, err = time.ParseDuration(cmd.Frequency)
+ if err != nil {
+ return err
+ }
}
- if _, err = sess.Insert(alertNotification); err != nil {
+ alertNotification := &m.AlertNotification{
+ OrgId: cmd.OrgId,
+ Name: cmd.Name,
+ Type: cmd.Type,
+ Settings: cmd.Settings,
+ SendReminder: cmd.SendReminder,
+ Frequency: frequency,
+ Created: time.Now(),
+ Updated: time.Now(),
+ IsDefault: cmd.IsDefault,
+ }
+
+ if _, err = sess.MustCols("send_reminder").Insert(alertNotification); err != nil {
return err
}
@@ -179,16 +201,74 @@ func UpdateAlertNotification(cmd *m.UpdateAlertNotificationCommand) error {
current.Name = cmd.Name
current.Type = cmd.Type
current.IsDefault = cmd.IsDefault
+ current.SendReminder = cmd.SendReminder
- sess.UseBool("is_default")
+ if current.SendReminder {
+ if cmd.Frequency == "" {
+ return m.ErrNotificationFrequencyNotFound
+ }
+
+ frequency, err := time.ParseDuration(cmd.Frequency)
+ if err != nil {
+ return err
+ }
+
+ current.Frequency = frequency
+ }
+
+ sess.UseBool("is_default", "send_reminder")
if affected, err := sess.ID(cmd.Id).Update(current); err != nil {
return err
} else if affected == 0 {
- return fmt.Errorf("Could not find alert notification")
+ return fmt.Errorf("Could not update alert notification")
}
cmd.Result = ¤t
return nil
})
}
+
+func RecordNotificationJournal(ctx context.Context, cmd *m.RecordNotificationJournalCommand) error {
+ return inTransactionCtx(ctx, func(sess *DBSession) error {
+ journalEntry := &m.AlertNotificationJournal{
+ OrgId: cmd.OrgId,
+ AlertId: cmd.AlertId,
+ NotifierId: cmd.NotifierId,
+ SentAt: cmd.SentAt,
+ Success: cmd.Success,
+ }
+
+ _, err := sess.Insert(journalEntry)
+ return err
+ })
+}
+
+func GetLatestNotification(ctx context.Context, cmd *m.GetLatestNotificationQuery) error {
+ return inTransactionCtx(ctx, func(sess *DBSession) error {
+ nj := &m.AlertNotificationJournal{}
+
+ _, err := sess.Desc("alert_notification_journal.sent_at").
+ Limit(1).
+ Where("alert_notification_journal.org_id = ? AND alert_notification_journal.alert_id = ? AND alert_notification_journal.notifier_id = ?", cmd.OrgId, cmd.AlertId, cmd.NotifierId).Get(nj)
+
+ if err != nil {
+ return err
+ }
+
+ if nj.AlertId == 0 && nj.Id == 0 && nj.NotifierId == 0 && nj.OrgId == 0 {
+ return m.ErrJournalingNotFound
+ }
+
+ cmd.Result = nj
+ return nil
+ })
+}
+
+func CleanNotificationJournal(ctx context.Context, cmd *m.CleanNotificationJournalCommand) error {
+ return inTransactionCtx(ctx, func(sess *DBSession) error {
+ sql := "DELETE FROM alert_notification_journal WHERE alert_notification_journal.org_id = ? AND alert_notification_journal.alert_id = ? AND alert_notification_journal.notifier_id = ?"
+ _, err := sess.Exec(sql, cmd.OrgId, cmd.AlertId, cmd.NotifierId)
+ return err
+ })
+}
diff --git a/pkg/services/sqlstore/alert_notification_test.go b/pkg/services/sqlstore/alert_notification_test.go
index 2dbf9de5ca8..83fb42db9bb 100644
--- a/pkg/services/sqlstore/alert_notification_test.go
+++ b/pkg/services/sqlstore/alert_notification_test.go
@@ -1,7 +1,9 @@
package sqlstore
import (
+ "context"
"testing"
+ "time"
"github.com/grafana/grafana/pkg/components/simplejson"
m "github.com/grafana/grafana/pkg/models"
@@ -11,7 +13,48 @@ import (
func TestAlertNotificationSQLAccess(t *testing.T) {
Convey("Testing Alert notification sql access", t, func() {
InitTestDB(t)
- var err error
+
+ Convey("Alert notification journal", func() {
+ var alertId int64 = 5
+ var orgId int64 = 5
+ var notifierId int64 = 5
+
+ Convey("Getting last journal should raise error if no one exists", func() {
+ query := &m.GetLatestNotificationQuery{AlertId: alertId, OrgId: orgId, NotifierId: notifierId}
+ err := GetLatestNotification(context.Background(), query)
+ So(err, ShouldEqual, m.ErrJournalingNotFound)
+
+ Convey("shoulbe be able to record two journaling events", func() {
+ createCmd := &m.RecordNotificationJournalCommand{AlertId: alertId, NotifierId: notifierId, OrgId: orgId, Success: true, SentAt: 1}
+
+ err := RecordNotificationJournal(context.Background(), createCmd)
+ So(err, ShouldBeNil)
+
+ createCmd.SentAt += 1000 //increase epoch
+
+ err = RecordNotificationJournal(context.Background(), createCmd)
+ So(err, ShouldBeNil)
+
+ Convey("get last journaling event", func() {
+ err := GetLatestNotification(context.Background(), query)
+ So(err, ShouldBeNil)
+ So(query.Result.SentAt, ShouldEqual, 1001)
+
+ Convey("be able to clear all journaling for an notifier", func() {
+ cmd := &m.CleanNotificationJournalCommand{AlertId: alertId, NotifierId: notifierId, OrgId: orgId}
+ err := CleanNotificationJournal(context.Background(), cmd)
+ So(err, ShouldBeNil)
+
+ Convey("querying for last junaling should raise error", func() {
+ query := &m.GetLatestNotificationQuery{AlertId: alertId, OrgId: orgId, NotifierId: notifierId}
+ err := GetLatestNotification(context.Background(), query)
+ So(err, ShouldEqual, m.ErrJournalingNotFound)
+ })
+ })
+ })
+ })
+ })
+ })
Convey("Alert notifications should be empty", func() {
cmd := &m.GetAlertNotificationsQuery{
@@ -24,19 +67,75 @@ func TestAlertNotificationSQLAccess(t *testing.T) {
So(cmd.Result, ShouldBeNil)
})
- Convey("Can save Alert Notification", func() {
+ Convey("Cannot save alert notifier with send reminder = true", func() {
cmd := &m.CreateAlertNotificationCommand{
- Name: "ops",
- Type: "email",
- OrgId: 1,
- Settings: simplejson.New(),
+ Name: "ops",
+ Type: "email",
+ OrgId: 1,
+ SendReminder: true,
+ Settings: simplejson.New(),
}
- err = CreateAlertNotificationCommand(cmd)
+ Convey("and missing frequency", func() {
+ err := CreateAlertNotificationCommand(cmd)
+ So(err, ShouldEqual, m.ErrNotificationFrequencyNotFound)
+ })
+
+ Convey("invalid frequency", func() {
+ cmd.Frequency = "invalid duration"
+
+ err := CreateAlertNotificationCommand(cmd)
+ So(err.Error(), ShouldEqual, "time: invalid duration invalid duration")
+ })
+ })
+
+ Convey("Cannot update alert notifier with send reminder = false", func() {
+ cmd := &m.CreateAlertNotificationCommand{
+ Name: "ops update",
+ Type: "email",
+ OrgId: 1,
+ SendReminder: false,
+ Settings: simplejson.New(),
+ }
+
+ err := CreateAlertNotificationCommand(cmd)
+ So(err, ShouldBeNil)
+
+ updateCmd := &m.UpdateAlertNotificationCommand{
+ Id: cmd.Result.Id,
+ SendReminder: true,
+ }
+
+ Convey("and missing frequency", func() {
+ err := UpdateAlertNotification(updateCmd)
+ So(err, ShouldEqual, m.ErrNotificationFrequencyNotFound)
+ })
+
+ Convey("invalid frequency", func() {
+ updateCmd.Frequency = "invalid duration"
+
+ err := UpdateAlertNotification(updateCmd)
+ So(err, ShouldNotBeNil)
+ So(err.Error(), ShouldEqual, "time: invalid duration invalid duration")
+ })
+ })
+
+ Convey("Can save Alert Notification", func() {
+ cmd := &m.CreateAlertNotificationCommand{
+ Name: "ops",
+ Type: "email",
+ OrgId: 1,
+ SendReminder: true,
+ Frequency: "10s",
+ Settings: simplejson.New(),
+ }
+
+ err := CreateAlertNotificationCommand(cmd)
So(err, ShouldBeNil)
So(cmd.Result.Id, ShouldNotEqual, 0)
So(cmd.Result.OrgId, ShouldNotEqual, 0)
So(cmd.Result.Type, ShouldEqual, "email")
+ So(cmd.Result.Frequency, ShouldEqual, 10*time.Second)
Convey("Cannot save Alert Notification with the same name", func() {
err = CreateAlertNotificationCommand(cmd)
@@ -45,25 +144,42 @@ func TestAlertNotificationSQLAccess(t *testing.T) {
Convey("Can update alert notification", func() {
newCmd := &m.UpdateAlertNotificationCommand{
- Name: "NewName",
- Type: "webhook",
- OrgId: cmd.Result.OrgId,
- Settings: simplejson.New(),
- Id: cmd.Result.Id,
+ Name: "NewName",
+ Type: "webhook",
+ OrgId: cmd.Result.OrgId,
+ SendReminder: true,
+ Frequency: "60s",
+ Settings: simplejson.New(),
+ Id: cmd.Result.Id,
}
err := UpdateAlertNotification(newCmd)
So(err, ShouldBeNil)
So(newCmd.Result.Name, ShouldEqual, "NewName")
+ So(newCmd.Result.Frequency, ShouldEqual, 60*time.Second)
+ })
+
+ Convey("Can update alert notification to disable sending of reminders", func() {
+ newCmd := &m.UpdateAlertNotificationCommand{
+ Name: "NewName",
+ Type: "webhook",
+ OrgId: cmd.Result.OrgId,
+ SendReminder: false,
+ Settings: simplejson.New(),
+ Id: cmd.Result.Id,
+ }
+ err := UpdateAlertNotification(newCmd)
+ So(err, ShouldBeNil)
+ So(newCmd.Result.SendReminder, ShouldBeFalse)
})
})
Convey("Can search using an array of ids", func() {
- cmd1 := m.CreateAlertNotificationCommand{Name: "nagios", Type: "webhook", OrgId: 1, Settings: simplejson.New()}
- cmd2 := m.CreateAlertNotificationCommand{Name: "slack", Type: "webhook", OrgId: 1, Settings: simplejson.New()}
- cmd3 := m.CreateAlertNotificationCommand{Name: "ops2", Type: "email", OrgId: 1, Settings: simplejson.New()}
- cmd4 := m.CreateAlertNotificationCommand{IsDefault: true, Name: "default", Type: "email", OrgId: 1, Settings: simplejson.New()}
+ cmd1 := m.CreateAlertNotificationCommand{Name: "nagios", Type: "webhook", OrgId: 1, SendReminder: true, Frequency: "10s", Settings: simplejson.New()}
+ cmd2 := m.CreateAlertNotificationCommand{Name: "slack", Type: "webhook", OrgId: 1, SendReminder: true, Frequency: "10s", Settings: simplejson.New()}
+ cmd3 := m.CreateAlertNotificationCommand{Name: "ops2", Type: "email", OrgId: 1, SendReminder: true, Frequency: "10s", Settings: simplejson.New()}
+ cmd4 := m.CreateAlertNotificationCommand{IsDefault: true, Name: "default", Type: "email", OrgId: 1, SendReminder: true, Frequency: "10s", Settings: simplejson.New()}
- otherOrg := m.CreateAlertNotificationCommand{Name: "default", Type: "email", OrgId: 2, Settings: simplejson.New()}
+ otherOrg := m.CreateAlertNotificationCommand{Name: "default", Type: "email", OrgId: 2, SendReminder: true, Frequency: "10s", Settings: simplejson.New()}
So(CreateAlertNotificationCommand(&cmd1), ShouldBeNil)
So(CreateAlertNotificationCommand(&cmd2), ShouldBeNil)
diff --git a/pkg/services/sqlstore/annotation.go b/pkg/services/sqlstore/annotation.go
index a65bc136554..274481baeca 100644
--- a/pkg/services/sqlstore/annotation.go
+++ b/pkg/services/sqlstore/annotation.go
@@ -110,7 +110,7 @@ func (r *SqlAnnotationRepo) Update(item *annotations.Item) error {
existing.Tags = item.Tags
- _, err = sess.Table("annotation").Id(existing.Id).Cols("epoch", "text", "region_id", "updated", "tags").Update(existing)
+ _, err = sess.Table("annotation").ID(existing.Id).Cols("epoch", "text", "region_id", "updated", "tags").Update(existing)
return err
})
}
@@ -211,7 +211,12 @@ func (r *SqlAnnotationRepo) Find(query *annotations.ItemQuery) ([]*annotations.I
)
`, strings.Join(keyValueFilters, " OR "))
- sql.WriteString(fmt.Sprintf(" AND (%s) = %d ", tagsSubQuery, len(tags)))
+ if query.MatchAny {
+ sql.WriteString(fmt.Sprintf(" AND (%s) > 0 ", tagsSubQuery))
+ } else {
+ sql.WriteString(fmt.Sprintf(" AND (%s) = %d ", tagsSubQuery, len(tags)))
+ }
+
}
}
@@ -223,7 +228,7 @@ func (r *SqlAnnotationRepo) Find(query *annotations.ItemQuery) ([]*annotations.I
items := make([]*annotations.ItemDTO, 0)
- if err := x.Sql(sql.String(), params...).Find(&items); err != nil {
+ if err := x.SQL(sql.String(), params...).Find(&items); err != nil {
return nil, err
}
diff --git a/pkg/services/sqlstore/annotation_test.go b/pkg/services/sqlstore/annotation_test.go
index c0d267f2578..d3459527e7d 100644
--- a/pkg/services/sqlstore/annotation_test.go
+++ b/pkg/services/sqlstore/annotation_test.go
@@ -78,7 +78,31 @@ func TestAnnotations(t *testing.T) {
So(err, ShouldBeNil)
So(annotation2.Id, ShouldBeGreaterThan, 0)
- Convey("Can query for annotation", func() {
+ globalAnnotation1 := &annotations.Item{
+ OrgId: 1,
+ UserId: 1,
+ Text: "deploy",
+ Type: "",
+ Epoch: 15,
+ Tags: []string{"deploy"},
+ }
+ err = repo.Save(globalAnnotation1)
+ So(err, ShouldBeNil)
+ So(globalAnnotation1.Id, ShouldBeGreaterThan, 0)
+
+ globalAnnotation2 := &annotations.Item{
+ OrgId: 1,
+ UserId: 1,
+ Text: "rollback",
+ Type: "",
+ Epoch: 17,
+ Tags: []string{"rollback"},
+ }
+ err = repo.Save(globalAnnotation2)
+ So(err, ShouldBeNil)
+ So(globalAnnotation2.Id, ShouldBeGreaterThan, 0)
+
+ Convey("Can query for annotation by dashboard id", func() {
items, err := repo.Find(&annotations.ItemQuery{
OrgId: 1,
DashboardId: 1,
@@ -165,7 +189,7 @@ func TestAnnotations(t *testing.T) {
OrgId: 1,
DashboardId: 1,
From: 1,
- To: 15,
+ To: 15, //this will exclude the second test annotation
Tags: []string{"outage", "error"},
})
@@ -173,6 +197,19 @@ func TestAnnotations(t *testing.T) {
So(items, ShouldHaveLength, 1)
})
+ Convey("Should find two annotations using partial match", func() {
+ items, err := repo.Find(&annotations.ItemQuery{
+ OrgId: 1,
+ From: 1,
+ To: 25,
+ MatchAny: true,
+ Tags: []string{"rollback", "deploy"},
+ })
+
+ So(err, ShouldBeNil)
+ So(items, ShouldHaveLength, 2)
+ })
+
Convey("Should find one when all key value tag filters does match", func() {
items, err := repo.Find(&annotations.ItemQuery{
OrgId: 1,
diff --git a/pkg/services/sqlstore/dashboard.go b/pkg/services/sqlstore/dashboard.go
index aff532bb3b5..e43279208e7 100644
--- a/pkg/services/sqlstore/dashboard.go
+++ b/pkg/services/sqlstore/dashboard.go
@@ -225,7 +225,7 @@ func findDashboards(query *search.FindPersistedDashboardsQuery) ([]DashboardSear
var res []DashboardSearchProjection
sql, params := sb.ToSql()
- err := x.Sql(sql, params...).Find(&res)
+ err := x.SQL(sql, params...).Find(&res)
if err != nil {
return nil, err
}
@@ -295,10 +295,11 @@ func GetDashboardTags(query *m.GetDashboardTagsQuery) error {
FROM dashboard
INNER JOIN dashboard_tag on dashboard_tag.dashboard_id = dashboard.id
WHERE dashboard.org_id=?
- GROUP BY term`
+ GROUP BY term
+ ORDER BY term`
query.Result = make([]*m.DashboardTagCloudItem, 0)
- sess := x.Sql(sql, query.OrgId)
+ sess := x.SQL(sql, query.OrgId)
err := sess.Find(&query.Result)
return err
}
@@ -412,7 +413,7 @@ func GetDashboardPermissionsForUser(query *m.GetDashboardPermissionsForUserQuery
params = append(params, query.UserId)
params = append(params, dialect.BooleanStr(false))
- err := x.Sql(sql, params...).Find(&query.Result)
+ err := x.SQL(sql, params...).Find(&query.Result)
for _, p := range query.Result {
p.PermissionName = p.Permission.String()
@@ -631,7 +632,7 @@ func HasEditPermissionInFolders(query *m.HasEditPermissionInFoldersQuery) error
}
resp := make([]*folderCount, 0)
- if err := x.Sql(builder.GetSqlString(), builder.params...).Find(&resp); err != nil {
+ if err := x.SQL(builder.GetSqlString(), builder.params...).Find(&resp); err != nil {
return err
}
diff --git a/pkg/services/sqlstore/migrations/alert_mig.go b/pkg/services/sqlstore/migrations/alert_mig.go
index 2a364d5f464..e27e64c6124 100644
--- a/pkg/services/sqlstore/migrations/alert_mig.go
+++ b/pkg/services/sqlstore/migrations/alert_mig.go
@@ -65,6 +65,13 @@ func addAlertMigrations(mg *Migrator) {
mg.AddMigration("Add column is_default", NewAddColumnMigration(alert_notification, &Column{
Name: "is_default", Type: DB_Bool, Nullable: false, Default: "0",
}))
+ mg.AddMigration("Add column frequency", NewAddColumnMigration(alert_notification, &Column{
+ Name: "frequency", Type: DB_BigInt, Nullable: true,
+ }))
+ mg.AddMigration("Add column send_reminder", NewAddColumnMigration(alert_notification, &Column{
+ Name: "send_reminder", Type: DB_Bool, Nullable: true, Default: "0",
+ }))
+
mg.AddMigration("add index alert_notification org_id & name", NewAddIndexMigration(alert_notification, alert_notification.Indices[0]))
mg.AddMigration("Update alert table charset", NewTableCharsetMigration("alert", []*Column{
@@ -82,4 +89,22 @@ func addAlertMigrations(mg *Migrator) {
{Name: "type", Type: DB_NVarchar, Length: 255, Nullable: false},
{Name: "settings", Type: DB_Text, Nullable: false},
}))
+
+ notification_journal := Table{
+ Name: "alert_notification_journal",
+ Columns: []*Column{
+ {Name: "id", Type: DB_BigInt, IsPrimaryKey: true, IsAutoIncrement: true},
+ {Name: "org_id", Type: DB_BigInt, Nullable: false},
+ {Name: "alert_id", Type: DB_BigInt, Nullable: false},
+ {Name: "notifier_id", Type: DB_BigInt, Nullable: false},
+ {Name: "sent_at", Type: DB_BigInt, Nullable: false},
+ {Name: "success", Type: DB_Bool, Nullable: false},
+ },
+ Indices: []*Index{
+ {Cols: []string{"org_id", "alert_id", "notifier_id"}, Type: IndexType},
+ },
+ }
+
+ mg.AddMigration("create notification_journal table v1", NewAddTableMigration(notification_journal))
+ mg.AddMigration("add index notification_journal org_id & alert_id & notifier_id", NewAddIndexMigration(notification_journal, notification_journal.Indices[0]))
}
diff --git a/pkg/services/sqlstore/migrations/annotation_mig.go b/pkg/services/sqlstore/migrations/annotation_mig.go
index d231d3283e2..49920dee490 100644
--- a/pkg/services/sqlstore/migrations/annotation_mig.go
+++ b/pkg/services/sqlstore/migrations/annotation_mig.go
@@ -105,7 +105,7 @@ func addAnnotationMig(mg *Migrator) {
}))
//
- // Convert epoch saved as seconds to miliseconds
+ // Convert epoch saved as seconds to milliseconds
//
updateEpochSql := "UPDATE annotation SET epoch = (epoch*1000) where epoch < 9999999999"
mg.AddMigration("Convert existing annotations from seconds to milliseconds", NewRawSqlMigration(updateEpochSql))
diff --git a/pkg/services/sqlstore/migrations/team_mig.go b/pkg/services/sqlstore/migrations/team_mig.go
index 9800d27f8ab..34c46ad13cf 100644
--- a/pkg/services/sqlstore/migrations/team_mig.go
+++ b/pkg/services/sqlstore/migrations/team_mig.go
@@ -51,4 +51,7 @@ func addTeamMigrations(mg *Migrator) {
Name: "email", Type: DB_NVarchar, Nullable: true, Length: 190,
}))
+ mg.AddMigration("Add column external to team_member table", NewAddColumnMigration(teamMemberV1, &Column{
+ Name: "external", Type: DB_Bool, Nullable: true,
+ }))
}
diff --git a/pkg/services/sqlstore/migrations/user_mig.go b/pkg/services/sqlstore/migrations/user_mig.go
index edcfbb7b889..e273cb7d542 100644
--- a/pkg/services/sqlstore/migrations/user_mig.go
+++ b/pkg/services/sqlstore/migrations/user_mig.go
@@ -1,6 +1,12 @@
package migrations
-import . "github.com/grafana/grafana/pkg/services/sqlstore/migrator"
+import (
+ "fmt"
+
+ "github.com/go-xorm/xorm"
+ . "github.com/grafana/grafana/pkg/services/sqlstore/migrator"
+ "github.com/grafana/grafana/pkg/util"
+)
func addUserMigrations(mg *Migrator) {
userV1 := Table{
@@ -107,4 +113,37 @@ func addUserMigrations(mg *Migrator) {
mg.AddMigration("Add last_seen_at column to user", NewAddColumnMigration(userV2, &Column{
Name: "last_seen_at", Type: DB_DateTime, Nullable: true,
}))
+
+ // Adds salt & rands for old users who used ldap or oauth
+ mg.AddMigration("Add missing user data", &AddMissingUserSaltAndRandsMigration{})
+}
+
+type AddMissingUserSaltAndRandsMigration struct {
+ MigrationBase
+}
+
+func (m *AddMissingUserSaltAndRandsMigration) Sql(dialect Dialect) string {
+ return "code migration"
+}
+
+type TempUserDTO struct {
+ Id int64
+ Login string
+}
+
+func (m *AddMissingUserSaltAndRandsMigration) Exec(sess *xorm.Session, mg *Migrator) error {
+ users := make([]*TempUserDTO, 0)
+
+ err := sess.SQL(fmt.Sprintf("SELECT id, login from %s WHERE rands = ''", mg.Dialect.Quote("user"))).Find(&users)
+ if err != nil {
+ return err
+ }
+
+ for _, user := range users {
+ _, err := sess.Exec("UPDATE "+mg.Dialect.Quote("user")+" SET salt = ?, rands = ? WHERE id = ?", util.GetRandomString(10), util.GetRandomString(10), user.Id)
+ if err != nil {
+ return err
+ }
+ }
+ return nil
}
diff --git a/pkg/services/sqlstore/migrator/migrator.go b/pkg/services/sqlstore/migrator/migrator.go
index 9bdaaf7cc14..dead6f2b416 100644
--- a/pkg/services/sqlstore/migrator/migrator.go
+++ b/pkg/services/sqlstore/migrator/migrator.go
@@ -12,7 +12,7 @@ import (
type Migrator struct {
x *xorm.Engine
- dialect Dialect
+ Dialect Dialect
migrations []Migration
Logger log.Logger
}
@@ -31,7 +31,7 @@ func NewMigrator(engine *xorm.Engine) *Migrator {
mg.x = engine
mg.Logger = log.New("migrator")
mg.migrations = make([]Migration, 0)
- mg.dialect = NewDialect(mg.x)
+ mg.Dialect = NewDialect(mg.x)
return mg
}
@@ -86,7 +86,7 @@ func (mg *Migrator) Start() error {
continue
}
- sql := m.Sql(mg.dialect)
+ sql := m.Sql(mg.Dialect)
record := MigrationLog{
MigrationId: m.Id(),
@@ -122,7 +122,7 @@ func (mg *Migrator) exec(m Migration, sess *xorm.Session) error {
condition := m.GetCondition()
if condition != nil {
- sql, args := condition.Sql(mg.dialect)
+ sql, args := condition.Sql(mg.Dialect)
results, err := sess.SQL(sql).Query(args...)
if err != nil || len(results) == 0 {
mg.Logger.Debug("Skipping migration condition not fulfilled", "id", m.Id())
@@ -130,7 +130,13 @@ func (mg *Migrator) exec(m Migration, sess *xorm.Session) error {
}
}
- _, err := sess.Exec(m.Sql(mg.dialect))
+ var err error
+ if codeMigration, ok := m.(CodeMigration); ok {
+ err = codeMigration.Exec(sess, mg)
+ } else {
+ _, err = sess.Exec(m.Sql(mg.Dialect))
+ }
+
if err != nil {
mg.Logger.Error("Executing migration failed", "id", m.Id(), "error", err)
return err
diff --git a/pkg/services/sqlstore/migrator/types.go b/pkg/services/sqlstore/migrator/types.go
index 26c46889daf..48354998d8d 100644
--- a/pkg/services/sqlstore/migrator/types.go
+++ b/pkg/services/sqlstore/migrator/types.go
@@ -3,6 +3,8 @@ package migrator
import (
"fmt"
"strings"
+
+ "github.com/go-xorm/xorm"
)
const (
@@ -19,6 +21,11 @@ type Migration interface {
GetCondition() MigrationCondition
}
+type CodeMigration interface {
+ Migration
+ Exec(sess *xorm.Session, migrator *Migrator) error
+}
+
type SQLType string
type ColumnType string
diff --git a/pkg/services/sqlstore/org.go b/pkg/services/sqlstore/org.go
index 8931f1cf0f5..e36a80322d8 100644
--- a/pkg/services/sqlstore/org.go
+++ b/pkg/services/sqlstore/org.go
@@ -133,7 +133,7 @@ func UpdateOrg(cmd *m.UpdateOrgCommand) error {
Updated: time.Now(),
}
- affectedRows, err := sess.Id(cmd.OrgId).Update(&org)
+ affectedRows, err := sess.ID(cmd.OrgId).Update(&org)
if err != nil {
return err
@@ -166,7 +166,7 @@ func UpdateOrgAddress(cmd *m.UpdateOrgAddressCommand) error {
Updated: time.Now(),
}
- if _, err := sess.Id(cmd.OrgId).Update(&org); err != nil {
+ if _, err := sess.ID(cmd.OrgId).Update(&org); err != nil {
return err
}
diff --git a/pkg/services/sqlstore/org_users.go b/pkg/services/sqlstore/org_users.go
index aad72cdacb4..14981cfde64 100644
--- a/pkg/services/sqlstore/org_users.go
+++ b/pkg/services/sqlstore/org_users.go
@@ -21,7 +21,7 @@ func AddOrgUser(cmd *m.AddOrgUserCommand) error {
return inTransaction(func(sess *DBSession) error {
// check if user exists
var user m.User
- if exists, err := sess.Id(cmd.UserId).Get(&user); err != nil {
+ if exists, err := sess.ID(cmd.UserId).Get(&user); err != nil {
return err
} else if !exists {
return m.ErrUserNotFound
@@ -85,7 +85,7 @@ func UpdateOrgUser(cmd *m.UpdateOrgUserCommand) error {
orgUser.Role = cmd.Role
orgUser.Updated = time.Now()
- _, err = sess.Id(orgUser.Id).Update(&orgUser)
+ _, err = sess.ID(orgUser.Id).Update(&orgUser)
if err != nil {
return err
}
@@ -138,7 +138,7 @@ func RemoveOrgUser(cmd *m.RemoveOrgUserCommand) error {
return inTransaction(func(sess *DBSession) error {
// check if user exists
var user m.User
- if exists, err := sess.Id(cmd.UserId).Get(&user); err != nil {
+ if exists, err := sess.ID(cmd.UserId).Get(&user); err != nil {
return err
} else if !exists {
return m.ErrUserNotFound
diff --git a/pkg/services/sqlstore/plugin_setting.go b/pkg/services/sqlstore/plugin_setting.go
index 676d26fad56..8fbf1b6be1c 100644
--- a/pkg/services/sqlstore/plugin_setting.go
+++ b/pkg/services/sqlstore/plugin_setting.go
@@ -26,7 +26,7 @@ func GetPluginSettings(query *m.GetPluginSettingsQuery) error {
params = append(params, query.OrgId)
}
- sess := x.Sql(sql, params...)
+ sess := x.SQL(sql, params...)
query.Result = make([]*m.PluginSettingInfoDTO, 0)
return sess.Find(&query.Result)
}
@@ -100,7 +100,7 @@ func UpdatePluginSetting(cmd *m.UpdatePluginSettingCmd) error {
pluginSetting.Pinned = cmd.Pinned
pluginSetting.PluginVersion = cmd.PluginVersion
- _, err = sess.Id(pluginSetting.Id).Update(&pluginSetting)
+ _, err = sess.ID(pluginSetting.Id).Update(&pluginSetting)
return err
})
}
diff --git a/pkg/services/sqlstore/preferences.go b/pkg/services/sqlstore/preferences.go
index 885837764fc..04e787971d9 100644
--- a/pkg/services/sqlstore/preferences.go
+++ b/pkg/services/sqlstore/preferences.go
@@ -94,7 +94,7 @@ func SavePreferences(cmd *m.SavePreferencesCommand) error {
prefs.Theme = cmd.Theme
prefs.Updated = time.Now()
prefs.Version += 1
- _, err = sess.Id(prefs.Id).AllCols().Update(&prefs)
+ _, err = sess.ID(prefs.Id).AllCols().Update(&prefs)
return err
})
}
diff --git a/pkg/services/sqlstore/quota.go b/pkg/services/sqlstore/quota.go
index 539555ddc50..7005b341268 100644
--- a/pkg/services/sqlstore/quota.go
+++ b/pkg/services/sqlstore/quota.go
@@ -38,7 +38,7 @@ func GetOrgQuotaByTarget(query *m.GetOrgQuotaByTargetQuery) error {
//get quota used.
rawSql := fmt.Sprintf("SELECT COUNT(*) as count from %s where org_id=?", dialect.Quote(query.Target))
resp := make([]*targetCount, 0)
- if err := x.Sql(rawSql, query.OrgId).Find(&resp); err != nil {
+ if err := x.SQL(rawSql, query.OrgId).Find(&resp); err != nil {
return err
}
@@ -81,7 +81,7 @@ func GetOrgQuotas(query *m.GetOrgQuotasQuery) error {
//get quota used.
rawSql := fmt.Sprintf("SELECT COUNT(*) as count from %s where org_id=?", dialect.Quote(q.Target))
resp := make([]*targetCount, 0)
- if err := x.Sql(rawSql, q.OrgId).Find(&resp); err != nil {
+ if err := x.SQL(rawSql, q.OrgId).Find(&resp); err != nil {
return err
}
result[i] = &m.OrgQuotaDTO{
@@ -116,7 +116,7 @@ func UpdateOrgQuota(cmd *m.UpdateOrgQuotaCmd) error {
}
} else {
//update existing quota entry in the DB.
- if _, err := sess.Id(quota.Id).Update("a); err != nil {
+ if _, err := sess.ID(quota.Id).Update("a); err != nil {
return err
}
}
@@ -140,7 +140,7 @@ func GetUserQuotaByTarget(query *m.GetUserQuotaByTargetQuery) error {
//get quota used.
rawSql := fmt.Sprintf("SELECT COUNT(*) as count from %s where user_id=?", dialect.Quote(query.Target))
resp := make([]*targetCount, 0)
- if err := x.Sql(rawSql, query.UserId).Find(&resp); err != nil {
+ if err := x.SQL(rawSql, query.UserId).Find(&resp); err != nil {
return err
}
@@ -183,7 +183,7 @@ func GetUserQuotas(query *m.GetUserQuotasQuery) error {
//get quota used.
rawSql := fmt.Sprintf("SELECT COUNT(*) as count from %s where user_id=?", dialect.Quote(q.Target))
resp := make([]*targetCount, 0)
- if err := x.Sql(rawSql, q.UserId).Find(&resp); err != nil {
+ if err := x.SQL(rawSql, q.UserId).Find(&resp); err != nil {
return err
}
result[i] = &m.UserQuotaDTO{
@@ -218,7 +218,7 @@ func UpdateUserQuota(cmd *m.UpdateUserQuotaCmd) error {
}
} else {
//update existing quota entry in the DB.
- if _, err := sess.Id(quota.Id).Update("a); err != nil {
+ if _, err := sess.ID(quota.Id).Update("a); err != nil {
return err
}
}
@@ -231,7 +231,7 @@ func GetGlobalQuotaByTarget(query *m.GetGlobalQuotaByTargetQuery) error {
//get quota used.
rawSql := fmt.Sprintf("SELECT COUNT(*) as count from %s", dialect.Quote(query.Target))
resp := make([]*targetCount, 0)
- if err := x.Sql(rawSql).Find(&resp); err != nil {
+ if err := x.SQL(rawSql).Find(&resp); err != nil {
return err
}
diff --git a/pkg/services/sqlstore/sqlstore.go b/pkg/services/sqlstore/sqlstore.go
index 13d706b6198..5477bc7b2d1 100644
--- a/pkg/services/sqlstore/sqlstore.go
+++ b/pkg/services/sqlstore/sqlstore.go
@@ -106,7 +106,7 @@ func (ss *SqlStore) inTransactionWithRetryCtx(ctx context.Context, callback dbTr
if len(sess.events) > 0 {
for _, e := range sess.events {
if err = bus.Publish(e); err != nil {
- log.Error(3, "Failed to publish event after commit", err)
+ log.Error(3, "Failed to publish event after commit. error: %v", err)
}
}
}
diff --git a/pkg/services/sqlstore/stats.go b/pkg/services/sqlstore/stats.go
index 6db481bf06b..2cec86e7239 100644
--- a/pkg/services/sqlstore/stats.go
+++ b/pkg/services/sqlstore/stats.go
@@ -13,11 +13,19 @@ func init() {
bus.AddHandler("sql", GetDataSourceStats)
bus.AddHandler("sql", GetDataSourceAccessStats)
bus.AddHandler("sql", GetAdminStats)
+ bus.AddHandlerCtx("sql", GetAlertNotifiersUsageStats)
bus.AddHandlerCtx("sql", GetSystemUserCountStats)
}
var activeUserTimeLimit = time.Hour * 24 * 30
+func GetAlertNotifiersUsageStats(ctx context.Context, query *m.GetAlertNotifierUsageStatsQuery) error {
+ var rawSql = `SELECT COUNT(*) as count, type FROM alert_notification GROUP BY type`
+ query.Result = make([]*m.NotifierUsageStats, 0)
+ err := x.SQL(rawSql).Find(&query.Result)
+ return err
+}
+
func GetDataSourceStats(query *m.GetDataSourceStatsQuery) error {
var rawSql = `SELECT COUNT(*) as count, type FROM data_source GROUP BY type`
query.Result = make([]*m.DataSourceStats, 0)
diff --git a/pkg/services/sqlstore/stats_test.go b/pkg/services/sqlstore/stats_test.go
index dae24952d17..6949a0dbda2 100644
--- a/pkg/services/sqlstore/stats_test.go
+++ b/pkg/services/sqlstore/stats_test.go
@@ -36,5 +36,11 @@ func TestStatsDataAccess(t *testing.T) {
err := GetDataSourceAccessStats(&query)
So(err, ShouldBeNil)
})
+
+ Convey("Get alert notifier stats should not results in error", func() {
+ query := m.GetAlertNotifierUsageStatsQuery{}
+ err := GetAlertNotifiersUsageStats(context.Background(), &query)
+ So(err, ShouldBeNil)
+ })
})
}
diff --git a/pkg/services/sqlstore/team.go b/pkg/services/sqlstore/team.go
index 72955df9a6a..a3010a086e5 100644
--- a/pkg/services/sqlstore/team.go
+++ b/pkg/services/sqlstore/team.go
@@ -74,7 +74,7 @@ func UpdateTeam(cmd *m.UpdateTeamCommand) error {
sess.MustCols("email")
- affectedRows, err := sess.Id(cmd.Id).Update(&team)
+ affectedRows, err := sess.ID(cmd.Id).Update(&team)
if err != nil {
return err
@@ -169,7 +169,7 @@ func SearchTeams(query *m.SearchTeamsQuery) error {
sql.WriteString(dialect.LimitOffset(int64(query.Limit), int64(offset)))
}
- if err := x.Sql(sql.String(), params...).Find(&query.Result.Teams); err != nil {
+ if err := x.SQL(sql.String(), params...).Find(&query.Result.Teams); err != nil {
return err
}
@@ -196,7 +196,7 @@ func GetTeamById(query *m.GetTeamByIdQuery) error {
sql.WriteString(` WHERE team.org_id = ? and team.id = ?`)
var team m.TeamDTO
- exists, err := x.Sql(sql.String(), query.OrgId, query.Id).Get(&team)
+ exists, err := x.SQL(sql.String(), query.OrgId, query.Id).Get(&team)
if err != nil {
return err
@@ -220,7 +220,7 @@ func GetTeamsByUser(query *m.GetTeamsByUserQuery) error {
sql.WriteString(` INNER JOIN team_member on team.id = team_member.team_id`)
sql.WriteString(` WHERE team.org_id = ? and team_member.user_id = ?`)
- err := x.Sql(sql.String(), query.OrgId, query.UserId).Find(&query.Result)
+ err := x.SQL(sql.String(), query.OrgId, query.UserId).Find(&query.Result)
return err
}
@@ -240,11 +240,12 @@ func AddTeamMember(cmd *m.AddTeamMemberCommand) error {
}
entity := m.TeamMember{
- OrgId: cmd.OrgId,
- TeamId: cmd.TeamId,
- UserId: cmd.UserId,
- Created: time.Now(),
- Updated: time.Now(),
+ OrgId: cmd.OrgId,
+ TeamId: cmd.TeamId,
+ UserId: cmd.UserId,
+ External: cmd.External,
+ Created: time.Now(),
+ Updated: time.Now(),
}
_, err := sess.Insert(&entity)
@@ -289,7 +290,10 @@ func GetTeamMembers(query *m.GetTeamMembersQuery) error {
if query.UserId != 0 {
sess.Where("team_member.user_id=?", query.UserId)
}
- sess.Cols("user.org_id", "team_member.team_id", "team_member.user_id", "user.email", "user.login")
+ if query.External {
+ sess.Where("team_member.external=?", dialect.BooleanStr(true))
+ }
+ sess.Cols("team_member.org_id", "team_member.team_id", "team_member.user_id", "user.email", "user.login", "team_member.external")
sess.Asc("user.login", "user.email")
err := sess.Find(&query.Result)
diff --git a/pkg/services/sqlstore/team_test.go b/pkg/services/sqlstore/team_test.go
index abaa973957d..8f243617262 100644
--- a/pkg/services/sqlstore/team_test.go
+++ b/pkg/services/sqlstore/team_test.go
@@ -50,13 +50,29 @@ func TestTeamCommandsAndQueries(t *testing.T) {
err = AddTeamMember(&m.AddTeamMemberCommand{OrgId: testOrgId, TeamId: team1.Id, UserId: userIds[0]})
So(err, ShouldBeNil)
+ err = AddTeamMember(&m.AddTeamMemberCommand{OrgId: testOrgId, TeamId: team1.Id, UserId: userIds[1], External: true})
+ So(err, ShouldBeNil)
q1 := &m.GetTeamMembersQuery{OrgId: testOrgId, TeamId: team1.Id}
err = GetTeamMembers(q1)
So(err, ShouldBeNil)
+ So(q1.Result, ShouldHaveLength, 2)
So(q1.Result[0].TeamId, ShouldEqual, team1.Id)
So(q1.Result[0].Login, ShouldEqual, "loginuser0")
So(q1.Result[0].OrgId, ShouldEqual, testOrgId)
+ So(q1.Result[1].TeamId, ShouldEqual, team1.Id)
+ So(q1.Result[1].Login, ShouldEqual, "loginuser1")
+ So(q1.Result[1].OrgId, ShouldEqual, testOrgId)
+ So(q1.Result[1].External, ShouldEqual, true)
+
+ q2 := &m.GetTeamMembersQuery{OrgId: testOrgId, TeamId: team1.Id, External: true}
+ err = GetTeamMembers(q2)
+ So(err, ShouldBeNil)
+ So(q2.Result, ShouldHaveLength, 1)
+ So(q2.Result[0].TeamId, ShouldEqual, team1.Id)
+ So(q2.Result[0].Login, ShouldEqual, "loginuser1")
+ So(q2.Result[0].OrgId, ShouldEqual, testOrgId)
+ So(q2.Result[0].External, ShouldEqual, true)
})
Convey("Should be able to search for teams", func() {
diff --git a/pkg/services/sqlstore/temp_user.go b/pkg/services/sqlstore/temp_user.go
index e93ba2fd641..f13752f8038 100644
--- a/pkg/services/sqlstore/temp_user.go
+++ b/pkg/services/sqlstore/temp_user.go
@@ -96,7 +96,7 @@ func GetTempUsersQuery(query *m.GetTempUsersQuery) error {
rawSql += " ORDER BY tu.created desc"
query.Result = make([]*m.TempUserDTO, 0)
- sess := x.Sql(rawSql, params...)
+ sess := x.SQL(rawSql, params...)
err := sess.Find(&query.Result)
return err
}
@@ -121,7 +121,7 @@ func GetTempUserByCode(query *m.GetTempUserByCodeQuery) error {
WHERE tu.code=?`
var tempUser m.TempUserDTO
- sess := x.Sql(rawSql, query.Code)
+ sess := x.SQL(rawSql, query.Code)
has, err := sess.Get(&tempUser)
if err != nil {
diff --git a/pkg/services/sqlstore/transactions.go b/pkg/services/sqlstore/transactions.go
index eccd37f9a43..edf29fffb8f 100644
--- a/pkg/services/sqlstore/transactions.go
+++ b/pkg/services/sqlstore/transactions.go
@@ -89,7 +89,7 @@ func inTransactionWithRetryCtx(ctx context.Context, callback dbTransactionFunc,
if len(sess.events) > 0 {
for _, e := range sess.events {
if err = bus.Publish(e); err != nil {
- log.Error(3, "Failed to publish event after commit", err)
+ log.Error(3, "Failed to publish event after commit. error: %v", err)
}
}
}
diff --git a/pkg/services/sqlstore/transactions_test.go b/pkg/services/sqlstore/transactions_test.go
index 937649921ba..41dedde5db4 100644
--- a/pkg/services/sqlstore/transactions_test.go
+++ b/pkg/services/sqlstore/transactions_test.go
@@ -39,7 +39,7 @@ func TestTransaction(t *testing.T) {
So(err, ShouldEqual, models.ErrInvalidApiKey)
})
- Convey("wont update if one handler fails", func() {
+ Convey("won't update if one handler fails", func() {
err := ss.InTransaction(context.Background(), func(ctx context.Context) error {
err := DeleteApiKeyCtx(ctx, deleteApiKeyCmd)
if err != nil {
diff --git a/pkg/services/sqlstore/user.go b/pkg/services/sqlstore/user.go
index 0ec1a947870..848a11d81ab 100644
--- a/pkg/services/sqlstore/user.go
+++ b/pkg/services/sqlstore/user.go
@@ -113,9 +113,10 @@ func CreateUser(ctx context.Context, cmd *m.CreateUserCommand) error {
LastSeenAt: time.Now().AddDate(-10, 0, 0),
}
+ user.Salt = util.GetRandomString(10)
+ user.Rands = util.GetRandomString(10)
+
if len(cmd.Password) > 0 {
- user.Salt = util.GetRandomString(10)
- user.Rands = util.GetRandomString(10)
user.Password = util.EncodePassword(cmd.Password, user.Salt)
}
@@ -239,7 +240,7 @@ func UpdateUser(cmd *m.UpdateUserCommand) error {
Updated: time.Now(),
}
- if _, err := sess.Id(cmd.UserId).Update(&user); err != nil {
+ if _, err := sess.ID(cmd.UserId).Update(&user); err != nil {
return err
}
@@ -263,22 +264,19 @@ func ChangeUserPassword(cmd *m.ChangeUserPasswordCommand) error {
Updated: time.Now(),
}
- _, err := sess.Id(cmd.UserId).Update(&user)
+ _, err := sess.ID(cmd.UserId).Update(&user)
return err
})
}
func UpdateUserLastSeenAt(cmd *m.UpdateUserLastSeenAtCommand) error {
return inTransaction(func(sess *DBSession) error {
- if cmd.UserId <= 0 {
- }
-
user := m.User{
Id: cmd.UserId,
LastSeenAt: time.Now(),
}
- _, err := sess.Id(cmd.UserId).Update(&user)
+ _, err := sess.ID(cmd.UserId).Update(&user)
return err
})
}
@@ -309,7 +307,7 @@ func setUsingOrgInTransaction(sess *DBSession, userID int64, orgID int64) error
OrgId: orgID,
}
- _, err := sess.Id(userID).Update(&user)
+ _, err := sess.ID(userID).Update(&user)
return err
}
@@ -371,11 +369,11 @@ func GetSignedInUser(query *m.GetSignedInUserQuery) error {
sess := x.Table("user")
if query.UserId > 0 {
- sess.Sql(rawSql+"WHERE u.id=?", query.UserId)
+ sess.SQL(rawSql+"WHERE u.id=?", query.UserId)
} else if query.Login != "" {
- sess.Sql(rawSql+"WHERE u.login=?", query.Login)
+ sess.SQL(rawSql+"WHERE u.login=?", query.Login)
} else if query.Email != "" {
- sess.Sql(rawSql+"WHERE u.email=?", query.Email)
+ sess.SQL(rawSql+"WHERE u.email=?", query.Email)
}
var user m.SignedInUser
@@ -471,11 +469,11 @@ func DeleteUser(cmd *m.DeleteUserCommand) error {
func UpdateUserPermissions(cmd *m.UpdateUserPermissionsCommand) error {
return inTransaction(func(sess *DBSession) error {
user := m.User{}
- sess.Id(cmd.UserId).Get(&user)
+ sess.ID(cmd.UserId).Get(&user)
user.IsAdmin = cmd.IsGrafanaAdmin
sess.UseBool("is_admin")
- _, err := sess.Id(user.Id).Update(&user)
+ _, err := sess.ID(user.Id).Update(&user)
return err
})
}
@@ -489,7 +487,7 @@ func SetUserHelpFlag(cmd *m.SetUserHelpFlagCommand) error {
Updated: time.Now(),
}
- _, err := sess.Id(cmd.UserId).Cols("help_flags1").Update(&user)
+ _, err := sess.ID(cmd.UserId).Cols("help_flags1").Update(&user)
return err
})
}
diff --git a/pkg/services/sqlstore/user_test.go b/pkg/services/sqlstore/user_test.go
index a76ae860b7d..b26dd235772 100644
--- a/pkg/services/sqlstore/user_test.go
+++ b/pkg/services/sqlstore/user_test.go
@@ -15,6 +15,28 @@ func TestUserDataAccess(t *testing.T) {
Convey("Testing DB", t, func() {
InitTestDB(t)
+ Convey("Creating a user", func() {
+ cmd := &m.CreateUserCommand{
+ Email: "usertest@test.com",
+ Name: "user name",
+ Login: "user_test_login",
+ }
+
+ err := CreateUser(context.Background(), cmd)
+ So(err, ShouldBeNil)
+
+ Convey("Loading a user", func() {
+ query := m.GetUserByIdQuery{Id: cmd.Result.Id}
+ err := GetUserById(&query)
+ So(err, ShouldBeNil)
+
+ So(query.Result.Email, ShouldEqual, "usertest@test.com")
+ So(query.Result.Password, ShouldEqual, "")
+ So(query.Result.Rands, ShouldHaveLength, 10)
+ So(query.Result.Salt, ShouldHaveLength, 10)
+ })
+ })
+
Convey("Given 5 users", func() {
var err error
var cmd *m.CreateUserCommand
diff --git a/pkg/setting/setting.go b/pkg/setting/setting.go
index eb61568261d..1a253b9b238 100644
--- a/pkg/setting/setting.go
+++ b/pkg/setting/setting.go
@@ -164,8 +164,10 @@ var (
Quota QuotaSettings
// Alerting
- AlertingEnabled bool
- ExecuteAlerts bool
+ AlertingEnabled bool
+ ExecuteAlerts bool
+ AlertingErrorOrTimeout string
+ AlertingNoDataOrNullValues string
// Explore UI
ExploreEnabled bool
@@ -197,9 +199,12 @@ type Cfg struct {
ImagesDir string
PhantomDir string
RendererUrl string
+ RendererCallbackUrl string
DisableBruteForceLoginProtection bool
TempDataLifetime time.Duration
+
+ MetricsEndpointEnabled bool
}
type CommandLineArgs struct {
@@ -324,7 +329,7 @@ func getCommandLineProperties(args []string) map[string]string {
trimmed := strings.TrimPrefix(arg, "cfg:")
parts := strings.Split(trimmed, "=")
if len(parts) != 2 {
- log.Fatal(3, "Invalid command line argument", arg)
+ log.Fatal(3, "Invalid command line argument. argument: %v", arg)
return nil
}
@@ -641,9 +646,22 @@ func (cfg *Cfg) Load(args *CommandLineArgs) error {
// Rendering
renderSec := iniFile.Section("rendering")
cfg.RendererUrl = renderSec.Key("server_url").String()
+ cfg.RendererCallbackUrl = renderSec.Key("callback_url").String()
+ if cfg.RendererCallbackUrl == "" {
+ cfg.RendererCallbackUrl = AppUrl
+ } else {
+ if cfg.RendererCallbackUrl[len(cfg.RendererCallbackUrl)-1] != '/' {
+ cfg.RendererCallbackUrl += "/"
+ }
+ _, err := url.Parse(cfg.RendererCallbackUrl)
+ if err != nil {
+ log.Fatal(4, "Invalid callback_url(%s): %s", cfg.RendererCallbackUrl, err)
+ }
+ }
cfg.ImagesDir = filepath.Join(DataPath, "png")
cfg.PhantomDir = filepath.Join(HomePath, "tools/phantomjs")
cfg.TempDataLifetime = iniFile.Section("paths").Key("temp_data_lifetime").MustDuration(time.Second * 3600 * 24)
+ cfg.MetricsEndpointEnabled = iniFile.Section("metrics").Key("enabled").MustBool(true)
analytics := iniFile.Section("analytics")
ReportingEnabled = analytics.Key("reporting_enabled").MustBool(true)
@@ -659,6 +677,8 @@ func (cfg *Cfg) Load(args *CommandLineArgs) error {
alerting := iniFile.Section("alerting")
AlertingEnabled = alerting.Key("enabled").MustBool(true)
ExecuteAlerts = alerting.Key("execute_alerts").MustBool(true)
+ AlertingErrorOrTimeout = alerting.Key("error_or_timeout").MustString("alerting")
+ AlertingNoDataOrNullValues = alerting.Key("nodata_or_nullvalues").MustString("no_data")
explore := iniFile.Section("explore")
ExploreEnabled = explore.Key("enabled").MustBool(false)
diff --git a/pkg/setting/setting_oauth.go b/pkg/setting/setting_oauth.go
index ee2e812415b..93b1ab6f101 100644
--- a/pkg/setting/setting_oauth.go
+++ b/pkg/setting/setting_oauth.go
@@ -5,6 +5,7 @@ type OAuthInfo struct {
Scopes []string
AuthUrl, TokenUrl string
Enabled bool
+ EmailAttributeName string
AllowedDomains []string
HostedDomain string
ApiUrl string
diff --git a/pkg/setting/setting_test.go b/pkg/setting/setting_test.go
index 9de22c86811..6524073e4da 100644
--- a/pkg/setting/setting_test.go
+++ b/pkg/setting/setting_test.go
@@ -20,6 +20,7 @@ func TestLoadingSettings(t *testing.T) {
So(err, ShouldBeNil)
So(AdminUser, ShouldEqual, "admin")
+ So(cfg.RendererCallbackUrl, ShouldEqual, "http://localhost:3000/")
})
Convey("Should be able to override via environment variables", func() {
@@ -96,7 +97,7 @@ func TestLoadingSettings(t *testing.T) {
Args: []string{
"cfg:default.server.domain=test2",
},
- Config: filepath.Join(HomePath, "tests/config-files/override.ini"),
+ Config: filepath.Join(HomePath, "pkg/setting/testdata/override.ini"),
})
So(Domain, ShouldEqual, "test2")
@@ -107,7 +108,7 @@ func TestLoadingSettings(t *testing.T) {
cfg := NewCfg()
cfg.Load(&CommandLineArgs{
HomePath: "../../",
- Config: filepath.Join(HomePath, "tests/config-files/override_windows.ini"),
+ Config: filepath.Join(HomePath, "pkg/setting/testdata/override_windows.ini"),
Args: []string{`cfg:default.paths.data=c:\tmp\data`},
})
@@ -116,7 +117,7 @@ func TestLoadingSettings(t *testing.T) {
cfg := NewCfg()
cfg.Load(&CommandLineArgs{
HomePath: "../../",
- Config: filepath.Join(HomePath, "tests/config-files/override.ini"),
+ Config: filepath.Join(HomePath, "pkg/setting/testdata/override.ini"),
Args: []string{"cfg:default.paths.data=/tmp/data"},
})
@@ -129,7 +130,7 @@ func TestLoadingSettings(t *testing.T) {
cfg := NewCfg()
cfg.Load(&CommandLineArgs{
HomePath: "../../",
- Config: filepath.Join(HomePath, "tests/config-files/override_windows.ini"),
+ Config: filepath.Join(HomePath, "pkg/setting/testdata/override_windows.ini"),
Args: []string{`cfg:paths.data=c:\tmp\data`},
})
@@ -138,7 +139,7 @@ func TestLoadingSettings(t *testing.T) {
cfg := NewCfg()
cfg.Load(&CommandLineArgs{
HomePath: "../../",
- Config: filepath.Join(HomePath, "tests/config-files/override.ini"),
+ Config: filepath.Join(HomePath, "pkg/setting/testdata/override.ini"),
Args: []string{"cfg:paths.data=/tmp/data"},
})
@@ -178,5 +179,15 @@ func TestLoadingSettings(t *testing.T) {
So(InstanceName, ShouldEqual, hostname)
})
+ Convey("Reading callback_url should add trailing slash", func() {
+ cfg := NewCfg()
+ cfg.Load(&CommandLineArgs{
+ HomePath: "../../",
+ Args: []string{"cfg:rendering.callback_url=http://myserver/renderer"},
+ })
+
+ So(cfg.RendererCallbackUrl, ShouldEqual, "http://myserver/renderer/")
+ })
+
})
}
diff --git a/tests/config-files/override.ini b/pkg/setting/testdata/override.ini
similarity index 100%
rename from tests/config-files/override.ini
rename to pkg/setting/testdata/override.ini
diff --git a/tests/config-files/override_windows.ini b/pkg/setting/testdata/override_windows.ini
similarity index 100%
rename from tests/config-files/override_windows.ini
rename to pkg/setting/testdata/override_windows.ini
diff --git a/pkg/social/generic_oauth.go b/pkg/social/generic_oauth.go
index 8c02076096d..a97d58334c7 100644
--- a/pkg/social/generic_oauth.go
+++ b/pkg/social/generic_oauth.go
@@ -20,6 +20,7 @@ type SocialGenericOAuth struct {
allowedOrganizations []string
apiUrl string
allowSignup bool
+ emailAttributeName string
teamIds []int
}
@@ -264,8 +265,9 @@ func (s *SocialGenericOAuth) extractEmail(data *UserInfoJson) string {
return data.Email
}
- if data.Attributes["email:primary"] != nil {
- return data.Attributes["email:primary"][0]
+ emails, ok := data.Attributes[s.emailAttributeName]
+ if ok && len(emails) != 0 {
+ return emails[0]
}
if data.Upn != "" {
diff --git a/pkg/social/gitlab_oauth.go b/pkg/social/gitlab_oauth.go
new file mode 100644
index 00000000000..21463dabf8f
--- /dev/null
+++ b/pkg/social/gitlab_oauth.go
@@ -0,0 +1,132 @@
+package social
+
+import (
+ "encoding/json"
+ "fmt"
+ "net/http"
+ "regexp"
+
+ "github.com/grafana/grafana/pkg/models"
+
+ "golang.org/x/oauth2"
+)
+
+type SocialGitlab struct {
+ *SocialBase
+ allowedDomains []string
+ allowedGroups []string
+ apiUrl string
+ allowSignup bool
+}
+
+var (
+ ErrMissingGroupMembership = &Error{"User not a member of one of the required groups"}
+)
+
+func (s *SocialGitlab) Type() int {
+ return int(models.GITLAB)
+}
+
+func (s *SocialGitlab) IsEmailAllowed(email string) bool {
+ return isEmailAllowed(email, s.allowedDomains)
+}
+
+func (s *SocialGitlab) IsSignupAllowed() bool {
+ return s.allowSignup
+}
+
+func (s *SocialGitlab) IsGroupMember(client *http.Client) bool {
+ if len(s.allowedGroups) == 0 {
+ return true
+ }
+
+ for groups, url := s.GetGroups(client, s.apiUrl+"/groups"); groups != nil; groups, url = s.GetGroups(client, url) {
+ for _, allowedGroup := range s.allowedGroups {
+ for _, group := range groups {
+ if group == allowedGroup {
+ return true
+ }
+ }
+ }
+ }
+
+ return false
+}
+
+func (s *SocialGitlab) GetGroups(client *http.Client, url string) ([]string, string) {
+ type Group struct {
+ FullPath string `json:"full_path"`
+ }
+
+ var (
+ groups []Group
+ next string
+ )
+
+ if url == "" {
+ return nil, next
+ }
+
+ response, err := HttpGet(client, url)
+ if err != nil {
+ s.log.Error("Error getting groups from GitLab API", "err", err)
+ return nil, next
+ }
+
+ if err := json.Unmarshal(response.Body, &groups); err != nil {
+ s.log.Error("Error parsing JSON from GitLab API", "err", err)
+ return nil, next
+ }
+
+ fullPaths := make([]string, len(groups))
+ for i, group := range groups {
+ fullPaths[i] = group.FullPath
+ }
+
+ if link, ok := response.Headers["Link"]; ok {
+ pattern := regexp.MustCompile(`<([^>]+)>; rel="next"`)
+ if matches := pattern.FindStringSubmatch(link[0]); matches != nil {
+ next = matches[1]
+ }
+ }
+
+ return fullPaths, next
+}
+
+func (s *SocialGitlab) UserInfo(client *http.Client, token *oauth2.Token) (*BasicUserInfo, error) {
+
+ var data struct {
+ Id int
+ Username string
+ Email string
+ Name string
+ State string
+ }
+
+ response, err := HttpGet(client, s.apiUrl+"/user")
+ if err != nil {
+ return nil, fmt.Errorf("Error getting user info: %s", err)
+ }
+
+ err = json.Unmarshal(response.Body, &data)
+ if err != nil {
+ return nil, fmt.Errorf("Error getting user info: %s", err)
+ }
+
+ if data.State != "active" {
+ return nil, fmt.Errorf("User %s is inactive", data.Username)
+ }
+
+ userInfo := &BasicUserInfo{
+ Id: fmt.Sprintf("%d", data.Id),
+ Name: data.Name,
+ Login: data.Username,
+ Email: data.Email,
+ }
+
+ if !s.IsGroupMember(client) {
+ return nil, ErrMissingGroupMembership
+ }
+
+ return userInfo, nil
+}
diff --git a/pkg/social/social.go b/pkg/social/social.go
index adbe5a912d9..721070ab789 100644
--- a/pkg/social/social.go
+++ b/pkg/social/social.go
@@ -49,32 +49,32 @@ func (e *Error) Error() string {
var (
SocialBaseUrl = "/login/"
SocialMap = make(map[string]SocialConnector)
+ allOauthes = []string{"github", "gitlab", "google", "generic_oauth", "grafananet", "grafana_com"}
)
func NewOAuthService() {
setting.OAuthService = &setting.OAuther{}
setting.OAuthService.OAuthInfos = make(map[string]*setting.OAuthInfo)
- allOauthes := []string{"github", "google", "generic_oauth", "grafananet", "grafana_com"}
-
for _, name := range allOauthes {
sec := setting.Raw.Section("auth." + name)
info := &setting.OAuthInfo{
- ClientId: sec.Key("client_id").String(),
- ClientSecret: sec.Key("client_secret").String(),
- Scopes: util.SplitString(sec.Key("scopes").String()),
- AuthUrl: sec.Key("auth_url").String(),
- TokenUrl: sec.Key("token_url").String(),
- ApiUrl: sec.Key("api_url").String(),
- Enabled: sec.Key("enabled").MustBool(),
- AllowedDomains: util.SplitString(sec.Key("allowed_domains").String()),
- HostedDomain: sec.Key("hosted_domain").String(),
- AllowSignup: sec.Key("allow_sign_up").MustBool(),
- Name: sec.Key("name").MustString(name),
- TlsClientCert: sec.Key("tls_client_cert").String(),
- TlsClientKey: sec.Key("tls_client_key").String(),
- TlsClientCa: sec.Key("tls_client_ca").String(),
- TlsSkipVerify: sec.Key("tls_skip_verify_insecure").MustBool(),
+ ClientId: sec.Key("client_id").String(),
+ ClientSecret: sec.Key("client_secret").String(),
+ Scopes: util.SplitString(sec.Key("scopes").String()),
+ AuthUrl: sec.Key("auth_url").String(),
+ TokenUrl: sec.Key("token_url").String(),
+ ApiUrl: sec.Key("api_url").String(),
+ Enabled: sec.Key("enabled").MustBool(),
+ EmailAttributeName: sec.Key("email_attribute_name").String(),
+ AllowedDomains: util.SplitString(sec.Key("allowed_domains").String()),
+ HostedDomain: sec.Key("hosted_domain").String(),
+ AllowSignup: sec.Key("allow_sign_up").MustBool(),
+ Name: sec.Key("name").MustString(name),
+ TlsClientCert: sec.Key("tls_client_cert").String(),
+ TlsClientKey: sec.Key("tls_client_key").String(),
+ TlsClientCa: sec.Key("tls_client_ca").String(),
+ TlsSkipVerify: sec.Key("tls_skip_verify_insecure").MustBool(),
}
if !info.Enabled {
@@ -115,6 +115,20 @@ func NewOAuthService() {
}
}
+ // GitLab.
+ if name == "gitlab" {
+ SocialMap["gitlab"] = &SocialGitlab{
+ SocialBase: &SocialBase{
+ Config: &config,
+ log: logger,
+ },
+ allowedDomains: info.AllowedDomains,
+ apiUrl: info.ApiUrl,
+ allowSignup: info.AllowSignup,
+ allowedGroups: util.SplitString(sec.Key("allowed_groups").String()),
+ }
+ }
+
// Google.
if name == "google" {
SocialMap["google"] = &SocialGoogle{
@@ -139,6 +153,7 @@ func NewOAuthService() {
allowedDomains: info.AllowedDomains,
apiUrl: info.ApiUrl,
allowSignup: info.AllowSignup,
+ emailAttributeName: info.EmailAttributeName,
teamIds: sec.Key("team_ids").Ints(","),
allowedOrganizations: util.SplitString(sec.Key("allowed_organizations").String()),
}
@@ -168,3 +183,26 @@ func NewOAuthService() {
}
}
}
+
+// GetOAuthProviders returns available oauth providers and if they're enabled or not
+var GetOAuthProviders = func(cfg *setting.Cfg) map[string]bool {
+ result := map[string]bool{}
+
+ if cfg == nil || cfg.Raw == nil {
+ return result
+ }
+
+ for _, name := range allOauthes {
+ if name == "grafananet" {
+ name = "grafana_com"
+ }
+
+ sec := cfg.Raw.Section("auth." + name)
+ if sec == nil {
+ continue
+ }
+ result[name] = sec.Key("enabled").MustBool()
+ }
+
+ return result
+}
diff --git a/pkg/tracing/tracing.go b/pkg/tracing/tracing.go
index 61f45af3635..fd7258b7a0a 100644
--- a/pkg/tracing/tracing.go
+++ b/pkg/tracing/tracing.go
@@ -58,7 +58,8 @@ func (ts *TracingService) parseSettings() {
func (ts *TracingService) initGlobalTracer() error {
cfg := jaegercfg.Configuration{
- Disabled: !ts.enabled,
+ ServiceName: "grafana",
+ Disabled: !ts.enabled,
Sampler: &jaegercfg.SamplerConfig{
Type: ts.samplerType,
Param: ts.samplerParam,
@@ -78,7 +79,7 @@ func (ts *TracingService) initGlobalTracer() error {
options = append(options, jaegercfg.Tag(tag, value))
}
- tracer, closer, err := cfg.New("grafana", options...)
+ tracer, closer, err := cfg.NewTracer(options...)
if err != nil {
return err
}
diff --git a/pkg/tsdb/cloudwatch/cloudwatch.go b/pkg/tsdb/cloudwatch/cloudwatch.go
index 92352a51315..be14c6f96ec 100644
--- a/pkg/tsdb/cloudwatch/cloudwatch.go
+++ b/pkg/tsdb/cloudwatch/cloudwatch.go
@@ -196,7 +196,7 @@ func (e *CloudWatchExecutor) executeQuery(ctx context.Context, query *CloudWatch
params.ExtendedStatistics = query.ExtendedStatistics
}
- // 1 minutes resolutin metrics is stored for 15 days, 15 * 24 * 60 = 21600
+ // 1 minutes resolution metrics is stored for 15 days, 15 * 24 * 60 = 21600
if query.HighResolution && (((endTime.Unix() - startTime.Unix()) / int64(query.Period)) > 21600) {
return nil, errors.New("too long query period")
}
@@ -267,7 +267,7 @@ func (e *CloudWatchExecutor) executeGetMetricDataQuery(ctx context.Context, regi
ScanBy: aws.String("TimestampAscending"),
}
for _, query := range queries {
- // 1 minutes resolutin metrics is stored for 15 days, 15 * 24 * 60 = 21600
+ // 1 minutes resolution metrics is stored for 15 days, 15 * 24 * 60 = 21600
if query.HighResolution && (((endTime.Unix() - startTime.Unix()) / int64(query.Period)) > 21600) {
return nil, errors.New("too long query period")
}
diff --git a/pkg/tsdb/cloudwatch/metric_find_query.go b/pkg/tsdb/cloudwatch/metric_find_query.go
index 136ee241c2e..e1e131d9f3a 100644
--- a/pkg/tsdb/cloudwatch/metric_find_query.go
+++ b/pkg/tsdb/cloudwatch/metric_find_query.go
@@ -46,6 +46,7 @@ func init() {
"AWS/CloudFront": {"Requests", "BytesDownloaded", "BytesUploaded", "TotalErrorRate", "4xxErrorRate", "5xxErrorRate"},
"AWS/CloudSearch": {"SuccessfulRequests", "SearchableDocuments", "IndexUtilization", "Partitions"},
"AWS/DMS": {"FreeableMemory", "WriteIOPS", "ReadIOPS", "WriteThroughput", "ReadThroughput", "WriteLatency", "ReadLatency", "SwapUsage", "NetworkTransmitThroughput", "NetworkReceiveThroughput", "FullLoadThroughputBandwidthSource", "FullLoadThroughputBandwidthTarget", "FullLoadThroughputRowsSource", "FullLoadThroughputRowsTarget", "CDCIncomingChanges", "CDCChangesMemorySource", "CDCChangesMemoryTarget", "CDCChangesDiskSource", "CDCChangesDiskTarget", "CDCThroughputBandwidthTarget", "CDCThroughputRowsSource", "CDCThroughputRowsTarget", "CDCLatencySource", "CDCLatencyTarget"},
+ "AWS/DX": {"ConnectionState", "ConnectionBpsEgress", "ConnectionBpsIngress", "ConnectionPpsEgress", "ConnectionPpsIngress", "ConnectionCRCErrorCount", "ConnectionLightLevelTx", "ConnectionLightLevelRx"},
"AWS/DynamoDB": {"ConditionalCheckFailedRequests", "ConsumedReadCapacityUnits", "ConsumedWriteCapacityUnits", "OnlineIndexConsumedWriteCapacity", "OnlineIndexPercentageProgress", "OnlineIndexThrottleEvents", "ProvisionedReadCapacityUnits", "ProvisionedWriteCapacityUnits", "ReadThrottleEvents", "ReturnedBytes", "ReturnedItemCount", "ReturnedRecordsCount", "SuccessfulRequestLatency", "SystemErrors", "TimeToLiveDeletedItemCount", "ThrottledRequests", "UserErrors", "WriteThrottleEvents"},
"AWS/EBS": {"VolumeReadBytes", "VolumeWriteBytes", "VolumeReadOps", "VolumeWriteOps", "VolumeTotalReadTime", "VolumeTotalWriteTime", "VolumeIdleTime", "VolumeQueueLength", "VolumeThroughputPercentage", "VolumeConsumedReadWriteOps", "BurstBalance"},
"AWS/EC2": {"CPUCreditUsage", "CPUCreditBalance", "CPUUtilization", "DiskReadOps", "DiskWriteOps", "DiskReadBytes", "DiskWriteBytes", "NetworkIn", "NetworkOut", "NetworkPacketsIn", "NetworkPacketsOut", "StatusCheckFailed", "StatusCheckFailed_Instance", "StatusCheckFailed_System"},
@@ -86,13 +87,14 @@ func init() {
"AWS/Kinesis": {"GetRecords.Bytes", "GetRecords.IteratorAge", "GetRecords.IteratorAgeMilliseconds", "GetRecords.Latency", "GetRecords.Records", "GetRecords.Success", "IncomingBytes", "IncomingRecords", "PutRecord.Bytes", "PutRecord.Latency", "PutRecord.Success", "PutRecords.Bytes", "PutRecords.Latency", "PutRecords.Records", "PutRecords.Success", "ReadProvisionedThroughputExceeded", "WriteProvisionedThroughputExceeded", "IteratorAgeMilliseconds", "OutgoingBytes", "OutgoingRecords"},
"AWS/KinesisAnalytics": {"Bytes", "MillisBehindLatest", "Records", "Success"},
"AWS/Lambda": {"Invocations", "Errors", "Duration", "Throttles", "IteratorAge"},
+ "AWS/AppSync": {"Latency", "4XXError", "5XXError"},
"AWS/Logs": {"IncomingBytes", "IncomingLogEvents", "ForwardedBytes", "ForwardedLogEvents", "DeliveryErrors", "DeliveryThrottling"},
"AWS/ML": {"PredictCount", "PredictFailureCount"},
"AWS/NATGateway": {"PacketsOutToDestination", "PacketsOutToSource", "PacketsInFromSource", "PacketsInFromDestination", "BytesOutToDestination", "BytesOutToSource", "BytesInFromSource", "BytesInFromDestination", "ErrorPortAllocation", "ActiveConnectionCount", "ConnectionAttemptCount", "ConnectionEstablishedCount", "IdleTimeoutCount", "PacketsDropCount"},
"AWS/NetworkELB": {"ActiveFlowCount", "ConsumedLCUs", "HealthyHostCount", "NewFlowCount", "ProcessedBytes", "TCP_Client_Reset_Count", "TCP_ELB_Reset_Count", "TCP_Target_Reset_Count", "UnHealthyHostCount"},
"AWS/OpsWorks": {"cpu_idle", "cpu_nice", "cpu_system", "cpu_user", "cpu_waitio", "load_1", "load_5", "load_15", "memory_buffers", "memory_cached", "memory_free", "memory_swap", "memory_total", "memory_used", "procs"},
- "AWS/Redshift": {"CPUUtilization", "DatabaseConnections", "HealthStatus", "MaintenanceMode", "NetworkReceiveThroughput", "NetworkTransmitThroughput", "PercentageDiskSpaceUsed", "ReadIOPS", "ReadLatency", "ReadThroughput", "WriteIOPS", "WriteLatency", "WriteThroughput"},
- "AWS/RDS": {"ActiveTransactions", "AuroraBinlogReplicaLag", "AuroraReplicaLag", "AuroraReplicaLagMaximum", "AuroraReplicaLagMinimum", "BinLogDiskUsage", "BlockedTransactions", "BufferCacheHitRatio", "CommitLatency", "CommitThroughput", "BinLogDiskUsage", "CPUCreditBalance", "CPUCreditUsage", "CPUUtilization", "DatabaseConnections", "DDLLatency", "DDLThroughput", "Deadlocks", "DeleteLatency", "DeleteThroughput", "DiskQueueDepth", "DMLLatency", "DMLThroughput", "EngineUptime", "FailedSqlStatements", "FreeableMemory", "FreeLocalStorage", "FreeStorageSpace", "InsertLatency", "InsertThroughput", "LoginFailures", "NetworkReceiveThroughput", "NetworkTransmitThroughput", "NetworkThroughput", "Queries", "ReadIOPS", "ReadLatency", "ReadThroughput", "ReplicaLag", "ResultSetCacheHitRatio", "SelectLatency", "SelectThroughput", "SwapUsage", "TotalConnections", "UpdateLatency", "UpdateThroughput", "VolumeBytesUsed", "VolumeReadIOPS", "VolumeWriteIOPS", "WriteIOPS", "WriteLatency", "WriteThroughput"},
+ "AWS/Redshift": {"CPUUtilization", "DatabaseConnections", "HealthStatus", "MaintenanceMode", "NetworkReceiveThroughput", "NetworkTransmitThroughput", "PercentageDiskSpaceUsed", "QueriesCompletedPerSecond", "QueryDuration", "QueryRuntimeBreakdown", "ReadIOPS", "ReadLatency", "ReadThroughput", "WLMQueriesCompletedPerSecond", "WLMQueryDuration", "WLMQueueLength", "WriteIOPS", "WriteLatency", "WriteThroughput"},
+ "AWS/RDS": {"ActiveTransactions", "AuroraBinlogReplicaLag", "AuroraReplicaLag", "AuroraReplicaLagMaximum", "AuroraReplicaLagMinimum", "BinLogDiskUsage", "BlockedTransactions", "BufferCacheHitRatio", "BurstBalance", "CommitLatency", "CommitThroughput", "BinLogDiskUsage", "CPUCreditBalance", "CPUCreditUsage", "CPUUtilization", "DatabaseConnections", "DDLLatency", "DDLThroughput", "Deadlocks", "DeleteLatency", "DeleteThroughput", "DiskQueueDepth", "DMLLatency", "DMLThroughput", "EngineUptime", "FailedSqlStatements", "FreeableMemory", "FreeLocalStorage", "FreeStorageSpace", "InsertLatency", "InsertThroughput", "LoginFailures", "NetworkReceiveThroughput", "NetworkTransmitThroughput", "NetworkThroughput", "Queries", "ReadIOPS", "ReadLatency", "ReadThroughput", "ReplicaLag", "ResultSetCacheHitRatio", "SelectLatency", "SelectThroughput", "SwapUsage", "TotalConnections", "UpdateLatency", "UpdateThroughput", "VolumeBytesUsed", "VolumeReadIOPS", "VolumeWriteIOPS", "WriteIOPS", "WriteLatency", "WriteThroughput"},
"AWS/Route53": {"ChildHealthCheckHealthyCount", "HealthCheckStatus", "HealthCheckPercentageHealthy", "ConnectionTime", "SSLHandshakeTime", "TimeToFirstByte"},
"AWS/S3": {"BucketSizeBytes", "NumberOfObjects", "AllRequests", "GetRequests", "PutRequests", "DeleteRequests", "HeadRequests", "PostRequests", "ListRequests", "BytesDownloaded", "BytesUploaded", "4xxErrors", "5xxErrors", "FirstByteLatency", "TotalRequestLatency"},
"AWS/SES": {"Bounce", "Complaint", "Delivery", "Reject", "Send"},
@@ -118,6 +120,7 @@ func init() {
"AWS/CloudFront": {"DistributionId", "Region"},
"AWS/CloudSearch": {},
"AWS/DMS": {"ReplicationInstanceIdentifier", "ReplicationTaskIdentifier"},
+ "AWS/DX": {"ConnectionId"},
"AWS/DynamoDB": {"TableName", "GlobalSecondaryIndexName", "Operation", "StreamLabel"},
"AWS/EBS": {"VolumeId"},
"AWS/EC2": {"AutoScalingGroupName", "ImageId", "InstanceId", "InstanceType"},
@@ -135,12 +138,13 @@ func init() {
"AWS/Kinesis": {"StreamName", "ShardId"},
"AWS/KinesisAnalytics": {"Flow", "Id", "Application"},
"AWS/Lambda": {"FunctionName", "Resource", "Version", "Alias"},
+ "AWS/AppSync": {"GraphQLAPIId"},
"AWS/Logs": {"LogGroupName", "DestinationType", "FilterName"},
"AWS/ML": {"MLModelId", "RequestMode"},
"AWS/NATGateway": {"NatGatewayId"},
"AWS/NetworkELB": {"LoadBalancer", "TargetGroup", "AvailabilityZone"},
"AWS/OpsWorks": {"StackId", "LayerId", "InstanceId"},
- "AWS/Redshift": {"NodeID", "ClusterIdentifier"},
+ "AWS/Redshift": {"NodeID", "ClusterIdentifier", "latency", "service class", "wmlid"},
"AWS/RDS": {"DBInstanceIdentifier", "DBClusterIdentifier", "DbClusterIdentifier", "DatabaseClass", "EngineName", "Role"},
"AWS/Route53": {"HealthCheckId", "Region"},
"AWS/S3": {"BucketName", "StorageType", "FilterId"},
@@ -462,6 +466,9 @@ func (e *CloudWatchExecutor) handleGetEc2InstanceAttribute(ctx context.Context,
return nil, errors.New("invalid attribute path")
}
v = v.FieldByName(key)
+ if !v.IsValid() {
+ return nil, errors.New("invalid attribute path")
+ }
}
if attr, ok := v.Interface().(*string); ok {
data = *attr
diff --git a/pkg/tsdb/elasticsearch/client/client.go b/pkg/tsdb/elasticsearch/client/client.go
index dff626a79eb..4ebe0db8f89 100644
--- a/pkg/tsdb/elasticsearch/client/client.go
+++ b/pkg/tsdb/elasticsearch/client/client.go
@@ -138,13 +138,13 @@ func (c *baseClientImpl) encodeBatchRequests(requests []*multiRequest) ([]byte,
}
body := string(reqBody)
- body = strings.Replace(body, "$__interval_ms", strconv.FormatInt(r.interval.Value.Nanoseconds()/int64(time.Millisecond), 10), -1)
+ body = strings.Replace(body, "$__interval_ms", strconv.FormatInt(r.interval.Milliseconds(), 10), -1)
body = strings.Replace(body, "$__interval", r.interval.Text, -1)
payload.WriteString(body + "\n")
}
- elapsed := time.Now().Sub(start)
+ elapsed := time.Since(start)
clientLog.Debug("Encoded batch requests to json", "took", elapsed)
return payload.Bytes(), nil
@@ -187,7 +187,7 @@ func (c *baseClientImpl) executeRequest(method, uriPath string, body []byte) (*h
start := time.Now()
defer func() {
- elapsed := time.Now().Sub(start)
+ elapsed := time.Since(start)
clientLog.Debug("Executed request", "took", elapsed)
}()
return ctxhttp.Do(c.ctx, httpClient, req)
@@ -215,7 +215,7 @@ func (c *baseClientImpl) ExecuteMultisearch(r *MultiSearchRequest) (*MultiSearch
return nil, err
}
- elapsed := time.Now().Sub(start)
+ elapsed := time.Since(start)
clientLog.Debug("Decoded multisearch json response", "took", elapsed)
msr.Status = res.StatusCode
diff --git a/pkg/tsdb/elasticsearch/client/client_test.go b/pkg/tsdb/elasticsearch/client/client_test.go
index 11d1cdb1d71..af9ac0d8fce 100644
--- a/pkg/tsdb/elasticsearch/client/client_test.go
+++ b/pkg/tsdb/elasticsearch/client/client_test.go
@@ -40,7 +40,7 @@ func TestClient(t *testing.T) {
So(err, ShouldNotBeNil)
})
- Convey("When unspported version set should return error", func() {
+ Convey("When unsupported version set should return error", func() {
ds := &models.DataSource{
JsonData: simplejson.NewFromAny(map[string]interface{}{
"esVersion": 6,
diff --git a/pkg/tsdb/elasticsearch/client/index_pattern.go b/pkg/tsdb/elasticsearch/client/index_pattern.go
index 8391e902ea4..952b5c4f806 100644
--- a/pkg/tsdb/elasticsearch/client/index_pattern.go
+++ b/pkg/tsdb/elasticsearch/client/index_pattern.go
@@ -248,13 +248,28 @@ var datePatternReplacements = map[string]string{
func formatDate(t time.Time, pattern string) string {
var datePattern string
- parts := strings.Split(strings.TrimLeft(pattern, "["), "]")
- base := parts[0]
- if len(parts) == 2 {
- datePattern = parts[1]
- } else {
- datePattern = base
- base = ""
+ base := ""
+ ltr := false
+
+ if strings.HasPrefix(pattern, "[") {
+ parts := strings.Split(strings.TrimLeft(pattern, "["), "]")
+ base = parts[0]
+ if len(parts) == 2 {
+ datePattern = parts[1]
+ } else {
+ datePattern = base
+ base = ""
+ }
+ ltr = true
+ } else if strings.HasSuffix(pattern, "]") {
+ parts := strings.Split(strings.TrimRight(pattern, "]"), "[")
+ datePattern = parts[0]
+ if len(parts) == 2 {
+ base = parts[1]
+ } else {
+ base = ""
+ }
+ ltr = false
}
formatted := t.Format(patternToLayout(datePattern))
@@ -293,7 +308,11 @@ func formatDate(t time.Time, pattern string) string {
formatted = strings.Replace(formatted, "", fmt.Sprintf("%d", t.Hour()), -1)
}
- return base + formatted
+ if ltr {
+ return base + formatted
+ }
+
+ return formatted + base
}
func patternToLayout(pattern string) string {
diff --git a/pkg/tsdb/elasticsearch/client/index_pattern_test.go b/pkg/tsdb/elasticsearch/client/index_pattern_test.go
index 3bd823d8c87..ca20b39d532 100644
--- a/pkg/tsdb/elasticsearch/client/index_pattern_test.go
+++ b/pkg/tsdb/elasticsearch/client/index_pattern_test.go
@@ -28,29 +28,54 @@ func TestIndexPattern(t *testing.T) {
to := fmt.Sprintf("%d", time.Date(2018, 5, 15, 17, 55, 0, 0, time.UTC).UnixNano()/int64(time.Millisecond))
indexPatternScenario(intervalHourly, "[data-]YYYY.MM.DD.HH", tsdb.NewTimeRange(from, to), func(indices []string) {
- //So(indices, ShouldHaveLength, 1)
+ So(indices, ShouldHaveLength, 1)
So(indices[0], ShouldEqual, "data-2018.05.15.17")
})
+ indexPatternScenario(intervalHourly, "YYYY.MM.DD.HH[-data]", tsdb.NewTimeRange(from, to), func(indices []string) {
+ So(indices, ShouldHaveLength, 1)
+ So(indices[0], ShouldEqual, "2018.05.15.17-data")
+ })
+
indexPatternScenario(intervalDaily, "[data-]YYYY.MM.DD", tsdb.NewTimeRange(from, to), func(indices []string) {
So(indices, ShouldHaveLength, 1)
So(indices[0], ShouldEqual, "data-2018.05.15")
})
+ indexPatternScenario(intervalDaily, "YYYY.MM.DD[-data]", tsdb.NewTimeRange(from, to), func(indices []string) {
+ So(indices, ShouldHaveLength, 1)
+ So(indices[0], ShouldEqual, "2018.05.15-data")
+ })
+
indexPatternScenario(intervalWeekly, "[data-]GGGG.WW", tsdb.NewTimeRange(from, to), func(indices []string) {
So(indices, ShouldHaveLength, 1)
So(indices[0], ShouldEqual, "data-2018.20")
})
+ indexPatternScenario(intervalWeekly, "GGGG.WW[-data]", tsdb.NewTimeRange(from, to), func(indices []string) {
+ So(indices, ShouldHaveLength, 1)
+ So(indices[0], ShouldEqual, "2018.20-data")
+ })
+
indexPatternScenario(intervalMonthly, "[data-]YYYY.MM", tsdb.NewTimeRange(from, to), func(indices []string) {
So(indices, ShouldHaveLength, 1)
So(indices[0], ShouldEqual, "data-2018.05")
})
+ indexPatternScenario(intervalMonthly, "YYYY.MM[-data]", tsdb.NewTimeRange(from, to), func(indices []string) {
+ So(indices, ShouldHaveLength, 1)
+ So(indices[0], ShouldEqual, "2018.05-data")
+ })
+
indexPatternScenario(intervalYearly, "[data-]YYYY", tsdb.NewTimeRange(from, to), func(indices []string) {
So(indices, ShouldHaveLength, 1)
So(indices[0], ShouldEqual, "data-2018")
})
+
+ indexPatternScenario(intervalYearly, "YYYY[-data]", tsdb.NewTimeRange(from, to), func(indices []string) {
+ So(indices, ShouldHaveLength, 1)
+ So(indices[0], ShouldEqual, "2018-data")
+ })
})
Convey("Hourly interval", t, func() {
diff --git a/pkg/tsdb/elasticsearch/client/search_request.go b/pkg/tsdb/elasticsearch/client/search_request.go
index 2b833ce78d3..4c577a2c31d 100644
--- a/pkg/tsdb/elasticsearch/client/search_request.go
+++ b/pkg/tsdb/elasticsearch/client/search_request.go
@@ -56,9 +56,7 @@ func (b *SearchRequestBuilder) Build() (*SearchRequest, error) {
if err != nil {
return nil, err
}
- for _, agg := range aggArray {
- sr.Aggs = append(sr.Aggs, agg)
- }
+ sr.Aggs = append(sr.Aggs, aggArray...)
}
}
@@ -112,7 +110,7 @@ func (b *SearchRequestBuilder) Query() *QueryBuilder {
return b.queryBuilder
}
-// Agg initaite and returns a new aggregation builder
+// Agg initiate and returns a new aggregation builder
func (b *SearchRequestBuilder) Agg() AggBuilder {
aggBuilder := newAggBuilder()
b.aggBuilders = append(b.aggBuilders, aggBuilder)
@@ -300,9 +298,7 @@ func (b *aggBuilderImpl) Build() (AggArray, error) {
return nil, err
}
- for _, childAgg := range childAggs {
- agg.Aggregation.Aggs = append(agg.Aggregation.Aggs, childAgg)
- }
+ agg.Aggregation.Aggs = append(agg.Aggregation.Aggs, childAggs...)
}
aggs = append(aggs, agg)
diff --git a/pkg/tsdb/elasticsearch/response_parser.go b/pkg/tsdb/elasticsearch/response_parser.go
index 7bdab60389c..0090754840a 100644
--- a/pkg/tsdb/elasticsearch/response_parser.go
+++ b/pkg/tsdb/elasticsearch/response_parser.go
@@ -92,7 +92,7 @@ func (rp *responseParser) processBuckets(aggs map[string]interface{}, target *Qu
} else {
for _, b := range esAgg.Get("buckets").MustArray() {
bucket := simplejson.NewFromAny(b)
- newProps := make(map[string]string, 0)
+ newProps := make(map[string]string)
for k, v := range props {
newProps[k] = v
@@ -122,7 +122,7 @@ func (rp *responseParser) processBuckets(aggs map[string]interface{}, target *Qu
for _, bucketKey := range bucketKeys {
bucket := simplejson.NewFromAny(buckets[bucketKey])
- newProps := make(map[string]string, 0)
+ newProps := make(map[string]string)
for k, v := range props {
newProps[k] = v
@@ -314,7 +314,6 @@ func (rp *responseParser) processAggregationDocs(esAgg *simplejson.Json, aggDef
switch metric.Type {
case "count":
addMetricValue(&values, rp.getMetricName(metric.Type), castToNullFloat(bucket.Get("doc_count")))
- break
case "extended_stats":
metaKeys := make([]string, 0)
meta := metric.Meta.MustMap()
@@ -355,7 +354,6 @@ func (rp *responseParser) processAggregationDocs(esAgg *simplejson.Json, aggDef
}
addMetricValue(&values, metricName, castToNullFloat(bucket.GetPath(metric.ID, "value")))
- break
}
}
diff --git a/pkg/tsdb/influxdb/query.go b/pkg/tsdb/influxdb/query.go
index 0637a5bbb44..7cb8f0ecd82 100644
--- a/pkg/tsdb/influxdb/query.go
+++ b/pkg/tsdb/influxdb/query.go
@@ -4,7 +4,6 @@ import (
"fmt"
"strconv"
"strings"
- "time"
"regexp"
@@ -34,7 +33,7 @@ func (query *Query) Build(queryContext *tsdb.TsdbQuery) (string, error) {
res = strings.Replace(res, "$timeFilter", query.renderTimeFilter(queryContext), -1)
res = strings.Replace(res, "$interval", interval.Text, -1)
- res = strings.Replace(res, "$__interval_ms", strconv.FormatInt(interval.Value.Nanoseconds()/int64(time.Millisecond), 10), -1)
+ res = strings.Replace(res, "$__interval_ms", strconv.FormatInt(interval.Milliseconds(), 10), -1)
res = strings.Replace(res, "$__interval", interval.Text, -1)
return res, nil
}
diff --git a/pkg/tsdb/influxdb/query_test.go b/pkg/tsdb/influxdb/query_test.go
index f1270560269..cc1358a72d7 100644
--- a/pkg/tsdb/influxdb/query_test.go
+++ b/pkg/tsdb/influxdb/query_test.go
@@ -158,7 +158,7 @@ func TestInfluxdbQueryBuilder(t *testing.T) {
So(strings.Join(query.renderTags(), ""), ShouldEqual, `"key" < 10001`)
})
- Convey("can render number greather then condition tags", func() {
+ Convey("can render number greater then condition tags", func() {
query := &Query{Tags: []*Tag{{Operator: ">", Value: "10001", Key: "key"}}}
So(strings.Join(query.renderTags(), ""), ShouldEqual, `"key" > 10001`)
diff --git a/pkg/tsdb/interval.go b/pkg/tsdb/interval.go
index 49904f27a37..fd6adee39d7 100644
--- a/pkg/tsdb/interval.go
+++ b/pkg/tsdb/interval.go
@@ -49,6 +49,10 @@ func NewIntervalCalculator(opt *IntervalOptions) *intervalCalculator {
return calc
}
+func (i *Interval) Milliseconds() int64 {
+ return i.Value.Nanoseconds() / int64(time.Millisecond)
+}
+
func (ic *intervalCalculator) Calculate(timerange *TimeRange, minInterval time.Duration) Interval {
to := timerange.MustGetTo().UnixNano()
from := timerange.MustGetFrom().UnixNano()
diff --git a/pkg/tsdb/mssql/macros.go b/pkg/tsdb/mssql/macros.go
index ad3d1edd5d7..9303712a480 100644
--- a/pkg/tsdb/mssql/macros.go
+++ b/pkg/tsdb/mssql/macros.go
@@ -6,30 +6,29 @@ import (
"strings"
"time"
- "strconv"
-
"github.com/grafana/grafana/pkg/tsdb"
)
const rsIdentifier = `([_a-zA-Z0-9]+)`
const sExpr = `\$` + rsIdentifier + `\(([^\)]*)\)`
-type MsSqlMacroEngine struct {
- TimeRange *tsdb.TimeRange
- Query *tsdb.Query
+type msSqlMacroEngine struct {
+ *tsdb.SqlMacroEngineBase
+ timeRange *tsdb.TimeRange
+ query *tsdb.Query
}
-func NewMssqlMacroEngine() tsdb.SqlMacroEngine {
- return &MsSqlMacroEngine{}
+func newMssqlMacroEngine() tsdb.SqlMacroEngine {
+ return &msSqlMacroEngine{SqlMacroEngineBase: tsdb.NewSqlMacroEngineBase()}
}
-func (m *MsSqlMacroEngine) Interpolate(query *tsdb.Query, timeRange *tsdb.TimeRange, sql string) (string, error) {
- m.TimeRange = timeRange
- m.Query = query
+func (m *msSqlMacroEngine) Interpolate(query *tsdb.Query, timeRange *tsdb.TimeRange, sql string) (string, error) {
+ m.timeRange = timeRange
+ m.query = query
rExp, _ := regexp.Compile(sExpr)
var macroError error
- sql = replaceAllStringSubmatchFunc(rExp, sql, func(groups []string) string {
+ sql = m.ReplaceAllStringSubmatchFunc(rExp, sql, func(groups []string) string {
args := strings.Split(groups[2], ",")
for i, arg := range args {
args[i] = strings.Trim(arg, " ")
@@ -49,24 +48,7 @@ func (m *MsSqlMacroEngine) Interpolate(query *tsdb.Query, timeRange *tsdb.TimeRa
return sql, nil
}
-func replaceAllStringSubmatchFunc(re *regexp.Regexp, str string, repl func([]string) string) string {
- result := ""
- lastIndex := 0
-
- for _, v := range re.FindAllSubmatchIndex([]byte(str), -1) {
- groups := []string{}
- for i := 0; i < len(v); i += 2 {
- groups = append(groups, str[v[i]:v[i+1]])
- }
-
- result += str[lastIndex:v[0]] + repl(groups)
- lastIndex = v[1]
- }
-
- return result + str[lastIndex:]
-}
-
-func (m *MsSqlMacroEngine) evaluateMacro(name string, args []string) (string, error) {
+func (m *msSqlMacroEngine) evaluateMacro(name string, args []string) (string, error) {
switch name {
case "__time":
if len(args) == 0 {
@@ -83,11 +65,11 @@ func (m *MsSqlMacroEngine) evaluateMacro(name string, args []string) (string, er
return "", fmt.Errorf("missing time column argument for macro %v", name)
}
- return fmt.Sprintf("%s BETWEEN '%s' AND '%s'", args[0], m.TimeRange.GetFromAsTimeUTC().Format(time.RFC3339), m.TimeRange.GetToAsTimeUTC().Format(time.RFC3339)), nil
+ return fmt.Sprintf("%s BETWEEN '%s' AND '%s'", args[0], m.timeRange.GetFromAsTimeUTC().Format(time.RFC3339), m.timeRange.GetToAsTimeUTC().Format(time.RFC3339)), nil
case "__timeFrom":
- return fmt.Sprintf("'%s'", m.TimeRange.GetFromAsTimeUTC().Format(time.RFC3339)), nil
+ return fmt.Sprintf("'%s'", m.timeRange.GetFromAsTimeUTC().Format(time.RFC3339)), nil
case "__timeTo":
- return fmt.Sprintf("'%s'", m.TimeRange.GetToAsTimeUTC().Format(time.RFC3339)), nil
+ return fmt.Sprintf("'%s'", m.timeRange.GetToAsTimeUTC().Format(time.RFC3339)), nil
case "__timeGroup":
if len(args) < 2 {
return "", fmt.Errorf("macro %v needs time column and interval", name)
@@ -97,28 +79,48 @@ func (m *MsSqlMacroEngine) evaluateMacro(name string, args []string) (string, er
return "", fmt.Errorf("error parsing interval %v", args[1])
}
if len(args) == 3 {
- m.Query.Model.Set("fill", true)
- m.Query.Model.Set("fillInterval", interval.Seconds())
- if args[2] == "NULL" {
- m.Query.Model.Set("fillNull", true)
- } else {
- floatVal, err := strconv.ParseFloat(args[2], 64)
- if err != nil {
- return "", fmt.Errorf("error parsing fill value %v", args[2])
- }
- m.Query.Model.Set("fillValue", floatVal)
+ err := tsdb.SetupFillmode(m.query, interval, args[2])
+ if err != nil {
+ return "", err
}
}
return fmt.Sprintf("FLOOR(DATEDIFF(second, '1970-01-01', %s)/%.0f)*%.0f", args[0], interval.Seconds(), interval.Seconds()), nil
+ case "__timeGroupAlias":
+ tg, err := m.evaluateMacro("__timeGroup", args)
+ if err == nil {
+ return tg + " AS [time]", err
+ }
+ return "", err
case "__unixEpochFilter":
if len(args) == 0 {
return "", fmt.Errorf("missing time column argument for macro %v", name)
}
- return fmt.Sprintf("%s >= %d AND %s <= %d", args[0], m.TimeRange.GetFromAsSecondsEpoch(), args[0], m.TimeRange.GetToAsSecondsEpoch()), nil
+ return fmt.Sprintf("%s >= %d AND %s <= %d", args[0], m.timeRange.GetFromAsSecondsEpoch(), args[0], m.timeRange.GetToAsSecondsEpoch()), nil
case "__unixEpochFrom":
- return fmt.Sprintf("%d", m.TimeRange.GetFromAsSecondsEpoch()), nil
+ return fmt.Sprintf("%d", m.timeRange.GetFromAsSecondsEpoch()), nil
case "__unixEpochTo":
- return fmt.Sprintf("%d", m.TimeRange.GetToAsSecondsEpoch()), nil
+ return fmt.Sprintf("%d", m.timeRange.GetToAsSecondsEpoch()), nil
+ case "__unixEpochGroup":
+ if len(args) < 2 {
+ return "", fmt.Errorf("macro %v needs time column and interval and optional fill value", name)
+ }
+ interval, err := time.ParseDuration(strings.Trim(args[1], `'`))
+ if err != nil {
+ return "", fmt.Errorf("error parsing interval %v", args[1])
+ }
+ if len(args) == 3 {
+ err := tsdb.SetupFillmode(m.query, interval, args[2])
+ if err != nil {
+ return "", err
+ }
+ }
+ return fmt.Sprintf("FLOOR(%s/%v)*%v", args[0], interval.Seconds(), interval.Seconds()), nil
+ case "__unixEpochGroupAlias":
+ tg, err := m.evaluateMacro("__unixEpochGroup", args)
+ if err == nil {
+ return tg + " AS [time]", err
+ }
+ return "", err
default:
return "", fmt.Errorf("Unknown macro %v", name)
}
diff --git a/pkg/tsdb/mssql/macros_test.go b/pkg/tsdb/mssql/macros_test.go
index 49368fe3631..8e0973b750c 100644
--- a/pkg/tsdb/mssql/macros_test.go
+++ b/pkg/tsdb/mssql/macros_test.go
@@ -14,7 +14,7 @@ import (
func TestMacroEngine(t *testing.T) {
Convey("MacroEngine", t, func() {
- engine := &MsSqlMacroEngine{}
+ engine := &msSqlMacroEngine{}
query := &tsdb.Query{
Model: simplejson.New(),
}
@@ -55,27 +55,46 @@ func TestMacroEngine(t *testing.T) {
Convey("interpolate __timeGroup function", func() {
sql, err := engine.Interpolate(query, timeRange, "GROUP BY $__timeGroup(time_column,'5m')")
So(err, ShouldBeNil)
+ sql2, err := engine.Interpolate(query, timeRange, "GROUP BY $__timeGroupAlias(time_column,'5m')")
+ So(err, ShouldBeNil)
So(sql, ShouldEqual, "GROUP BY FLOOR(DATEDIFF(second, '1970-01-01', time_column)/300)*300")
+ So(sql2, ShouldEqual, sql+" AS [time]")
})
Convey("interpolate __timeGroup function with spaces around arguments", func() {
sql, err := engine.Interpolate(query, timeRange, "GROUP BY $__timeGroup(time_column , '5m')")
So(err, ShouldBeNil)
+ sql2, err := engine.Interpolate(query, timeRange, "GROUP BY $__timeGroupAlias(time_column , '5m')")
+ So(err, ShouldBeNil)
So(sql, ShouldEqual, "GROUP BY FLOOR(DATEDIFF(second, '1970-01-01', time_column)/300)*300")
+ So(sql2, ShouldEqual, sql+" AS [time]")
})
Convey("interpolate __timeGroup function with fill (value = NULL)", func() {
_, err := engine.Interpolate(query, timeRange, "GROUP BY $__timeGroup(time_column,'5m', NULL)")
fill := query.Model.Get("fill").MustBool()
- fillNull := query.Model.Get("fillNull").MustBool()
+ fillMode := query.Model.Get("fillMode").MustString()
fillInterval := query.Model.Get("fillInterval").MustInt()
So(err, ShouldBeNil)
So(fill, ShouldBeTrue)
- So(fillNull, ShouldBeTrue)
+ So(fillMode, ShouldEqual, "null")
+ So(fillInterval, ShouldEqual, 5*time.Minute.Seconds())
+ })
+
+ Convey("interpolate __timeGroup function with fill (value = previous)", func() {
+ _, err := engine.Interpolate(query, timeRange, "GROUP BY $__timeGroup(time_column,'5m', previous)")
+
+ fill := query.Model.Get("fill").MustBool()
+ fillMode := query.Model.Get("fillMode").MustString()
+ fillInterval := query.Model.Get("fillInterval").MustInt()
+
+ So(err, ShouldBeNil)
+ So(fill, ShouldBeTrue)
+ So(fillMode, ShouldEqual, "previous")
So(fillInterval, ShouldEqual, 5*time.Minute.Seconds())
})
@@ -126,6 +145,18 @@ func TestMacroEngine(t *testing.T) {
So(sql, ShouldEqual, fmt.Sprintf("select %d", to.Unix()))
})
+
+ Convey("interpolate __unixEpochGroup function", func() {
+
+ sql, err := engine.Interpolate(query, timeRange, "SELECT $__unixEpochGroup(time_column,'5m')")
+ So(err, ShouldBeNil)
+ sql2, err := engine.Interpolate(query, timeRange, "SELECT $__unixEpochGroupAlias(time_column,'5m')")
+ So(err, ShouldBeNil)
+
+ So(sql, ShouldEqual, "SELECT FLOOR(time_column/300)*300")
+ So(sql2, ShouldEqual, sql+" AS [time]")
+ })
+
})
Convey("Given a time range between 1960-02-01 07:00 and 1965-02-03 08:00", func() {
diff --git a/pkg/tsdb/mssql/mssql.go b/pkg/tsdb/mssql/mssql.go
index eb71259b46b..72e57d03fa0 100644
--- a/pkg/tsdb/mssql/mssql.go
+++ b/pkg/tsdb/mssql/mssql.go
@@ -1,49 +1,40 @@
package mssql
import (
- "container/list"
- "context"
"database/sql"
"fmt"
"strconv"
"strings"
- "math"
-
_ "github.com/denisenkom/go-mssqldb"
"github.com/go-xorm/core"
- "github.com/grafana/grafana/pkg/components/null"
"github.com/grafana/grafana/pkg/log"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/tsdb"
)
-type MssqlQueryEndpoint struct {
- sqlEngine tsdb.SqlEngine
- log log.Logger
-}
-
func init() {
- tsdb.RegisterTsdbQueryEndpoint("mssql", NewMssqlQueryEndpoint)
+ tsdb.RegisterTsdbQueryEndpoint("mssql", newMssqlQueryEndpoint)
}
-func NewMssqlQueryEndpoint(datasource *models.DataSource) (tsdb.TsdbQueryEndpoint, error) {
- endpoint := &MssqlQueryEndpoint{
- log: log.New("tsdb.mssql"),
- }
-
- endpoint.sqlEngine = &tsdb.DefaultSqlEngine{
- MacroEngine: NewMssqlMacroEngine(),
- }
+func newMssqlQueryEndpoint(datasource *models.DataSource) (tsdb.TsdbQueryEndpoint, error) {
+ logger := log.New("tsdb.mssql")
cnnstr := generateConnectionString(datasource)
- endpoint.log.Debug("getEngine", "connection", cnnstr)
+ logger.Debug("getEngine", "connection", cnnstr)
- if err := endpoint.sqlEngine.InitEngine("mssql", datasource, cnnstr); err != nil {
- return nil, err
+ config := tsdb.SqlQueryEndpointConfiguration{
+ DriverName: "mssql",
+ ConnectionString: cnnstr,
+ Datasource: datasource,
+ MetricColumnTypes: []string{"VARCHAR", "CHAR", "NVARCHAR", "NCHAR"},
}
- return endpoint, nil
+ rowTransformer := mssqlRowTransformer{
+ log: logger,
+ }
+
+ return tsdb.NewSqlQueryEndpoint(&config, &rowTransformer, newMssqlMacroEngine(), logger)
}
func generateConnectionString(datasource *models.DataSource) string {
@@ -70,71 +61,16 @@ func generateConnectionString(datasource *models.DataSource) string {
)
}
-// Query is the main function for the MssqlQueryEndpoint
-func (e *MssqlQueryEndpoint) Query(ctx context.Context, dsInfo *models.DataSource, tsdbQuery *tsdb.TsdbQuery) (*tsdb.Response, error) {
- return e.sqlEngine.Query(ctx, dsInfo, tsdbQuery, e.transformToTimeSeries, e.transformToTable)
+type mssqlRowTransformer struct {
+ log log.Logger
}
-func (e MssqlQueryEndpoint) transformToTable(query *tsdb.Query, rows *core.Rows, result *tsdb.QueryResult, tsdbQuery *tsdb.TsdbQuery) error {
- columnNames, err := rows.Columns()
- columnCount := len(columnNames)
+func (t *mssqlRowTransformer) Transform(columnTypes []*sql.ColumnType, rows *core.Rows) (tsdb.RowValues, error) {
+ values := make([]interface{}, len(columnTypes))
+ valuePtrs := make([]interface{}, len(columnTypes))
- if err != nil {
- return err
- }
-
- rowLimit := 1000000
- rowCount := 0
- timeIndex := -1
-
- table := &tsdb.Table{
- Columns: make([]tsdb.TableColumn, columnCount),
- Rows: make([]tsdb.RowValues, 0),
- }
-
- for i, name := range columnNames {
- table.Columns[i].Text = name
-
- // check if there is a column named time
- switch name {
- case "time":
- timeIndex = i
- }
- }
-
- columnTypes, err := rows.ColumnTypes()
- if err != nil {
- return err
- }
-
- for ; rows.Next(); rowCount++ {
- if rowCount > rowLimit {
- return fmt.Errorf("MsSQL query row limit exceeded, limit %d", rowLimit)
- }
-
- values, err := e.getTypedRowData(columnTypes, rows)
- if err != nil {
- return err
- }
-
- // converts column named time to unix timestamp in milliseconds
- // to make native mssql datetime types and epoch dates work in
- // annotation and table queries.
- tsdb.ConvertSqlTimeColumnToEpochMs(values, timeIndex)
- table.Rows = append(table.Rows, values)
- }
-
- result.Tables = append(result.Tables, table)
- result.Meta.Set("rowCount", rowCount)
- return nil
-}
-
-func (e MssqlQueryEndpoint) getTypedRowData(types []*sql.ColumnType, rows *core.Rows) (tsdb.RowValues, error) {
- values := make([]interface{}, len(types))
- valuePtrs := make([]interface{}, len(types))
-
- for i, stype := range types {
- e.log.Debug("type", "type", stype)
+ for i, stype := range columnTypes {
+ t.log.Debug("type", "type", stype)
valuePtrs[i] = &values[i]
}
@@ -144,17 +80,17 @@ func (e MssqlQueryEndpoint) getTypedRowData(types []*sql.ColumnType, rows *core.
// convert types not handled by denisenkom/go-mssqldb
// unhandled types are returned as []byte
- for i := 0; i < len(types); i++ {
+ for i := 0; i < len(columnTypes); i++ {
if value, ok := values[i].([]byte); ok {
- switch types[i].DatabaseTypeName() {
+ switch columnTypes[i].DatabaseTypeName() {
case "MONEY", "SMALLMONEY", "DECIMAL":
if v, err := strconv.ParseFloat(string(value), 64); err == nil {
values[i] = v
} else {
- e.log.Debug("Rows", "Error converting numeric to float", value)
+ t.log.Debug("Rows", "Error converting numeric to float", value)
}
default:
- e.log.Debug("Rows", "Unknown database type", types[i].DatabaseTypeName(), "value", value)
+ t.log.Debug("Rows", "Unknown database type", columnTypes[i].DatabaseTypeName(), "value", value)
values[i] = string(value)
}
}
@@ -162,157 +98,3 @@ func (e MssqlQueryEndpoint) getTypedRowData(types []*sql.ColumnType, rows *core.
return values, nil
}
-
-func (e MssqlQueryEndpoint) transformToTimeSeries(query *tsdb.Query, rows *core.Rows, result *tsdb.QueryResult, tsdbQuery *tsdb.TsdbQuery) error {
- pointsBySeries := make(map[string]*tsdb.TimeSeries)
- seriesByQueryOrder := list.New()
-
- columnNames, err := rows.Columns()
- if err != nil {
- return err
- }
-
- columnTypes, err := rows.ColumnTypes()
- if err != nil {
- return err
- }
-
- rowLimit := 1000000
- rowCount := 0
- timeIndex := -1
- metricIndex := -1
-
- // check columns of resultset: a column named time is mandatory
- // the first text column is treated as metric name unless a column named metric is present
- for i, col := range columnNames {
- switch col {
- case "time":
- timeIndex = i
- case "metric":
- metricIndex = i
- default:
- if metricIndex == -1 {
- switch columnTypes[i].DatabaseTypeName() {
- case "VARCHAR", "CHAR", "NVARCHAR", "NCHAR":
- metricIndex = i
- }
- }
- }
- }
-
- if timeIndex == -1 {
- return fmt.Errorf("Found no column named time")
- }
-
- fillMissing := query.Model.Get("fill").MustBool(false)
- var fillInterval float64
- fillValue := null.Float{}
- if fillMissing {
- fillInterval = query.Model.Get("fillInterval").MustFloat64() * 1000
- if !query.Model.Get("fillNull").MustBool(false) {
- fillValue.Float64 = query.Model.Get("fillValue").MustFloat64()
- fillValue.Valid = true
- }
- }
-
- for rows.Next() {
- var timestamp float64
- var value null.Float
- var metric string
-
- if rowCount > rowLimit {
- return fmt.Errorf("MSSQL query row limit exceeded, limit %d", rowLimit)
- }
-
- values, err := e.getTypedRowData(columnTypes, rows)
- if err != nil {
- return err
- }
-
- // converts column named time to unix timestamp in milliseconds to make
- // native mysql datetime types and epoch dates work in
- // annotation and table queries.
- tsdb.ConvertSqlTimeColumnToEpochMs(values, timeIndex)
-
- switch columnValue := values[timeIndex].(type) {
- case int64:
- timestamp = float64(columnValue)
- case float64:
- timestamp = columnValue
- default:
- return fmt.Errorf("Invalid type for column time, must be of type timestamp or unix timestamp, got: %T %v", columnValue, columnValue)
- }
-
- if metricIndex >= 0 {
- if columnValue, ok := values[metricIndex].(string); ok {
- metric = columnValue
- } else {
- return fmt.Errorf("Column metric must be of type CHAR, VARCHAR, NCHAR or NVARCHAR. metric column name: %s type: %s but datatype is %T", columnNames[metricIndex], columnTypes[metricIndex].DatabaseTypeName(), values[metricIndex])
- }
- }
-
- for i, col := range columnNames {
- if i == timeIndex || i == metricIndex {
- continue
- }
-
- if value, err = tsdb.ConvertSqlValueColumnToFloat(col, values[i]); err != nil {
- return err
- }
-
- if metricIndex == -1 {
- metric = col
- }
-
- series, exist := pointsBySeries[metric]
- if !exist {
- series = &tsdb.TimeSeries{Name: metric}
- pointsBySeries[metric] = series
- seriesByQueryOrder.PushBack(metric)
- }
-
- if fillMissing {
- var intervalStart float64
- if !exist {
- intervalStart = float64(tsdbQuery.TimeRange.MustGetFrom().UnixNano() / 1e6)
- } else {
- intervalStart = series.Points[len(series.Points)-1][1].Float64 + fillInterval
- }
-
- // align interval start
- intervalStart = math.Floor(intervalStart/fillInterval) * fillInterval
-
- for i := intervalStart; i < timestamp; i += fillInterval {
- series.Points = append(series.Points, tsdb.TimePoint{fillValue, null.FloatFrom(i)})
- rowCount++
- }
- }
-
- series.Points = append(series.Points, tsdb.TimePoint{value, null.FloatFrom(timestamp)})
-
- e.log.Debug("Rows", "metric", metric, "time", timestamp, "value", value)
- }
- }
-
- for elem := seriesByQueryOrder.Front(); elem != nil; elem = elem.Next() {
- key := elem.Value.(string)
- result.Series = append(result.Series, pointsBySeries[key])
-
- if fillMissing {
- series := pointsBySeries[key]
- // fill in values from last fetched value till interval end
- intervalStart := series.Points[len(series.Points)-1][1].Float64
- intervalEnd := float64(tsdbQuery.TimeRange.MustGetTo().UnixNano() / 1e6)
-
- // align interval start
- intervalStart = math.Floor(intervalStart/fillInterval) * fillInterval
- for i := intervalStart + fillInterval; i < intervalEnd; i += fillInterval {
- series.Points = append(series.Points, tsdb.TimePoint{fillValue, null.FloatFrom(i)})
- rowCount++
- }
- }
- }
-
- result.Meta.Set("rowCount", rowCount)
- return nil
-}
diff --git a/pkg/tsdb/mssql/mssql_test.go b/pkg/tsdb/mssql/mssql_test.go
index db04d6d1f02..f9525fc37ac 100644
--- a/pkg/tsdb/mssql/mssql_test.go
+++ b/pkg/tsdb/mssql/mssql_test.go
@@ -8,8 +8,9 @@ import (
"time"
"github.com/go-xorm/xorm"
+ "github.com/grafana/grafana/pkg/components/securejsondata"
"github.com/grafana/grafana/pkg/components/simplejson"
- "github.com/grafana/grafana/pkg/log"
+ "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/sqlstore/sqlutil"
"github.com/grafana/grafana/pkg/tsdb"
. "github.com/smartystreets/goconvey/convey"
@@ -19,8 +20,9 @@ import (
// The tests require a MSSQL db named grafanatest and a user/password grafana/Password!
// Use the docker/blocks/mssql_tests/docker-compose.yaml to spin up a
// preconfigured MSSQL server suitable for running these tests.
-// There is also a dashboard.json in same directory that you can import to Grafana
-// once you've created a datasource for the test server/database.
+// There is also a datasource and dashboard provisioned by devenv scripts that you can
+// use to verify that the generated data are vizualized as expected, see
+// devenv/README.md for setup instructions.
// If needed, change the variable below to the IP address of the database.
var serverIP = "localhost"
@@ -28,19 +30,31 @@ func TestMSSQL(t *testing.T) {
SkipConvey("MSSQL", t, func() {
x := InitMSSQLTestDB(t)
- endpoint := &MssqlQueryEndpoint{
- sqlEngine: &tsdb.DefaultSqlEngine{
- MacroEngine: NewMssqlMacroEngine(),
- XormEngine: x,
- },
- log: log.New("tsdb.mssql"),
+ origXormEngine := tsdb.NewXormEngine
+ tsdb.NewXormEngine = func(d, c string) (*xorm.Engine, error) {
+ return x, nil
}
- sess := x.NewSession()
- defer sess.Close()
+ origInterpolate := tsdb.Interpolate
+ tsdb.Interpolate = func(query *tsdb.Query, timeRange *tsdb.TimeRange, sql string) (string, error) {
+ return sql, nil
+ }
+ endpoint, err := newMssqlQueryEndpoint(&models.DataSource{
+ JsonData: simplejson.New(),
+ SecureJsonData: securejsondata.SecureJsonData{},
+ })
+ So(err, ShouldBeNil)
+
+ sess := x.NewSession()
fromStart := time.Date(2018, 3, 15, 13, 0, 0, 0, time.UTC).In(time.Local)
+ Reset(func() {
+ sess.Close()
+ tsdb.NewXormEngine = origXormEngine
+ tsdb.Interpolate = origInterpolate
+ })
+
Convey("Given a table with different native data types", func() {
sql := `
IF OBJECT_ID('dbo.[mssql_types]', 'U') IS NOT NULL
@@ -287,6 +301,40 @@ func TestMSSQL(t *testing.T) {
})
+ Convey("When doing a metric query using timeGroup and $__interval", func() {
+ mockInterpolate := tsdb.Interpolate
+ tsdb.Interpolate = origInterpolate
+
+ Reset(func() {
+ tsdb.Interpolate = mockInterpolate
+ })
+
+ Convey("Should replace $__interval", func() {
+ query := &tsdb.TsdbQuery{
+ Queries: []*tsdb.Query{
+ {
+ DataSource: &models.DataSource{},
+ Model: simplejson.NewFromAny(map[string]interface{}{
+ "rawSql": "SELECT $__timeGroup(time, $__interval) AS time, avg(value) as value FROM metric GROUP BY $__timeGroup(time, $__interval) ORDER BY 1",
+ "format": "time_series",
+ }),
+ RefId: "A",
+ },
+ },
+ TimeRange: &tsdb.TimeRange{
+ From: fmt.Sprintf("%v", fromStart.Unix()*1000),
+ To: fmt.Sprintf("%v", fromStart.Add(30*time.Minute).Unix()*1000),
+ },
+ }
+
+ resp, err := endpoint.Query(nil, nil, query)
+ So(err, ShouldBeNil)
+ queryResult := resp.Results["A"]
+ So(queryResult.Error, ShouldBeNil)
+ So(queryResult.Meta.Get("sql").MustString(), ShouldEqual, "SELECT FLOOR(DATEDIFF(second, '1970-01-01', time)/60)*60 AS time, avg(value) as value FROM metric GROUP BY FLOOR(DATEDIFF(second, '1970-01-01', time)/60)*60 ORDER BY 1")
+ })
+ })
+
Convey("When doing a metric query using timeGroup with float fill enabled", func() {
query := &tsdb.TsdbQuery{
Queries: []*tsdb.Query{
@@ -602,6 +650,31 @@ func TestMSSQL(t *testing.T) {
So(queryResult.Series[1].Name, ShouldEqual, "valueTwo")
})
+ Convey("When doing a metric query with metric column and multiple value columns", func() {
+ query := &tsdb.TsdbQuery{
+ Queries: []*tsdb.Query{
+ {
+ Model: simplejson.NewFromAny(map[string]interface{}{
+ "rawSql": "SELECT $__timeEpoch(time), measurement, valueOne, valueTwo FROM metric_values ORDER BY 1",
+ "format": "time_series",
+ }),
+ RefId: "A",
+ },
+ },
+ }
+
+ resp, err := endpoint.Query(nil, nil, query)
+ So(err, ShouldBeNil)
+ queryResult := resp.Results["A"]
+ So(queryResult.Error, ShouldBeNil)
+
+ So(len(queryResult.Series), ShouldEqual, 4)
+ So(queryResult.Series[0].Name, ShouldEqual, "Metric A valueOne")
+ So(queryResult.Series[1].Name, ShouldEqual, "Metric A valueTwo")
+ So(queryResult.Series[2].Name, ShouldEqual, "Metric B valueOne")
+ So(queryResult.Series[3].Name, ShouldEqual, "Metric B valueTwo")
+ })
+
Convey("Given a stored procedure that takes @from and @to in epoch time", func() {
sql := `
IF object_id('sp_test_epoch') IS NOT NULL
@@ -627,21 +700,9 @@ func TestMSSQL(t *testing.T) {
SELECT
CAST(ROUND(DATEDIFF(second, '1970-01-01', time)/CAST(@dInterval as float), 0) as bigint)*@dInterval as time,
- measurement + ' - value one' as metric,
- avg(valueOne) as value
- FROM
- metric_values
- WHERE
- time BETWEEN DATEADD(s, @from, '1970-01-01') AND DATEADD(s, @to, '1970-01-01') AND
- (@metric = 'ALL' OR measurement = @metric)
- GROUP BY
- CAST(ROUND(DATEDIFF(second, '1970-01-01', time)/CAST(@dInterval as float), 0) as bigint)*@dInterval,
- measurement
- UNION ALL
- SELECT
- CAST(ROUND(DATEDIFF(second, '1970-01-01', time)/CAST(@dInterval as float), 0) as bigint)*@dInterval as time,
- measurement + ' - value two' as metric,
- avg(valueTwo) as value
+ measurement as metric,
+ avg(valueOne) as valueOne,
+ avg(valueTwo) as valueTwo
FROM
metric_values
WHERE
@@ -684,10 +745,10 @@ func TestMSSQL(t *testing.T) {
So(queryResult.Error, ShouldBeNil)
So(len(queryResult.Series), ShouldEqual, 4)
- So(queryResult.Series[0].Name, ShouldEqual, "Metric A - value one")
- So(queryResult.Series[1].Name, ShouldEqual, "Metric B - value one")
- So(queryResult.Series[2].Name, ShouldEqual, "Metric A - value two")
- So(queryResult.Series[3].Name, ShouldEqual, "Metric B - value two")
+ So(queryResult.Series[0].Name, ShouldEqual, "Metric A valueOne")
+ So(queryResult.Series[1].Name, ShouldEqual, "Metric A valueTwo")
+ So(queryResult.Series[2].Name, ShouldEqual, "Metric B valueOne")
+ So(queryResult.Series[3].Name, ShouldEqual, "Metric B valueTwo")
})
})
@@ -716,21 +777,9 @@ func TestMSSQL(t *testing.T) {
SELECT
CAST(ROUND(DATEDIFF(second, '1970-01-01', time)/CAST(@dInterval as float), 0) as bigint)*@dInterval as time,
- measurement + ' - value one' as metric,
- avg(valueOne) as value
- FROM
- metric_values
- WHERE
- time BETWEEN @from AND @to AND
- (@metric = 'ALL' OR measurement = @metric)
- GROUP BY
- CAST(ROUND(DATEDIFF(second, '1970-01-01', time)/CAST(@dInterval as float), 0) as bigint)*@dInterval,
- measurement
- UNION ALL
- SELECT
- CAST(ROUND(DATEDIFF(second, '1970-01-01', time)/CAST(@dInterval as float), 0) as bigint)*@dInterval as time,
- measurement + ' - value two' as metric,
- avg(valueTwo) as value
+ measurement as metric,
+ avg(valueOne) as valueOne,
+ avg(valueTwo) as valueTwo
FROM
metric_values
WHERE
@@ -773,10 +822,10 @@ func TestMSSQL(t *testing.T) {
So(queryResult.Error, ShouldBeNil)
So(len(queryResult.Series), ShouldEqual, 4)
- So(queryResult.Series[0].Name, ShouldEqual, "Metric A - value one")
- So(queryResult.Series[1].Name, ShouldEqual, "Metric B - value one")
- So(queryResult.Series[2].Name, ShouldEqual, "Metric A - value two")
- So(queryResult.Series[3].Name, ShouldEqual, "Metric B - value two")
+ So(queryResult.Series[0].Name, ShouldEqual, "Metric A valueOne")
+ So(queryResult.Series[1].Name, ShouldEqual, "Metric A valueTwo")
+ So(queryResult.Series[2].Name, ShouldEqual, "Metric B valueOne")
+ So(queryResult.Series[3].Name, ShouldEqual, "Metric B valueTwo")
})
})
})
diff --git a/pkg/tsdb/mysql/macros.go b/pkg/tsdb/mysql/macros.go
index 584f731f3b8..0f1c4fcaf2c 100644
--- a/pkg/tsdb/mysql/macros.go
+++ b/pkg/tsdb/mysql/macros.go
@@ -3,33 +3,32 @@ package mysql
import (
"fmt"
"regexp"
- "strconv"
"strings"
"time"
"github.com/grafana/grafana/pkg/tsdb"
)
-//const rsString = `(?:"([^"]*)")`;
const rsIdentifier = `([_a-zA-Z0-9]+)`
const sExpr = `\$` + rsIdentifier + `\(([^\)]*)\)`
-type MySqlMacroEngine struct {
- TimeRange *tsdb.TimeRange
- Query *tsdb.Query
+type mySqlMacroEngine struct {
+ *tsdb.SqlMacroEngineBase
+ timeRange *tsdb.TimeRange
+ query *tsdb.Query
}
-func NewMysqlMacroEngine() tsdb.SqlMacroEngine {
- return &MySqlMacroEngine{}
+func newMysqlMacroEngine() tsdb.SqlMacroEngine {
+ return &mySqlMacroEngine{SqlMacroEngineBase: tsdb.NewSqlMacroEngineBase()}
}
-func (m *MySqlMacroEngine) Interpolate(query *tsdb.Query, timeRange *tsdb.TimeRange, sql string) (string, error) {
- m.TimeRange = timeRange
- m.Query = query
+func (m *mySqlMacroEngine) Interpolate(query *tsdb.Query, timeRange *tsdb.TimeRange, sql string) (string, error) {
+ m.timeRange = timeRange
+ m.query = query
rExp, _ := regexp.Compile(sExpr)
var macroError error
- sql = replaceAllStringSubmatchFunc(rExp, sql, func(groups []string) string {
+ sql = m.ReplaceAllStringSubmatchFunc(rExp, sql, func(groups []string) string {
args := strings.Split(groups[2], ",")
for i, arg := range args {
args[i] = strings.Trim(arg, " ")
@@ -49,24 +48,7 @@ func (m *MySqlMacroEngine) Interpolate(query *tsdb.Query, timeRange *tsdb.TimeRa
return sql, nil
}
-func replaceAllStringSubmatchFunc(re *regexp.Regexp, str string, repl func([]string) string) string {
- result := ""
- lastIndex := 0
-
- for _, v := range re.FindAllSubmatchIndex([]byte(str), -1) {
- groups := []string{}
- for i := 0; i < len(v); i += 2 {
- groups = append(groups, str[v[i]:v[i+1]])
- }
-
- result += str[lastIndex:v[0]] + repl(groups)
- lastIndex = v[1]
- }
-
- return result + str[lastIndex:]
-}
-
-func (m *MySqlMacroEngine) evaluateMacro(name string, args []string) (string, error) {
+func (m *mySqlMacroEngine) evaluateMacro(name string, args []string) (string, error) {
switch name {
case "__timeEpoch", "__time":
if len(args) == 0 {
@@ -78,11 +60,11 @@ func (m *MySqlMacroEngine) evaluateMacro(name string, args []string) (string, er
return "", fmt.Errorf("missing time column argument for macro %v", name)
}
- return fmt.Sprintf("%s BETWEEN '%s' AND '%s'", args[0], m.TimeRange.GetFromAsTimeUTC().Format(time.RFC3339), m.TimeRange.GetToAsTimeUTC().Format(time.RFC3339)), nil
+ return fmt.Sprintf("%s BETWEEN '%s' AND '%s'", args[0], m.timeRange.GetFromAsTimeUTC().Format(time.RFC3339), m.timeRange.GetToAsTimeUTC().Format(time.RFC3339)), nil
case "__timeFrom":
- return fmt.Sprintf("'%s'", m.TimeRange.GetFromAsTimeUTC().Format(time.RFC3339)), nil
+ return fmt.Sprintf("'%s'", m.timeRange.GetFromAsTimeUTC().Format(time.RFC3339)), nil
case "__timeTo":
- return fmt.Sprintf("'%s'", m.TimeRange.GetToAsTimeUTC().Format(time.RFC3339)), nil
+ return fmt.Sprintf("'%s'", m.timeRange.GetToAsTimeUTC().Format(time.RFC3339)), nil
case "__timeGroup":
if len(args) < 2 {
return "", fmt.Errorf("macro %v needs time column and interval", name)
@@ -92,28 +74,48 @@ func (m *MySqlMacroEngine) evaluateMacro(name string, args []string) (string, er
return "", fmt.Errorf("error parsing interval %v", args[1])
}
if len(args) == 3 {
- m.Query.Model.Set("fill", true)
- m.Query.Model.Set("fillInterval", interval.Seconds())
- if args[2] == "NULL" {
- m.Query.Model.Set("fillNull", true)
- } else {
- floatVal, err := strconv.ParseFloat(args[2], 64)
- if err != nil {
- return "", fmt.Errorf("error parsing fill value %v", args[2])
- }
- m.Query.Model.Set("fillValue", floatVal)
+ err := tsdb.SetupFillmode(m.query, interval, args[2])
+ if err != nil {
+ return "", err
}
}
return fmt.Sprintf("UNIX_TIMESTAMP(%s) DIV %.0f * %.0f", args[0], interval.Seconds(), interval.Seconds()), nil
+ case "__timeGroupAlias":
+ tg, err := m.evaluateMacro("__timeGroup", args)
+ if err == nil {
+ return tg + " AS \"time\"", err
+ }
+ return "", err
case "__unixEpochFilter":
if len(args) == 0 {
return "", fmt.Errorf("missing time column argument for macro %v", name)
}
- return fmt.Sprintf("%s >= %d AND %s <= %d", args[0], m.TimeRange.GetFromAsSecondsEpoch(), args[0], m.TimeRange.GetToAsSecondsEpoch()), nil
+ return fmt.Sprintf("%s >= %d AND %s <= %d", args[0], m.timeRange.GetFromAsSecondsEpoch(), args[0], m.timeRange.GetToAsSecondsEpoch()), nil
case "__unixEpochFrom":
- return fmt.Sprintf("%d", m.TimeRange.GetFromAsSecondsEpoch()), nil
+ return fmt.Sprintf("%d", m.timeRange.GetFromAsSecondsEpoch()), nil
case "__unixEpochTo":
- return fmt.Sprintf("%d", m.TimeRange.GetToAsSecondsEpoch()), nil
+ return fmt.Sprintf("%d", m.timeRange.GetToAsSecondsEpoch()), nil
+ case "__unixEpochGroup":
+ if len(args) < 2 {
+ return "", fmt.Errorf("macro %v needs time column and interval and optional fill value", name)
+ }
+ interval, err := time.ParseDuration(strings.Trim(args[1], `'`))
+ if err != nil {
+ return "", fmt.Errorf("error parsing interval %v", args[1])
+ }
+ if len(args) == 3 {
+ err := tsdb.SetupFillmode(m.query, interval, args[2])
+ if err != nil {
+ return "", err
+ }
+ }
+ return fmt.Sprintf("%s DIV %v * %v", args[0], interval.Seconds(), interval.Seconds()), nil
+ case "__unixEpochGroupAlias":
+ tg, err := m.evaluateMacro("__unixEpochGroup", args)
+ if err == nil {
+ return tg + " AS \"time\"", err
+ }
+ return "", err
default:
return "", fmt.Errorf("Unknown macro %v", name)
}
diff --git a/pkg/tsdb/mysql/macros_test.go b/pkg/tsdb/mysql/macros_test.go
index 2561661b385..fe153ca3e2d 100644
--- a/pkg/tsdb/mysql/macros_test.go
+++ b/pkg/tsdb/mysql/macros_test.go
@@ -12,7 +12,7 @@ import (
func TestMacroEngine(t *testing.T) {
Convey("MacroEngine", t, func() {
- engine := &MySqlMacroEngine{}
+ engine := &mySqlMacroEngine{}
query := &tsdb.Query{}
Convey("Given a time range between 2018-04-12 00:00 and 2018-04-12 00:05", func() {
@@ -38,16 +38,22 @@ func TestMacroEngine(t *testing.T) {
sql, err := engine.Interpolate(query, timeRange, "GROUP BY $__timeGroup(time_column,'5m')")
So(err, ShouldBeNil)
+ sql2, err := engine.Interpolate(query, timeRange, "GROUP BY $__timeGroupAlias(time_column,'5m')")
+ So(err, ShouldBeNil)
So(sql, ShouldEqual, "GROUP BY UNIX_TIMESTAMP(time_column) DIV 300 * 300")
+ So(sql2, ShouldEqual, sql+" AS \"time\"")
})
Convey("interpolate __timeGroup function with spaces around arguments", func() {
sql, err := engine.Interpolate(query, timeRange, "GROUP BY $__timeGroup(time_column , '5m')")
So(err, ShouldBeNil)
+ sql2, err := engine.Interpolate(query, timeRange, "GROUP BY $__timeGroupAlias(time_column , '5m')")
+ So(err, ShouldBeNil)
So(sql, ShouldEqual, "GROUP BY UNIX_TIMESTAMP(time_column) DIV 300 * 300")
+ So(sql2, ShouldEqual, sql+" AS \"time\"")
})
Convey("interpolate __timeFilter function", func() {
@@ -91,6 +97,18 @@ func TestMacroEngine(t *testing.T) {
So(sql, ShouldEqual, fmt.Sprintf("select %d", to.Unix()))
})
+
+ Convey("interpolate __unixEpochGroup function", func() {
+
+ sql, err := engine.Interpolate(query, timeRange, "SELECT $__unixEpochGroup(time_column,'5m')")
+ So(err, ShouldBeNil)
+ sql2, err := engine.Interpolate(query, timeRange, "SELECT $__unixEpochGroupAlias(time_column,'5m')")
+ So(err, ShouldBeNil)
+
+ So(sql, ShouldEqual, "SELECT time_column DIV 300 * 300")
+ So(sql2, ShouldEqual, sql+" AS \"time\"")
+ })
+
})
Convey("Given a time range between 1960-02-01 07:00 and 1965-02-03 08:00", func() {
diff --git a/pkg/tsdb/mysql/mysql.go b/pkg/tsdb/mysql/mysql.go
index 7eceaffdb09..645f6b49bbb 100644
--- a/pkg/tsdb/mysql/mysql.go
+++ b/pkg/tsdb/mysql/mysql.go
@@ -1,39 +1,24 @@
package mysql
import (
- "container/list"
- "context"
"database/sql"
"fmt"
- "math"
"reflect"
"strconv"
"github.com/go-sql-driver/mysql"
"github.com/go-xorm/core"
- "github.com/grafana/grafana/pkg/components/null"
"github.com/grafana/grafana/pkg/log"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/tsdb"
)
-type MysqlQueryEndpoint struct {
- sqlEngine tsdb.SqlEngine
- log log.Logger
-}
-
func init() {
- tsdb.RegisterTsdbQueryEndpoint("mysql", NewMysqlQueryEndpoint)
+ tsdb.RegisterTsdbQueryEndpoint("mysql", newMysqlQueryEndpoint)
}
-func NewMysqlQueryEndpoint(datasource *models.DataSource) (tsdb.TsdbQueryEndpoint, error) {
- endpoint := &MysqlQueryEndpoint{
- log: log.New("tsdb.mysql"),
- }
-
- endpoint.sqlEngine = &tsdb.DefaultSqlEngine{
- MacroEngine: NewMysqlMacroEngine(),
- }
+func newMysqlQueryEndpoint(datasource *models.DataSource) (tsdb.TsdbQueryEndpoint, error) {
+ logger := log.New("tsdb.mysql")
cnnstr := fmt.Sprintf("%s:%s@%s(%s)/%s?collation=utf8mb4_unicode_ci&parseTime=true&loc=UTC&allowNativePasswords=true",
datasource.User,
@@ -42,85 +27,35 @@ func NewMysqlQueryEndpoint(datasource *models.DataSource) (tsdb.TsdbQueryEndpoin
datasource.Url,
datasource.Database,
)
- endpoint.log.Debug("getEngine", "connection", cnnstr)
+ logger.Debug("getEngine", "connection", cnnstr)
- if err := endpoint.sqlEngine.InitEngine("mysql", datasource, cnnstr); err != nil {
- return nil, err
+ config := tsdb.SqlQueryEndpointConfiguration{
+ DriverName: "mysql",
+ ConnectionString: cnnstr,
+ Datasource: datasource,
+ TimeColumnNames: []string{"time", "time_sec"},
+ MetricColumnTypes: []string{"CHAR", "VARCHAR", "TINYTEXT", "TEXT", "MEDIUMTEXT", "LONGTEXT"},
}
- return endpoint, nil
+ rowTransformer := mysqlRowTransformer{
+ log: logger,
+ }
+
+ return tsdb.NewSqlQueryEndpoint(&config, &rowTransformer, newMysqlMacroEngine(), logger)
}
-// Query is the main function for the MysqlExecutor
-func (e *MysqlQueryEndpoint) Query(ctx context.Context, dsInfo *models.DataSource, tsdbQuery *tsdb.TsdbQuery) (*tsdb.Response, error) {
- return e.sqlEngine.Query(ctx, dsInfo, tsdbQuery, e.transformToTimeSeries, e.transformToTable)
+type mysqlRowTransformer struct {
+ log log.Logger
}
-func (e MysqlQueryEndpoint) transformToTable(query *tsdb.Query, rows *core.Rows, result *tsdb.QueryResult, tsdbQuery *tsdb.TsdbQuery) error {
- columnNames, err := rows.Columns()
- columnCount := len(columnNames)
-
- if err != nil {
- return err
- }
-
- table := &tsdb.Table{
- Columns: make([]tsdb.TableColumn, columnCount),
- Rows: make([]tsdb.RowValues, 0),
- }
-
- for i, name := range columnNames {
- table.Columns[i].Text = name
- }
-
- rowLimit := 1000000
- rowCount := 0
- timeIndex := -1
-
- // check if there is a column named time
- for i, col := range columnNames {
- switch col {
- case "time", "time_sec":
- timeIndex = i
- }
- }
-
- for ; rows.Next(); rowCount++ {
- if rowCount > rowLimit {
- return fmt.Errorf("MySQL query row limit exceeded, limit %d", rowLimit)
- }
-
- values, err := e.getTypedRowData(rows)
- if err != nil {
- return err
- }
-
- // converts column named time to unix timestamp in milliseconds to make
- // native mysql datetime types and epoch dates work in
- // annotation and table queries.
- tsdb.ConvertSqlTimeColumnToEpochMs(values, timeIndex)
-
- table.Rows = append(table.Rows, values)
- }
-
- result.Tables = append(result.Tables, table)
- result.Meta.Set("rowCount", rowCount)
- return nil
-}
-
-func (e MysqlQueryEndpoint) getTypedRowData(rows *core.Rows) (tsdb.RowValues, error) {
- types, err := rows.ColumnTypes()
- if err != nil {
- return nil, err
- }
-
- values := make([]interface{}, len(types))
+func (t *mysqlRowTransformer) Transform(columnTypes []*sql.ColumnType, rows *core.Rows) (tsdb.RowValues, error) {
+ values := make([]interface{}, len(columnTypes))
for i := range values {
- scanType := types[i].ScanType()
+ scanType := columnTypes[i].ScanType()
values[i] = reflect.New(scanType).Interface()
- if types[i].DatabaseTypeName() == "BIT" {
+ if columnTypes[i].DatabaseTypeName() == "BIT" {
values[i] = new([]byte)
}
}
@@ -129,7 +64,7 @@ func (e MysqlQueryEndpoint) getTypedRowData(rows *core.Rows) (tsdb.RowValues, er
return nil, err
}
- for i := 0; i < len(types); i++ {
+ for i := 0; i < len(columnTypes); i++ {
typeName := reflect.ValueOf(values[i]).Type().String()
switch typeName {
@@ -158,7 +93,7 @@ func (e MysqlQueryEndpoint) getTypedRowData(rows *core.Rows) (tsdb.RowValues, er
}
}
- if types[i].DatabaseTypeName() == "DECIMAL" {
+ if columnTypes[i].DatabaseTypeName() == "DECIMAL" {
f, err := strconv.ParseFloat(values[i].(string), 64)
if err == nil {
@@ -171,159 +106,3 @@ func (e MysqlQueryEndpoint) getTypedRowData(rows *core.Rows) (tsdb.RowValues, er
return values, nil
}
-
-func (e MysqlQueryEndpoint) transformToTimeSeries(query *tsdb.Query, rows *core.Rows, result *tsdb.QueryResult, tsdbQuery *tsdb.TsdbQuery) error {
- pointsBySeries := make(map[string]*tsdb.TimeSeries)
- seriesByQueryOrder := list.New()
-
- columnNames, err := rows.Columns()
- if err != nil {
- return err
- }
-
- columnTypes, err := rows.ColumnTypes()
- if err != nil {
- return err
- }
-
- rowLimit := 1000000
- rowCount := 0
- timeIndex := -1
- metricIndex := -1
-
- // check columns of resultset: a column named time is mandatory
- // the first text column is treated as metric name unless a column named metric is present
- for i, col := range columnNames {
- switch col {
- case "time", "time_sec":
- timeIndex = i
- case "metric":
- metricIndex = i
- default:
- if metricIndex == -1 {
- switch columnTypes[i].DatabaseTypeName() {
- case "CHAR", "VARCHAR", "TINYTEXT", "TEXT", "MEDIUMTEXT", "LONGTEXT":
- metricIndex = i
- }
- }
- }
- }
-
- if timeIndex == -1 {
- return fmt.Errorf("Found no column named time or time_sec")
- }
-
- fillMissing := query.Model.Get("fill").MustBool(false)
- var fillInterval float64
- fillValue := null.Float{}
- if fillMissing {
- fillInterval = query.Model.Get("fillInterval").MustFloat64() * 1000
- if !query.Model.Get("fillNull").MustBool(false) {
- fillValue.Float64 = query.Model.Get("fillValue").MustFloat64()
- fillValue.Valid = true
- }
- }
-
- for rows.Next() {
- var timestamp float64
- var value null.Float
- var metric string
-
- if rowCount > rowLimit {
- return fmt.Errorf("PostgreSQL query row limit exceeded, limit %d", rowLimit)
- }
-
- values, err := e.getTypedRowData(rows)
- if err != nil {
- return err
- }
-
- // converts column named time to unix timestamp in milliseconds to make
- // native mysql datetime types and epoch dates work in
- // annotation and table queries.
- tsdb.ConvertSqlTimeColumnToEpochMs(values, timeIndex)
-
- switch columnValue := values[timeIndex].(type) {
- case int64:
- timestamp = float64(columnValue)
- case float64:
- timestamp = columnValue
- default:
- return fmt.Errorf("Invalid type for column time/time_sec, must be of type timestamp or unix timestamp, got: %T %v", columnValue, columnValue)
- }
-
- if metricIndex >= 0 {
- if columnValue, ok := values[metricIndex].(string); ok {
- metric = columnValue
- } else {
- return fmt.Errorf("Column metric must be of type char,varchar or text, got: %T %v", values[metricIndex], values[metricIndex])
- }
- }
-
- for i, col := range columnNames {
- if i == timeIndex || i == metricIndex {
- continue
- }
-
- if value, err = tsdb.ConvertSqlValueColumnToFloat(col, values[i]); err != nil {
- return err
- }
-
- if metricIndex == -1 {
- metric = col
- }
-
- series, exist := pointsBySeries[metric]
- if !exist {
- series = &tsdb.TimeSeries{Name: metric}
- pointsBySeries[metric] = series
- seriesByQueryOrder.PushBack(metric)
- }
-
- if fillMissing {
- var intervalStart float64
- if !exist {
- intervalStart = float64(tsdbQuery.TimeRange.MustGetFrom().UnixNano() / 1e6)
- } else {
- intervalStart = series.Points[len(series.Points)-1][1].Float64 + fillInterval
- }
-
- // align interval start
- intervalStart = math.Floor(intervalStart/fillInterval) * fillInterval
-
- for i := intervalStart; i < timestamp; i += fillInterval {
- series.Points = append(series.Points, tsdb.TimePoint{fillValue, null.FloatFrom(i)})
- rowCount++
- }
- }
-
- series.Points = append(series.Points, tsdb.TimePoint{value, null.FloatFrom(timestamp)})
-
- e.log.Debug("Rows", "metric", metric, "time", timestamp, "value", value)
- rowCount++
-
- }
- }
-
- for elem := seriesByQueryOrder.Front(); elem != nil; elem = elem.Next() {
- key := elem.Value.(string)
- result.Series = append(result.Series, pointsBySeries[key])
-
- if fillMissing {
- series := pointsBySeries[key]
- // fill in values from last fetched value till interval end
- intervalStart := series.Points[len(series.Points)-1][1].Float64
- intervalEnd := float64(tsdbQuery.TimeRange.MustGetTo().UnixNano() / 1e6)
-
- // align interval start
- intervalStart = math.Floor(intervalStart/fillInterval) * fillInterval
- for i := intervalStart + fillInterval; i < intervalEnd; i += fillInterval {
- series.Points = append(series.Points, tsdb.TimePoint{fillValue, null.FloatFrom(i)})
- rowCount++
- }
- }
- }
-
- result.Meta.Set("rowCount", rowCount)
- return nil
-}
diff --git a/pkg/tsdb/mysql/mysql_test.go b/pkg/tsdb/mysql/mysql_test.go
index 850a37617e2..13d9040a738 100644
--- a/pkg/tsdb/mysql/mysql_test.go
+++ b/pkg/tsdb/mysql/mysql_test.go
@@ -8,8 +8,9 @@ import (
"time"
"github.com/go-xorm/xorm"
+ "github.com/grafana/grafana/pkg/components/securejsondata"
"github.com/grafana/grafana/pkg/components/simplejson"
- "github.com/grafana/grafana/pkg/log"
+ "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/sqlstore"
"github.com/grafana/grafana/pkg/services/sqlstore/sqlutil"
"github.com/grafana/grafana/pkg/tsdb"
@@ -21,8 +22,9 @@ import (
// The tests require a MySQL db named grafana_ds_tests and a user/password grafana/password
// Use the docker/blocks/mysql_tests/docker-compose.yaml to spin up a
// preconfigured MySQL server suitable for running these tests.
-// There is also a dashboard.json in same directory that you can import to Grafana
-// once you've created a datasource for the test server/database.
+// There is also a datasource and dashboard provisioned by devenv scripts that you can
+// use to verify that the generated data are vizualized as expected, see
+// devenv/README.md for setup instructions.
func TestMySQL(t *testing.T) {
// change to true to run the MySQL tests
runMySqlTests := false
@@ -35,19 +37,31 @@ func TestMySQL(t *testing.T) {
Convey("MySQL", t, func() {
x := InitMySQLTestDB(t)
- endpoint := &MysqlQueryEndpoint{
- sqlEngine: &tsdb.DefaultSqlEngine{
- MacroEngine: NewMysqlMacroEngine(),
- XormEngine: x,
- },
- log: log.New("tsdb.mysql"),
+ origXormEngine := tsdb.NewXormEngine
+ tsdb.NewXormEngine = func(d, c string) (*xorm.Engine, error) {
+ return x, nil
}
- sess := x.NewSession()
- defer sess.Close()
+ origInterpolate := tsdb.Interpolate
+ tsdb.Interpolate = func(query *tsdb.Query, timeRange *tsdb.TimeRange, sql string) (string, error) {
+ return sql, nil
+ }
+ endpoint, err := newMysqlQueryEndpoint(&models.DataSource{
+ JsonData: simplejson.New(),
+ SecureJsonData: securejsondata.SecureJsonData{},
+ })
+ So(err, ShouldBeNil)
+
+ sess := x.NewSession()
fromStart := time.Date(2018, 3, 15, 13, 0, 0, 0, time.UTC)
+ Reset(func() {
+ sess.Close()
+ tsdb.NewXormEngine = origXormEngine
+ tsdb.Interpolate = origInterpolate
+ })
+
Convey("Given a table with different native data types", func() {
if exists, err := sess.IsTableExist("mysql_types"); err != nil || exists {
So(err, ShouldBeNil)
@@ -287,7 +301,41 @@ func TestMySQL(t *testing.T) {
})
- Convey("When doing a metric query using timeGroup with float fill enabled", func() {
+ Convey("When doing a metric query using timeGroup and $__interval", func() {
+ mockInterpolate := tsdb.Interpolate
+ tsdb.Interpolate = origInterpolate
+
+ Reset(func() {
+ tsdb.Interpolate = mockInterpolate
+ })
+
+ Convey("Should replace $__interval", func() {
+ query := &tsdb.TsdbQuery{
+ Queries: []*tsdb.Query{
+ {
+ DataSource: &models.DataSource{},
+ Model: simplejson.NewFromAny(map[string]interface{}{
+ "rawSql": "SELECT $__timeGroup(time, $__interval) AS time, avg(value) as value FROM metric GROUP BY 1 ORDER BY 1",
+ "format": "time_series",
+ }),
+ RefId: "A",
+ },
+ },
+ TimeRange: &tsdb.TimeRange{
+ From: fmt.Sprintf("%v", fromStart.Unix()*1000),
+ To: fmt.Sprintf("%v", fromStart.Add(30*time.Minute).Unix()*1000),
+ },
+ }
+
+ resp, err := endpoint.Query(nil, nil, query)
+ So(err, ShouldBeNil)
+ queryResult := resp.Results["A"]
+ So(queryResult.Error, ShouldBeNil)
+ So(queryResult.Meta.Get("sql").MustString(), ShouldEqual, "SELECT UNIX_TIMESTAMP(time) DIV 60 * 60 AS time, avg(value) as value FROM metric GROUP BY 1 ORDER BY 1")
+ })
+ })
+
+ Convey("When doing a metric query using timeGroup with value fill enabled", func() {
query := &tsdb.TsdbQuery{
Queries: []*tsdb.Query{
{
@@ -312,6 +360,35 @@ func TestMySQL(t *testing.T) {
points := queryResult.Series[0].Points
So(points[3][0].Float64, ShouldEqual, 1.5)
})
+
+ Convey("When doing a metric query using timeGroup with previous fill enabled", func() {
+ query := &tsdb.TsdbQuery{
+ Queries: []*tsdb.Query{
+ {
+ Model: simplejson.NewFromAny(map[string]interface{}{
+ "rawSql": "SELECT $__timeGroup(time, '5m', previous) as time_sec, avg(value) as value FROM metric GROUP BY 1 ORDER BY 1",
+ "format": "time_series",
+ }),
+ RefId: "A",
+ },
+ },
+ TimeRange: &tsdb.TimeRange{
+ From: fmt.Sprintf("%v", fromStart.Unix()*1000),
+ To: fmt.Sprintf("%v", fromStart.Add(34*time.Minute).Unix()*1000),
+ },
+ }
+
+ resp, err := endpoint.Query(nil, nil, query)
+ So(err, ShouldBeNil)
+ queryResult := resp.Results["A"]
+ So(queryResult.Error, ShouldBeNil)
+
+ points := queryResult.Series[0].Points
+ So(points[2][0].Float64, ShouldEqual, 15.0)
+ So(points[3][0].Float64, ShouldEqual, 15.0)
+ So(points[6][0].Float64, ShouldEqual, 20.0)
+ })
+
})
Convey("Given a table with metrics having multiple values and measurements", func() {
@@ -626,6 +703,31 @@ func TestMySQL(t *testing.T) {
So(queryResult.Series[1].Name, ShouldEqual, "Metric B - value one")
})
+ Convey("When doing a metric query with metric column and multiple value columns", func() {
+ query := &tsdb.TsdbQuery{
+ Queries: []*tsdb.Query{
+ {
+ Model: simplejson.NewFromAny(map[string]interface{}{
+ "rawSql": `SELECT $__time(time), measurement as metric, valueOne, valueTwo FROM metric_values ORDER BY 1,2`,
+ "format": "time_series",
+ }),
+ RefId: "A",
+ },
+ },
+ }
+
+ resp, err := endpoint.Query(nil, nil, query)
+ So(err, ShouldBeNil)
+ queryResult := resp.Results["A"]
+ So(queryResult.Error, ShouldBeNil)
+
+ So(len(queryResult.Series), ShouldEqual, 4)
+ So(queryResult.Series[0].Name, ShouldEqual, "Metric A valueOne")
+ So(queryResult.Series[1].Name, ShouldEqual, "Metric A valueTwo")
+ So(queryResult.Series[2].Name, ShouldEqual, "Metric B valueOne")
+ So(queryResult.Series[3].Name, ShouldEqual, "Metric B valueTwo")
+ })
+
Convey("When doing a metric query grouping by time should return correct series", func() {
query := &tsdb.TsdbQuery{
Queries: []*tsdb.Query{
diff --git a/pkg/tsdb/postgres/macros.go b/pkg/tsdb/postgres/macros.go
index 61e88418ff4..16f4adb68a6 100644
--- a/pkg/tsdb/postgres/macros.go
+++ b/pkg/tsdb/postgres/macros.go
@@ -3,33 +3,53 @@ package postgres
import (
"fmt"
"regexp"
- "strconv"
"strings"
"time"
"github.com/grafana/grafana/pkg/tsdb"
)
-//const rsString = `(?:"([^"]*)")`;
const rsIdentifier = `([_a-zA-Z0-9]+)`
const sExpr = `\$` + rsIdentifier + `\(([^\)]*)\)`
-type PostgresMacroEngine struct {
- TimeRange *tsdb.TimeRange
- Query *tsdb.Query
+type postgresMacroEngine struct {
+ *tsdb.SqlMacroEngineBase
+ timeRange *tsdb.TimeRange
+ query *tsdb.Query
+ timescaledb bool
}
-func NewPostgresMacroEngine() tsdb.SqlMacroEngine {
- return &PostgresMacroEngine{}
+func newPostgresMacroEngine(timescaledb bool) tsdb.SqlMacroEngine {
+ return &postgresMacroEngine{
+ SqlMacroEngineBase: tsdb.NewSqlMacroEngineBase(),
+ timescaledb: timescaledb,
+ }
}
-func (m *PostgresMacroEngine) Interpolate(query *tsdb.Query, timeRange *tsdb.TimeRange, sql string) (string, error) {
- m.TimeRange = timeRange
- m.Query = query
+func (m *postgresMacroEngine) Interpolate(query *tsdb.Query, timeRange *tsdb.TimeRange, sql string) (string, error) {
+ m.timeRange = timeRange
+ m.query = query
rExp, _ := regexp.Compile(sExpr)
var macroError error
- sql = replaceAllStringSubmatchFunc(rExp, sql, func(groups []string) string {
+ sql = m.ReplaceAllStringSubmatchFunc(rExp, sql, func(groups []string) string {
+
+ // detect if $__timeGroup is supposed to add AS time for pre 5.3 compatibility
+ // if there is a ',' directly after the macro call $__timeGroup is probably used
+ // in the old way. Inside window function ORDER BY $__timeGroup will be followed
+ // by ')'
+ if groups[1] == "__timeGroup" {
+ if index := strings.Index(sql, groups[0]); index >= 0 {
+ index += len(groups[0])
+ if len(sql) > index {
+ // check for character after macro expression
+ if sql[index] == ',' {
+ groups[1] = "__timeGroupAlias"
+ }
+ }
+ }
+ }
+
args := strings.Split(groups[2], ",")
for i, arg := range args {
args[i] = strings.Trim(arg, " ")
@@ -49,24 +69,7 @@ func (m *PostgresMacroEngine) Interpolate(query *tsdb.Query, timeRange *tsdb.Tim
return sql, nil
}
-func replaceAllStringSubmatchFunc(re *regexp.Regexp, str string, repl func([]string) string) string {
- result := ""
- lastIndex := 0
-
- for _, v := range re.FindAllSubmatchIndex([]byte(str), -1) {
- groups := []string{}
- for i := 0; i < len(v); i += 2 {
- groups = append(groups, str[v[i]:v[i+1]])
- }
-
- result += str[lastIndex:v[0]] + repl(groups)
- lastIndex = v[1]
- }
-
- return result + str[lastIndex:]
-}
-
-func (m *PostgresMacroEngine) evaluateMacro(name string, args []string) (string, error) {
+func (m *postgresMacroEngine) evaluateMacro(name string, args []string) (string, error) {
switch name {
case "__time":
if len(args) == 0 {
@@ -83,11 +86,11 @@ func (m *PostgresMacroEngine) evaluateMacro(name string, args []string) (string,
return "", fmt.Errorf("missing time column argument for macro %v", name)
}
- return fmt.Sprintf("%s BETWEEN '%s' AND '%s'", args[0], m.TimeRange.GetFromAsTimeUTC().Format(time.RFC3339), m.TimeRange.GetToAsTimeUTC().Format(time.RFC3339)), nil
+ return fmt.Sprintf("%s BETWEEN '%s' AND '%s'", args[0], m.timeRange.GetFromAsTimeUTC().Format(time.RFC3339), m.timeRange.GetToAsTimeUTC().Format(time.RFC3339)), nil
case "__timeFrom":
- return fmt.Sprintf("'%s'", m.TimeRange.GetFromAsTimeUTC().Format(time.RFC3339)), nil
+ return fmt.Sprintf("'%s'", m.timeRange.GetFromAsTimeUTC().Format(time.RFC3339)), nil
case "__timeTo":
- return fmt.Sprintf("'%s'", m.TimeRange.GetToAsTimeUTC().Format(time.RFC3339)), nil
+ return fmt.Sprintf("'%s'", m.timeRange.GetToAsTimeUTC().Format(time.RFC3339)), nil
case "__timeGroup":
if len(args) < 2 {
return "", fmt.Errorf("macro %v needs time column and interval and optional fill value", name)
@@ -97,28 +100,53 @@ func (m *PostgresMacroEngine) evaluateMacro(name string, args []string) (string,
return "", fmt.Errorf("error parsing interval %v", args[1])
}
if len(args) == 3 {
- m.Query.Model.Set("fill", true)
- m.Query.Model.Set("fillInterval", interval.Seconds())
- if args[2] == "NULL" {
- m.Query.Model.Set("fillNull", true)
- } else {
- floatVal, err := strconv.ParseFloat(args[2], 64)
- if err != nil {
- return "", fmt.Errorf("error parsing fill value %v", args[2])
- }
- m.Query.Model.Set("fillValue", floatVal)
+ err := tsdb.SetupFillmode(m.query, interval, args[2])
+ if err != nil {
+ return "", err
}
}
- return fmt.Sprintf("floor(extract(epoch from %s)/%v)*%v AS time", args[0], interval.Seconds(), interval.Seconds()), nil
+
+ if m.timescaledb {
+ return fmt.Sprintf("time_bucket('%vs',%s)", interval.Seconds(), args[0]), nil
+ } else {
+ return fmt.Sprintf("floor(extract(epoch from %s)/%v)*%v", args[0], interval.Seconds(), interval.Seconds()), nil
+ }
+ case "__timeGroupAlias":
+ tg, err := m.evaluateMacro("__timeGroup", args)
+ if err == nil {
+ return tg + " AS \"time\"", err
+ }
+ return "", err
case "__unixEpochFilter":
if len(args) == 0 {
return "", fmt.Errorf("missing time column argument for macro %v", name)
}
- return fmt.Sprintf("%s >= %d AND %s <= %d", args[0], m.TimeRange.GetFromAsSecondsEpoch(), args[0], m.TimeRange.GetToAsSecondsEpoch()), nil
+ return fmt.Sprintf("%s >= %d AND %s <= %d", args[0], m.timeRange.GetFromAsSecondsEpoch(), args[0], m.timeRange.GetToAsSecondsEpoch()), nil
case "__unixEpochFrom":
- return fmt.Sprintf("%d", m.TimeRange.GetFromAsSecondsEpoch()), nil
+ return fmt.Sprintf("%d", m.timeRange.GetFromAsSecondsEpoch()), nil
case "__unixEpochTo":
- return fmt.Sprintf("%d", m.TimeRange.GetToAsSecondsEpoch()), nil
+ return fmt.Sprintf("%d", m.timeRange.GetToAsSecondsEpoch()), nil
+ case "__unixEpochGroup":
+ if len(args) < 2 {
+ return "", fmt.Errorf("macro %v needs time column and interval and optional fill value", name)
+ }
+ interval, err := time.ParseDuration(strings.Trim(args[1], `'`))
+ if err != nil {
+ return "", fmt.Errorf("error parsing interval %v", args[1])
+ }
+ if len(args) == 3 {
+ err := tsdb.SetupFillmode(m.query, interval, args[2])
+ if err != nil {
+ return "", err
+ }
+ }
+ return fmt.Sprintf("floor(%s/%v)*%v", args[0], interval.Seconds(), interval.Seconds()), nil
+ case "__unixEpochGroupAlias":
+ tg, err := m.evaluateMacro("__unixEpochGroup", args)
+ if err == nil {
+ return tg + " AS \"time\"", err
+ }
+ return "", err
default:
return "", fmt.Errorf("Unknown macro %v", name)
}
diff --git a/pkg/tsdb/postgres/macros_test.go b/pkg/tsdb/postgres/macros_test.go
index 8c581850430..b0b7a28ddd4 100644
--- a/pkg/tsdb/postgres/macros_test.go
+++ b/pkg/tsdb/postgres/macros_test.go
@@ -12,7 +12,10 @@ import (
func TestMacroEngine(t *testing.T) {
Convey("MacroEngine", t, func() {
- engine := NewPostgresMacroEngine()
+ timescaledbEnabled := false
+ engine := newPostgresMacroEngine(timescaledbEnabled)
+ timescaledbEnabled = true
+ engineTS := newPostgresMacroEngine(timescaledbEnabled)
query := &tsdb.Query{}
Convey("Given a time range between 2018-04-12 00:00 and 2018-04-12 00:05", func() {
@@ -48,20 +51,55 @@ func TestMacroEngine(t *testing.T) {
So(sql, ShouldEqual, fmt.Sprintf("select '%s'", from.Format(time.RFC3339)))
})
- Convey("interpolate __timeGroup function", func() {
+ Convey("interpolate __timeGroup function pre 5.3 compatibility", func() {
- sql, err := engine.Interpolate(query, timeRange, "GROUP BY $__timeGroup(time_column,'5m')")
+ sql, err := engine.Interpolate(query, timeRange, "SELECT $__timeGroup(time_column,'5m'), value")
So(err, ShouldBeNil)
- So(sql, ShouldEqual, "GROUP BY floor(extract(epoch from time_column)/300)*300 AS time")
+ So(sql, ShouldEqual, "SELECT floor(extract(epoch from time_column)/300)*300 AS \"time\", value")
+
+ sql, err = engine.Interpolate(query, timeRange, "SELECT $__timeGroup(time_column,'5m') as time, value")
+ So(err, ShouldBeNil)
+
+ So(sql, ShouldEqual, "SELECT floor(extract(epoch from time_column)/300)*300 as time, value")
+ })
+
+ Convey("interpolate __timeGroup function", func() {
+
+ sql, err := engine.Interpolate(query, timeRange, "SELECT $__timeGroup(time_column,'5m')")
+ So(err, ShouldBeNil)
+ sql2, err := engine.Interpolate(query, timeRange, "SELECT $__timeGroupAlias(time_column,'5m')")
+ So(err, ShouldBeNil)
+
+ So(sql, ShouldEqual, "SELECT floor(extract(epoch from time_column)/300)*300")
+ So(sql2, ShouldEqual, sql+" AS \"time\"")
})
Convey("interpolate __timeGroup function with spaces between args", func() {
- sql, err := engine.Interpolate(query, timeRange, "GROUP BY $__timeGroup(time_column , '5m')")
+ sql, err := engine.Interpolate(query, timeRange, "$__timeGroup(time_column , '5m')")
+ So(err, ShouldBeNil)
+ sql2, err := engine.Interpolate(query, timeRange, "$__timeGroupAlias(time_column , '5m')")
So(err, ShouldBeNil)
- So(sql, ShouldEqual, "GROUP BY floor(extract(epoch from time_column)/300)*300 AS time")
+ So(sql, ShouldEqual, "floor(extract(epoch from time_column)/300)*300")
+ So(sql2, ShouldEqual, sql+" AS \"time\"")
+ })
+
+ Convey("interpolate __timeGroup function with TimescaleDB enabled", func() {
+
+ sql, err := engineTS.Interpolate(query, timeRange, "GROUP BY $__timeGroup(time_column,'5m')")
+ So(err, ShouldBeNil)
+
+ So(sql, ShouldEqual, "GROUP BY time_bucket('300s',time_column)")
+ })
+
+ Convey("interpolate __timeGroup function with spaces between args and TimescaleDB enabled", func() {
+
+ sql, err := engineTS.Interpolate(query, timeRange, "GROUP BY $__timeGroup(time_column , '5m')")
+ So(err, ShouldBeNil)
+
+ So(sql, ShouldEqual, "GROUP BY time_bucket('300s',time_column)")
})
Convey("interpolate __timeTo function", func() {
@@ -91,6 +129,18 @@ func TestMacroEngine(t *testing.T) {
So(sql, ShouldEqual, fmt.Sprintf("select %d", to.Unix()))
})
+
+ Convey("interpolate __unixEpochGroup function", func() {
+
+ sql, err := engine.Interpolate(query, timeRange, "SELECT $__unixEpochGroup(time_column,'5m')")
+ So(err, ShouldBeNil)
+ sql2, err := engine.Interpolate(query, timeRange, "SELECT $__unixEpochGroupAlias(time_column,'5m')")
+ So(err, ShouldBeNil)
+
+ So(sql, ShouldEqual, "SELECT floor(time_column/300)*300")
+ So(sql2, ShouldEqual, sql+" AS \"time\"")
+ })
+
})
Convey("Given a time range between 1960-02-01 07:00 and 1965-02-03 08:00", func() {
diff --git a/pkg/tsdb/postgres/postgres.go b/pkg/tsdb/postgres/postgres.go
index f19e4fb54f4..4bcf06638f4 100644
--- a/pkg/tsdb/postgres/postgres.go
+++ b/pkg/tsdb/postgres/postgres.go
@@ -1,46 +1,40 @@
package postgres
import (
- "container/list"
- "context"
- "fmt"
- "math"
+ "database/sql"
"net/url"
"strconv"
"github.com/go-xorm/core"
- "github.com/grafana/grafana/pkg/components/null"
"github.com/grafana/grafana/pkg/log"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/tsdb"
)
-type PostgresQueryEndpoint struct {
- sqlEngine tsdb.SqlEngine
- log log.Logger
-}
-
func init() {
- tsdb.RegisterTsdbQueryEndpoint("postgres", NewPostgresQueryEndpoint)
+ tsdb.RegisterTsdbQueryEndpoint("postgres", newPostgresQueryEndpoint)
}
-func NewPostgresQueryEndpoint(datasource *models.DataSource) (tsdb.TsdbQueryEndpoint, error) {
- endpoint := &PostgresQueryEndpoint{
- log: log.New("tsdb.postgres"),
- }
-
- endpoint.sqlEngine = &tsdb.DefaultSqlEngine{
- MacroEngine: NewPostgresMacroEngine(),
- }
+func newPostgresQueryEndpoint(datasource *models.DataSource) (tsdb.TsdbQueryEndpoint, error) {
+ logger := log.New("tsdb.postgres")
cnnstr := generateConnectionString(datasource)
- endpoint.log.Debug("getEngine", "connection", cnnstr)
+ logger.Debug("getEngine", "connection", cnnstr)
- if err := endpoint.sqlEngine.InitEngine("postgres", datasource, cnnstr); err != nil {
- return nil, err
+ config := tsdb.SqlQueryEndpointConfiguration{
+ DriverName: "postgres",
+ ConnectionString: cnnstr,
+ Datasource: datasource,
+ MetricColumnTypes: []string{"UNKNOWN", "TEXT", "VARCHAR", "CHAR"},
}
- return endpoint, nil
+ rowTransformer := postgresRowTransformer{
+ log: logger,
+ }
+
+ timescaledb := datasource.JsonData.Get("timescaledb").MustBool(false)
+
+ return tsdb.NewSqlQueryEndpoint(&config, &rowTransformer, newPostgresMacroEngine(timescaledb), logger)
}
func generateConnectionString(datasource *models.DataSource) string {
@@ -63,70 +57,15 @@ func generateConnectionString(datasource *models.DataSource) string {
return u.String()
}
-func (e *PostgresQueryEndpoint) Query(ctx context.Context, dsInfo *models.DataSource, tsdbQuery *tsdb.TsdbQuery) (*tsdb.Response, error) {
- return e.sqlEngine.Query(ctx, dsInfo, tsdbQuery, e.transformToTimeSeries, e.transformToTable)
+type postgresRowTransformer struct {
+ log log.Logger
}
-func (e PostgresQueryEndpoint) transformToTable(query *tsdb.Query, rows *core.Rows, result *tsdb.QueryResult, tsdbQuery *tsdb.TsdbQuery) error {
- columnNames, err := rows.Columns()
- if err != nil {
- return err
- }
+func (t *postgresRowTransformer) Transform(columnTypes []*sql.ColumnType, rows *core.Rows) (tsdb.RowValues, error) {
+ values := make([]interface{}, len(columnTypes))
+ valuePtrs := make([]interface{}, len(columnTypes))
- table := &tsdb.Table{
- Columns: make([]tsdb.TableColumn, len(columnNames)),
- Rows: make([]tsdb.RowValues, 0),
- }
-
- for i, name := range columnNames {
- table.Columns[i].Text = name
- }
-
- rowLimit := 1000000
- rowCount := 0
- timeIndex := -1
-
- // check if there is a column named time
- for i, col := range columnNames {
- switch col {
- case "time":
- timeIndex = i
- }
- }
-
- for ; rows.Next(); rowCount++ {
- if rowCount > rowLimit {
- return fmt.Errorf("PostgreSQL query row limit exceeded, limit %d", rowLimit)
- }
-
- values, err := e.getTypedRowData(rows)
- if err != nil {
- return err
- }
-
- // converts column named time to unix timestamp in milliseconds to make
- // native postgres datetime types and epoch dates work in
- // annotation and table queries.
- tsdb.ConvertSqlTimeColumnToEpochMs(values, timeIndex)
-
- table.Rows = append(table.Rows, values)
- }
-
- result.Tables = append(result.Tables, table)
- result.Meta.Set("rowCount", rowCount)
- return nil
-}
-
-func (e PostgresQueryEndpoint) getTypedRowData(rows *core.Rows) (tsdb.RowValues, error) {
- types, err := rows.ColumnTypes()
- if err != nil {
- return nil, err
- }
-
- values := make([]interface{}, len(types))
- valuePtrs := make([]interface{}, len(types))
-
- for i := 0; i < len(types); i++ {
+ for i := 0; i < len(columnTypes); i++ {
valuePtrs[i] = &values[i]
}
@@ -136,20 +75,20 @@ func (e PostgresQueryEndpoint) getTypedRowData(rows *core.Rows) (tsdb.RowValues,
// convert types not handled by lib/pq
// unhandled types are returned as []byte
- for i := 0; i < len(types); i++ {
+ for i := 0; i < len(columnTypes); i++ {
if value, ok := values[i].([]byte); ok {
- switch types[i].DatabaseTypeName() {
+ switch columnTypes[i].DatabaseTypeName() {
case "NUMERIC":
if v, err := strconv.ParseFloat(string(value), 64); err == nil {
values[i] = v
} else {
- e.log.Debug("Rows", "Error converting numeric to float", value)
+ t.log.Debug("Rows", "Error converting numeric to float", value)
}
case "UNKNOWN", "CIDR", "INET", "MACADDR":
// char literals have type UNKNOWN
values[i] = string(value)
default:
- e.log.Debug("Rows", "Unknown database type", types[i].DatabaseTypeName(), "value", value)
+ t.log.Debug("Rows", "Unknown database type", columnTypes[i].DatabaseTypeName(), "value", value)
values[i] = string(value)
}
}
@@ -157,159 +96,3 @@ func (e PostgresQueryEndpoint) getTypedRowData(rows *core.Rows) (tsdb.RowValues,
return values, nil
}
-
-func (e PostgresQueryEndpoint) transformToTimeSeries(query *tsdb.Query, rows *core.Rows, result *tsdb.QueryResult, tsdbQuery *tsdb.TsdbQuery) error {
- pointsBySeries := make(map[string]*tsdb.TimeSeries)
- seriesByQueryOrder := list.New()
-
- columnNames, err := rows.Columns()
- if err != nil {
- return err
- }
-
- columnTypes, err := rows.ColumnTypes()
- if err != nil {
- return err
- }
-
- rowLimit := 1000000
- rowCount := 0
- timeIndex := -1
- metricIndex := -1
-
- // check columns of resultset: a column named time is mandatory
- // the first text column is treated as metric name unless a column named metric is present
- for i, col := range columnNames {
- switch col {
- case "time":
- timeIndex = i
- case "metric":
- metricIndex = i
- default:
- if metricIndex == -1 {
- switch columnTypes[i].DatabaseTypeName() {
- case "UNKNOWN", "TEXT", "VARCHAR", "CHAR":
- metricIndex = i
- }
- }
- }
- }
-
- if timeIndex == -1 {
- return fmt.Errorf("Found no column named time")
- }
-
- fillMissing := query.Model.Get("fill").MustBool(false)
- var fillInterval float64
- fillValue := null.Float{}
- if fillMissing {
- fillInterval = query.Model.Get("fillInterval").MustFloat64() * 1000
- if !query.Model.Get("fillNull").MustBool(false) {
- fillValue.Float64 = query.Model.Get("fillValue").MustFloat64()
- fillValue.Valid = true
- }
- }
-
- for rows.Next() {
- var timestamp float64
- var value null.Float
- var metric string
-
- if rowCount > rowLimit {
- return fmt.Errorf("PostgreSQL query row limit exceeded, limit %d", rowLimit)
- }
-
- values, err := e.getTypedRowData(rows)
- if err != nil {
- return err
- }
-
- // converts column named time to unix timestamp in milliseconds to make
- // native mysql datetime types and epoch dates work in
- // annotation and table queries.
- tsdb.ConvertSqlTimeColumnToEpochMs(values, timeIndex)
-
- switch columnValue := values[timeIndex].(type) {
- case int64:
- timestamp = float64(columnValue)
- case float64:
- timestamp = columnValue
- default:
- return fmt.Errorf("Invalid type for column time, must be of type timestamp or unix timestamp, got: %T %v", columnValue, columnValue)
- }
-
- if metricIndex >= 0 {
- if columnValue, ok := values[metricIndex].(string); ok {
- metric = columnValue
- } else {
- return fmt.Errorf("Column metric must be of type char,varchar or text, got: %T %v", values[metricIndex], values[metricIndex])
- }
- }
-
- for i, col := range columnNames {
- if i == timeIndex || i == metricIndex {
- continue
- }
-
- if value, err = tsdb.ConvertSqlValueColumnToFloat(col, values[i]); err != nil {
- return err
- }
-
- if metricIndex == -1 {
- metric = col
- }
-
- series, exist := pointsBySeries[metric]
- if !exist {
- series = &tsdb.TimeSeries{Name: metric}
- pointsBySeries[metric] = series
- seriesByQueryOrder.PushBack(metric)
- }
-
- if fillMissing {
- var intervalStart float64
- if !exist {
- intervalStart = float64(tsdbQuery.TimeRange.MustGetFrom().UnixNano() / 1e6)
- } else {
- intervalStart = series.Points[len(series.Points)-1][1].Float64 + fillInterval
- }
-
- // align interval start
- intervalStart = math.Floor(intervalStart/fillInterval) * fillInterval
-
- for i := intervalStart; i < timestamp; i += fillInterval {
- series.Points = append(series.Points, tsdb.TimePoint{fillValue, null.FloatFrom(i)})
- rowCount++
- }
- }
-
- series.Points = append(series.Points, tsdb.TimePoint{value, null.FloatFrom(timestamp)})
-
- e.log.Debug("Rows", "metric", metric, "time", timestamp, "value", value)
- rowCount++
-
- }
- }
-
- for elem := seriesByQueryOrder.Front(); elem != nil; elem = elem.Next() {
- key := elem.Value.(string)
- result.Series = append(result.Series, pointsBySeries[key])
-
- if fillMissing {
- series := pointsBySeries[key]
- // fill in values from last fetched value till interval end
- intervalStart := series.Points[len(series.Points)-1][1].Float64
- intervalEnd := float64(tsdbQuery.TimeRange.MustGetTo().UnixNano() / 1e6)
-
- // align interval start
- intervalStart = math.Floor(intervalStart/fillInterval) * fillInterval
- for i := intervalStart + fillInterval; i < intervalEnd; i += fillInterval {
- series.Points = append(series.Points, tsdb.TimePoint{fillValue, null.FloatFrom(i)})
- rowCount++
- }
- }
- }
-
- result.Meta.Set("rowCount", rowCount)
- return nil
-}
diff --git a/pkg/tsdb/postgres/postgres_test.go b/pkg/tsdb/postgres/postgres_test.go
index a3a6d6546df..fc1a5f34253 100644
--- a/pkg/tsdb/postgres/postgres_test.go
+++ b/pkg/tsdb/postgres/postgres_test.go
@@ -8,8 +8,9 @@ import (
"time"
"github.com/go-xorm/xorm"
+ "github.com/grafana/grafana/pkg/components/securejsondata"
"github.com/grafana/grafana/pkg/components/simplejson"
- "github.com/grafana/grafana/pkg/log"
+ "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/sqlstore"
"github.com/grafana/grafana/pkg/services/sqlstore/sqlutil"
"github.com/grafana/grafana/pkg/tsdb"
@@ -22,10 +23,11 @@ import (
// The tests require a PostgreSQL db named grafanadstest and a user/password grafanatest/grafanatest!
// Use the docker/blocks/postgres_tests/docker-compose.yaml to spin up a
// preconfigured Postgres server suitable for running these tests.
-// There is also a dashboard.json in same directory that you can import to Grafana
-// once you've created a datasource for the test server/database.
+// There is also a datasource and dashboard provisioned by devenv scripts that you can
+// use to verify that the generated data are vizualized as expected, see
+// devenv/README.md for setup instructions.
func TestPostgres(t *testing.T) {
- // change to true to run the MySQL tests
+ // change to true to run the PostgreSQL tests
runPostgresTests := false
// runPostgresTests := true
@@ -36,19 +38,31 @@ func TestPostgres(t *testing.T) {
Convey("PostgreSQL", t, func() {
x := InitPostgresTestDB(t)
- endpoint := &PostgresQueryEndpoint{
- sqlEngine: &tsdb.DefaultSqlEngine{
- MacroEngine: NewPostgresMacroEngine(),
- XormEngine: x,
- },
- log: log.New("tsdb.postgres"),
+ origXormEngine := tsdb.NewXormEngine
+ tsdb.NewXormEngine = func(d, c string) (*xorm.Engine, error) {
+ return x, nil
}
- sess := x.NewSession()
- defer sess.Close()
+ origInterpolate := tsdb.Interpolate
+ tsdb.Interpolate = func(query *tsdb.Query, timeRange *tsdb.TimeRange, sql string) (string, error) {
+ return sql, nil
+ }
+ endpoint, err := newPostgresQueryEndpoint(&models.DataSource{
+ JsonData: simplejson.New(),
+ SecureJsonData: securejsondata.SecureJsonData{},
+ })
+ So(err, ShouldBeNil)
+
+ sess := x.NewSession()
fromStart := time.Date(2018, 3, 15, 13, 0, 0, 0, time.UTC).In(time.Local)
+ Reset(func() {
+ sess.Close()
+ tsdb.NewXormEngine = origXormEngine
+ tsdb.Interpolate = origInterpolate
+ })
+
Convey("Given a table with different native data types", func() {
sql := `
DROP TABLE IF EXISTS postgres_types;
@@ -175,7 +189,7 @@ func TestPostgres(t *testing.T) {
Queries: []*tsdb.Query{
{
Model: simplejson.NewFromAny(map[string]interface{}{
- "rawSql": "SELECT $__timeGroup(time, '5m'), avg(value) as value FROM metric GROUP BY 1 ORDER BY 1",
+ "rawSql": "SELECT $__timeGroup(time, '5m') AS time, avg(value) as value FROM metric GROUP BY 1 ORDER BY 1",
"format": "time_series",
}),
RefId: "A",
@@ -214,12 +228,46 @@ func TestPostgres(t *testing.T) {
}
})
+ Convey("When doing a metric query using timeGroup and $__interval", func() {
+ mockInterpolate := tsdb.Interpolate
+ tsdb.Interpolate = origInterpolate
+
+ Reset(func() {
+ tsdb.Interpolate = mockInterpolate
+ })
+
+ Convey("Should replace $__interval", func() {
+ query := &tsdb.TsdbQuery{
+ Queries: []*tsdb.Query{
+ {
+ DataSource: &models.DataSource{},
+ Model: simplejson.NewFromAny(map[string]interface{}{
+ "rawSql": "SELECT $__timeGroup(time, $__interval) AS time, avg(value) as value FROM metric GROUP BY 1 ORDER BY 1",
+ "format": "time_series",
+ }),
+ RefId: "A",
+ },
+ },
+ TimeRange: &tsdb.TimeRange{
+ From: fmt.Sprintf("%v", fromStart.Unix()*1000),
+ To: fmt.Sprintf("%v", fromStart.Add(30*time.Minute).Unix()*1000),
+ },
+ }
+
+ resp, err := endpoint.Query(nil, nil, query)
+ So(err, ShouldBeNil)
+ queryResult := resp.Results["A"]
+ So(queryResult.Error, ShouldBeNil)
+ So(queryResult.Meta.Get("sql").MustString(), ShouldEqual, "SELECT floor(extract(epoch from time)/60)*60 AS time, avg(value) as value FROM metric GROUP BY 1 ORDER BY 1")
+ })
+ })
+
Convey("When doing a metric query using timeGroup with NULL fill enabled", func() {
query := &tsdb.TsdbQuery{
Queries: []*tsdb.Query{
{
Model: simplejson.NewFromAny(map[string]interface{}{
- "rawSql": "SELECT $__timeGroup(time, '5m', NULL), avg(value) as value FROM metric GROUP BY 1 ORDER BY 1",
+ "rawSql": "SELECT $__timeGroup(time, '5m', NULL) AS time, avg(value) as value FROM metric GROUP BY 1 ORDER BY 1",
"format": "time_series",
}),
RefId: "A",
@@ -268,12 +316,12 @@ func TestPostgres(t *testing.T) {
})
- Convey("When doing a metric query using timeGroup with float fill enabled", func() {
+ Convey("When doing a metric query using timeGroup with value fill enabled", func() {
query := &tsdb.TsdbQuery{
Queries: []*tsdb.Query{
{
Model: simplejson.NewFromAny(map[string]interface{}{
- "rawSql": "SELECT $__timeGroup(time, '5m', 1.5), avg(value) as value FROM metric GROUP BY 1 ORDER BY 1",
+ "rawSql": "SELECT $__timeGroup(time, '5m', 1.5) AS time, avg(value) as value FROM metric GROUP BY 1 ORDER BY 1",
"format": "time_series",
}),
RefId: "A",
@@ -295,6 +343,34 @@ func TestPostgres(t *testing.T) {
})
})
+ Convey("When doing a metric query using timeGroup with previous fill enabled", func() {
+ query := &tsdb.TsdbQuery{
+ Queries: []*tsdb.Query{
+ {
+ Model: simplejson.NewFromAny(map[string]interface{}{
+ "rawSql": "SELECT $__timeGroup(time, '5m', previous), avg(value) as value FROM metric GROUP BY 1 ORDER BY 1",
+ "format": "time_series",
+ }),
+ RefId: "A",
+ },
+ },
+ TimeRange: &tsdb.TimeRange{
+ From: fmt.Sprintf("%v", fromStart.Unix()*1000),
+ To: fmt.Sprintf("%v", fromStart.Add(34*time.Minute).Unix()*1000),
+ },
+ }
+
+ resp, err := endpoint.Query(nil, nil, query)
+ So(err, ShouldBeNil)
+ queryResult := resp.Results["A"]
+ So(queryResult.Error, ShouldBeNil)
+
+ points := queryResult.Series[0].Points
+ So(points[2][0].Float64, ShouldEqual, 15.0)
+ So(points[3][0].Float64, ShouldEqual, 15.0)
+ So(points[6][0].Float64, ShouldEqual, 20.0)
+ })
+
Convey("Given a table with metrics having multiple values and measurements", func() {
type metric_values struct {
Time time.Time
@@ -560,6 +636,31 @@ func TestPostgres(t *testing.T) {
So(queryResult.Series[1].Name, ShouldEqual, "Metric B - value one")
})
+ Convey("When doing a metric query with metric column and multiple value columns", func() {
+ query := &tsdb.TsdbQuery{
+ Queries: []*tsdb.Query{
+ {
+ Model: simplejson.NewFromAny(map[string]interface{}{
+ "rawSql": `SELECT $__timeEpoch(time), measurement as metric, "valueOne", "valueTwo" FROM metric_values ORDER BY 1`,
+ "format": "time_series",
+ }),
+ RefId: "A",
+ },
+ },
+ }
+
+ resp, err := endpoint.Query(nil, nil, query)
+ So(err, ShouldBeNil)
+ queryResult := resp.Results["A"]
+ So(queryResult.Error, ShouldBeNil)
+
+ So(len(queryResult.Series), ShouldEqual, 4)
+ So(queryResult.Series[0].Name, ShouldEqual, "Metric A valueOne")
+ So(queryResult.Series[1].Name, ShouldEqual, "Metric A valueTwo")
+ So(queryResult.Series[2].Name, ShouldEqual, "Metric B valueOne")
+ So(queryResult.Series[3].Name, ShouldEqual, "Metric B valueTwo")
+ })
+
Convey("When doing a metric query grouping by time should return correct series", func() {
query := &tsdb.TsdbQuery{
Queries: []*tsdb.Query{
diff --git a/pkg/tsdb/prometheus/prometheus.go b/pkg/tsdb/prometheus/prometheus.go
index bf9fe9f152c..83bb683fccf 100644
--- a/pkg/tsdb/prometheus/prometheus.go
+++ b/pkg/tsdb/prometheus/prometheus.go
@@ -92,12 +92,12 @@ func (e *PrometheusExecutor) Query(ctx context.Context, dsInfo *models.DataSourc
return nil, err
}
- querys, err := parseQuery(dsInfo, tsdbQuery.Queries, tsdbQuery)
+ queries, err := parseQuery(dsInfo, tsdbQuery.Queries, tsdbQuery)
if err != nil {
return nil, err
}
- for _, query := range querys {
+ for _, query := range queries {
timeRange := apiv1.Range{
Start: query.Start,
End: query.End,
diff --git a/pkg/tsdb/sql_engine.go b/pkg/tsdb/sql_engine.go
index ec908aeb9de..18e02e328d1 100644
--- a/pkg/tsdb/sql_engine.go
+++ b/pkg/tsdb/sql_engine.go
@@ -1,11 +1,19 @@
package tsdb
import (
+ "container/list"
"context"
+ "database/sql"
"fmt"
+ "math"
+ "regexp"
+ "strconv"
+ "strings"
"sync"
"time"
+ "github.com/grafana/grafana/pkg/log"
+
"github.com/grafana/grafana/pkg/components/null"
"github.com/go-xorm/core"
@@ -14,27 +22,15 @@ import (
"github.com/grafana/grafana/pkg/models"
)
-// SqlEngine is a wrapper class around xorm for relational database data sources.
-type SqlEngine interface {
- InitEngine(driverName string, dsInfo *models.DataSource, cnnstr string) error
- Query(
- ctx context.Context,
- ds *models.DataSource,
- query *TsdbQuery,
- transformToTimeSeries func(query *Query, rows *core.Rows, result *QueryResult, tsdbQuery *TsdbQuery) error,
- transformToTable func(query *Query, rows *core.Rows, result *QueryResult, tsdbQuery *TsdbQuery) error,
- ) (*Response, error)
-}
-
// SqlMacroEngine interpolates macros into sql. It takes in the Query to have access to query context and
// timeRange to be able to generate queries that use from and to.
type SqlMacroEngine interface {
Interpolate(query *Query, timeRange *TimeRange, sql string) (string, error)
}
-type DefaultSqlEngine struct {
- MacroEngine SqlMacroEngine
- XormEngine *xorm.Engine
+// SqlTableRowTransformer transforms a query result row to RowValues with proper types.
+type SqlTableRowTransformer interface {
+ Transform(columnTypes []*sql.ColumnType, rows *core.Rows) (RowValues, error)
}
type engineCacheType struct {
@@ -48,69 +44,108 @@ var engineCache = engineCacheType{
versions: make(map[int64]int),
}
-// InitEngine creates the db connection and inits the xorm engine or loads it from the engine cache
-func (e *DefaultSqlEngine) InitEngine(driverName string, dsInfo *models.DataSource, cnnstr string) error {
+var sqlIntervalCalculator = NewIntervalCalculator(nil)
+
+var NewXormEngine = func(driverName string, connectionString string) (*xorm.Engine, error) {
+ return xorm.NewEngine(driverName, connectionString)
+}
+
+type sqlQueryEndpoint struct {
+ macroEngine SqlMacroEngine
+ rowTransformer SqlTableRowTransformer
+ engine *xorm.Engine
+ timeColumnNames []string
+ metricColumnTypes []string
+ log log.Logger
+}
+
+type SqlQueryEndpointConfiguration struct {
+ DriverName string
+ Datasource *models.DataSource
+ ConnectionString string
+ TimeColumnNames []string
+ MetricColumnTypes []string
+}
+
+var NewSqlQueryEndpoint = func(config *SqlQueryEndpointConfiguration, rowTransformer SqlTableRowTransformer, macroEngine SqlMacroEngine, log log.Logger) (TsdbQueryEndpoint, error) {
+ queryEndpoint := sqlQueryEndpoint{
+ rowTransformer: rowTransformer,
+ macroEngine: macroEngine,
+ timeColumnNames: []string{"time"},
+ log: log,
+ }
+
+ if len(config.TimeColumnNames) > 0 {
+ queryEndpoint.timeColumnNames = config.TimeColumnNames
+ }
+
+ if len(config.MetricColumnTypes) > 0 {
+ queryEndpoint.metricColumnTypes = config.MetricColumnTypes
+ }
+
engineCache.Lock()
defer engineCache.Unlock()
- if engine, present := engineCache.cache[dsInfo.Id]; present {
- if version := engineCache.versions[dsInfo.Id]; version == dsInfo.Version {
- e.XormEngine = engine
- return nil
+ if engine, present := engineCache.cache[config.Datasource.Id]; present {
+ if version := engineCache.versions[config.Datasource.Id]; version == config.Datasource.Version {
+ queryEndpoint.engine = engine
+ return &queryEndpoint, nil
}
}
- engine, err := xorm.NewEngine(driverName, cnnstr)
+ engine, err := NewXormEngine(config.DriverName, config.ConnectionString)
if err != nil {
- return err
+ return nil, err
}
engine.SetMaxOpenConns(10)
engine.SetMaxIdleConns(10)
- engineCache.versions[dsInfo.Id] = dsInfo.Version
- engineCache.cache[dsInfo.Id] = engine
- e.XormEngine = engine
+ engineCache.versions[config.Datasource.Id] = config.Datasource.Version
+ engineCache.cache[config.Datasource.Id] = engine
+ queryEndpoint.engine = engine
- return nil
+ return &queryEndpoint, nil
}
-// Query is a default implementation of the Query method for an SQL data source.
-// The caller of this function must implement transformToTimeSeries and transformToTable and
-// pass them in as parameters.
-func (e *DefaultSqlEngine) Query(
- ctx context.Context,
- dsInfo *models.DataSource,
- tsdbQuery *TsdbQuery,
- transformToTimeSeries func(query *Query, rows *core.Rows, result *QueryResult, tsdbQuery *TsdbQuery) error,
- transformToTable func(query *Query, rows *core.Rows, result *QueryResult, tsdbQuery *TsdbQuery) error,
-) (*Response, error) {
+const rowLimit = 1000000
+
+// Query is the main function for the SqlQueryEndpoint
+func (e *sqlQueryEndpoint) Query(ctx context.Context, dsInfo *models.DataSource, tsdbQuery *TsdbQuery) (*Response, error) {
result := &Response{
Results: make(map[string]*QueryResult),
}
- session := e.XormEngine.NewSession()
+ session := e.engine.NewSession()
defer session.Close()
db := session.DB()
for _, query := range tsdbQuery.Queries {
- rawSql := query.Model.Get("rawSql").MustString()
- if rawSql == "" {
+ rawSQL := query.Model.Get("rawSql").MustString()
+ if rawSQL == "" {
continue
}
queryResult := &QueryResult{Meta: simplejson.New(), RefId: query.RefId}
result.Results[query.RefId] = queryResult
- rawSql, err := e.MacroEngine.Interpolate(query, tsdbQuery.TimeRange, rawSql)
+ // global substitutions
+ rawSQL, err := Interpolate(query, tsdbQuery.TimeRange, rawSQL)
if err != nil {
queryResult.Error = err
continue
}
- queryResult.Meta.Set("sql", rawSql)
+ // datasource specific substitutions
+ rawSQL, err = e.macroEngine.Interpolate(query, tsdbQuery.TimeRange, rawSQL)
+ if err != nil {
+ queryResult.Error = err
+ continue
+ }
- rows, err := db.Query(rawSql)
+ queryResult.Meta.Set("sql", rawSQL)
+
+ rows, err := db.Query(rawSQL)
if err != nil {
queryResult.Error = err
continue
@@ -122,13 +157,13 @@ func (e *DefaultSqlEngine) Query(
switch format {
case "time_series":
- err := transformToTimeSeries(query, rows, queryResult, tsdbQuery)
+ err := e.transformToTimeSeries(query, rows, queryResult, tsdbQuery)
if err != nil {
queryResult.Error = err
continue
}
case "table":
- err := transformToTable(query, rows, queryResult, tsdbQuery)
+ err := e.transformToTable(query, rows, queryResult, tsdbQuery)
if err != nil {
queryResult.Error = err
continue
@@ -139,6 +174,270 @@ func (e *DefaultSqlEngine) Query(
return result, nil
}
+// global macros/substitutions for all sql datasources
+var Interpolate = func(query *Query, timeRange *TimeRange, sql string) (string, error) {
+ minInterval, err := GetIntervalFrom(query.DataSource, query.Model, time.Second*60)
+ if err != nil {
+ return sql, nil
+ }
+ interval := sqlIntervalCalculator.Calculate(timeRange, minInterval)
+
+ sql = strings.Replace(sql, "$__interval_ms", strconv.FormatInt(interval.Milliseconds(), 10), -1)
+ sql = strings.Replace(sql, "$__interval", interval.Text, -1)
+
+ return sql, nil
+}
+
+func (e *sqlQueryEndpoint) transformToTable(query *Query, rows *core.Rows, result *QueryResult, tsdbQuery *TsdbQuery) error {
+ columnNames, err := rows.Columns()
+ columnCount := len(columnNames)
+
+ if err != nil {
+ return err
+ }
+
+ rowCount := 0
+ timeIndex := -1
+
+ table := &Table{
+ Columns: make([]TableColumn, columnCount),
+ Rows: make([]RowValues, 0),
+ }
+
+ for i, name := range columnNames {
+ table.Columns[i].Text = name
+
+ for _, tc := range e.timeColumnNames {
+ if name == tc {
+ timeIndex = i
+ break
+ }
+ }
+ }
+
+ columnTypes, err := rows.ColumnTypes()
+ if err != nil {
+ return err
+ }
+
+ for ; rows.Next(); rowCount++ {
+ if rowCount > rowLimit {
+ return fmt.Errorf("query row limit exceeded, limit %d", rowLimit)
+ }
+
+ values, err := e.rowTransformer.Transform(columnTypes, rows)
+ if err != nil {
+ return err
+ }
+
+ // converts column named time to unix timestamp in milliseconds
+ // to make native mssql datetime types and epoch dates work in
+ // annotation and table queries.
+ ConvertSqlTimeColumnToEpochMs(values, timeIndex)
+ table.Rows = append(table.Rows, values)
+ }
+
+ result.Tables = append(result.Tables, table)
+ result.Meta.Set("rowCount", rowCount)
+ return nil
+}
+
+func (e *sqlQueryEndpoint) transformToTimeSeries(query *Query, rows *core.Rows, result *QueryResult, tsdbQuery *TsdbQuery) error {
+ pointsBySeries := make(map[string]*TimeSeries)
+ seriesByQueryOrder := list.New()
+
+ columnNames, err := rows.Columns()
+ if err != nil {
+ return err
+ }
+
+ columnTypes, err := rows.ColumnTypes()
+ if err != nil {
+ return err
+ }
+
+ rowCount := 0
+ timeIndex := -1
+ metricIndex := -1
+ metricPrefix := false
+ var metricPrefixValue string
+
+ // check columns of resultset: a column named time is mandatory
+ // the first text column is treated as metric name unless a column named metric is present
+ for i, col := range columnNames {
+ for _, tc := range e.timeColumnNames {
+ if col == tc {
+ timeIndex = i
+ continue
+ }
+ }
+ switch col {
+ case "metric":
+ metricIndex = i
+ default:
+ if metricIndex == -1 {
+ columnType := columnTypes[i].DatabaseTypeName()
+
+ for _, mct := range e.metricColumnTypes {
+ if columnType == mct {
+ metricIndex = i
+ continue
+ }
+ }
+ }
+ }
+ }
+
+ // use metric column as prefix with multiple value columns
+ if metricIndex != -1 && len(columnNames) > 3 {
+ metricPrefix = true
+ }
+
+ if timeIndex == -1 {
+ return fmt.Errorf("Found no column named %s", strings.Join(e.timeColumnNames, " or "))
+ }
+
+ fillMissing := query.Model.Get("fill").MustBool(false)
+ var fillInterval float64
+ fillValue := null.Float{}
+ fillPrevious := false
+
+ if fillMissing {
+ fillInterval = query.Model.Get("fillInterval").MustFloat64() * 1000
+ switch query.Model.Get("fillMode").MustString() {
+ case "null":
+ case "previous":
+ fillPrevious = true
+ case "value":
+ fillValue.Float64 = query.Model.Get("fillValue").MustFloat64()
+ fillValue.Valid = true
+ }
+ }
+
+ for rows.Next() {
+ var timestamp float64
+ var value null.Float
+ var metric string
+
+ if rowCount > rowLimit {
+ return fmt.Errorf("query row limit exceeded, limit %d", rowLimit)
+ }
+
+ values, err := e.rowTransformer.Transform(columnTypes, rows)
+ if err != nil {
+ return err
+ }
+
+ // converts column named time to unix timestamp in milliseconds to make
+ // native mysql datetime types and epoch dates work in
+ // annotation and table queries.
+ ConvertSqlTimeColumnToEpochMs(values, timeIndex)
+
+ switch columnValue := values[timeIndex].(type) {
+ case int64:
+ timestamp = float64(columnValue)
+ case float64:
+ timestamp = columnValue
+ default:
+ return fmt.Errorf("Invalid type for column time, must be of type timestamp or unix timestamp, got: %T %v", columnValue, columnValue)
+ }
+
+ if metricIndex >= 0 {
+ if columnValue, ok := values[metricIndex].(string); ok {
+ if metricPrefix {
+ metricPrefixValue = columnValue
+ } else {
+ metric = columnValue
+ }
+ } else {
+ return fmt.Errorf("Column metric must be of type %s. metric column name: %s type: %s but datatype is %T", strings.Join(e.metricColumnTypes, ", "), columnNames[metricIndex], columnTypes[metricIndex].DatabaseTypeName(), values[metricIndex])
+ }
+ }
+
+ for i, col := range columnNames {
+ if i == timeIndex || i == metricIndex {
+ continue
+ }
+
+ if value, err = ConvertSqlValueColumnToFloat(col, values[i]); err != nil {
+ return err
+ }
+
+ if metricIndex == -1 {
+ metric = col
+ } else if metricPrefix {
+ metric = metricPrefixValue + " " + col
+ }
+
+ series, exist := pointsBySeries[metric]
+ if !exist {
+ series = &TimeSeries{Name: metric}
+ pointsBySeries[metric] = series
+ seriesByQueryOrder.PushBack(metric)
+ }
+
+ if fillMissing {
+ var intervalStart float64
+ if !exist {
+ intervalStart = float64(tsdbQuery.TimeRange.MustGetFrom().UnixNano() / 1e6)
+ } else {
+ intervalStart = series.Points[len(series.Points)-1][1].Float64 + fillInterval
+ }
+
+ if fillPrevious {
+ if len(series.Points) > 0 {
+ fillValue = series.Points[len(series.Points)-1][0]
+ } else {
+ fillValue.Valid = false
+ }
+ }
+
+ // align interval start
+ intervalStart = math.Floor(intervalStart/fillInterval) * fillInterval
+
+ for i := intervalStart; i < timestamp; i += fillInterval {
+ series.Points = append(series.Points, TimePoint{fillValue, null.FloatFrom(i)})
+ rowCount++
+ }
+ }
+
+ series.Points = append(series.Points, TimePoint{value, null.FloatFrom(timestamp)})
+
+ e.log.Debug("Rows", "metric", metric, "time", timestamp, "value", value)
+ }
+ }
+
+ for elem := seriesByQueryOrder.Front(); elem != nil; elem = elem.Next() {
+ key := elem.Value.(string)
+ result.Series = append(result.Series, pointsBySeries[key])
+
+ if fillMissing {
+ series := pointsBySeries[key]
+ // fill in values from last fetched value till interval end
+ intervalStart := series.Points[len(series.Points)-1][1].Float64
+ intervalEnd := float64(tsdbQuery.TimeRange.MustGetTo().UnixNano() / 1e6)
+
+ if fillPrevious {
+ if len(series.Points) > 0 {
+ fillValue = series.Points[len(series.Points)-1][0]
+ } else {
+ fillValue.Valid = false
+ }
+ }
+
+ // align interval start
+ intervalStart = math.Floor(intervalStart/fillInterval) * fillInterval
+ for i := intervalStart + fillInterval; i < intervalEnd; i += fillInterval {
+ series.Points = append(series.Points, TimePoint{fillValue, null.FloatFrom(i)})
+ rowCount++
+ }
+ }
+ }
+
+ result.Meta.Set("rowCount", rowCount)
+ return nil
+}
+
// ConvertSqlTimeColumnToEpochMs converts column named time to unix timestamp in milliseconds
// to make native datetime types and epoch dates work in annotation and table queries.
func ConvertSqlTimeColumnToEpochMs(values RowValues, timeIndex int) {
@@ -295,3 +594,46 @@ func ConvertSqlValueColumnToFloat(columnName string, columnValue interface{}) (n
return value, nil
}
+
+func SetupFillmode(query *Query, interval time.Duration, fillmode string) error {
+ query.Model.Set("fill", true)
+ query.Model.Set("fillInterval", interval.Seconds())
+ switch fillmode {
+ case "NULL":
+ query.Model.Set("fillMode", "null")
+ case "previous":
+ query.Model.Set("fillMode", "previous")
+ default:
+ query.Model.Set("fillMode", "value")
+ floatVal, err := strconv.ParseFloat(fillmode, 64)
+ if err != nil {
+ return fmt.Errorf("error parsing fill value %v", fillmode)
+ }
+ query.Model.Set("fillValue", floatVal)
+ }
+
+ return nil
+}
+
+type SqlMacroEngineBase struct{}
+
+func NewSqlMacroEngineBase() *SqlMacroEngineBase {
+ return &SqlMacroEngineBase{}
+}
+
+func (m *SqlMacroEngineBase) ReplaceAllStringSubmatchFunc(re *regexp.Regexp, str string, repl func([]string) string) string {
+ result := ""
+ lastIndex := 0
+
+ for _, v := range re.FindAllSubmatchIndex([]byte(str), -1) {
+ groups := []string{}
+ for i := 0; i < len(v); i += 2 {
+ groups = append(groups, str[v[i]:v[i+1]])
+ }
+
+ result += str[lastIndex:v[0]] + repl(groups)
+ lastIndex = v[1]
+ }
+
+ return result + str[lastIndex:]
+}
diff --git a/pkg/tsdb/sql_engine_test.go b/pkg/tsdb/sql_engine_test.go
index 854734fac31..05b8a51ae6f 100644
--- a/pkg/tsdb/sql_engine_test.go
+++ b/pkg/tsdb/sql_engine_test.go
@@ -5,6 +5,8 @@ import (
"time"
"github.com/grafana/grafana/pkg/components/null"
+ "github.com/grafana/grafana/pkg/components/simplejson"
+ "github.com/grafana/grafana/pkg/models"
. "github.com/smartystreets/goconvey/convey"
)
@@ -14,6 +16,35 @@ func TestSqlEngine(t *testing.T) {
dt := time.Date(2018, 3, 14, 21, 20, 6, int(527345*time.Microsecond), time.UTC)
earlyDt := time.Date(1970, 3, 14, 21, 20, 6, int(527345*time.Microsecond), time.UTC)
+ Convey("Given a time range between 2018-04-12 00:00 and 2018-04-12 00:05", func() {
+ from := time.Date(2018, 4, 12, 18, 0, 0, 0, time.UTC)
+ to := from.Add(5 * time.Minute)
+ timeRange := NewFakeTimeRange("5m", "now", to)
+ query := &Query{DataSource: &models.DataSource{}, Model: simplejson.New()}
+
+ Convey("interpolate $__interval", func() {
+ sql, err := Interpolate(query, timeRange, "select $__interval ")
+ So(err, ShouldBeNil)
+
+ So(sql, ShouldEqual, "select 1m ")
+ })
+
+ Convey("interpolate $__interval in $__timeGroup", func() {
+ sql, err := Interpolate(query, timeRange, "select $__timeGroupAlias(time,$__interval)")
+ So(err, ShouldBeNil)
+
+ So(sql, ShouldEqual, "select $__timeGroupAlias(time,1m)")
+ })
+
+ Convey("interpolate $__interval_ms", func() {
+ sql, err := Interpolate(query, timeRange, "select $__interval_ms ")
+ So(err, ShouldBeNil)
+
+ So(sql, ShouldEqual, "select 60000 ")
+ })
+
+ })
+
Convey("Given row values with time.Time as time columns", func() {
var nilPointer *time.Time
diff --git a/pkg/util/md5_test.go b/pkg/util/md5_test.go
index 1338d42bb51..43c685b8763 100644
--- a/pkg/util/md5_test.go
+++ b/pkg/util/md5_test.go
@@ -3,14 +3,14 @@ package util
import "testing"
func TestMd5Sum(t *testing.T) {
- input := "dont hash passwords with md5"
+ input := "don't hash passwords with md5"
have, err := Md5SumString(input)
if err != nil {
t.Fatal("expected err to be nil")
}
- want := "2d6a56c82d09d374643b926d3417afba"
+ want := "dd1f7fdb3466c0d09c2e839d1f1530f8"
if have != want {
t.Fatalf("expected: %s got: %s", want, have)
}
diff --git a/pkg/util/url.go b/pkg/util/url.go
index c82dcef67c5..fad2d79a6d0 100644
--- a/pkg/util/url.go
+++ b/pkg/util/url.go
@@ -10,7 +10,7 @@ type UrlQueryReader struct {
}
func NewUrlQueryReader(urlInfo *url.URL) (*UrlQueryReader, error) {
- u, err := url.ParseQuery(urlInfo.String())
+ u, err := url.ParseQuery(urlInfo.RawQuery)
if err != nil {
return nil, err
}
diff --git a/pkg/util/url_test.go b/pkg/util/url_test.go
index 4dd221b9e0b..ee29956f60d 100644
--- a/pkg/util/url_test.go
+++ b/pkg/util/url_test.go
@@ -4,6 +4,7 @@ import (
"testing"
. "github.com/smartystreets/goconvey/convey"
+ "net/url"
)
func TestUrl(t *testing.T) {
@@ -43,4 +44,30 @@ func TestUrl(t *testing.T) {
So(result, ShouldEqual, "http://localhost:8080/api/")
})
+
+ Convey("When joining two urls where lefthand side has a trailing slash and righthand side has preceding slash", t, func() {
+ result := JoinUrlFragments("http://localhost:8080/", "/api/")
+
+ So(result, ShouldEqual, "http://localhost:8080/api/")
+ })
+}
+
+func TestNewUrlQueryReader(t *testing.T) {
+ u, _ := url.Parse("http://www.abc.com/foo?bar=baz&bar2=baz2")
+ uqr, _ := NewUrlQueryReader(u)
+
+ Convey("when trying to retrieve the first query value", t, func() {
+ result := uqr.Get("bar", "foodef")
+ So(result, ShouldEqual, "baz")
+ })
+
+ Convey("when trying to retrieve the second query value", t, func() {
+ result := uqr.Get("bar2", "foodef")
+ So(result, ShouldEqual, "baz2")
+ })
+
+ Convey("when trying to retrieve from a non-existent key, the default value is returned", t, func() {
+ result := uqr.Get("bar3", "foodef")
+ So(result, ShouldEqual, "foodef")
+ })
}
diff --git a/pkg/util/validation_test.go b/pkg/util/validation_test.go
new file mode 100644
index 00000000000..124da1b744b
--- /dev/null
+++ b/pkg/util/validation_test.go
@@ -0,0 +1,22 @@
+package util
+
+import (
+ "testing"
+
+ . "github.com/smartystreets/goconvey/convey"
+)
+
+func TestIsEmail(t *testing.T) {
+
+ Convey("When validating a string that is a valid email", t, func() {
+ result := IsEmail("abc@def.com")
+
+ So(result, ShouldEqual, true)
+ })
+
+ Convey("When validating a string that is not a valid email", t, func() {
+ result := IsEmail("abcdef.com")
+
+ So(result, ShouldEqual, false)
+ })
+}
diff --git a/public/app/app.ts b/public/app/app.ts
index 9ac76b8ec91..8e30747072e 100644
--- a/public/app/app.ts
+++ b/public/app/app.ts
@@ -21,7 +21,7 @@ import _ from 'lodash';
import moment from 'moment';
// add move to lodash for backward compatabiltiy
-_.move = function(array, fromIndex, toIndex) {
+_.move = (array, fromIndex, toIndex) => {
array.splice(toIndex, 0, array.splice(fromIndex, 1)[0]);
return array;
};
@@ -53,7 +53,7 @@ export class GrafanaApp {
}
init() {
- var app = angular.module('grafana', []);
+ const app = angular.module('grafana', []);
moment.locale(config.bootData.user.locale);
@@ -76,9 +76,9 @@ export class GrafanaApp {
$provide.decorator('$http', [
'$delegate',
'$templateCache',
- function($delegate, $templateCache) {
- var get = $delegate.get;
- $delegate.get = function(url, config) {
+ ($delegate, $templateCache) => {
+ const get = $delegate.get;
+ $delegate.get = (url, config) => {
if (url.match(/\.html$/)) {
// some template's already exist in the cache
if (!$templateCache.get(url)) {
@@ -105,10 +105,10 @@ export class GrafanaApp {
'react',
];
- var module_types = ['controllers', 'directives', 'factories', 'services', 'filters', 'routes'];
+ const moduleTypes = ['controllers', 'directives', 'factories', 'services', 'filters', 'routes'];
- _.each(module_types, type => {
- var moduleName = 'grafana.' + type;
+ _.each(moduleTypes, type => {
+ const moduleName = 'grafana.' + type;
this.useModule(angular.module(moduleName, []));
});
@@ -119,7 +119,7 @@ export class GrafanaApp {
coreModule.config(setupAngularRoutes);
registerAngularDirectives();
- var preBootRequires = [System.import('app/features/all')];
+ const preBootRequires = [System.import('app/features/all')];
Promise.all(preBootRequires)
.then(() => {
@@ -135,7 +135,7 @@ export class GrafanaApp {
this.preBootModules = null;
});
})
- .catch(function(err) {
+ .catch(err => {
console.log('Application boot failed:', err);
});
}
diff --git a/public/app/containers/AlertRuleList/AlertRuleList.jest.tsx b/public/app/containers/AlertRuleList/AlertRuleList.jest.tsx
deleted file mode 100644
index eac18a6c69d..00000000000
--- a/public/app/containers/AlertRuleList/AlertRuleList.jest.tsx
+++ /dev/null
@@ -1,69 +0,0 @@
-import React from 'react';
-import moment from 'moment';
-import { AlertRuleList } from './AlertRuleList';
-import { RootStore } from 'app/stores/RootStore/RootStore';
-import { backendSrv, createNavTree } from 'test/mocks/common';
-import { mount } from 'enzyme';
-import toJson from 'enzyme-to-json';
-
-describe('AlertRuleList', () => {
- let page, store;
-
- beforeAll(() => {
- backendSrv.get.mockReturnValue(
- Promise.resolve([
- {
- id: 11,
- dashboardId: 58,
- panelId: 3,
- name: 'Panel Title alert',
- state: 'ok',
- newStateDate: moment()
- .subtract(5, 'minutes')
- .format(),
- evalData: {},
- executionError: '',
- url: 'd/ufkcofof/my-goal',
- canEdit: true,
- },
- ])
- );
-
- store = RootStore.create(
- {},
- {
- backendSrv: backendSrv,
- navTree: createNavTree('alerting', 'alert-list'),
- }
- );
-
- page = mount();
- });
-
- it('should call api to get rules', () => {
- expect(backendSrv.get.mock.calls[0][0]).toEqual('/api/alerts');
- });
-
- it('should render 1 rule', () => {
- page.update();
- let ruleNode = page.find('.alert-rule-item');
- expect(toJson(ruleNode)).toMatchSnapshot();
- });
-
- it('toggle state should change pause rule if not paused', async () => {
- backendSrv.post.mockReturnValue(
- Promise.resolve({
- state: 'paused',
- })
- );
-
- page.find('.fa-pause').simulate('click');
-
- // wait for api call to resolve
- await Promise.resolve();
- page.update();
-
- expect(store.alertList.rules[0].state).toBe('paused');
- expect(page.find('.fa-play')).toHaveLength(1);
- });
-});
diff --git a/public/app/containers/AlertRuleList/AlertRuleList.tsx b/public/app/containers/AlertRuleList/AlertRuleList.tsx
deleted file mode 100644
index b61c7fbaac3..00000000000
--- a/public/app/containers/AlertRuleList/AlertRuleList.tsx
+++ /dev/null
@@ -1,178 +0,0 @@
-import React from 'react';
-import { hot } from 'react-hot-loader';
-import classNames from 'classnames';
-import { inject, observer } from 'mobx-react';
-import PageHeader from 'app/core/components/PageHeader/PageHeader';
-import { IAlertRule } from 'app/stores/AlertListStore/AlertListStore';
-import appEvents from 'app/core/app_events';
-import IContainerProps from 'app/containers/IContainerProps';
-import Highlighter from 'react-highlight-words';
-
-@inject('view', 'nav', 'alertList')
-@observer
-export class AlertRuleList extends React.Component {
- stateFilters = [
- { text: 'All', value: 'all' },
- { text: 'OK', value: 'ok' },
- { text: 'Not OK', value: 'not_ok' },
- { text: 'Alerting', value: 'alerting' },
- { text: 'No Data', value: 'no_data' },
- { text: 'Paused', value: 'paused' },
- ];
-
- constructor(props) {
- super(props);
-
- this.props.nav.load('alerting', 'alert-list');
- this.fetchRules();
- }
-
- onStateFilterChanged = evt => {
- this.props.view.updateQuery({ state: evt.target.value });
- this.fetchRules();
- };
-
- fetchRules() {
- this.props.alertList.loadRules({
- state: this.props.view.query.get('state') || 'all',
- });
- }
-
- onOpenHowTo = () => {
- appEvents.emit('show-modal', {
- src: 'public/app/features/alerting/partials/alert_howto.html',
- modalClass: 'confirm-modal',
- model: {},
- });
- };
-
- onSearchQueryChange = evt => {
- this.props.alertList.setSearchQuery(evt.target.value);
- };
-
- render() {
- const { nav, alertList } = this.props;
-
- return (
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- How to add an alert
-
-
-
-
-
- {alertList.filteredRules.map(rule => (
-
- ))}
-
-
-
-
- );
- }
-}
-
-function AlertStateFilterOption({ text, value }) {
- return (
-
- );
-}
-
-export interface AlertRuleItemProps {
- rule: IAlertRule;
- search: string;
-}
-
-@observer
-export class AlertRuleItem extends React.Component {
- toggleState = () => {
- this.props.rule.togglePaused();
- };
-
- renderText(text: string) {
- return (
-
- );
- }
-
- render() {
- const { rule } = this.props;
-
- let stateClass = classNames({
- fa: true,
- 'fa-play': rule.isPaused,
- 'fa-pause': !rule.isPaused,
- });
-
- let ruleUrl = `${rule.url}?panelId=${rule.panelId}&fullscreen=true&edit=true&tab=alert`;
-
- return (
-
-
-
-
-
-
-
-
- {this.renderText(rule.stateText)}
- for {rule.stateAge}
-
-
- {rule.info &&
{this.renderText(rule.info)}
}
-
-
-
-
- );
- }
-}
-
-export default hot(module)(AlertRuleList);
diff --git a/public/app/containers/Explore/Explore.tsx b/public/app/containers/Explore/Explore.tsx
index 178e53198d4..16175747a06 100644
--- a/public/app/containers/Explore/Explore.tsx
+++ b/public/app/containers/Explore/Explore.tsx
@@ -4,6 +4,7 @@ import Select from 'react-select';
import kbn from 'app/core/utils/kbn';
import colors from 'app/core/utils/colors';
+import store from 'app/core/store';
import TimeSeries from 'app/core/time_series2';
import { decodePathComponent } from 'app/core/utils/location_util';
import { parse as parseDate } from 'app/core/utils/datemath';
@@ -16,6 +17,18 @@ import Table from './Table';
import TimePicker, { DEFAULT_RANGE } from './TimePicker';
import { ensureQueries, generateQueryKey, hasQuery } from './utils/query';
+const MAX_HISTORY_ITEMS = 100;
+
+function makeHints(hints) {
+ const hintsByIndex = [];
+ hints.forEach(hint => {
+ if (hint) {
+ hintsByIndex[hint.index] = hint;
+ }
+ });
+ return hintsByIndex;
+}
+
function makeTimeSeriesList(dataList, options) {
return dataList.map((seriesData, index) => {
const datapoints = seriesData.datapoints || [];
@@ -34,7 +47,7 @@ function makeTimeSeriesList(dataList, options) {
});
}
-function parseInitialState(initial: string | undefined) {
+function parseUrlState(initial: string | undefined) {
if (initial) {
try {
const parsed = JSON.parse(decodePathComponent(initial));
@@ -50,18 +63,20 @@ function parseInitialState(initial: string | undefined) {
return { datasource: null, queries: [], range: DEFAULT_RANGE };
}
-interface IExploreState {
+interface ExploreState {
datasource: any;
datasourceError: any;
datasourceLoading: boolean | null;
datasourceMissing: boolean;
graphResult: any;
+ history: any[];
initialDatasource?: string;
latency: number;
loading: any;
logsResult: any;
- queries: any;
- queryError: any;
+ queries: any[];
+ queryErrors: any[];
+ queryHints: any[];
range: any;
requestOptions: any;
showingGraph: boolean;
@@ -73,12 +88,13 @@ interface IExploreState {
tableResult: any;
}
-export class Explore extends React.Component {
+export class Explore extends React.Component {
el: any;
constructor(props) {
super(props);
- const { datasource, queries, range } = parseInitialState(props.routeParams.state);
+ const initialState: ExploreState = props.initialState;
+ const { datasource, queries, range } = parseUrlState(props.routeParams.state);
this.state = {
datasource: null,
datasourceError: null,
@@ -86,11 +102,13 @@ export class Explore extends React.Component {
datasourceMissing: false,
graphResult: null,
initialDatasource: datasource,
+ history: [],
latency: 0,
loading: false,
logsResult: null,
queries: ensureQueries(queries),
- queryError: null,
+ queryErrors: [],
+ queryHints: [],
range: range || { ...DEFAULT_RANGE },
requestOptions: null,
showingGraph: true,
@@ -100,7 +118,7 @@ export class Explore extends React.Component {
supportsLogs: null,
supportsTable: null,
tableResult: null,
- ...props.initialState,
+ ...initialState,
};
}
@@ -138,6 +156,7 @@ export class Explore extends React.Component {
const supportsGraph = datasource.meta.metrics;
const supportsLogs = datasource.meta.logs;
const supportsTable = datasource.meta.metrics;
+ const datasourceId = datasource.meta.id;
let datasourceError = null;
try {
@@ -147,16 +166,31 @@ export class Explore extends React.Component {
datasourceError = (error && error.statusText) || error;
}
+ const historyKey = `grafana.explore.history.${datasourceId}`;
+ const history = store.getObject(historyKey, []);
+
+ if (datasource.init) {
+ datasource.init();
+ }
+
+ // Keep queries but reset edit state
+ const nextQueries = this.state.queries.map(q => ({
+ ...q,
+ edited: false,
+ }));
+
this.setState(
{
datasource,
datasourceError,
+ history,
supportsGraph,
supportsLogs,
supportsTable,
datasourceLoading: false,
+ queries: nextQueries,
},
- () => datasourceError === null && this.handleSubmit()
+ () => datasourceError === null && this.onSubmit()
);
}
@@ -164,7 +198,7 @@ export class Explore extends React.Component {
this.el = el;
};
- handleAddQueryRow = index => {
+ onAddQueryRow = index => {
const { queries } = this.state;
const nextQueries = [
...queries.slice(0, index + 1),
@@ -174,74 +208,137 @@ export class Explore extends React.Component {
this.setState({ queries: nextQueries });
};
- handleChangeDatasource = async option => {
+ onChangeDatasource = async option => {
this.setState({
datasource: null,
datasourceError: null,
datasourceLoading: true,
graphResult: null,
+ latency: 0,
logsResult: null,
+ queryErrors: [],
+ queryHints: [],
tableResult: null,
});
const datasource = await this.props.datasourceSrv.get(option.value);
this.setDatasource(datasource);
};
- handleChangeQuery = (query, index) => {
+ onChangeQuery = (value: string, index: number, override?: boolean) => {
const { queries } = this.state;
+ let { queryErrors, queryHints } = this.state;
+ const prevQuery = queries[index];
+ const edited = override ? false : prevQuery.query !== value;
const nextQuery = {
...queries[index],
- query,
+ edited,
+ query: value,
};
const nextQueries = [...queries];
nextQueries[index] = nextQuery;
- this.setState({ queries: nextQueries });
+ if (override) {
+ queryErrors = [];
+ queryHints = [];
+ }
+ this.setState(
+ {
+ queryErrors,
+ queryHints,
+ queries: nextQueries,
+ },
+ override ? () => this.onSubmit() : undefined
+ );
};
- handleChangeTime = nextRange => {
+ onChangeTime = nextRange => {
const range = {
from: nextRange.from,
to: nextRange.to,
};
- this.setState({ range }, () => this.handleSubmit());
+ this.setState({ range }, () => this.onSubmit());
};
- handleClickCloseSplit = () => {
+ onClickClear = () => {
+ this.setState({
+ graphResult: null,
+ logsResult: null,
+ latency: 0,
+ queries: ensureQueries(),
+ queryErrors: [],
+ queryHints: [],
+ tableResult: null,
+ });
+ };
+
+ onClickCloseSplit = () => {
const { onChangeSplit } = this.props;
if (onChangeSplit) {
onChangeSplit(false);
}
};
- handleClickGraphButton = () => {
+ onClickGraphButton = () => {
this.setState(state => ({ showingGraph: !state.showingGraph }));
};
- handleClickLogsButton = () => {
+ onClickLogsButton = () => {
this.setState(state => ({ showingLogs: !state.showingLogs }));
};
- handleClickSplit = () => {
+ onClickSplit = () => {
const { onChangeSplit } = this.props;
+ const state = { ...this.state };
+ state.queries = state.queries.map(({ edited, ...rest }) => rest);
if (onChangeSplit) {
- onChangeSplit(true, this.state);
+ onChangeSplit(true, state);
}
};
- handleClickTableButton = () => {
+ onClickTableButton = () => {
this.setState(state => ({ showingTable: !state.showingTable }));
};
- handleRemoveQueryRow = index => {
+ onClickTableCell = (columnKey: string, rowValue: string) => {
+ this.onModifyQueries({ type: 'ADD_FILTER', key: columnKey, value: rowValue });
+ };
+
+ onModifyQueries = (action: object, index?: number) => {
+ const { datasource, queries } = this.state;
+ if (datasource && datasource.modifyQuery) {
+ let nextQueries;
+ if (index === undefined) {
+ // Modify all queries
+ nextQueries = queries.map(q => ({
+ ...q,
+ edited: false,
+ query: datasource.modifyQuery(q.query, action),
+ }));
+ } else {
+ // Modify query only at index
+ nextQueries = [
+ ...queries.slice(0, index),
+ {
+ ...queries[index],
+ edited: false,
+ query: datasource.modifyQuery(queries[index].query, action),
+ },
+ ...queries.slice(index + 1),
+ ];
+ }
+ this.setState({ queries: nextQueries }, () => this.onSubmit());
+ }
+ };
+
+ onRemoveQueryRow = index => {
const { queries } = this.state;
if (queries.length <= 1) {
return;
}
const nextQueries = [...queries.slice(0, index), ...queries.slice(index + 1)];
- this.setState({ queries: nextQueries }, () => this.handleSubmit());
+ this.setState({ queries: nextQueries }, () => this.onSubmit());
};
- handleSubmit = () => {
+ onSubmit = () => {
const { showingLogs, showingGraph, showingTable, supportsGraph, supportsLogs, supportsTable } = this.state;
if (showingTable && supportsTable) {
this.runTableQuery();
@@ -254,7 +351,33 @@ export class Explore extends React.Component {
}
};
- buildQueryOptions(targetOptions: { format: string; instant?: boolean }) {
+ onQuerySuccess(datasourceId: string, queries: any[]): void {
+ // save queries to history
+ let { history } = this.state;
+ const { datasource } = this.state;
+
+ if (datasource.meta.id !== datasourceId) {
+ // Navigated away, queries did not matter
+ return;
+ }
+
+ const ts = Date.now();
+ queries.forEach(q => {
+ const { query } = q;
+ history = [{ query, ts }, ...history];
+ });
+
+ if (history.length > MAX_HISTORY_ITEMS) {
+ history = history.slice(0, MAX_HISTORY_ITEMS);
+ }
+
+ // Combine all queries of a datasource type into one history
+ const historyKey = `grafana.explore.history.${datasourceId}`;
+ store.setObject(historyKey, history);
+ this.setState({ history });
+ }
+
+ buildQueryOptions(targetOptions: { format: string; hinting?: boolean; instant?: boolean }) {
const { datasource, queries, range } = this.state;
const resolution = this.el.offsetWidth;
const absoluteRange = {
@@ -278,18 +401,20 @@ export class Explore extends React.Component {
if (!hasQuery(queries)) {
return;
}
- this.setState({ latency: 0, loading: true, graphResult: null, queryError: null });
+ this.setState({ latency: 0, loading: true, graphResult: null, queryErrors: [], queryHints: [] });
const now = Date.now();
- const options = this.buildQueryOptions({ format: 'time_series', instant: false });
+ const options = this.buildQueryOptions({ format: 'time_series', instant: false, hinting: true });
try {
const res = await datasource.query(options);
const result = makeTimeSeriesList(res.data, options);
+ const queryHints = res.hints ? makeHints(res.hints) : [];
const latency = Date.now() - now;
- this.setState({ latency, loading: false, graphResult: result, requestOptions: options });
+ this.setState({ latency, loading: false, graphResult: result, queryHints, requestOptions: options });
+ this.onQuerySuccess(datasource.meta.id, queries);
} catch (response) {
console.error(response);
const queryError = response.data ? response.data.error : response;
- this.setState({ loading: false, queryError });
+ this.setState({ loading: false, queryErrors: [queryError] });
}
}
@@ -298,7 +423,7 @@ export class Explore extends React.Component {
if (!hasQuery(queries)) {
return;
}
- this.setState({ latency: 0, loading: true, queryError: null, tableResult: null });
+ this.setState({ latency: 0, loading: true, queryErrors: [], queryHints: [], tableResult: null });
const now = Date.now();
const options = this.buildQueryOptions({
format: 'table',
@@ -309,10 +434,11 @@ export class Explore extends React.Component {
const tableModel = res.data[0];
const latency = Date.now() - now;
this.setState({ latency, loading: false, tableResult: tableModel, requestOptions: options });
+ this.onQuerySuccess(datasource.meta.id, queries);
} catch (response) {
console.error(response);
const queryError = response.data ? response.data.error : response;
- this.setState({ loading: false, queryError });
+ this.setState({ loading: false, queryErrors: [queryError] });
}
}
@@ -321,7 +447,7 @@ export class Explore extends React.Component {
if (!hasQuery(queries)) {
return;
}
- this.setState({ latency: 0, loading: true, queryError: null, logsResult: null });
+ this.setState({ latency: 0, loading: true, queryErrors: [], queryHints: [], logsResult: null });
const now = Date.now();
const options = this.buildQueryOptions({
format: 'logs',
@@ -332,10 +458,11 @@ export class Explore extends React.Component {
const logsData = res.data;
const latency = Date.now() - now;
this.setState({ latency, loading: false, logsResult: logsData, requestOptions: options });
+ this.onQuerySuccess(datasource.meta.id, queries);
} catch (response) {
console.error(response);
const queryError = response.data ? response.data.error : response;
- this.setState({ loading: false, queryError });
+ this.setState({ loading: false, queryErrors: [queryError] });
}
}
@@ -352,11 +479,13 @@ export class Explore extends React.Component {
datasourceLoading,
datasourceMissing,
graphResult,
+ history,
latency,
loading,
logsResult,
queries,
- queryError,
+ queryErrors,
+ queryHints,
range,
requestOptions,
showingGraph,
@@ -391,7 +520,7 @@ export class Explore extends React.Component {
) : (
-
@@ -401,7 +530,7 @@ export class Explore extends React.Component {