Quellcode durchsuchen

Merge remote-tracking branch 'grafana/master'

* grafana/master: (878 commits)
  changelog: adds note about closing #13945
  removed file I added accidentally
  fixed to template PR issues, #13938
  alerting: increase default duration for queries
  Load hash based styles in error.html, too
  Add [hash] to filename of grafana.{light,dark}.css
  Fix minor JSON typo in HTTP API docs
  Added new backend setting for license file
  changelog: add notes about closing #13925
  fix for responsive rule for footer
  Updated login page logo & wordmark and responsive behavior
  added new workmarks
  fixed react whitespace warning on teams page
  renamed org files to match new naming guide
  moved profile pages to it's own feature folder
  moved new teams page
  reload page after preferences update
  Add delta window function to postgres query builder
  Increase Telegram captions length limit.
  Explore: async starts of language provider
  ...
ryan vor 7 Jahren
Ursprung
Commit
eeb80a43f4
100 geänderte Dateien mit 3734 neuen und 471 gelöschten Zeilen
  1. 1 0
      .bra.toml
  2. 143 22
      .circleci/config.yml
  3. 0 22
      .github/CONTRIBUTING.md
  4. 4 1
      .gitignore
  5. 78 6
      CHANGELOG.md
  6. 56 0
      CONTRIBUTING.md
  7. 20 2
      Gopkg.lock
  8. 8 0
      Gopkg.toml
  9. 5 0
      Gruntfile.js
  10. 8 2
      Makefile
  11. 2 2
      PLUGIN_DEV.md
  12. 16 16
      README.md
  13. 89 0
      UPGRADING_DEPENDENCIES.md
  14. 1 1
      appveyor.yml
  15. 13 0
      build.go
  16. 7 0
      conf/defaults.ini
  17. 6 1
      conf/sample.ini
  18. 123 6
      devenv/dev-dashboards/panel_tests_graph.json
  19. 1166 0
      devenv/dev-dashboards/panel_tests_slow_queries_and_annotations.json
  20. 1 1
      devenv/docker/blocks/influxdb/docker-compose.yaml
  21. 1 0
      devenv/docker/ha_test/.gitignore
  22. 137 0
      devenv/docker/ha_test/README.md
  23. 156 0
      devenv/docker/ha_test/alerts.sh
  24. 78 0
      devenv/docker/ha_test/docker-compose.yaml
  25. 202 0
      devenv/docker/ha_test/grafana/provisioning/alerts.jsonnet
  26. 8 0
      devenv/docker/ha_test/grafana/provisioning/dashboards/alerts.yaml
  27. 172 0
      devenv/docker/ha_test/grafana/provisioning/dashboards/alerts/overview.json
  28. 11 0
      devenv/docker/ha_test/grafana/provisioning/datasources/datasources.yaml
  29. 39 0
      devenv/docker/ha_test/prometheus/prometheus.yml
  30. 2 2
      docs/sources/administration/permissions.md
  31. 10 6
      docs/sources/administration/provisioning.md
  32. 2 2
      docs/sources/alerting/notifications.md
  33. 3 0
      docs/sources/auth/generic-oauth.md
  34. 3 0
      docs/sources/auth/github.md
  35. 5 2
      docs/sources/auth/gitlab.md
  36. 3 0
      docs/sources/auth/google.md
  37. 1 0
      docs/sources/auth/ldap.md
  38. 1 1
      docs/sources/auth/overview.md
  39. 1 1
      docs/sources/contribute/cla.md
  40. 4 3
      docs/sources/features/datasources/cloudwatch.md
  41. 11 3
      docs/sources/features/datasources/mssql.md
  42. 60 1
      docs/sources/features/datasources/mysql.md
  43. 1 1
      docs/sources/features/datasources/opentsdb.md
  44. 6 0
      docs/sources/features/datasources/postgres.md
  45. 253 0
      docs/sources/features/datasources/stackdriver.md
  46. 1 1
      docs/sources/features/panels/alertlist.md
  47. 1 1
      docs/sources/features/panels/heatmap.md
  48. 1 1
      docs/sources/guides/whats-new-in-v2-5.md
  49. 1 1
      docs/sources/guides/whats-new-in-v2.md
  50. 2 2
      docs/sources/guides/whats-new-in-v3-1.md
  51. 1 1
      docs/sources/guides/whats-new-in-v3.md
  52. 1 1
      docs/sources/guides/whats-new-in-v4-2.md
  53. 2 2
      docs/sources/guides/whats-new-in-v4-5.md
  54. 1 1
      docs/sources/guides/whats-new-in-v4-6.md
  55. 74 0
      docs/sources/guides/whats-new-in-v5-3.md
  56. 2 2
      docs/sources/http_api/alerting.md
  57. 1 1
      docs/sources/http_api/dashboard_versions.md
  58. 17 9
      docs/sources/index.md
  59. 4 1
      docs/sources/installation/configuration.md
  60. 3 1
      docs/sources/installation/debian.md
  61. 16 1
      docs/sources/installation/docker.md
  62. 2 0
      docs/sources/installation/mac.md
  63. 2 0
      docs/sources/installation/rpm.md
  64. 3 0
      docs/sources/installation/windows.md
  65. 2 3
      docs/sources/plugins/developing/development.md
  66. 1 1
      docs/sources/project/building_from_source.md
  67. 3 1
      docs/sources/reference/annotations.md
  68. 1 0
      docs/sources/reference/templating.md
  69. 2 2
      docs/sources/tutorials/ha_setup.md
  70. 6 1
      docs/sources/tutorials/index.md
  71. 2 1
      docs/versions.json
  72. 2 2
      latest.json
  73. 8 5
      package.json
  74. 1 1
      packaging/docker/Dockerfile
  75. 12 0
      packaging/docker/build-enterprise.sh
  76. 0 29
      packaging/release_process.md
  77. 65 65
      pkg/api/api.go
  78. 0 9
      pkg/api/avatar/avatar.go
  79. 3 2
      pkg/api/dashboard.go
  80. 2 1
      pkg/api/dashboard_test.go
  81. 33 2
      pkg/api/dataproxy.go
  82. 19 0
      pkg/api/dataproxy_test.go
  83. 7 7
      pkg/api/datasources.go
  84. 26 23
      pkg/api/dtos/alerting.go
  85. 1 0
      pkg/api/dtos/index.go
  86. 0 10
      pkg/api/folder_test.go
  87. 21 4
      pkg/api/frontendsettings.go
  88. 3 1
      pkg/api/http_server.go
  89. 19 6
      pkg/api/index.go
  90. 0 3
      pkg/api/live/hub.go
  91. 2 2
      pkg/api/login.go
  92. 16 10
      pkg/api/org_users.go
  93. 171 0
      pkg/api/pluginproxy/access_token_provider.go
  94. 94 0
      pkg/api/pluginproxy/access_token_provider_test.go
  95. 109 0
      pkg/api/pluginproxy/ds_auth_provider.go
  96. 21 0
      pkg/api/pluginproxy/ds_auth_provider_test.go
  97. 3 130
      pkg/api/pluginproxy/ds_proxy.go
  98. 22 17
      pkg/api/pluginproxy/ds_proxy_test.go
  99. 3 3
      pkg/api/user.go
  100. 2 0
      pkg/cmd/grafana-cli/commands/commands.go

+ 1 - 0
.bra.toml

@@ -4,6 +4,7 @@ init_cmds = [
 	["./bin/grafana-server", "cfg:app_mode=development"]
 ]
 watch_all = true
+follow_symlinks = true
 watch_dirs = [
 	"$WORKDIR/pkg",
 	"$WORKDIR/public/views",

+ 143 - 22
.circleci/config.yml

@@ -83,13 +83,14 @@ jobs:
       - checkout
       - run: 'go get -u github.com/alecthomas/gometalinter'
       - run: 'go get -u github.com/tsenart/deadcode'
+      - run: 'go get -u github.com/jgautheron/goconst/cmd/goconst'
       - run: 'go get -u github.com/gordonklaus/ineffassign'
       - run: 'go get -u github.com/opennota/check/cmd/structcheck'
       - run: 'go get -u github.com/mdempsky/unconvert'
       - run: 'go get -u github.com/opennota/check/cmd/varcheck'
       - run:
           name: run linters
-          command: 'gometalinter --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=goconst --enable=ineffassign --enable=structcheck --enable=unconvert --enable=varcheck ./...'
       - run:
           name: run go vet
           command: 'go vet ./pkg/...'
@@ -125,7 +126,7 @@ jobs:
 
   build-all:
     docker:
-     - image: grafana/build-container:1.1.0
+     - image: grafana/build-container:1.2.0
     working_directory: /go/src/github.com/grafana/grafana
     steps:
       - checkout
@@ -157,18 +158,23 @@ jobs:
           name: sha-sum packages
           command: 'go run build.go sha-dist'
       - run:
-          name: Build Grafana.com publisher
+          name: Build Grafana.com master publisher
           command: 'go build -o scripts/publish scripts/build/publish.go'
+      - run:
+          name: Build Grafana.com release publisher
+          command: 'cd scripts/build/release_publisher && go build -o release_publisher .'
       - persist_to_workspace:
           root: .
           paths:
             - dist/grafana*
             - scripts/*.sh
             - scripts/publish
+            - scripts/build/release_publisher/release_publisher
+            - scripts/build/publish.sh
 
   build:
     docker:
-     - image: grafana/build-container:1.1.0
+     - image: grafana/build-container:1.2.0
     working_directory: /go/src/github.com/grafana/grafana
     steps:
       - checkout
@@ -200,6 +206,10 @@ jobs:
       - run: docker info
       - run: cp dist/grafana-latest.linux-x64.tar.gz packaging/docker
       - run: cd packaging/docker && ./build-deploy.sh "master-${CIRCLE_SHA1}"
+      - run: rm packaging/docker/grafana-latest.linux-x64.tar.gz
+      - run: cp enterprise-dist/grafana-enterprise-*.linux-amd64.tar.gz packaging/docker/grafana-latest.linux-x64.tar.gz
+      - run: cd packaging/docker && ./build-enterprise.sh "master"
+
 
   grafana-docker-pr:
     docker:
@@ -224,16 +234,28 @@ jobs:
         - run: docker info
         - run: cp dist/grafana-latest.linux-x64.tar.gz packaging/docker
         - run: cd packaging/docker && ./build-deploy.sh "${CIRCLE_TAG}"
+        - run: rm packaging/docker/grafana-latest.linux-x64.tar.gz
+        - run: cp enterprise-dist/grafana-enterprise-*.linux-amd64.tar.gz packaging/docker/grafana-latest.linux-x64.tar.gz
+        - run: cd packaging/docker && ./build-enterprise.sh "${CIRCLE_TAG}"
 
   build-enterprise:
     docker:
-     - image: grafana/build-container:v0.1
+     - image: grafana/build-container:1.2.0
     working_directory: /go/src/github.com/grafana/grafana
     steps:
       - checkout
       - run:
-          name: build, test and package grafana enterprise
-          command: './scripts/build/build_enterprise.sh'
+          name: prepare build tools
+          command: '/tmp/bootstrap.sh'
+      - run:
+          name: checkout enterprise
+          command: './scripts/build/prepare-enterprise.sh'
+      - run:
+          name: test enterprise
+          command: 'go test ./pkg/extensions/...'
+      - run:
+          name: build and package enterprise
+          command: './scripts/build/build.sh -enterprise'
       - run:
           name: sign packages
           command: './scripts/build/sign_packages.sh'
@@ -248,28 +270,98 @@ jobs:
           paths:
             - enterprise-dist/grafana-enterprise*
 
+  build-all-enterprise:
+    docker:
+    - image: grafana/build-container:1.2.0
+    working_directory: /go/src/github.com/grafana/grafana
+    steps:
+    - checkout
+    - run:
+        name: prepare build tools
+        command: '/tmp/bootstrap.sh'
+    - run:
+        name: checkout enterprise
+        command: './scripts/build/prepare-enterprise.sh'
+    - restore_cache:
+        key: phantomjs-binaries-{{ checksum "scripts/build/download-phantomjs.sh" }}
+    - run:
+        name: download phantomjs binaries
+        command: './scripts/build/download-phantomjs.sh'
+    - save_cache:
+        key: phantomjs-binaries-{{ checksum "scripts/build/download-phantomjs.sh" }}
+        paths:
+        - /tmp/phantomjs
+    - run:
+        name: test enterprise
+        command: 'go test ./pkg/extensions/...'
+    - run:
+        name: build and package grafana
+        command: './scripts/build/build-all.sh -enterprise'
+    - 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'
+    - run:
+        name: move enterprise packages into their own folder
+        command: 'mv dist enterprise-dist'
+    - persist_to_workspace:
+        root: .
+        paths:
+        - enterprise-dist/grafana-enterprise*
+
   deploy-enterprise-master:
     docker:
-      - image: circleci/python:2.7-stretch
+      - image: grafana/grafana-ci-deploy:1.0.0
     steps:
       - attach_workspace:
           at: .
       - run:
-          name: install awscli
-          command: 'sudo pip install awscli'
+          name: gcp credentials
+          command: 'echo ${GCP_GRAFANA_UPLOAD_KEY} > /tmp/gcpkey.json'
+      - run:
+          name: sign in to gcp
+          command: '/opt/google-cloud-sdk/bin/gcloud auth activate-service-account --key-file=/tmp/gcpkey.json'
       - run:
           name: deploy to s3
           command: 'aws s3 sync ./enterprise-dist s3://$ENTERPRISE_BUCKET_NAME/master'
+      - run:
+          name: deploy to gcp
+          command: '/opt/google-cloud-sdk/bin/gsutil cp ./enterprise-dist/* gs://$GCP_BUCKET_NAME/enterprise/master'
+
+
+  deploy-enterprise-release:
+    docker:
+    - image: grafana/grafana-ci-deploy:1.0.0
+    steps:
+      - attach_workspace:
+         at: .
+      - run:
+          name: gcp credentials
+          command: 'echo ${GCP_GRAFANA_UPLOAD_KEY} > /tmp/gcpkey.json'
+      - run:
+          name: sign in to gcp
+          command: '/opt/google-cloud-sdk/bin/gcloud auth activate-service-account --key-file=/tmp/gcpkey.json'
+      - run:
+          name: deploy to s3
+          command: 'aws s3 sync ./enterprise-dist s3://$ENTERPRISE_BUCKET_NAME/release'
+      - run:
+          name: deploy to gcp
+          command: '/opt/google-cloud-sdk/bin/gsutil cp ./enterprise-dist/* gs://$GCP_BUCKET_NAME/enterprise/release'
 
   deploy-master:
     docker:
-      - image: circleci/python:2.7-stretch
+      - image: grafana/grafana-ci-deploy:1.0.0
     steps:
       - attach_workspace:
           at: .
-      - run:
-          name: install awscli
-          command: 'sudo pip install awscli'
       - run:
           name: deploy to s3
           command: |
@@ -279,6 +371,15 @@ jobs:
       - run:
           name: Trigger Windows build
           command: './scripts/trigger_windows_build.sh ${APPVEYOR_TOKEN} ${CIRCLE_SHA1} master'
+      - run:
+          name: gcp credentials
+          command: 'echo ${GCP_GRAFANA_UPLOAD_KEY} > /tmp/gcpkey.json'
+      - run:
+          name: sign in to gcp
+          command: '/opt/google-cloud-sdk/bin/gcloud auth activate-service-account --key-file=/tmp/gcpkey.json'
+      - run:
+          name: deploy to gcp
+          command: '/opt/google-cloud-sdk/bin/gsutil cp ./dist/* gs://$GCP_BUCKET_NAME/oss/master'
       - run:
           name: Publish to Grafana.com
           command: |
@@ -287,19 +388,25 @@ jobs:
 
   deploy-release:
     docker:
-      - image: circleci/python:2.7-stretch
+      - image: grafana/grafana-ci-deploy:1.0.0
     steps:
       - attach_workspace:
           at: .
-      - run:
-          name: install awscli
-          command: 'sudo pip install awscli'
       - run:
           name: deploy to s3
           command: 'aws s3 sync ./dist s3://$BUCKET_NAME/release'
       - run:
-          name: Trigger Windows build
-          command: './scripts/trigger_windows_build.sh ${APPVEYOR_TOKEN} ${CIRCLE_SHA1} release'
+          name: gcp credentials
+          command: 'echo ${GCP_GRAFANA_UPLOAD_KEY} > /tmp/gcpkey.json'
+      - run:
+          name: sign in to gcp
+          command: '/opt/google-cloud-sdk/bin/gcloud auth activate-service-account --key-file=/tmp/gcpkey.json'
+      - run:
+          name: deploy to gcp
+          command: '/opt/google-cloud-sdk/bin/gsutil cp ./dist/* gs://R/oss/release'
+      - run:
+          name: Deploy to Grafana.com
+          command: './scripts/build/publish.sh'
 
 workflows:
   version: 2
@@ -307,7 +414,7 @@ workflows:
     jobs:
       - build-all:
           filters: *filter-only-master
-      - build-enterprise:
+      - build-all-enterprise:
           filters: *filter-only-master
       - codespell:
           filters: *filter-only-master
@@ -334,6 +441,7 @@ workflows:
       - grafana-docker-master:
           requires:
             - build-all
+            - build-all-enterprise
             - test-backend
             - test-frontend
             - codespell
@@ -350,13 +458,15 @@ workflows:
             - gometalinter
             - mysql-integration-test
             - postgres-integration-test
-            - build-enterprise
+            - build-all-enterprise
           filters: *filter-only-master
 
   release:
     jobs:
       - build-all:
           filters: *filter-only-release
+      - build-all-enterprise:
+          filters: *filter-only-release
       - codespell:
           filters: *filter-only-release
       - gometalinter:
@@ -379,6 +489,17 @@ workflows:
             - mysql-integration-test
             - postgres-integration-test
           filters: *filter-only-release
+      - deploy-enterprise-release:
+          requires:
+            - build-all
+            - build-all-enterprise
+            - test-backend
+            - test-frontend
+            - codespell
+            - gometalinter
+            - mysql-integration-test
+            - postgres-integration-test
+          filters: *filter-only-release
       - grafana-docker-release:
           requires:
             - build-all

+ 0 - 22
.github/CONTRIBUTING.md

@@ -1,22 +0,0 @@
-Follow the setup guide in README.md
-
-### Rebuild frontend assets on source change
-```
-yarn watch
-```
-
-### Rerun tests on source change
-```
-yarn jest
-```
-
-### Run tests for backend assets before commit
-```
-test -z "$(gofmt -s -l . | grep -v -E 'vendor/(github.com|golang.org|gopkg.in)' | tee /dev/stderr)"
-```
-
-### Run tests for frontend assets before commit
-```
-yarn test
-go test -v ./pkg/...
-```

+ 4 - 1
.gitignore

@@ -8,6 +8,7 @@ awsconfig
 /dist
 /public/build
 /public/views/index.html
+/public/views/error.html
 /emails/dist
 /public_gen
 /public/vendor/npm
@@ -54,6 +55,7 @@ profile.cov
 /pkg/cmd/grafana-server/grafana-server
 /pkg/cmd/grafana-server/debug
 /pkg/extensions
+/public/app/extensions
 debug.test
 /examples/*/dist
 /packaging/**/*.rpm
@@ -68,8 +70,9 @@ debug.test
 /vendor/**/*.yml
 /vendor/**/*_test.go
 /vendor/**/.editorconfig
-/vendor/**/appengine*
 *.orig
 
 /devenv/bulk-dashboards/*.json
 /devenv/bulk_alerting_dashboards/*.json
+
+/scripts/build/release_publisher/release_publisher

+ 78 - 6
CHANGELOG.md

@@ -2,22 +2,94 @@
 
 ### New Features
 
+* **Alerting**: Option to disable OK alert notifications [#12330](https://github.com/grafana/grafana/issues/12330) & [#6696](https://github.com/grafana/grafana/issues/6696), thx [@davewat](https://github.com/davewat)
+* **Postgres/MySQL/MSSQL**: Adds support for configuration of max open/idle connections and connection max lifetime. Also, panels with multiple SQL queries will now be executed concurrently [#11711](https://github.com/grafana/grafana/issues/11711), thx [@connection-reset](https://github.com/connection-reset)
+* **MySQL**: Graphical query builder [#13762](https://github.com/grafana/grafana/issues/13762), thx [svenklemm](https://github.com/svenklemm)
+* **MySQL**: Support connecting thru Unix socket for MySQL datasource [#12342](https://github.com/grafana/grafana/issues/12342), thx [@Yukinoshita-Yukino](https://github.com/Yukinoshita-Yukino)
+* **MSSQL**: Add encrypt setting to allow configuration of how data sent between client and server are encrypted [#13629](https://github.com/grafana/grafana/issues/13629), thx [@ramiro](https://github.com/ramiro)
+* **Stackdriver**: Not possible to authenticate using GCE metadata server [#13669](https://github.com/grafana/grafana/issues/13669)
+
+### Minor
+
+* **Cloudwatch**: Show all available CloudWatch regions [#12308](https://github.com/grafana/grafana/issues/12308), thx [@mtanda](https://github.com/mtanda)
+* **Postgres**: Add delta window function to postgres query builder [#13925](https://github.com/grafana/grafana/issues/13925), thx [svenklemm](https://github.com/svenklemm)
+* **Units**: New clock time format, to format ms or second values as for example `01h:59m`, [#13635](https://github.com/grafana/grafana/issues/13635), thx [@franciscocpg](https://github.com/franciscocpg)
+* **Datasource Proxy**: Keep trailing slash for datasource proxy requests [#13326](https://github.com/grafana/grafana/pull/13326), thx [@ryantxu](https://github.com/ryantxu)
+* **DingDing**: Can't receive DingDing alert when alert is triggered [#13723](https://github.com/grafana/grafana/issues/13723), thx [@Yukinoshita-Yukino](https://github.com/Yukinoshita-Yukino)
+* **Internal metrics**: Renamed `grafana_info` to `grafana_build_info` and added branch, goversion and revision [#13876](https://github.com/grafana/grafana/pull/13876)
+* **Alerting**: Increaste default duration for queries [#13945](https://github.com/grafana/grafana/pull/13945)
+
+### Breaking changes
+
+* Postgres/MySQL/MSSQL datasources now per default uses `max open connections` = `unlimited` (earlier 10), `max idle connections` = `2` (earlier 10) and `connection max lifetime` = `4` hours (earlier unlimited)
+
+# 5.3.3 (unreleased)
+
+* **MySQL**: Fix `$__timeFilter()` should respect local time zone [#13769](https://github.com/grafana/grafana/issues/13769)
+
+# 5.3.2 (2018-10-24)
+
+* **InfluxDB/Graphite/Postgres**: Prevent cross site scripting (XSS) in query editor [#13667](https://github.com/grafana/grafana/issues/13667), thx [@svenklemm](https://github.com/svenklemm)
+* **Postgres**: Fix template variables error [#13692](https://github.com/grafana/grafana/issues/13692), thx [@svenklemm](https://github.com/svenklemm)
+* **Cloudwatch**: Fix service panic because of race conditions [#13674](https://github.com/grafana/grafana/issues/13674), thx [@mtanda](https://github.com/mtanda)
+* **Cloudwatch**: Fix check for invalid percentile statistics [#13633](https://github.com/grafana/grafana/issues/13633), thx [@apalaniuk](https://github.com/apalaniuk)
+* **Stackdriver/Cloudwatch**: Allow user to change unit in graph panel if cloudwatch/stackdriver datasource response doesn't include unit [#13718](https://github.com/grafana/grafana/issues/13718), thx [@mtanda](https://github.com/mtanda)
+* **Stackdriver**: stackdriver user-metrics duplicated response when multiple resource types [#13691](https://github.com/grafana/grafana/issues/13691)
+* **Variables**: Fix text box template variable doesn't work properly without a default value [#13666](https://github.com/grafana/grafana/issues/13666)
+* **Variables**: Fix variable dependency check when using `${var}` format [#13600](https://github.com/grafana/grafana/issues/13600)
+* **Dashboard**: Fix kiosk=1 url parameter should put dashboard in kiosk mode [#13764](https://github.com/grafana/grafana/pull/13764)
+* **LDAP**: Fix super admins can also be admins of orgs [#13710](https://github.com/grafana/grafana/issues/13710), thx [@adrien-f](https://github.com/adrien-f)
+* **Provisioning**: Fix deleting provisioned dashboard folder should cleanup provisioning meta data [#13280](https://github.com/grafana/grafana/issues/13280)
+
+### Minor
+
+* **Docker**: adds curl back into the docker image for utility. [#13794](https://github.com/grafana/grafana/pull/13794)
+
+# 5.3.1 (2018-10-16)
+
+* **Render**: Fix PhantomJS render of graph panel when legend displayed as table to the right [#13616](https://github.com/grafana/grafana/issues/13616)
+* **Stackdriver**: Filter option disappears after removing initial filter [#13607](https://github.com/grafana/grafana/issues/13607)
+* **Elasticsearch**: Fix no limit size in terms aggregation for alerting queries [#13172](https://github.com/grafana/grafana/issues/13172), thx [@Yukinoshita-Yukino](https://github.com/Yukinoshita-Yukino)
+* **InfluxDB**: Fix for annotation issue that caused text to be shown twice [#13553](https://github.com/grafana/grafana/issues/13553)
+* **Variables**: Fix nesting variables leads to exception and missing refresh [#13628](https://github.com/grafana/grafana/issues/13628)
+* **Variables**: Prometheus: Single letter labels are not supported [#13641](https://github.com/grafana/grafana/issues/13641), thx [@olshansky](https://github.com/olshansky)
+* **Graph**: Fix graph time formatting for Last 24h ranges [#13650](https://github.com/grafana/grafana/issues/13650)
+* **Playlist**: Fix cannot add dashboards with long names to playlist [#13464](https://github.com/grafana/grafana/issues/13464), thx [@neufeldtech](https://github.com/neufeldtech)
+* **HTTP API**: Fix /api/org/users so that query and limit querystrings works
+
+# 5.3.0 (2018-10-10)
+
+* **Stackdriver**: Filter wildcards and regex matching are not yet supported [#13495](https://github.com/grafana/grafana/issues/13495)
+* **Stackdriver**: Support the distribution metric type for heatmaps [#13559](https://github.com/grafana/grafana/issues/13559)
+* **Cloudwatch**: Automatically set graph yaxis unit [#13575](https://github.com/grafana/grafana/issues/13575), thx [@mtanda](https://github.com/mtanda)
+
+# 5.3.0-beta3 (2018-10-03)
+
+* **Stackdriver**: Fix for missing ngInject [#13511](https://github.com/grafana/grafana/pull/13511)
+* **Permissions**: Fix for broken permissions selector [#13507](https://github.com/grafana/grafana/issues/13507)
+* **Alerting**: Alert reminders deduping not working as expected when running multiple Grafana instances [#13492](https://github.com/grafana/grafana/issues/13492)
+
+# 5.3.0-beta2 (2018-10-01)
+
+### New Features
+
 * **Annotations**: Enable template variables in tagged annotations queries [#9735](https://github.com/grafana/grafana/issues/9735)
+* **Stackdriver**: Support for Google Stackdriver Datasource [#13289](https://github.com/grafana/grafana/pull/13289)
 
 ### Minor
 
+* **Provisioning**: Dashboard Provisioning now support symlinks that changes target [#12534](https://github.com/grafana/grafana/issues/12534), thx [@auhlig](https://github.com/auhlig)
 * **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
-
+* **Dashboard**: Prevent auto refresh from starting when loading dashboard with absolute time range [#12030](https://github.com/grafana/grafana/issues/12030)
+* **Templating**: New templating variable type `Text box` that allows free text input [#3173](https://github.com/grafana/grafana/issues/3173)
 * **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)
+* **Alerting**: Fixes a bug where all alerts would send reminders after upgrade & restart [#13402](https://github.com/grafana/grafana/pull/13402)
+* **Alerting**: Concurrent render limit for graphs used in notifications [#13401](https://github.com/grafana/grafana/pull/13401)
 * **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)
@@ -36,7 +108,7 @@
 * **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)
+* **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)
 

+ 56 - 0
CONTRIBUTING.md

@@ -0,0 +1,56 @@
+
+# Contributing
+
+Grafana uses GitHub to manage contributions.
+Contributions take the form of pull requests that will be reviewed by the core team.
+
+* If you are a new contributor see: [Steps to Contribute](#steps-to-contribute)
+
+* If you have a trivial fix or improvement, go ahead and create a pull request.
+
+* If you plan to do something more involved, discuss your idea on the respective [issue](https://github.com/grafana/grafana/issues) or create a [new issue](https://github.com/grafana/grafana/issues/new) if it does not exist. This will avoid unnecessary work and surely give you and us a good deal of inspiration. 
+
+
+## Steps to Contribute
+
+Should you wish to work on a GitHub issue, check first if it is not already assigned to someone. If it is free, you claim it by commenting on the issue that you want to work on it. This is to prevent duplicated efforts from contributors on the same issue.
+
+Please check the [`beginner friendly`](https://github.com/grafana/grafana/issues?q=is%3Aopen+is%3Aissue+label%3A%22beginner+friendly%22) label to find issues that are good for getting started. If you have questions about one of the issues, with or without the tag, please comment on them and one of the core team or the original poster will clarify it.
+
+
+
+## Setup
+
+Follow the setup guide in README.md
+
+### Rebuild frontend assets on source change
+```
+yarn watch
+```
+
+### Rerun tests on source change
+```
+yarn jest
+```
+
+### Run tests for backend assets before commit
+```
+test -z "$(gofmt -s -l . | grep -v -E 'vendor/(github.com|golang.org|gopkg.in)' | tee /dev/stderr)"
+```
+
+### Run tests for frontend assets before commit
+```
+yarn test
+go test -v ./pkg/...
+```
+
+
+## Pull Request Checklist
+
+* Branch from the master branch and, if needed, rebase to the current master branch before submitting your pull request. If it doesn't merge cleanly with master you may be asked to rebase your changes.
+
+* Commits should be as small as possible, while ensuring that each commit is correct independently (i.e., each commit should compile and pass tests).
+
+* If your patch is not getting reviewed or you need a specific person to review it, you can @-reply a reviewer asking for a review in the pull request or a comment.
+
+* Add tests relevant to the fixed bug or new feature.

+ 20 - 2
Gopkg.lock

@@ -19,6 +19,12 @@
   packages = ["."]
   revision = "7677a1d7c1137cd3dd5ba7a076d0c898a1ef4520"
 
+[[projects]]
+  branch = "master"
+  name = "github.com/VividCortex/mysqlerr"
+  packages = ["."]
+  revision = "6c6b55f8796f578c870b7e19bafb16103bc40095"
+
 [[projects]]
   name = "github.com/aws/aws-sdk-go"
   packages = [
@@ -258,7 +264,7 @@
   branch = "master"
   name = "github.com/hashicorp/yamux"
   packages = ["."]
-  revision = "2658be15c5f05e76244154714161f17e3e77de2e"
+  revision = "7221087c3d281fda5f794e28c2ea4c6e4d5c4558"
 
 [[projects]]
   name = "github.com/inconshreveable/log15"
@@ -501,6 +507,8 @@
   branch = "master"
   name = "golang.org/x/crypto"
   packages = [
+    "ed25519",
+    "ed25519/internal/edwards25519",
     "md4",
     "pbkdf2"
   ]
@@ -664,6 +672,16 @@
   revision = "e6179049628164864e6e84e973cfb56335748dea"
   version = "v2.3.2"
 
+[[projects]]
+  name = "gopkg.in/square/go-jose.v2"
+  packages = [
+    ".",
+    "cipher",
+    "json"
+  ]
+  revision = "ef984e69dd356202fd4e4910d4d9c24468bdf0b8"
+  version = "v2.1.9"
+
 [[projects]]
   name = "gopkg.in/yaml.v2"
   packages = ["."]
@@ -673,6 +691,6 @@
 [solve-meta]
   analyzer-name = "dep"
   analyzer-version = 1
-  inputs-digest = "81a37e747b875cf870c1b9486fa3147e704dea7db8ba86f7cb942d3ddc01d3e3"
+  inputs-digest = "6f7f271afd27f78b7d8ebe27436fee72c9925fb82a978bdc57fde44e01f3ca51"
   solver-name = "gps-cdcl"
   solver-version = 1

+ 8 - 0
Gopkg.toml

@@ -203,3 +203,11 @@ ignored = [
 [[constraint]]
   name = "github.com/denisenkom/go-mssqldb"
   revision = "270bc3860bb94dd3a3ffd047377d746c5e276726"
+
+[[constraint]]
+  name = "github.com/VividCortex/mysqlerr"
+  branch = "master"
+
+[[constraint]]
+  name = "gopkg.in/square/go-jose.v2"
+  version = "2.1.9"

+ 5 - 0
Gruntfile.js

@@ -9,12 +9,17 @@ module.exports = function (grunt) {
     destDir: 'dist',
     tempDir: 'tmp',
     platform: process.platform.replace('win32', 'windows'),
+    enterprise: false,
   };
 
   if (grunt.option('platform')) {
     config.platform = grunt.option('platform');
   }
 
+  if (grunt.option('enterprise')) {
+    config.enterprise = true;
+  }
+
   if (grunt.option('arch')) {
     config.arch = grunt.option('arch');
   } else {

+ 8 - 2
Makefile

@@ -5,8 +5,7 @@ all: deps build
 deps-go:
 	go run build.go setup
 
-deps-js:
-	yarn install --pure-lockfile --no-progress
+deps-js: node_modules
 
 deps: deps-js
 
@@ -43,3 +42,10 @@ test: test-go test-js
 
 run:
 	./bin/grafana-server
+
+clean:
+	rm -rf node_modules
+	rm -rf public/build
+
+node_modules: package.json yarn.lock
+	yarn install --pure-lockfile --no-progress

+ 2 - 2
PLUGIN_DEV.md

@@ -6,8 +6,8 @@ upgrading Grafana please check here before creating an issue.
 
 ## Links
 
-- [Datasource plugin written in typescript](https://github.com/grafana/typescript-template-datasource)
-- [Simple json dataource plugin](https://github.com/grafana/simple-json-datasource)
+- [Datasource plugin written in TypeScript](https://github.com/grafana/typescript-template-datasource)
+- [Simple JSON datasource plugin](https://github.com/grafana/simple-json-datasource)
 - [Plugin development guide](http://docs.grafana.org/plugins/developing/development/)
 - [Webpack Grafana plugin template project](https://github.com/CorpGlory/grafana-plugin-template-webpack)
 

+ 16 - 16
README.md

@@ -24,7 +24,7 @@ the latest master builds [here](https://grafana.com/grafana/download)
 
 ### Dependencies
 
-- Go 1.11
+- Go (Latest Stable)
 - NodeJS LTS
 
 ### Building the backend
@@ -69,15 +69,27 @@ 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)
+### Building a Docker image
 
-This builds a docker image from your local sources:
+There are two different ways to build a Grafana docker image. If you're machine is setup for Grafana development and you run linux/amd64 you can build just the image. Otherwise, there is the option to build Grafana completely within Docker.
+
+Run the image you have built using: `docker run --rm -p 3000:3000 grafana/grafana:dev`
+
+#### Building on linux/amd64 (fast)
 
 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`
 
+#### Building anywhere (slower)
+
+Choose this option to build on platforms other than linux/amd64 and/or not have to setup the Grafana development environment.
+
+1. `make build-docker-full` or `docker build -t grafana/grafana: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.
@@ -113,18 +125,6 @@ GRAFANA_TEST_DB=mysql 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.
@@ -138,5 +138,5 @@ plugin development.
 
 ## License
 
-Grafana is distributed under Apache 2.0 License.
+Grafana is distributed under [Apache 2.0 License](https://github.com/grafana/grafana/blob/master/LICENSE.md).
 

+ 89 - 0
UPGRADING_DEPENDENCIES.md

@@ -0,0 +1,89 @@
+# Guide to Upgrading Dependencies
+
+Upgrading Go or Node.js requires making changes in many different files. See below for a list and explanation for each.
+
+## Go
+
+- CircleCi
+- `grafana/build-container`
+- Appveyor
+- Dockerfile
+
+## Node.js
+
+- CircleCI
+- `grafana/build-container`
+- Appveyor
+- Dockerfile
+
+## Go Dependencies
+
+Updated using `dep`.
+
+- `Gopkg.toml`
+- `Gopkg.lock`
+
+## Node.js Dependencies
+
+Updated using `yarn`.
+
+- `package.json`
+
+## Where to make changes
+
+### CircleCI
+
+Our builds run on CircleCI through our build script.
+
+#### Files
+
+- `.circleci/config.yml`.
+
+#### Dependencies
+
+- nodejs
+- golang
+- grafana/build-container (our custom docker build container)
+
+### grafana/build-container
+
+The main build step (in CircleCI) is built using a custom build container that comes pre-baked with some of the neccesary dependencies.
+
+Link: [grafana-build-container](https://github.com/grafana/grafana-build-container)
+
+#### Dependencies
+
+- fpm
+- nodejs
+- golang
+- crosscompiling (several compilers)
+
+### Appveyor
+
+Master and release builds trigger test runs on Appveyors build environment so that tests will run on Windows.
+
+#### Files:
+
+- `appveyor.yml`
+
+#### Dependencies
+
+- nodejs
+- golang
+
+### Dockerfile
+
+There is a Docker build for Grafana in the root of the project that allows anyone to build Grafana just using Docker.
+
+#### Files
+
+- `Dockerfile`
+
+#### Dependencies
+
+- nodejs
+- golang
+
+### Local developer environments
+
+Please send out a notice in the grafana-dev slack channel when updating Go or Node.js to make it easier for everyone to update their local developer environments.

+ 1 - 1
appveyor.yml

@@ -5,7 +5,7 @@ os: Windows Server 2012 R2
 clone_folder: c:\gopath\src\github.com\grafana\grafana
 
 environment:
-  nodejs_version: "6"
+  nodejs_version: "8"
   GOPATH: C:\gopath
   GOVERSION: 1.11
 

+ 13 - 0
build.go

@@ -403,6 +403,10 @@ func gruntBuildArg(task string) []string {
 	if phjsToRelease != "" {
 		args = append(args, fmt.Sprintf("--phjsToRelease=%v", phjsToRelease))
 	}
+	if enterprise {
+		args = append(args, "--enterprise")
+	}
+
 	args = append(args, fmt.Sprintf("--platform=%v", goos))
 
 	return args
@@ -467,6 +471,7 @@ func ldflags() string {
 	b.WriteString(fmt.Sprintf(" -X main.version=%s", version))
 	b.WriteString(fmt.Sprintf(" -X main.commit=%s", getGitSha()))
 	b.WriteString(fmt.Sprintf(" -X main.buildstamp=%d", buildStamp()))
+	b.WriteString(fmt.Sprintf(" -X main.buildBranch=%s", getGitBranch()))
 	return b.String()
 }
 
@@ -514,6 +519,14 @@ func setBuildEnv() {
 	}
 }
 
+func getGitBranch() string {
+	v, err := runError("git", "rev-parse", "--abbrev-ref", "HEAD")
+	if err != nil {
+		return "master"
+	}
+	return string(v)
+}
+
 func getGitSha() string {
 	v, err := runError("git", "rev-parse", "--short", "HEAD")
 	if err != nil {

+ 7 - 0
conf/defaults.ini

@@ -554,3 +554,10 @@ container_name =
 # Options to configure external image rendering server like https://github.com/grafana/grafana-image-renderer
 server_url =
 callback_url =
+
+[panels]
+enable_alpha = false
+
+[enterprise]
+license_path =
+

+ 6 - 1
conf/sample.ini

@@ -435,7 +435,7 @@ log_queries =
 ;sampler_param = 1
 
 #################################### Grafana.com integration  ##########################
-# Url used to to import dashboards directly from Grafana.com
+# Url used to import dashboards directly from Grafana.com
 [grafana_com]
 ;url = https://grafana.com
 
@@ -475,3 +475,8 @@ log_queries =
 # Options to configure external image rendering server like https://github.com/grafana/grafana-image-renderer
 ;server_url =
 ;callback_url =
+
+[enterprise]
+# Path to a valid Grafana Enterprise license.jwt file
+;license_path =
+

+ 123 - 6
devenv/dev-dashboards/panel_tests_graph.json

@@ -927,6 +927,123 @@
       "title": "",
       "type": "text"
     },
+    {
+      "aliasColors": {},
+      "bars": false,
+      "dashLength": 10,
+      "dashes": false,
+      "datasource": "gdev-testdata",
+      "editable": true,
+      "error": false,
+      "fill": 0,
+      "gridPos": {
+        "h": 7,
+        "w": 16,
+        "x": 0,
+        "y": 44
+      },
+      "id": 21,
+      "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": 5,
+      "points": false,
+      "renderer": "flot",
+      "seriesOverrides": [
+        {
+          "alias": "C-series",
+          "steppedLine": true
+        }
+      ],
+      "spaceLength": 10,
+      "stack": false,
+      "steppedLine": false,
+      "targets": [
+        {
+          "alias": "",
+          "hide": false,
+          "refId": "B",
+          "scenarioId": "csv_metric_values",
+          "stringInput": "1,null,40,null,90,null,null,100,null,null,100,null,null,80,null",
+          "target": ""
+        },
+        {
+          "alias": "",
+          "hide": false,
+          "refId": "C",
+          "scenarioId": "csv_metric_values",
+          "stringInput": "20,null40,null,null,50,null,70,null,100,null,10,null,30,null",
+          "target": ""
+        }
+      ],
+      "thresholds": [],
+      "timeFrom": null,
+      "timeShift": null,
+      "title": "Null between points",
+      "tooltip": {
+        "msResolution": false,
+        "shared": true,
+        "sort": 0,
+        "value_type": "cumulative"
+      },
+      "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
+      }
+    },
+    {
+      "content": "Left is showing null between values for a normal line graph and staircase graph. Orphaned data points should be rendered as points",
+      "editable": true,
+      "error": false,
+      "gridPos": {
+        "h": 7,
+        "w": 8,
+        "x": 16,
+        "y": 44
+      },
+      "id": 22,
+      "links": [],
+      "mode": "markdown",
+      "title": "",
+      "type": "text"
+    },
     {
       "aliasColors": {},
       "bars": false,
@@ -939,7 +1056,7 @@
         "h": 7,
         "w": 24,
         "x": 0,
-        "y": 44
+        "y": 51
       },
       "id": 20,
       "legend": {
@@ -1024,7 +1141,7 @@
         "h": 7,
         "w": 12,
         "x": 0,
-        "y": 51
+        "y": 58
       },
       "id": 16,
       "legend": {
@@ -1127,7 +1244,7 @@
         "h": 7,
         "w": 12,
         "x": 12,
-        "y": 51
+        "y": 58
       },
       "id": 17,
       "legend": {
@@ -1266,7 +1383,7 @@
         "h": 7,
         "w": 12,
         "x": 0,
-        "y": 58
+        "y": 65
       },
       "id": 18,
       "legend": {
@@ -1370,7 +1487,7 @@
         "h": 7,
         "w": 12,
         "x": 12,
-        "y": 58
+        "y": 65
       },
       "id": 19,
       "legend": {
@@ -1554,5 +1671,5 @@
   "timezone": "browser",
   "title": "Panel Tests - Graph",
   "uid": "5SdHCadmz",
-  "version": 3
+  "version": 1
 }

+ 1166 - 0
devenv/dev-dashboards/panel_tests_slow_queries_and_annotations.json

@@ -0,0 +1,1166 @@
+{
+  "annotations": {
+    "list": [
+      {
+        "builtIn": 1,
+        "datasource": "-- Grafana --",
+        "enable": true,
+        "hide": true,
+        "iconColor": "rgba(0, 211, 255, 1)",
+        "name": "Annotations & Alerts",
+        "type": "dashboard"
+      },
+      {
+        "datasource": "-- Grafana --",
+        "enable": true,
+        "hide": false,
+        "iconColor": "rgba(255, 96, 96, 1)",
+        "limit": 100,
+        "matchAny": false,
+        "name": "annotations",
+        "showIn": 0,
+        "tags": [
+          "asd"
+        ],
+        "type": "tags"
+      }
+    ]
+  },
+  "editable": true,
+  "gnetId": null,
+  "graphTooltip": 0,
+  "links": [],
+  "panels": [
+    {
+      "aliasColors": {},
+      "bars": false,
+      "dashLength": 10,
+      "dashes": false,
+      "datasource": "gdev-testdata",
+      "fill": 1,
+      "gridPos": {
+        "h": 7,
+        "w": 13,
+        "x": 0,
+        "y": 0
+      },
+      "id": 6,
+      "legend": {
+        "avg": false,
+        "current": false,
+        "max": false,
+        "min": false,
+        "show": true,
+        "total": false,
+        "values": false
+      },
+      "lines": true,
+      "linewidth": 1,
+      "nullPointMode": "null",
+      "percentage": false,
+      "pointradius": 5,
+      "points": false,
+      "renderer": "flot",
+      "seriesOverrides": [],
+      "spaceLength": 10,
+      "stack": false,
+      "steppedLine": false,
+      "targets": [
+        {
+          "expr": "",
+          "format": "time_series",
+          "intervalFactor": 1,
+          "refId": "A",
+          "scenarioId": "slow_query",
+          "stringInput": "30s"
+        }
+      ],
+      "thresholds": [],
+      "timeFrom": null,
+      "timeShift": null,
+      "title": "Panel Title",
+      "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": "gdev-testdata",
+      "fill": 1,
+      "gridPos": {
+        "h": 7,
+        "w": 11,
+        "x": 13,
+        "y": 0
+      },
+      "id": 7,
+      "legend": {
+        "avg": false,
+        "current": false,
+        "max": false,
+        "min": false,
+        "show": true,
+        "total": false,
+        "values": false
+      },
+      "lines": true,
+      "linewidth": 1,
+      "nullPointMode": "null",
+      "percentage": false,
+      "pointradius": 5,
+      "points": false,
+      "renderer": "flot",
+      "seriesOverrides": [],
+      "spaceLength": 10,
+      "stack": false,
+      "steppedLine": false,
+      "targets": [
+        {
+          "expr": "",
+          "format": "time_series",
+          "intervalFactor": 1,
+          "refId": "A",
+          "scenarioId": "slow_query",
+          "stringInput": "30s"
+        }
+      ],
+      "thresholds": [],
+      "timeFrom": null,
+      "timeShift": null,
+      "title": "Panel Title",
+      "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": "gdev-testdata",
+      "fill": 1,
+      "gridPos": {
+        "h": 7,
+        "w": 8,
+        "x": 0,
+        "y": 7
+      },
+      "id": 8,
+      "legend": {
+        "avg": false,
+        "current": false,
+        "max": false,
+        "min": false,
+        "show": true,
+        "total": false,
+        "values": false
+      },
+      "lines": true,
+      "linewidth": 1,
+      "nullPointMode": "null",
+      "percentage": false,
+      "pointradius": 5,
+      "points": false,
+      "renderer": "flot",
+      "seriesOverrides": [],
+      "spaceLength": 10,
+      "stack": false,
+      "steppedLine": false,
+      "targets": [
+        {
+          "expr": "",
+          "format": "time_series",
+          "intervalFactor": 1,
+          "refId": "A",
+          "scenarioId": "slow_query",
+          "stringInput": "30s"
+        }
+      ],
+      "thresholds": [],
+      "timeFrom": null,
+      "timeShift": null,
+      "title": "Panel Title",
+      "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": "gdev-testdata",
+      "fill": 1,
+      "gridPos": {
+        "h": 7,
+        "w": 8,
+        "x": 8,
+        "y": 7
+      },
+      "id": 18,
+      "legend": {
+        "avg": false,
+        "current": false,
+        "max": false,
+        "min": false,
+        "show": true,
+        "total": false,
+        "values": false
+      },
+      "lines": true,
+      "linewidth": 1,
+      "nullPointMode": "null",
+      "percentage": false,
+      "pointradius": 5,
+      "points": false,
+      "renderer": "flot",
+      "seriesOverrides": [],
+      "spaceLength": 10,
+      "stack": false,
+      "steppedLine": false,
+      "targets": [
+        {
+          "expr": "",
+          "format": "time_series",
+          "intervalFactor": 1,
+          "refId": "A",
+          "scenarioId": "slow_query",
+          "stringInput": "30s"
+        }
+      ],
+      "thresholds": [],
+      "timeFrom": null,
+      "timeShift": null,
+      "title": "Panel Title",
+      "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": "gdev-testdata",
+      "fill": 1,
+      "gridPos": {
+        "h": 7,
+        "w": 8,
+        "x": 16,
+        "y": 7
+      },
+      "id": 17,
+      "legend": {
+        "avg": false,
+        "current": false,
+        "max": false,
+        "min": false,
+        "show": true,
+        "total": false,
+        "values": false
+      },
+      "lines": true,
+      "linewidth": 1,
+      "nullPointMode": "null",
+      "percentage": false,
+      "pointradius": 5,
+      "points": false,
+      "renderer": "flot",
+      "seriesOverrides": [],
+      "spaceLength": 10,
+      "stack": false,
+      "steppedLine": false,
+      "targets": [
+        {
+          "expr": "",
+          "format": "time_series",
+          "intervalFactor": 1,
+          "refId": "A",
+          "scenarioId": "slow_query",
+          "stringInput": "30s"
+        }
+      ],
+      "thresholds": [],
+      "timeFrom": null,
+      "timeShift": null,
+      "title": "Panel Title",
+      "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": "gdev-testdata",
+      "fill": 1,
+      "gridPos": {
+        "h": 5,
+        "w": 8,
+        "x": 0,
+        "y": 14
+      },
+      "id": 10,
+      "legend": {
+        "avg": false,
+        "current": false,
+        "max": false,
+        "min": false,
+        "show": true,
+        "total": false,
+        "values": false
+      },
+      "lines": true,
+      "linewidth": 1,
+      "nullPointMode": "null",
+      "percentage": false,
+      "pointradius": 5,
+      "points": false,
+      "renderer": "flot",
+      "seriesOverrides": [],
+      "spaceLength": 10,
+      "stack": false,
+      "steppedLine": false,
+      "targets": [
+        {
+          "expr": "",
+          "format": "time_series",
+          "intervalFactor": 1,
+          "refId": "A",
+          "scenarioId": "slow_query",
+          "stringInput": "5s"
+        }
+      ],
+      "thresholds": [],
+      "timeFrom": null,
+      "timeShift": null,
+      "title": "Panel Title",
+      "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": "gdev-testdata",
+      "fill": 1,
+      "gridPos": {
+        "h": 5,
+        "w": 8,
+        "x": 8,
+        "y": 14
+      },
+      "id": 9,
+      "legend": {
+        "avg": false,
+        "current": false,
+        "max": false,
+        "min": false,
+        "show": true,
+        "total": false,
+        "values": false
+      },
+      "lines": true,
+      "linewidth": 1,
+      "nullPointMode": "null",
+      "percentage": false,
+      "pointradius": 5,
+      "points": false,
+      "renderer": "flot",
+      "seriesOverrides": [],
+      "spaceLength": 10,
+      "stack": false,
+      "steppedLine": false,
+      "targets": [
+        {
+          "expr": "",
+          "format": "time_series",
+          "intervalFactor": 1,
+          "refId": "A",
+          "scenarioId": "slow_query",
+          "stringInput": "5s"
+        }
+      ],
+      "thresholds": [],
+      "timeFrom": null,
+      "timeShift": null,
+      "title": "Panel Title",
+      "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": "gdev-testdata",
+      "fill": 1,
+      "gridPos": {
+        "h": 5,
+        "w": 8,
+        "x": 16,
+        "y": 14
+      },
+      "id": 11,
+      "legend": {
+        "avg": false,
+        "current": false,
+        "max": false,
+        "min": false,
+        "show": true,
+        "total": false,
+        "values": false
+      },
+      "lines": true,
+      "linewidth": 1,
+      "nullPointMode": "null",
+      "percentage": false,
+      "pointradius": 5,
+      "points": false,
+      "renderer": "flot",
+      "seriesOverrides": [],
+      "spaceLength": 10,
+      "stack": false,
+      "steppedLine": false,
+      "targets": [
+        {
+          "expr": "",
+          "format": "time_series",
+          "intervalFactor": 1,
+          "refId": "A",
+          "scenarioId": "slow_query",
+          "stringInput": "5s"
+        }
+      ],
+      "thresholds": [],
+      "timeFrom": null,
+      "timeShift": null,
+      "title": "Panel Title",
+      "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": "gdev-testdata",
+      "fill": 1,
+      "gridPos": {
+        "h": 5,
+        "w": 8,
+        "x": 0,
+        "y": 19
+      },
+      "id": 14,
+      "legend": {
+        "avg": false,
+        "current": false,
+        "max": false,
+        "min": false,
+        "show": true,
+        "total": false,
+        "values": false
+      },
+      "lines": true,
+      "linewidth": 1,
+      "nullPointMode": "null",
+      "percentage": false,
+      "pointradius": 5,
+      "points": false,
+      "renderer": "flot",
+      "seriesOverrides": [],
+      "spaceLength": 10,
+      "stack": false,
+      "steppedLine": false,
+      "targets": [
+        {
+          "expr": "",
+          "format": "time_series",
+          "intervalFactor": 1,
+          "refId": "A",
+          "scenarioId": "slow_query",
+          "stringInput": "5s"
+        }
+      ],
+      "thresholds": [],
+      "timeFrom": null,
+      "timeShift": null,
+      "title": "Panel Title",
+      "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": "gdev-testdata",
+      "fill": 1,
+      "gridPos": {
+        "h": 5,
+        "w": 8,
+        "x": 8,
+        "y": 19
+      },
+      "id": 15,
+      "legend": {
+        "avg": false,
+        "current": false,
+        "max": false,
+        "min": false,
+        "show": true,
+        "total": false,
+        "values": false
+      },
+      "lines": true,
+      "linewidth": 1,
+      "nullPointMode": "null",
+      "percentage": false,
+      "pointradius": 5,
+      "points": false,
+      "renderer": "flot",
+      "seriesOverrides": [],
+      "spaceLength": 10,
+      "stack": false,
+      "steppedLine": false,
+      "targets": [
+        {
+          "expr": "",
+          "format": "time_series",
+          "intervalFactor": 1,
+          "refId": "A",
+          "scenarioId": "slow_query",
+          "stringInput": "5s"
+        }
+      ],
+      "thresholds": [],
+      "timeFrom": null,
+      "timeShift": null,
+      "title": "Panel Title",
+      "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": "gdev-testdata",
+      "fill": 1,
+      "gridPos": {
+        "h": 5,
+        "w": 8,
+        "x": 16,
+        "y": 19
+      },
+      "id": 12,
+      "legend": {
+        "avg": false,
+        "current": false,
+        "max": false,
+        "min": false,
+        "show": true,
+        "total": false,
+        "values": false
+      },
+      "lines": true,
+      "linewidth": 1,
+      "nullPointMode": "null",
+      "percentage": false,
+      "pointradius": 5,
+      "points": false,
+      "renderer": "flot",
+      "seriesOverrides": [],
+      "spaceLength": 10,
+      "stack": false,
+      "steppedLine": false,
+      "targets": [
+        {
+          "expr": "",
+          "format": "time_series",
+          "intervalFactor": 1,
+          "refId": "A",
+          "scenarioId": "slow_query",
+          "stringInput": "5s"
+        }
+      ],
+      "thresholds": [],
+      "timeFrom": null,
+      "timeShift": null,
+      "title": "Panel Title",
+      "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": "gdev-testdata",
+      "fill": 1,
+      "gridPos": {
+        "h": 6,
+        "w": 16,
+        "x": 0,
+        "y": 24
+      },
+      "id": 13,
+      "legend": {
+        "avg": false,
+        "current": false,
+        "max": false,
+        "min": false,
+        "show": true,
+        "total": false,
+        "values": false
+      },
+      "lines": true,
+      "linewidth": 1,
+      "nullPointMode": "null",
+      "percentage": false,
+      "pointradius": 5,
+      "points": false,
+      "renderer": "flot",
+      "seriesOverrides": [],
+      "spaceLength": 10,
+      "stack": false,
+      "steppedLine": false,
+      "targets": [
+        {
+          "expr": "",
+          "format": "time_series",
+          "intervalFactor": 1,
+          "refId": "A",
+          "scenarioId": "slow_query",
+          "stringInput": "5s"
+        }
+      ],
+      "thresholds": [],
+      "timeFrom": null,
+      "timeShift": null,
+      "title": "Panel Title",
+      "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": "gdev-testdata",
+      "fill": 1,
+      "gridPos": {
+        "h": 6,
+        "w": 8,
+        "x": 16,
+        "y": 24
+      },
+      "id": 16,
+      "legend": {
+        "avg": false,
+        "current": false,
+        "max": false,
+        "min": false,
+        "show": true,
+        "total": false,
+        "values": false
+      },
+      "lines": true,
+      "linewidth": 1,
+      "nullPointMode": "null",
+      "percentage": false,
+      "pointradius": 5,
+      "points": false,
+      "renderer": "flot",
+      "seriesOverrides": [],
+      "spaceLength": 10,
+      "stack": false,
+      "steppedLine": false,
+      "targets": [
+        {
+          "expr": "",
+          "format": "time_series",
+          "intervalFactor": 1,
+          "refId": "A",
+          "scenarioId": "slow_query",
+          "stringInput": "5s"
+        }
+      ],
+      "thresholds": [],
+      "timeFrom": null,
+      "timeShift": null,
+      "title": "Panel Title",
+      "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
+      }
+    }
+  ],
+  "schemaVersion": 16,
+  "style": "dark",
+  "tags": [],
+  "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 - Slow Queries & Annotations",
+  "uid": "xtY_uCAiz",
+  "version": 11
+}

+ 1 - 1
devenv/docker/blocks/influxdb/docker-compose.yaml

@@ -6,7 +6,7 @@
       - "8083:8083"
       - "8086:8086"
     volumes:
-      - ./blocks/influxdb/influxdb.conf:/etc/influxdb/influxdb.conf
+      - ./docker/blocks/influxdb/influxdb.conf:/etc/influxdb/influxdb.conf
 
   fake-influxdb-data:
     image: grafana/fake-data-gen

+ 1 - 0
devenv/docker/ha_test/.gitignore

@@ -0,0 +1 @@
+grafana/provisioning/dashboards/alerts/alert-*

+ 137 - 0
devenv/docker/ha_test/README.md

@@ -0,0 +1,137 @@
+# Grafana High Availability (HA) test setup
+
+A set of docker compose services which together creates a Grafana HA test setup with capability of easily
+scaling up/down number of Grafana instances.
+
+Included services
+
+* Grafana
+* Mysql - Grafana configuration database and session storage
+* Prometheus - Monitoring of Grafana and used as datasource of provisioned alert rules
+* Nginx - Reverse proxy for Grafana and Prometheus. Enables browsing Grafana/Prometheus UI using a hostname
+
+## Prerequisites
+
+### Build grafana docker container
+
+Build a Grafana docker container from current branch and commit and tag it as grafana/grafana:dev.
+
+```bash
+$ cd <grafana repo>
+$ make build-docker-full
+```
+
+### Virtual host names
+
+#### Alternative 1 - Use dnsmasq
+
+```bash
+$ sudo apt-get install dnsmasq
+$ echo 'address=/loc/127.0.0.1' | sudo tee /etc/dnsmasq.d/dnsmasq-loc.conf > /dev/null
+$ sudo /etc/init.d/dnsmasq restart
+$ ping whatever.loc
+PING whatever.loc (127.0.0.1) 56(84) bytes of data.
+64 bytes from localhost (127.0.0.1): icmp_seq=1 ttl=64 time=0.076 ms
+--- whatever.loc ping statistics ---
+1 packet transmitted, 1 received, 0% packet loss, time 1998ms
+```
+
+#### Alternative 2 - Manually update /etc/hosts
+
+Update your `/etc/hosts` to be able to access Grafana and/or Prometheus UI using a hostname.
+
+```bash
+$ cat /etc/hosts
+127.0.0.1       grafana.loc
+127.0.0.1       prometheus.loc
+```
+
+## Start services
+
+```bash
+$ docker-compose up -d
+```
+
+Browse
+* http://grafana.loc/
+* http://prometheus.loc/
+
+Check for any errors
+
+```bash
+$ docker-compose logs | grep error
+```
+
+### Scale Grafana instances up/down
+
+Scale number of Grafana instances to `<instances>`
+
+```bash
+$ docker-compose up --scale grafana=<instances> -d
+# for example 3 instances
+$ docker-compose up --scale grafana=3 -d
+```
+
+## Test alerting
+
+### Create notification channels
+
+Creates default notification channels, if not already exists
+
+```bash
+$ ./alerts.sh setup
+```
+
+### Slack notifications
+
+Disable
+
+```bash
+$ ./alerts.sh slack -d
+```
+
+Enable and configure url
+
+```bash
+$ ./alerts.sh slack -u https://hooks.slack.com/services/...
+```
+
+Enable, configure url and enable reminders
+
+```bash
+$ ./alerts.sh slack -u https://hooks.slack.com/services/... -r -e 10m
+```
+
+### Provision alert dashboards with alert rules
+
+Provision 1 dashboard/alert rule (default)
+
+```bash
+$ ./alerts.sh provision
+```
+
+Provision 10 dashboards/alert rules
+
+```bash
+$ ./alerts.sh provision -a 10
+```
+
+Provision 10 dashboards/alert rules and change condition to `gt > 100`
+
+```bash
+$ ./alerts.sh provision -a 10 -c 100
+```
+
+### Pause/unpause all alert rules
+
+Pause
+
+```bash
+$ ./alerts.sh pause
+```
+
+Unpause
+
+```bash
+$ ./alerts.sh unpause
+```

+ 156 - 0
devenv/docker/ha_test/alerts.sh

@@ -0,0 +1,156 @@
+#!/bin/bash
+
+requiresJsonnet() {
+		if ! type "jsonnet" > /dev/null; then
+				echo "you need you install jsonnet to run this script"
+				echo "follow the instructions on https://github.com/google/jsonnet"
+				exit 1
+		fi
+}
+
+setup() {
+	STATUS=$(curl -s -o /dev/null -w '%{http_code}' http://admin:admin@grafana.loc/api/alert-notifications/1)
+  if [ $STATUS -eq 200 ]; then
+    echo "Email already exists, skipping..."
+  else
+		curl -H "Content-Type: application/json" \
+		-d '{
+			"name": "Email",
+			"type":  "email",
+			"isDefault": false,
+			"sendReminder": false,
+			"uploadImage": true,
+			"settings": {
+				"addresses": "user@test.com"
+			}
+		}' \
+		http://admin:admin@grafana.loc/api/alert-notifications
+  fi
+
+	STATUS=$(curl -s -o /dev/null -w '%{http_code}' http://admin:admin@grafana.loc/api/alert-notifications/2)
+  if [ $STATUS -eq 200 ]; then
+    echo "Slack already exists, skipping..."
+  else
+		curl -H "Content-Type: application/json" \
+		-d '{
+			"name": "Slack",
+			"type":  "slack",
+			"isDefault": false,
+			"sendReminder": false,
+			"uploadImage": true
+		}' \
+		http://admin:admin@grafana.loc/api/alert-notifications
+  fi
+}
+
+slack() {
+	enabled=true
+	url=''
+	remind=false
+	remindEvery='10m'
+
+	while getopts ":e:u:dr" o; do
+    case "${o}" in
+				e)
+            remindEvery=${OPTARG}
+            ;;
+				u)
+            url=${OPTARG}
+            ;;
+				d)
+            enabled=false
+            ;;
+				r)
+            remind=true
+            ;;
+    esac
+	done
+	shift $((OPTIND-1))
+
+	curl -X PUT \
+		-H "Content-Type: application/json" \
+		-d '{
+			"id": 2,
+			"name": "Slack",
+			"type":  "slack",
+			"isDefault": '$enabled',
+			"sendReminder": '$remind',
+			"frequency": "'$remindEvery'",
+			"uploadImage": true,
+			"settings": {
+				"url": "'$url'"
+			}
+		}' \
+		http://admin:admin@grafana.loc/api/alert-notifications/2
+}
+
+provision() {
+	alerts=1
+	condition=65
+	while getopts ":a:c:" o; do
+    case "${o}" in
+        a)
+            alerts=${OPTARG}
+            ;;
+				c)
+            condition=${OPTARG}
+            ;;
+    esac
+	done
+	shift $((OPTIND-1))
+
+	requiresJsonnet
+
+	rm -rf grafana/provisioning/dashboards/alerts/alert-*.json
+	jsonnet -m grafana/provisioning/dashboards/alerts grafana/provisioning/alerts.jsonnet --ext-code alerts=$alerts --ext-code condition=$condition
+}
+
+pause() {
+	curl -H "Content-Type: application/json" \
+  -d '{"paused":true}' \
+  http://admin:admin@grafana.loc/api/admin/pause-all-alerts
+}
+
+unpause() {
+	curl -H "Content-Type: application/json" \
+  -d '{"paused":false}' \
+  http://admin:admin@grafana.loc/api/admin/pause-all-alerts
+}
+
+usage() {
+	echo -e "Usage: ./alerts.sh COMMAND [OPTIONS]\n"
+	echo -e "Commands"
+	echo -e "  setup\t\t creates default alert notification channels"
+	echo -e "  slack\t\t configure slack notification channel"
+	echo -e "    [-d]\t\t\t disable notifier, default enabled"
+	echo -e "    [-u]\t\t\t url"
+	echo -e "    [-r]\t\t\t send reminders"
+	echo -e "    [-e <remind every>]\t\t default 10m\n"
+	echo -e "  provision\t provision alerts"
+	echo -e "    [-a <alert rule count>]\t default 1"
+	echo -e "    [-c <condition value>]\t default 65\n"
+	echo -e "  pause\t\t pause all alerts"
+	echo -e "  unpause\t unpause all alerts"
+}
+
+main() {
+	local cmd=$1
+
+	if [[ $cmd == "setup" ]]; then
+		setup
+	elif [[ $cmd == "slack" ]]; then
+		slack "${@:2}"
+	elif [[ $cmd == "provision" ]]; then
+		provision "${@:2}"
+	elif [[ $cmd == "pause" ]]; then
+		pause
+	elif [[ $cmd == "unpause" ]]; then
+		unpause
+	fi
+
+  if [[ -z "$cmd" ]]; then
+		usage
+	fi
+}
+
+main "$@"

+ 78 - 0
devenv/docker/ha_test/docker-compose.yaml

@@ -0,0 +1,78 @@
+version: "2.1"
+
+services:
+  nginx-proxy:
+    image: jwilder/nginx-proxy
+    ports:
+      - "80:80"
+    volumes:
+      - /var/run/docker.sock:/tmp/docker.sock:ro
+
+  db:
+    image: mysql
+    environment:
+      MYSQL_ROOT_PASSWORD: rootpass
+      MYSQL_DATABASE: grafana
+      MYSQL_USER: grafana
+      MYSQL_PASSWORD: password
+    ports:
+      - 3306
+    healthcheck:
+      test: ["CMD", "mysqladmin" ,"ping", "-h", "localhost"]
+      timeout: 10s
+      retries: 10
+
+  # db:
+  #   image: postgres:9.3
+  #   environment:
+  #     POSTGRES_DATABASE: grafana
+  #     POSTGRES_USER: grafana
+  #     POSTGRES_PASSWORD: password
+  #   ports:
+  #     - 5432
+  #   healthcheck:
+  #     test: ["CMD-SHELL", "pg_isready -d grafana -U grafana"]
+  #     timeout: 10s
+  #     retries: 10
+
+  grafana:
+    image: grafana/grafana:dev
+    volumes:
+      - ./grafana/provisioning/:/etc/grafana/provisioning/
+    environment:
+      - VIRTUAL_HOST=grafana.loc
+      - GF_SERVER_ROOT_URL=http://grafana.loc
+      - GF_DATABASE_NAME=grafana
+      - GF_DATABASE_USER=grafana
+      - GF_DATABASE_PASSWORD=password
+      - GF_DATABASE_TYPE=mysql
+      - GF_DATABASE_HOST=db:3306
+      - GF_SESSION_PROVIDER=mysql
+      - GF_SESSION_PROVIDER_CONFIG=grafana:password@tcp(db:3306)/grafana?allowNativePasswords=true
+      # - GF_DATABASE_TYPE=postgres
+      # - GF_DATABASE_HOST=db:5432
+      # - GF_DATABASE_SSL_MODE=disable
+      # - GF_SESSION_PROVIDER=postgres
+      # - GF_SESSION_PROVIDER_CONFIG=user=grafana password=password host=db port=5432 dbname=grafana sslmode=disable
+      - GF_LOG_FILTERS=alerting.notifier:debug,alerting.notifier.slack:debug
+    ports:
+      - 3000
+    depends_on:
+      db:
+        condition: service_healthy
+
+  prometheus:
+    image: prom/prometheus:v2.4.2
+    volumes:
+      - ./prometheus/:/etc/prometheus/
+    environment:
+      - VIRTUAL_HOST=prometheus.loc
+    ports:
+      - 9090
+
+  # mysqld-exporter:
+  #   image: prom/mysqld-exporter
+  #   environment:
+  #     - DATA_SOURCE_NAME=grafana:password@(mysql:3306)/
+  #   ports:
+  #     - 9104

+ 202 - 0
devenv/docker/ha_test/grafana/provisioning/alerts.jsonnet

@@ -0,0 +1,202 @@
+local numAlerts = std.extVar('alerts');
+local condition = std.extVar('condition');
+local arr = std.range(1, numAlerts);
+
+local alertDashboardTemplate = {
+  "editable": true,
+  "gnetId": null,
+  "graphTooltip": 0,
+  "id": null,
+  "links": [],
+  "panels": [
+    {
+      "alert": {
+        "conditions": [
+          {
+            "evaluator": {
+              "params": [
+                65
+              ],
+              "type": "gt"
+            },
+            "operator": {
+              "type": "and"
+            },
+            "query": {
+              "params": [
+                "A",
+                "5m",
+                "now"
+              ]
+            },
+            "reducer": {
+              "params": [],
+              "type": "avg"
+            },
+            "type": "query"
+          }
+        ],
+        "executionErrorState": "alerting",
+        "frequency": "10s",
+        "handler": 1,
+        "name": "bulk alerting",
+        "noDataState": "no_data",
+        "notifications": [
+          {
+            "id": 2
+          }
+        ]
+      },
+      "aliasColors": {},
+      "bars": false,
+      "dashLength": 10,
+      "dashes": false,
+      "datasource": "Prometheus",
+      "fill": 1,
+      "gridPos": {
+        "h": 9,
+        "w": 12,
+        "x": 0,
+        "y": 0
+      },
+      "id": 2,
+      "legend": {
+        "avg": false,
+        "current": false,
+        "max": false,
+        "min": false,
+        "show": true,
+        "total": false,
+        "values": false
+      },
+      "lines": true,
+      "linewidth": 1,
+      "nullPointMode": "null",
+      "percentage": false,
+      "pointradius": 5,
+      "points": false,
+      "renderer": "flot",
+      "seriesOverrides": [],
+      "spaceLength": 10,
+      "stack": false,
+      "steppedLine": false,
+      "targets": [
+        {
+          "$$hashKey": "object:117",
+          "expr": "go_goroutines",
+          "format": "time_series",
+          "intervalFactor": 1,
+          "refId": "A"
+        }
+      ],
+      "thresholds": [
+        {
+          "colorMode": "critical",
+          "fill": true,
+          "line": true,
+          "op": "gt",
+          "value": 50
+        }
+      ],
+      "timeFrom": null,
+      "timeShift": null,
+      "title": "Panel Title",
+      "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
+        }
+      ]
+    }
+  ],
+  "schemaVersion": 16,
+  "style": "dark",
+  "tags": [],
+  "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": "New dashboard",
+  "uid": null,
+  "version": 0
+};
+
+
+{
+  ['alert-' + std.toString(x) + '.json']:
+    alertDashboardTemplate + {
+      panels: [
+        alertDashboardTemplate.panels[0] +
+        {
+          alert+: {
+            name: 'Alert rule ' + x,
+            conditions: [
+              alertDashboardTemplate.panels[0].alert.conditions[0] +
+              {
+                evaluator+: {
+                  params: [condition]
+                }
+              },
+            ],
+          },
+        },
+      ],
+      uid: 'alert-' + x,
+      title: 'Alert ' + x
+    },
+      for x in arr
+}

+ 8 - 0
devenv/docker/ha_test/grafana/provisioning/dashboards/alerts.yaml

@@ -0,0 +1,8 @@
+apiVersion: 1
+
+providers:
+ - name: 'Alerts'
+   folder: 'Alerts'
+   type: file
+   options:
+     path: /etc/grafana/provisioning/dashboards/alerts

+ 172 - 0
devenv/docker/ha_test/grafana/provisioning/dashboards/alerts/overview.json

@@ -0,0 +1,172 @@
+{
+  "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": [
+    {
+      "aliasColors": {
+        "Active alerts": "#bf1b00"
+      },
+      "bars": false,
+      "dashLength": 10,
+      "dashes": false,
+      "datasource": "Prometheus",
+      "fill": 1,
+      "gridPos": {
+        "h": 12,
+        "w": 24,
+        "x": 0,
+        "y": 0
+      },
+      "id": 2,
+      "interval": "",
+      "legend": {
+        "alignAsTable": true,
+        "avg": false,
+        "current": true,
+        "max": false,
+        "min": false,
+        "rightSide": true,
+        "show": true,
+        "total": false,
+        "values": true
+      },
+      "lines": true,
+      "linewidth": 2,
+      "links": [],
+      "nullPointMode": "null",
+      "percentage": false,
+      "pointradius": 5,
+      "points": false,
+      "renderer": "flot",
+      "seriesOverrides": [
+        {
+          "alias": "Active grafana instances",
+          "dashes": true,
+          "fill": 0
+        }
+      ],
+      "spaceLength": 10,
+      "stack": false,
+      "steppedLine": false,
+      "targets": [
+        {
+          "expr": "sum(increase(grafana_alerting_notification_sent_total[1m])) by(job)",
+          "format": "time_series",
+          "instant": false,
+          "interval": "1m",
+          "intervalFactor": 1,
+          "legendFormat": "Notifications sent",
+          "refId": "A"
+        },
+        {
+          "expr": "min(grafana_alerting_active_alerts) without(instance)",
+          "format": "time_series",
+          "interval": "1m",
+          "intervalFactor": 1,
+          "legendFormat": "Active alerts",
+          "refId": "B"
+        },
+        {
+          "expr": "count(up{job=\"grafana\"})",
+          "format": "time_series",
+          "intervalFactor": 1,
+          "legendFormat": "Active grafana instances",
+          "refId": "C"
+        }
+      ],
+      "thresholds": [],
+      "timeFrom": null,
+      "timeShift": null,
+      "title": "Notifications sent vs active alerts",
+      "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": 3
+      }
+    }
+  ],
+  "schemaVersion": 16,
+  "style": "dark",
+  "tags": [],
+  "templating": {
+    "list": []
+  },
+  "time": {
+    "from": "now-1h",
+    "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": "Overview",
+  "uid": "xHy7-hAik",
+  "version": 6
+}

+ 11 - 0
devenv/docker/ha_test/grafana/provisioning/datasources/datasources.yaml

@@ -0,0 +1,11 @@
+apiVersion: 1
+
+datasources:
+  - name: Prometheus
+    type: prometheus
+    access: proxy
+    url: http://prometheus:9090
+    jsonData:
+      timeInterval: 10s
+      queryTimeout: 30s
+      httpMethod: POST

+ 39 - 0
devenv/docker/ha_test/prometheus/prometheus.yml

@@ -0,0 +1,39 @@
+# my global config
+global:
+  scrape_interval:     10s # By default, scrape targets every 15 seconds.
+  evaluation_interval: 10s # By default, scrape targets every 15 seconds.
+  # scrape_timeout is set to the global default (10s).
+
+# Load and evaluate rules in this file every 'evaluation_interval' seconds.
+#rule_files:
+# - "alert.rules"
+# - "first.rules"
+# - "second.rules"
+
+# alerting:
+#   alertmanagers:
+#   - scheme: http
+#     static_configs:
+#     - targets:
+#       - "127.0.0.1:9093"
+
+scrape_configs:
+  - job_name: 'prometheus'
+    static_configs:
+      - targets: ['localhost:9090']
+
+  - job_name: 'grafana'
+    dns_sd_configs:
+      - names:
+        - 'grafana'
+        type: 'A'
+        port: 3000
+        refresh_interval: 10s
+
+  # - job_name: 'mysql'
+  #   dns_sd_configs:
+  #     - names:
+  #       - 'mysqld-exporter'
+  #       type: 'A'
+  #       port: 9104
+  #       refresh_interval: 10s

+ 2 - 2
docs/sources/administration/permissions.md

@@ -55,7 +55,7 @@ This admin flag makes a user a `Super Admin`. This means they can access the `Se
 {{< 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
-remove the default role based permssions for Editors and Viewers. It's here you can add and assign permissions to specific **Users** and **Teams**.
+remove the default role based permissions for Editors and Viewers. It's here you can add and assign permissions to specific **Users** and **Teams**.
 
 You can assign & remove permissions for **Organization Roles**, **Users** and **Teams**.
 
@@ -102,7 +102,7 @@ Permissions for a dashboard:
 
 Result: You cannot override to a lower permission. `user1` has Admin permission as the highest permission always wins.
 
-- **View**: Can only view existing dashboars/folders.
+- **View**: Can only view existing dashboards/folders.
 - You cannot override permissions for users with **Org Admin Role**
 - A more specific permission with lower permission level will not have any effect if a more general rule exists with higher permission level. For example if "Everyone with Editor Role Can Edit" exists in the ACL list then **John Doe** will still have Edit permission even after you have specifically added a permission for this user with the permission set to **View**. You need to remove or lower the permission level of the more general rule.
 

+ 10 - 6
docs/sources/administration/provisioning.md

@@ -123,7 +123,7 @@ datasources:
   withCredentials:
   # <bool> mark as default datasource. Max one per org
   isDefault:
-  # <map> fields that will be converted to json and stored in json_data
+  # <map> fields that will be converted to json and stored in jsonData
   jsonData:
      graphiteVersion: "1.1"
      tlsAuth: true
@@ -147,7 +147,7 @@ Please refer to each datasource documentation for specific provisioning examples
 
 #### Json Data
 
-Since not all datasources have the same configuration settings we only have the most common ones as fields. The rest should be stored as a json blob in the `json_data` field. Here are the most common settings that the core datasources use.
+Since not all datasources have the same configuration settings we only have the most common ones as fields. The rest should be stored as a json blob in the `jsonData` field. Here are the most common settings that the core datasources use.
 
 | Name | Type | Datasource | Description |
 | ---- | ---- | ---- | ---- |
@@ -156,9 +156,9 @@ Since not all datasources have the same configuration settings we only have the
 | tlsSkipVerify | boolean | *All* | Controls whether a client verifies the server's certificate chain and host name. |
 | graphiteVersion | string | Graphite |  Graphite version  |
 | 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 |
+| esVersion | number | Elasticsearch | Elasticsearch version as a number (2/5/56) |
+| timeField | string | Elasticsearch | Which field that should be used as timestamp |
+| interval | string | Elasticsearch | Index date time format. nil(No Pattern), 'Hourly', 'Daily', 'Weekly', 'Monthly' or 'Yearly' |
 | authType | string | Cloudwatch | Auth provider. keys/credentials/arn |
 | assumeRoleArn | string | Cloudwatch | ARN of Assume Role |
 | defaultRegion | string | Cloudwatch | AWS region |
@@ -166,8 +166,12 @@ 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' |
+| encrypt | string | MSSQL | Connection SSL encryption handling. 'disable', 'false' or 'true' |
 | 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 |
+| maxOpenConns | number | MySQL, PostgreSQL & MSSQL | Maximum number of open connections to the database (Grafana v5.4+) |
+| maxIdleConns | number | MySQL, PostgreSQL & MSSQL | Maximum number of connections in the idle connection pool (Grafana v5.4+) |
+| connMaxLifetime | number | MySQL, PostgreSQL & MSSQL | Maximum amount of time in seconds a connection may be reused (Grafana v5.4+) |
 
 #### Secure Json Data
 
@@ -217,7 +221,7 @@ Note: The JSON shown in input field and when using `Copy JSON to Clipboard` and/
 
 {{< docs-imagebox img="/img/docs/v51/provisioning_cannot_save_dashboard.png" max-width="500px" class="docs-image--no-shadow" >}}
 
-### Reuseable Dashboard Urls
+### Reusable Dashboard Urls
 
 If the dashboard in the json file contains an [uid](/reference/dashboard/#json-fields), Grafana will force insert/update on that uid. This allows you to migrate dashboards betweens Grafana instances and provisioning Grafana from configuration without breaking the urls given since the new dashboard url uses the uid as identifier.
 When Grafana starts, it will update/insert all dashboards available in the configured folders. If you modify the file, the dashboard will also be updated.

+ 2 - 2
docs/sources/alerting/notifications.md

@@ -128,7 +128,7 @@ Example json body:
 
 In DingTalk PC Client:
 
-1. Click "more" icon on left bottom of the panel.
+1. Click "more" icon on upper right of the panel.
 
 2. Click "Robot Manage" item in the pop menu, there will be a new panel call "Robot Manage".
 
@@ -140,7 +140,7 @@ In DingTalk PC Client:
 
 6. There will be a Webhook URL in the panel, looks like this: https://oapi.dingtalk.com/robot/send?access_token=xxxxxxxxx. Copy this URL to the grafana Dingtalk setting page and then click "finish".
 
-Dingtalk supports the following "message type": `text`, `link` and `markdown`. Only the `text` message type is supported.
+Dingtalk supports the following "message type": `text`, `link` and `markdown`. Only the `link` message type is supported.
 
 ### Kafka
 

+ 3 - 0
docs/sources/auth/generic-oauth.md

@@ -17,6 +17,9 @@ 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`.
 
+You may have to set the `root_url` option of `[server]` for the callback URL to be 
+correct. For example in case you are serving Grafana behind a proxy.
+
 Example config:
 
 ```bash

+ 3 - 0
docs/sources/auth/github.md

@@ -46,6 +46,9 @@ team_ids =
 allowed_organizations =
 ```
 
+You may have to set the `root_url` option of `[server]` for the callback URL to be 
+correct. For example in case you are serving Grafana behind a proxy.
+
 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.

+ 5 - 2
docs/sources/auth/gitlab.md

@@ -58,6 +58,9 @@ api_url = https://gitlab.com/api/v4
 allowed_groups =
 ```
 
+You may have to set the `root_url` option of `[server]` for the callback URL to be 
+correct. For example in case you are serving Grafana behind a proxy.
+
 Restart the Grafana backend for your changes to take effect.
 
 If you use your own instance of GitLab instead of `gitlab.com`, adjust
@@ -97,12 +100,12 @@ 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
+Here's a complete example with `allow_sign_up` enabled, and access limited to
 the `example` and `foo/bar` groups:
 
 ```ini
 [auth.gitlab]
-enabled = false
+enabled = true
 allow_sign_up = true
 client_id = GITLAB_APPLICATION_ID
 client_secret = GITLAB_SECRET

+ 3 - 0
docs/sources/auth/google.md

@@ -45,6 +45,9 @@ allowed_domains = mycompany.com mycompany.org
 allow_sign_up = true
 ```
 
+You may have to set the `root_url` option of `[server]` for the callback URL to be 
+correct. For example in case you are serving Grafana behind a proxy.
+
 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.

+ 1 - 0
docs/sources/auth/ldap.md

@@ -181,6 +181,7 @@ 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 more information on AD searches see [Microsoft's Search Filter Syntax](https://docs.microsoft.com/en-us/windows/desktop/adsi/search-filter-syntax) documentation.
 
 For troubleshooting, by changing `member_of` in `[servers.attributes]` to "dn" it will show you more accurate group memberships when [debug is enabled](#troubleshooting).
 

+ 1 - 1
docs/sources/auth/overview.md

@@ -58,7 +58,7 @@ If you change your organization name in the Grafana UI this setting needs to be
 ### Basic authentication
 
 Basic auth is enabled by default and works with the built in Grafana user password authentication system and LDAP
-authenticaten integration.
+authentication integration.
 
 To disable basic auth:
 

+ 1 - 1
docs/sources/contribute/cla.md

@@ -101,4 +101,4 @@ TO THE MAXIMUM EXTENT PERMITTED BY APPLICABLE LAW, IN NO EVENT WILL YOU [OR US]
 <br>
 <br>
 <br>
-This CLA agreement is based on the [Harmony Contributor Aggrement Template (combined)](http://www.harmonyagreements.org/agreements.html), [Creative Commons Attribution 3.0 Unported License](https://creativecommons.org/licenses/by/3.0/)
+This CLA agreement is based on the [Harmony Contributor Agreement Template (combined)](http://www.harmonyagreements.org/agreements.html), [Creative Commons Attribution 3.0 Unported License](https://creativecommons.org/licenses/by/3.0/)

+ 4 - 3
docs/sources/features/datasources/cloudwatch.md

@@ -46,7 +46,7 @@ Checkout AWS docs on [IAM Roles](http://docs.aws.amazon.com/AWSEC2/latest/UserGu
 ## IAM Policies
 
 Grafana needs permissions granted via IAM to be able to read CloudWatch metrics
-and EC2 tags/instances. You can attach these permissions to IAM roles and
+and EC2 tags/instances/regions. You can attach these permissions to IAM roles and
 utilize Grafana's built-in support for assuming roles.
 
 Here is a minimal policy example:
@@ -65,11 +65,12 @@ Here is a minimal policy example:
             "Resource": "*"
         },
         {
-            "Sid": "AllowReadingTagsFromEC2",
+            "Sid": "AllowReadingTagsInstancesRegionsFromEC2",
             "Effect": "Allow",
             "Action": [
                 "ec2:DescribeTags",
-                "ec2:DescribeInstances"
+                "ec2:DescribeInstances",
+                "ec2:DescribeRegions"
             ],
             "Resource": "*"
         }

+ 11 - 3
docs/sources/features/datasources/mssql.md

@@ -32,6 +32,10 @@ Name | Description
 *Database* | Name of your MSSQL database.
 *User* | Database user's login/username
 *Password* | Database user's password
+*Encrypt* | This option determines whether or to which extent a secure SSL TCP/IP connection will be negotiated with the server, default `false` (Grafana v5.4+).
+*Max open* | The maximum number of open connections to the database, default `unlimited` (Grafana v5.4+).
+*Max idle* | The maximum number of connections in the idle connection pool, default `2` (Grafana v5.4+).
+*Max lifetime* | The maximum amount of time in seconds a connection may be reused, default `14400`/4 hours (Grafana v5.4+).
 
 ### Min time interval
 
@@ -69,8 +73,8 @@ Make sure the user does not get any unwanted privileges from the public role.
 
 ### Known Issues
 
-MSSQL 2008 and 2008 R2 engine cannot handle login records when SSL encryption is not disabled. Due to this you may receive an `Login error: EOF` error when trying to create your datasource.
-To fix MSSQL 2008 R2 issue, install MSSQL 2008 R2 Service Pack 2. To fix MSSQL 2008 issue, install Microsoft MSSQL 2008 Service Pack 3 and Cumulative update package 3 for MSSQL 2008 SP3.
+If you're using an older version of Microsoft SQL Server like 2008 and 2008R2 you may need to disable encryption to be able to connect.
+If possible, we recommend you to use the latest service pack available for optimal compatibility.
 
 ## Query Editor
 
@@ -225,7 +229,7 @@ When above query are used in a graph panel the result will be two series named `
 
 {{< docs-imagebox img="/img/docs/v51/mssql_time_series_two.png" class="docs-image--no-shadow docs-image--right" >}}
 
-**Example with multiple `value` culumns:**
+**Example with multiple `value` columns:**
 
 ```sql
 SELECT
@@ -585,6 +589,10 @@ datasources:
     url: localhost:1433
     database: grafana
     user: grafana
+    jsonData:
+      maxOpenConns: 0         # Grafana v5.4+
+      maxIdleConns: 2         # Grafana v5.4+
+      connMaxLifetime: 14400  # Grafana v5.4+
     secureJsonData:
       password: "Password!"
 

+ 60 - 1
docs/sources/features/datasources/mysql.md

@@ -35,6 +35,9 @@ Name | Description
 *Database* | Name of your MySQL database.
 *User* | Database user's login/username
 *Password* | Database user's password
+*Max open* | The maximum number of open connections to the database, default `unlimited` (Grafana v5.4+).
+*Max idle* | The maximum number of connections in the idle connection pool, default `2` (Grafana v5.4+).
+*Max lifetime* | The maximum amount of time in seconds a connection may be reused, default `14400`/4 hours. This should always be lower than configured [wait_timeout](https://dev.mysql.com/doc/refman/8.0/en/server-system-variables.html#sysvar_wait_timeout) in MySQL (Grafana v5.4+).
 
 ### Min time interval
 
@@ -59,7 +62,7 @@ Identifier | Description
 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 `USE otherdb;` and `DROP TABLE user;` would be
-executed. To protect against this we **Highly** recommmend you create a specific mysql user with restricted permissions.
+executed. To protect against this we **Highly** recommend you create a specific mysql user with restricted permissions.
 
 Example:
 
@@ -70,6 +73,58 @@ Example:
 
 You can use wildcards (`*`)  in place of database or table if you want to grant access to more databases and tables.
 
+## Query Editor
+
+> Only available in Grafana v5.4+.
+
+{{< docs-imagebox img="/img/docs/v54/mysql_query_still.png" class="docs-image--no-shadow" animated-gif="/img/docs/v54/mysql_query.gif" >}}
+
+You find the MySQL query editor in the metrics tab in a 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 configured database. To select a table or view in another database that your database user has access to you can manually enter a fully qualified name (database.table) like `otherDb.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 (text, tinytext, mediumtext, longtext, varchar, char).
+If you want to use a column with a different datatype as metric column you may enter the column name with a cast: `CAST(numericColumn as CHAR)`.
+You may also enter arbitrary SQL expressions in the metric column field that evaluate to a text datatype like
+`CONCAT(column1, " ", CAST(numericColumn as CHAR))`.
+
+### Columns 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`.
+
+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.
+
+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.
@@ -316,4 +371,8 @@ datasources:
     database: grafana
     user: grafana
     password: password
+    jsonData:
+      maxOpenConns: 0         # Grafana v5.4+
+      maxIdleConns: 2         # Grafana v5.4+
+      connMaxLifetime: 14400  # Grafana v5.4+
 ```

+ 1 - 1
docs/sources/features/datasources/opentsdb.md

@@ -84,7 +84,7 @@ Some examples are mentioned below to make nested template queries work successfu
 Query | Description
 ------------ | -------------
 *tag_values(cpu, hostname, env=$env)*  | Return tag values for cpu metric, selected env tag value and tag key hostname
-*tag_values(cpu, hostanme, env=$env, region=$region)* | Return tag values for cpu metric, selected env tag value, selected region tag value and tag key hostname
+*tag_values(cpu, hostname, env=$env, region=$region)* | Return tag values for cpu metric, selected env tag value, selected region tag value and tag key hostname
 
 For details on OpenTSDB metric queries checkout the official [OpenTSDB documentation](http://opentsdb.net/docs/build/html/index.html)
 

+ 6 - 0
docs/sources/features/datasources/postgres.md

@@ -31,6 +31,9 @@ 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.
+*Max open* | The maximum number of open connections to the database, default `unlimited` (Grafana v5.4+).
+*Max idle* | The maximum number of connections in the idle connection pool, default `2` (Grafana v5.4+).
+*Max lifetime* | The maximum amount of time in seconds a connection may be reused, default `14400`/4 hours (Grafana v5.4+).
 *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+).
 
@@ -374,6 +377,9 @@ datasources:
       password: "Password!"
     jsonData:
       sslmode: "disable" # disable/require/verify-ca/verify-full
+      maxOpenConns: 0         # Grafana v5.4+
+      maxIdleConns: 2         # Grafana v5.4+
+      connMaxLifetime: 14400  # Grafana v5.4+
       postgresVersion: 903 # 903=9.3, 904=9.4, 905=9.5, 906=9.6, 1000=10
       timescaledb: false
 ```

+ 253 - 0
docs/sources/features/datasources/stackdriver.md

@@ -0,0 +1,253 @@
++++
+title = "Using Stackdriver in Grafana"
+description = "Guide for using Stackdriver in Grafana"
+keywords = ["grafana", "stackdriver", "google", "guide"]
+type = "docs"
+aliases = ["/datasources/stackdriver"]
+[menu.docs]
+name = "Stackdriver"
+parent = "datasources"
+weight = 11
++++
+
+# Using Google Stackdriver in Grafana
+
+> Only available in Grafana v5.3+.
+> The datasource is currently a beta feature and is subject to change.
+
+Grafana ships with built-in support for Google Stackdriver. Just add it as a datasource and you are ready to build dashboards for your Stackdriver metrics.
+
+## Adding the data source to Grafana
+
+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`.
+3. Click the `+ Add data source` button in the top header.
+4. Select `Stackdriver` from the _Type_ dropdown.
+5. Upload or paste in the Service Account Key file. See below for steps on how to create a Service Account Key file.
+
+> NOTE: If you're not seeing the `Data Sources` link in your side menu it means that your current user does not have the `Admin` role for the current organization.
+
+| Name                  | Description                                                                         |
+| --------------------- | ----------------------------------------------------------------------------------- |
+| _Name_                | The datasource name. This is how you refer to the datasource in panels & queries.   |
+| _Default_             | Default datasource means that it will be pre-selected for new panels.               |
+| _Service Account Key_ | Service Account Key File for a GCP Project. Instructions below on how to create it. |
+
+## Authentication
+
+There are two ways to authenticate the Stackdriver plugin - either by uploading a Google JWT file, or by automatically retrieving credentials from Google metadata server. The latter option is only available when running Grafana on GCE virtual machine.
+
+### Using a Google Service Account Key File
+
+To authenticate with the Stackdriver API, you need to create a Google Cloud Platform (GCP) Service Account for the Project you want to show data for. A Grafana datasource integrates with one GCP Project. If you want to visualize data from multiple GCP Projects then you need to create one datasource per GCP Project.
+
+#### Enable APIs
+
+The following APIs need to be enabled first:
+
+* [Monitoring API](https://console.cloud.google.com/apis/library/monitoring.googleapis.com)
+* [Cloud Resource Manager API](https://console.cloud.google.com/apis/library/cloudresourcemanager.googleapis.com)
+
+Click on the links above and click the `Enable` button:
+
+{{< docs-imagebox img="/img/docs/v53/stackdriver_enable_api.png" class="docs-image--no-shadow" caption="Enable GCP APIs" >}}
+
+#### Create a GCP Service Account for a Project
+
+1. Navigate to the [APIs & Services Credentials page](https://console.cloud.google.com/apis/credentials).
+2. Click on the `Create credentials` dropdown/button and choose the `Service account key` option.
+
+    {{< docs-imagebox img="/img/docs/v53/stackdriver_create_service_account_button.png" class="docs-image--no-shadow" caption="Create service account button" >}}
+
+3. On the `Create service account key` page, choose key type `JSON`. Then in the `Service Account` dropdown, choose the `New service account` option:
+
+    {{< docs-imagebox img="/img/docs/v53/stackdriver_create_service_account_key.png" class="docs-image--no-shadow" caption="Create service account key" >}}
+
+4. Some new fields will appear. Fill in a name for the service account in the `Service account name` field and then choose the `Monitoring Viewer` role from the `Role` dropdown:
+
+    {{< docs-imagebox img="/img/docs/v53/stackdriver_service_account_choose_role.png" class="docs-image--no-shadow" caption="Choose role" >}}
+  
+5. Click the Create button. A JSON key file will be created and downloaded to your computer. Store this file in a secure place as it allows access to your Stackdriver data.
+6. Upload it to Grafana on the datasource Configuration page. You can either upload the file or paste in the contents of the file.
+
+    {{< docs-imagebox img="/img/docs/v53/stackdriver_grafana_upload_key.png" class="docs-image--no-shadow" caption="Upload service key file to Grafana" >}}
+
+7. The file contents will be encrypted and saved in the Grafana database. Don't forget to save after uploading the file!
+
+    {{< docs-imagebox img="/img/docs/v53/stackdriver_grafana_key_uploaded.png" class="docs-image--no-shadow" caption="Service key file is uploaded to Grafana" >}}
+
+### Using GCE Default Service Account
+
+If Grafana is running on a Google Compute Engine (GCE) virtual machine, it is possible for Grafana to automatically retrieve default credentials from the metadata server. This has the advantage of not needing to generate a private key file for the service account and also not having to upload the file to Grafana. However for this to work, there are a few preconditions that need to be met.
+
+1. First of all, you need to create a Service Account that can be used by the GCE virtual machine. See detailed instructions on how to do that [here](https://cloud.google.com/compute/docs/access/create-enable-service-accounts-for-instances#createanewserviceaccount).
+2. Make sure the GCE virtual machine instance is being run as the service account that you just created. See instructions [here](https://cloud.google.com/compute/docs/access/create-enable-service-accounts-for-instances#using).
+3. Allow access to the `Stackdriver Monitoring API` scope. See instructions [here](changeserviceaccountandscopes).
+
+Read more about creating and enabling service accounts for GCE VM instances [here](https://cloud.google.com/compute/docs/access/create-enable-service-accounts-for-instances).
+
+## Metric Query Editor
+
+{{< docs-imagebox img="/img/docs/v53/stackdriver_query_editor.png" max-width= "400px" class="docs-image--right" >}}
+
+The Stackdriver query editor allows you to select metrics, group/aggregate by labels and by time, and use filters to specify which time series you want in the results.
+
+Begin by choosing a `Service` and then a metric from the `Metric` dropdown. Use the plus and minus icons in the filter and group by sections to add/remove filters or group by clauses.
+
+Stackdriver metrics can be of different kinds (GAUGE, DELTA, CUMULATIVE) and these kinds have support for different aggregation options (reducers and aligners). The Grafana query editor shows the list of available aggregation methods for a selected metric and sets a default reducer and aligner when you select the metric. Units for the Y-axis are also automatically selected by the query editor.
+
+### Filter
+
+To add a filter, click the plus icon and choose a field to filter by and enter a filter value e.g. `instance_name = grafana-1`. You can remove the filter by clicking on the filter name and select `--remove filter--`.
+
+#### Simple wildcards
+
+When the operator is set to `=` or `!=` it is possible to add wildcards to the filter value field. E.g `us-*` will capture all values that starts with "us-" and `*central-a` will capture all values that ends with "central-a". `*-central-*` captures all values that has the substring of -central-. Simple wildcards are less expensive than regular expressions.
+
+#### Regular expressions
+
+When the operator is set to `=~` or `!=~` it is possible to add regular expressions to the filter value field. E.g `us-central[1-3]-[af]` would match all values that starts with "us-central", is followed by a number in the range of 1 to 3, a dash and then either an "a" or an "f". Leading and trailing slashes are not needed when creating regular expressions.
+
+### Aggregation
+
+The aggregation field lets you combine time series based on common statistics. Read more about this option [here](https://cloud.google.com/monitoring/charts/metrics-selector#aggregation-options).
+
+The `Aligner` field allows you to align multiple time series after the same group by time interval. Read more about how it works [here](https://cloud.google.com/monitoring/charts/metrics-selector#alignment).
+
+#### Alignment Period/Group by Time
+
+The `Alignment Period` groups a metric by time if an aggregation is chosen. The default is to use the GCP Stackdriver default groupings (which allows you to compare graphs in Grafana with graphs in the Stackdriver UI).
+The option is called `Stackdriver auto` and the defaults are:
+
+* 1m for time ranges < 23 hours
+* 5m for time ranges >= 23 hours and < 6 days
+* 1h for time ranges >= 6 days
+
+The other automatic option is `Grafana auto`. This will automatically set the group by time depending on the time range chosen and the width of the graph panel. Read more about the details [here](http://docs.grafana.org/reference/templating/#the-interval-variable).
+
+It is also possible to choose fixed time intervals to group by, like `1h` or `1d`.
+
+### Group By
+
+Group by resource or metric labels to reduce the number of time series and to aggregate the results by a group by. E.g. Group by instance_name to see an aggregated metric for a Compute instance.
+
+### Alias Patterns
+
+The Alias By field allows you to control the format of the legend keys. The default is to show the metric name and labels. This can be long and hard to read. Using the following patterns in the alias field, you can format the legend key the way you want it.
+
+#### Metric Type Patterns
+
+| Alias Pattern        | Description                  | Example Result                                    |
+| -------------------- | ---------------------------- | ------------------------------------------------- |
+| `{{metric.type}}`    | returns the full Metric Type | `compute.googleapis.com/instance/cpu/utilization` |
+| `{{metric.name}}`    | returns the metric name part | `instance/cpu/utilization`                        |
+| `{{metric.service}}` | returns the service part     | `compute`                                         |
+
+#### Label Patterns
+
+In the Group By dropdown, you can see a list of metric and resource labels for a metric. These can be included in the legend key using alias patterns.
+
+| Alias Pattern Format     | Description                      | Alias Pattern Example            | Example Result   |
+| ------------------------ | -------------------------------- | -------------------------------- | ---------------- |
+| `{{metric.label.xxx}}`   | returns the metric label value   | `{{metric.label.instance_name}}` | `grafana-1-prod` |
+| `{{resource.label.xxx}}` | returns the resource label value | `{{resource.label.zone}}`        | `us-east1-b`     |
+
+Example Alias By: `{{metric.type}} - {{metric.labels.instance_name}}`
+
+Example Result: `compute.googleapis.com/instance/cpu/usage_time - server1-prod`
+
+It is also possible to resolve the name of the Monitored Resource Type. 
+
+| Alias Pattern Format     | Description                                     | Example Result   |
+| ------------------------ | ------------------------------------------------| ---------------- |
+| `{{resource.type}}`      | returns the name of the monitored resource type | `gce_instance`     |
+
+Example Alias By: `{{resource.type}} - {{metric.type}}`
+
+Example Result: `gce_instance - compute.googleapis.com/instance/cpu/usage_time`
+
+## Templating
+
+Instead of hard-coding things like server, application and sensor name in you metric queries you can use variables in their place.
+Variables are shown as dropdown select boxes at the top of the dashboard. These dropdowns makes it easy to change the data
+being displayed in your dashboard.
+
+Checkout the [Templating]({{< relref "reference/templating.md" >}}) documentation for an introduction to the templating feature and the different
+types of template variables.
+
+### Query Variable
+
+Writing variable queries is not supported yet.
+
+### Using variables in queries
+
+There are two syntaxes:
+
+* `$<varname>` Example: `metric.label.$metric_label`
+* `[[varname]]` Example: `metric.label.[[metric_label]]`
+
+Why two ways? The first syntax is easier to read and write but does not allow you to use a variable in the middle of a word. When the _Multi-value_ or _Include all value_ options are enabled, Grafana converts the labels from plain text to a regex compatible string, which means you have to use `=~` instead of `=`.
+
+## Annotations
+
+{{< docs-imagebox img="/img/docs/v53/stackdriver_annotations_query_editor.png" max-width= "400px" class="docs-image--right" >}}
+
+[Annotations]({{< relref "reference/annotations.md" >}}) allows you to overlay rich event information on top of graphs. You add annotation
+queries via the Dashboard menu / Annotations view. Annotation rendering is expensive so it is important to limit the number of rows returned. There is no support for showing Stackdriver annotations and events yet but it works well with [custom metrics](https://cloud.google.com/monitoring/custom-metrics/) in Stackdriver.
+
+With the query editor for annotations, you can select a metric and filters. The `Title` and `Text` fields support templating and can use data returned from the query. For example, the Title field could have the following text:
+
+`{{metric.type}} has value: {{metric.value}}`
+
+Example Result: `monitoring.googleapis.com/uptime_check/http_status has this value: 502`
+
+### Patterns for the Annotation Query Editor
+
+| Alias Pattern Format     | Description                      | Alias Pattern Example            | Example Result                                    |
+| ------------------------ | -------------------------------- | -------------------------------- | ------------------------------------------------- |
+| `{{metric.value}}`       | value of the metric/point        | `{{metric.value}}`               | `555`                                             |
+| `{{metric.type}}`        | returns the full Metric Type     | `{{metric.type}}`                | `compute.googleapis.com/instance/cpu/utilization` |
+| `{{metric.name}}`        | returns the metric name part     | `{{metric.name}}`                | `instance/cpu/utilization`                        |
+| `{{metric.service}}`     | returns the service part         | `{{metric.service}}`             | `compute`                                         |
+| `{{metric.label.xxx}}`   | returns the metric label value   | `{{metric.label.instance_name}}` | `grafana-1-prod`                                  |
+| `{{resource.label.xxx}}` | returns the resource label value | `{{resource.label.zone}}`        | `us-east1-b`                                      |
+
+## Configure the Datasource with Provisioning
+
+It's now possible to configure datasources using config files with Grafana's provisioning system. You can read more about how it works and all the settings you can set for datasources on the [provisioning docs page](/administration/provisioning/#datasources)
+
+Here is a provisioning example using the JWT (Service Account key file) authentication type.
+
+```yaml
+apiVersion: 1
+
+datasources:
+  - name: Stackdriver
+    type: stackdriver
+    access: proxy
+    jsonData:
+      tokenUri: https://oauth2.googleapis.com/token
+      clientEmail: stackdriver@myproject.iam.gserviceaccount.com
+      authenticationType: jwt
+      defaultProject: my-project-name
+    secureJsonData:
+      privateKey: |
+        -----BEGIN PRIVATE KEY-----
+        POSEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCb1u1Srw8ICYHS
+        ...
+        yA+23427282348234=
+        -----END PRIVATE KEY-----
+```
+
+Here is a provisioning example using GCE Default Service Account authentication.
+
+```yaml
+apiVersion: 1
+
+datasources:
+  - name: Stackdriver
+    type: stackdriver
+    access: proxy
+    jsonData:
+      authenticationType: gce
+```

+ 1 - 1
docs/sources/features/panels/alertlist.md

@@ -22,6 +22,6 @@ The alert list panel allows you to display your dashboards alerts. The list can
 
 1. **Show**: Lets you choose between current state or recent state changes.
 2. **Max Items**: Max items set the maximum of items in a list.
-3. **Sort Order**: Lets you sort your list alphabeticaly(asc/desc) or by importance.
+3. **Sort Order**: Lets you sort your list alphabetically(asc/desc) or by importance.
 4. **Alerts From** This Dashboard`: Shows alerts only from the dashboard the alert list is in.
 5. **State Filter**: Here you can filter your list by one or more parameters.

+ 1 - 1
docs/sources/features/panels/heatmap.md

@@ -80,7 +80,7 @@ the upper or lower bound of the interval.
 There are a number of datasources supporting histogram over time like Elasticsearch (by using a Histogram bucket
 aggregation) or Prometheus (with [histogram](https://prometheus.io/docs/concepts/metric_types/#histogram) metric type
 and *Format as* option set to Heatmap). But generally, any datasource could be used if it meets the requirements:
-returns series with names representing bucket bound or returns sereis sorted by the bound in ascending order.
+returns series with names representing bucket bound or returns series sorted by the bound in ascending order.
 
 With Elasticsearch you control the size of the buckets using the Histogram interval (Y-Axis) and the Date Histogram interval (X-axis).
 

+ 1 - 1
docs/sources/guides/whats-new-in-v2-5.md

@@ -25,7 +25,7 @@ correctly in UTC mode.
 <br>
 
 This release brings a fully featured query editor for Elasticsearch. You will now be able to visualize
-logs or any kind of data stored in Elasticserarch. The query editor allows you to build both simple
+logs or any kind of data stored in Elasticsearch. The query editor allows you to build both simple
 and complex queries for logs or metrics.
 
 - Compute metrics from your documents, supported Elasticsearch aggregations:

+ 1 - 1
docs/sources/guides/whats-new-in-v2.md

@@ -34,7 +34,7 @@ Organizations via a role. That role can be:
 
 There are currently no permissions on individual dashboards.
 
-Read more about Grafanas new user model on the [Admin section](../reference/admin/)
+Read more about Grafana's new user model on the [Admin section](../reference/admin/)
 
 ## Dashboard Snapshot sharing
 

+ 2 - 2
docs/sources/guides/whats-new-in-v3-1.md

@@ -21,7 +21,7 @@ The export feature is now accessed from the share menu.
 Dashboards exported from Grafana 3.1 are now more portable and easier for others to import than before.
 The export process extracts information data source types used by panels and adds these to a new `inputs`
 section in the dashboard json. So when you or another person tries to import the dashboard they will be asked to
-select data source and optional metrix prefix options.
+select data source and optional metric prefix options.
 
 <img src="/img/docs/v31/import_step1.png">
 
@@ -53,7 +53,7 @@ Grafana url to share with a colleague without having to use the Share modal.
 
 ## Internal metrics
 
-Do you want metrics about viewing metrics? Ofc you do! In this release we added support for sending metrics about Grafana to graphite.
+Do you want metrics about viewing metrics? Of course you do! In this release we added support for sending metrics about Grafana to graphite.
 You can configure interval and server in the config file.
 
 ## Logging

+ 1 - 1
docs/sources/guides/whats-new-in-v3.md

@@ -197,7 +197,7 @@ you can install it manually from [Grafana.com](https://grafana.com)
 ## Plugin showcase
 
 Discovering and installing plugins is very quick and easy with Grafana 3.0 and [Grafana.com](https://grafana.com). Here
-are a couple that I incurage you try!
+are a couple that I encourage you try!
 
 #### [Clock Panel](https://grafana.com/plugins/grafana-clock-panel)
 Support's both current time and count down mode.

+ 1 - 1
docs/sources/guides/whats-new-in-v4-2.md

@@ -45,7 +45,7 @@ We might add more global built in variables in the future and if we do we will p
 
 ### Dedupe alert notifications when running multiple servers
 
-In this release we will dedupe alert notificiations when you are running multiple servers.
+In this release we will dedupe alert notifications when you are running multiple servers.
 This makes it possible to run alerting on multiple servers and only get one notification.
 
 We currently solve this with sql transactions which puts some limitations for how many servers you can use to execute the same rules.

+ 2 - 2
docs/sources/guides/whats-new-in-v4-5.md

@@ -45,7 +45,7 @@ More information [here](https://community.grafana.com/t/using-grafanas-query-ins
 ### Enhancements
 
 * **GitHub OAuth**: Support for GitHub organizations with 100+ teams. [#8846](https://github.com/grafana/grafana/issues/8846), thx [@skwashd](https://github.com/skwashd)
-* **Graphite**: Calls to Graphite api /metrics/find now include panel or dashboad time range (from & until) in most cases, [#8055](https://github.com/grafana/grafana/issues/8055)
+* **Graphite**: Calls to Graphite api /metrics/find now include panel or dashboard time range (from & until) in most cases, [#8055](https://github.com/grafana/grafana/issues/8055)
 * **Graphite**: Added new graphite 1.0 functions, available if you set version to 1.0.x in data source settings. New Functions: mapSeries, reduceSeries, isNonNull, groupByNodes, offsetToZero, grep, weightedAverage, removeEmptySeries, aggregateLine, averageOutsidePercentile, delay, exponentialMovingAverage, fallbackSeries, integralByInterval, interpolate, invert, linearRegression, movingMin, movingMax, movingSum, multiplySeriesWithWildcards, pow, powSeries, removeBetweenPercentile, squareRoot, timeSlice, closes [#8261](https://github.com/grafana/grafana/issues/8261)
 - **Elasticsearch**: Ad-hoc filters now use query phrase match filters instead of term filters, works on non keyword/raw fields [#9095](https://github.com/grafana/grafana/issues/9095).
 
@@ -53,7 +53,7 @@ More information [here](https://community.grafana.com/t/using-grafanas-query-ins
 
 * **InfluxDB/Elasticsearch**: The panel & data source option named "Group by time interval" is now named "Min time interval" and does now always define a lower limit for the auto group by time. Without having to use `>` prefix (that prefix still works). This should in theory have close to zero actual impact on existing dashboards. It does mean that if you used this setting to define a hard group by time interval of, say "1d", if you zoomed to a time range wide enough the time range could increase above the "1d" range as the setting is now always considered a lower limit.
 
-This option is now rennamed (and moved to Options sub section above your queries):
+This option is now renamed (and moved to Options sub section above your queries):
 ![image|519x120](upload://ySjHOVpavV6yk9LHQxL9nq2HIsT.png)
 
 Datas source selection & options & help are now above your metric queries.

+ 1 - 1
docs/sources/guides/whats-new-in-v4-6.md

@@ -61,7 +61,7 @@ This makes exploring and filtering Prometheus data much easier.
 ### Minor Changes
 
 * **SMTP**: Make it possible to set specific EHLO for smtp client. [#9319](https://github.com/grafana/grafana/issues/9319)
-* **Dataproxy**: Allow grafan to renegotiate tls connection [#9250](https://github.com/grafana/grafana/issues/9250)
+* **Dataproxy**: Allow Grafana to renegotiate tls connection [#9250](https://github.com/grafana/grafana/issues/9250)
 * **HTTP**: set net.Dialer.DualStack to true for all http clients [#9367](https://github.com/grafana/grafana/pull/9367)
 * **Alerting**: Add diff and percent diff as series reducers [#9386](https://github.com/grafana/grafana/pull/9386), thx [@shanhuhai5739](https://github.com/shanhuhai5739)
 * **Slack**: Allow images to be uploaded to slack when Token is present [#7175](https://github.com/grafana/grafana/issues/7175), thx [@xginn8](https://github.com/xginn8)

+ 74 - 0
docs/sources/guides/whats-new-in-v5-3.md

@@ -12,6 +12,80 @@ weight = -9
 
 # What's New in Grafana v5.3
 
+Grafana v5.3 brings new features, many enhancements and bug fixes. This article will detail the major new features and enhancements.
+
+- [Google Stackdriver]({{< relref "#google-stackdriver" >}}) as a core datasource!
+- [TV mode]({{< relref "#tv-and-kiosk-mode" >}}) is improved and more accessible
+- [Alerting]({{< relref "#notification-reminders" >}}) with notification reminders
+- [Postgres]({{< relref "#postgres-query-builder" >}}) gets a new query builder!
+- [OAuth]({{< relref "#improved-oauth-support-for-gitlab" >}}) support for Gitlab is improved
+- [Annotations]({{< relref "#annotations" >}}) with template variable filtering
+- [Variables]({{< relref "#variables" >}}) with free text support
+
+## Google Stackdriver
+
+{{< docs-imagebox img="/img/docs/v53/stackdriver-with-heatmap.png"  max-width= "600px" class="docs-image--no-shadow docs-image--right" >}}
+
+Grafana v5.3 ships with built-in support for [Google Stackdriver](https://cloud.google.com/stackdriver/) and enables you to visualize your Stackdriver metrics in Grafana. 
+
+Getting started with the plugin is easy. Simply create a GCE Service account that has access to the Stackdriver API scope, download the Service Account key file from Google and upload it on the Stackdriver datasource config page in Grafana and you should have a secure server-to-server authentication setup. Like other core plugins, Stackdriver has built-in support for alerting. It also comes with support for heatmaps and basic variables.
+
+If you're already accustomed to the Stackdriver Metrics Explorer UI, you'll notice that there are a lot of similarities to the query editor in Grafana. It is possible to add filters using wildcards and regular expressions. You can do Group By, Primary Aggregation and Alignment.
+
+Alias By allows you to format the legend the way you want, and it's a feature that is not yet present in the Metrics Explorer. Two other features that are only supported in the Grafana plugin are the abilities to manually set the Alignment Period in the query editor and to add Annotations queries.
+
+The Grafana Stackdriver plugin comes with support for automatic unit detection. Grafana will try to map the Stackdriver unit type to a corresponding unit type in Grafana, and if successful the panel Y-axes will be updated accordingly to display the correct unit of measure. This is the first core plugin to provide support for unit detection, and it is our intention to provide support for this in other core plugins in the near future.
+
+The datasource is still in the `beta` phase, meaning it's currently in active development and is still missing one important feature - templating queries.
+Please try it out, but be aware of that it might be subject to changes and possible bugs. We would love to hear your feedback.
+
+Please read [Using Google Stackdriver in Grafana](/features/datasources/stackdriver/) for more detailed information on how to get started and use it.
+
+## TV and Kiosk Mode
+
+{{< docs-imagebox img="/img/docs/v53/tv_mode_still.png" max-width="600px" class="docs-image--no-shadow docs-image--right" animated-gif="/img/docs/v53/tv_mode.gif" >}}
+
+We've improved the TV & kiosk mode to make it easier to use. There's now an icon in the top bar that will let you cycle through the different view modes.
+
+1. In the first view mode, the sidebar and most of the buttons in the top bar will be hidden.
+2. In the second view mode, the top bar is completely hidden so that only the dashboard itself is shown.
+3. Hit the escape key to go back to the default view mode.
+
+When switching view modes, the url will be updated to reflect the view mode selected. This allows a dashboard to be opened with a
+certain view mode enabled. Additionally, this also enables [playlists](/reference/playlist) to be started with a certain view mode enabled.
+
+<div class="clearfix"></div>
+
+## Notification Reminders
+
+Do you use Grafana alerting and have some notifications that are more important than others? Then it's possible to set reminders so that you continue to be alerted until the problem is fixed. This is done on the notification channel itself and will affect all alerts that use that channel.
+For additional examples of why reminders might be useful for you, see [multiple series](/alerting/rules/#multiple-series).
+
+Learn how to enable and configure reminders [here](/alerting/notifications/#send-reminders).
+
+## Postgres Query Builder
+
+Grafana 5.3 comes with a new graphical query builder for Postgres. This brings Postgres integration more in line with some of the other datasources and makes it easier for both advanced users and beginners to work with timeseries in Postgres. Learn more about it in the [documentation](/features/datasources/postgres/#query-editor).
+
+{{< docs-imagebox img="/img/docs/v53/postgres_query_still.png" class="docs-image--no-shadow" animated-gif="/img/docs/v53/postgres_query.gif" >}}
+
+## Improved OAuth Support for Gitlab
+
+Grafana 5.3 comes with a new OAuth integration for Gitlab that enables configuration to only allow users that are a member of certain Gitlab groups to authenticate. This makes it possible to use Gitlab OAuth with Grafana in a shared environment without giving everyone access to Grafana.
+Learn how to enable and configure it in the [documentation](/auth/gitlab/).
+
+## Annotations
+
+Grafana 5.3 brings improved support for [native annotations](/reference/annotations/#native-annotations) and makes it possible to use template variables when filtering by tags.
+Learn more about it in the [documentation](/reference/annotations/#query-by-tag).
+
+{{< docs-imagebox img="/img/docs/v53/annotation_tag_filter_variable.png" max-width="600px" >}}
+
+## Variables
+
+Grafana 5.3 ships with a brand new variable type named `Text box` which makes it easier and more convenient to provide free text input to a variable.
+This new variable type will display as a free text input field with an optional prefilled default value.
+
 ## Changelog
 
 Checkout the [CHANGELOG.md](https://github.com/grafana/grafana/blob/master/CHANGELOG.md) file for a complete list

+ 2 - 2
docs/sources/http_api/alerting.md

@@ -227,7 +227,7 @@ Content-Type: application/json
 
 ## Create alert notification
 
-You can find the full list of [supported notifers](/alerting/notifications/#all-supported-notifier) at the alert notifiers page.
+You can find the full list of [supported notifiers](/alerting/notifications/#all-supported-notifier) at the alert notifiers page.
 
 `POST /api/alert-notifications`
 
@@ -290,7 +290,7 @@ Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
   "sendReminder": true,
   "frequency": "15m",
   "settings": {
-    "addresses: "carl@grafana.com;dev@grafana.com"
+    "addresses": "carl@grafana.com;dev@grafana.com"
   }
 }
 ```

+ 1 - 1
docs/sources/http_api/dashboard_versions.md

@@ -291,7 +291,7 @@ Content-Type: text/html; charset=UTF-8
 </p>
 ```
 
-The response is a textual respresentation of the diff, with the dashboard values being in JSON, similar to the diffs seen on sites like GitHub or GitLab.
+The response is a textual representation of the diff, with the dashboard values being in JSON, similar to the diffs seen on sites like GitHub or GitLab.
 
 Status Codes:
 

+ 17 - 9
docs/sources/index.md

@@ -60,9 +60,9 @@ aliases = ["v1.1", "guides/reference/admin"]
         <h4>Provisioning</h4>
         <p>A guide to help you automate your Grafana setup & configuration.</p>
     </a>
-    <a href="{{< relref "guides/whats-new-in-v5-2.md" >}}" class="nav-cards__item nav-cards__item--guide">
-        <h4>What's new in v5.2</h4>
-        <p>Article on all the new cool features and enhancements in v5.2</p>
+    <a href="{{< relref "guides/whats-new-in-v5-3.md" >}}" class="nav-cards__item nav-cards__item--guide">
+        <h4>What's new in v5.3</h4>
+        <p>Article on all the new cool features and enhancements in v5.3</p>
     </a>
     <a href="{{< relref "tutorials/screencasts.md" >}}" class="nav-cards__item nav-cards__item--guide">
         <h4>Screencasts</h4>
@@ -88,9 +88,13 @@ aliases = ["v1.1", "guides/reference/admin"]
       <img src="/img/docs/logos/icon_prometheus.svg" >
       <h5>Prometheus</h5>
     </a>
-    <a href="{{< relref "features/datasources/opentsdb.md" >}}" class="nav-cards__item nav-cards__item--ds">
-      <img src="/img/docs/logos/icon_opentsdb.png" >
-      <h5>OpenTSDB</h5>
+    <a href="{{< relref "features/datasources/stackdriver.md" >}}" class="nav-cards__item nav-cards__item--ds">
+      <img src="/img/docs/logos/stackdriver_logo.png">
+      <h5>Google Stackdriver</h5>
+    </a>
+    <a href="{{< relref "features/datasources/cloudwatch.md" >}}" class="nav-cards__item nav-cards__item--ds">
+      <img src="/img/docs/logos/icon_cloudwatch.svg">
+      <h5>Cloudwatch</h5>
     </a>
     <a href="{{< relref "features/datasources/mysql.md" >}}" class="nav-cards__item nav-cards__item--ds">
       <img src="/img/docs/logos/icon_mysql.png" >
@@ -100,8 +104,12 @@ aliases = ["v1.1", "guides/reference/admin"]
       <img src="/img/docs/logos/icon_postgres.svg" >
       <h5>Postgres</h5>
     </a>
-    <a href="{{< relref "features/datasources/cloudwatch.md" >}}" class="nav-cards__item nav-cards__item--ds">
-      <img src="/img/docs/logos/icon_cloudwatch.svg">
-      <h5>Cloudwatch</h5>
+    <a href="{{< relref "features/datasources/mssql.md" >}}" class="nav-cards__item nav-cards__item--ds">
+      <img src="/img/docs/logos/sql_server_logo.svg">
+      <h5>Microsoft SQL Server</h5>
+    </a>
+    <a href="{{< relref "features/datasources/opentsdb.md" >}}" class="nav-cards__item nav-cards__item--ds">
+      <img src="/img/docs/logos/icon_opentsdb.png" >
+      <h5>OpenTSDB</h5>
     </a>
 </div>

+ 4 - 1
docs/sources/installation/configuration.md

@@ -127,10 +127,13 @@ Another way is put a webserver like Nginx or Apache in front of Grafana and have
 
 ### protocol
 
-`http` or `https`
+`http`,`https` or `socket`
 
 > **Note** Grafana versions earlier than 3.0 are vulnerable to [POODLE](https://en.wikipedia.org/wiki/POODLE). So we strongly recommend to upgrade to 3.x or use a reverse proxy for ssl termination.
 
+### socket
+Path where the socket should be created when `protocol=socket`. Please make sure that Grafana has appropriate permissions.
+
 ### domain
 
 This setting is only used in as a part of the `root_url` setting (see below). Important if you

+ 3 - 1
docs/sources/installation/debian.md

@@ -28,7 +28,7 @@ installation.
 ```bash
 wget <debian package url>
 sudo apt-get install -y adduser libfontconfig
-sudo dpkg -i grafana_5.1.4_amd64.deb
+sudo dpkg -i grafana_<version>_amd64.deb
 ```
 
 Example:
@@ -100,6 +100,8 @@ This will start the `grafana-server` process as the `grafana` user,
 which was created during the package installation. The default HTTP port
 is `3000` and default user and group is `admin`.
 
+Default login and password `admin`/ `admin`
+
 To configure the Grafana server to start at boot time:
 
 ```bash

+ 16 - 1
docs/sources/installation/docker.md

@@ -87,7 +87,7 @@ docker run \
 
 ## Building a custom Grafana image with pre-installed plugins
 
-In the [grafana-docker](https://github.com/grafana/grafana-docker/)  there is a folder called `custom/` which includes a `Dockerfile` that can be used to build a custom Grafana image.  It accepts `GRAFANA_VERSION` and `GF_INSTALL_PLUGINS` as build arguments.
+In the [grafana-docker](https://github.com/grafana/grafana/tree/master/packaging/docker)  there is a folder called `custom/` which includes a `Dockerfile` that can be used to build a custom Grafana image.  It accepts `GRAFANA_VERSION` and `GF_INSTALL_PLUGINS` as build arguments.
 
 Example of how to build and run:
 ```bash
@@ -103,6 +103,21 @@ docker run \
   grafana:latest-with-plugins
 ```
 
+## Installing Plugins from other sources
+
+> Only available in Grafana v5.3.1+
+
+It's possible to install plugins from custom url:s by specifying the url like this: `GF_INSTALL_PLUGINS=<url to plugin zip>;<plugin name>`
+
+```bash
+docker run \
+  -d \
+  -p 3000:3000 \
+  --name=grafana \
+  -e "GF_INSTALL_PLUGINS=http://plugin-domain.com/my-custom-plugin.zip;custom-plugin" \
+  grafana/grafana
+```
+
 ## Configuring AWS Credentials for CloudWatch Support
 
 ```bash

+ 2 - 0
docs/sources/installation/mac.md

@@ -60,6 +60,8 @@ Then start Grafana using:
 brew services start grafana
 ```
 
+Default login and password `admin`/ `admin`
+
 
 ### Configuration
 

+ 2 - 0
docs/sources/installation/rpm.md

@@ -115,6 +115,8 @@ This will start the `grafana-server` process as the `grafana` user,
 which is created during package installation. The default HTTP port is
 `3000`, and default user and group is `admin`.
 
+Default login and password `admin`/ `admin`
+
 To configure the Grafana server to start at boot time:
 
 ```bash

+ 3 - 0
docs/sources/installation/windows.md

@@ -31,6 +31,9 @@ on windows. Edit `custom.ini` and uncomment the `http_port`
 configuration option (`;` is the comment character in ini files) and change it to something like `8080` or similar.
 That port should not require extra Windows privileges.
 
+Default login and password `admin`/ `admin`
+
+
 Start Grafana by executing `grafana-server.exe`, located in the `bin` directory, preferably from the
 command line. If you want to run Grafana as windows service, download
 [NSSM](https://nssm.cc/). It is very easy to add Grafana as a Windows

+ 2 - 3
docs/sources/plugins/developing/development.md

@@ -10,7 +10,7 @@ weight = 1
 
 # Developer Guide
 
-You can extend Grafana by writing your own plugins and then share then with other users in [our plugin repository](https://grafana.com/plugins).
+You can extend Grafana by writing your own plugins and then share them with other users in [our plugin repository](https://grafana.com/plugins).
 
 ## Short version
 
@@ -33,7 +33,7 @@ There are two blog posts about authoring a plugin that might also be of interest
 ## What languages?
 
 Since everything turns into javascript it's up to you to choose which language you want. That said it's probably a good idea to choose es6 or typescript since
-we use es6 classes in Grafana. So it's easier to get inspiration from the Grafana repo is you choose one of those languages.
+we use es6 classes in Grafana. So it's easier to get inspiration from the Grafana repo if you choose one of those languages.
 
 ## Buildscript
 
@@ -60,7 +60,6 @@ and [apps]({{< relref "apps.md" >}}) plugins in the documentation.
 The Grafana SDK is quite small so far and can be found here:
 
 - [SDK file in Grafana](https://github.com/grafana/grafana/blob/master/public/app/plugins/sdk.ts)
-- [SDK Readme](https://github.com/grafana/grafana/blob/master/public/app/plugins/plugin_api.md)
 
 The SDK contains three different plugin classes: PanelCtrl, MetricsPanelCtrl and QueryCtrl. For plugins of the panel type, the module.js file should export one of these. There are some extra classes for [data sources]({{< relref "datasources.md" >}}).
 

+ 1 - 1
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.11](https://golang.org/dl/)
+- [Go (Latest Stable)](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.

+ 3 - 1
docs/sources/reference/annotations.md

@@ -47,7 +47,9 @@ can still show them if you add a new **Annotation Query** and filter by tags. Bu
 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. 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.
+In Grafana v5.3+ it's possible to use template variables in the tag query. So if you have a dashboard showing stats for different services and a 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.
+
+{{< docs-imagebox img="/img/docs/v53/annotation_tag_filter_variable.png" max-width="600px" >}}
 
 ## Querying other data sources
 

+ 1 - 0
docs/sources/reference/templating.md

@@ -90,6 +90,7 @@ Type | Description
 *Custom* | Define the variable options manually using a comma separated list.
 *Constant* | Define a hidden constant. Useful for metric path prefixes for dashboards you want to share. During dashboard export, constant variables will be made into an import option.
 *Ad hoc filters* | Very special kind of variable that only works with some data sources, InfluxDB & Elasticsearch currently. It allows you to add key/value filters that will automatically be added to all metric queries that use the specified data source.
+*Text box* | This variable type will display as a free text input field with an optional default value.
 
 ### Query options
 

+ 2 - 2
docs/sources/tutorials/ha_setup.md

@@ -26,9 +26,9 @@ 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.
+The second thing to consider is how to deal with user sessions and how to configure your load balancer in front of Grafana.
 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
+If you want to store sessions on disk you can use `sticky sessions` in your load balancer. 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).
 
 ### Sticky sessions

+ 6 - 1
docs/sources/tutorials/index.md

@@ -1,5 +1,6 @@
 +++
 title = "Tutorials"
+type = "docs"
 [menu.docs]
 identifier = "tutorials"
 weight = 6
@@ -11,7 +12,11 @@ This section of the docs contains a series for tutorials and stack setup guides.
 
 ## Articles
 
-- [How to integrate Hubot with Grafana](hubot_howto.md)
+- [Running Grafana behind a reverse proxy]({{< relref "behind_proxy.md" >}})
+- [API Tutorial: How To Create API Tokens And Dashboards For A Specific Organization]({{< relref "api_org_token_howto.md" >}})
+- [How to Use IIS with URL Rewrite as a Reverse Proxy for Grafana on Windows]({{< relref "iis.md" >}})
+- [How to integrate Hubot with Grafana]({{< relref "hubot_howto.md" >}})
+- [How to setup Grafana for high availability]({{< relref "ha_setup.md" >}})
 
 ## External links
 

+ 2 - 1
docs/versions.json

@@ -1,5 +1,6 @@
 [
-  { "version": "v5.2", "path": "/", "archived": false, "current": true },
+  { "version": "v5.3", "path": "/", "archived": false, "current": true },
+  { "version": "v5.2", "path": "/v5.2", "archived": true },
   { "version": "v5.1", "path": "/v5.1", "archived": true },
   { "version": "v5.0", "path": "/v5.0", "archived": true },
   { "version": "v4.6", "path": "/v4.6", "archived": true },

+ 2 - 2
latest.json

@@ -1,4 +1,4 @@
 {
-  "stable": "5.2.4",
-  "testing": "5.2.4"
+  "stable": "5.3.2",
+  "testing": "5.3.2"
 }

+ 8 - 5
package.json

@@ -4,7 +4,7 @@
     "company": "Grafana Labs"
   },
   "name": "grafana",
-  "version": "5.3.0-pre1",
+  "version": "5.4.0-pre1",
   "repository": {
     "type": "git",
     "url": "http://github.com/grafana/grafana.git"
@@ -12,11 +12,12 @@
   "devDependencies": {
     "@types/d3": "^4.10.1",
     "@types/enzyme": "^3.1.13",
-    "@types/jest": "^21.1.4",
+    "@types/jest": "^23.3.2",
     "@types/node": "^8.0.31",
     "@types/react": "^16.4.14",
     "@types/react-custom-scrollbars": "^4.0.5",
     "@types/react-dom": "^16.0.7",
+    "@types/react-select": "^2.0.4",
     "angular-mocks": "1.6.6",
     "autoprefixer": "^6.4.0",
     "axios": "^0.17.1",
@@ -46,6 +47,7 @@
     "grunt-contrib-copy": "~1.0.0",
     "grunt-contrib-cssmin": "~1.0.2",
     "grunt-exec": "^1.0.1",
+    "grunt-newer": "^1.3.0",
     "grunt-notify": "^0.4.5",
     "grunt-postcss": "^0.8.0",
     "grunt-sass": "^2.0.0",
@@ -79,14 +81,14 @@
     "style-loader": "^0.21.0",
     "systemjs": "0.20.19",
     "systemjs-plugin-css": "^0.1.36",
-    "ts-jest": "^23.1.4",
+    "ts-jest": "^23.10.4",
     "ts-loader": "^5.1.0",
     "tslib": "^1.9.3",
     "tslint": "^5.8.0",
     "tslint-loader": "^3.5.3",
     "typescript": "^3.0.3",
     "uglifyjs-webpack-plugin": "^1.2.7",
-    "webpack": "^4.8.0",
+    "webpack": "4.19.1",
     "webpack-bundle-analyzer": "^2.9.0",
     "webpack-cleanup-plugin": "^0.5.1",
     "webpack-cli": "^2.1.4",
@@ -157,8 +159,9 @@
     "react-highlight-words": "^0.10.0",
     "react-popper": "^0.7.5",
     "react-redux": "^5.0.7",
-    "react-select": "^1.1.0",
+    "react-select": "2.1.0",
     "react-sizeme": "^2.3.6",
+    "react-table": "^6.8.6",
     "react-transition-group": "^2.2.1",
     "redux": "^4.0.0",
     "redux-logger": "^3.0.6",

+ 1 - 1
packaging/docker/Dockerfile

@@ -25,7 +25,7 @@ ENV PATH=/usr/share/grafana/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bi
 
 WORKDIR $GF_PATHS_HOME
 
-RUN apt-get update && apt-get install -qq -y libfontconfig ca-certificates && \
+RUN apt-get update && apt-get install -qq -y libfontconfig ca-certificates curl && \
     apt-get autoremove -y && \
     rm -rf /var/lib/apt/lists/*
 

+ 12 - 0
packaging/docker/build-enterprise.sh

@@ -0,0 +1,12 @@
+#!/bin/sh
+set -e
+
+_grafana_tag=$1
+_docker_repo=${2:-grafana/grafana-enterprise}
+
+docker build \
+  --tag "${_docker_repo}:${_grafana_tag}"\
+  --no-cache=true \
+  .
+
+docker push "${_docker_repo}:${_grafana_tag}"

+ 0 - 29
packaging/release_process.md

@@ -1,29 +0,0 @@
-# New Grafana Release Processes
-
-## Building release packages
-
-1) Update package.json so that it has the right version.
-2) Create a git tag for the release: `git tag -a v3.0.4 -m "3.0.4 release"`
-3) Push branch & tag to github!
-2) Packages from master a built automatically by circle CI for this repo [grafana/grafana-packer](https://github.com/grafana/grafana-packer)
-
-### Non master branch
-
-When building from non master branch create a new branch in repo [grafana/grafana-packer](https://github.com/grafana/grafana-packer)
-and configure circle.yml to deploy that branch as well, https://github.com/grafana/grafana-packer/blob/master/circle.yml#L25,
-you also need to update https://github.com/grafana/grafana-packer/blob/v3.1.x/deploy.sh#L7.
-
-### Windows build
-
-Sign into ci.appveyor.com and the Grafana project's build history page. Builds for windows take a long time (around 20min)
-and fail quite often for random reasons so I usually continue with the release process without a windows build already built.
-
-1) Click on the green build that has the correct version and tag
-2) Click on `DEPLOYMENTS`
-3) Click on `NEW DEPLOYMENT`
-4) Select GrafanaBuildS3
-4) Select the build you want to deploy.
-
-The deployment should be quick (just uploads the release zip file to S3)
-
-

+ 65 - 65
pkg/api/api.go

@@ -10,10 +10,10 @@ import (
 )
 
 func (hs *HTTPServer) registerRoutes() {
-	reqSignedIn := middleware.Auth(&middleware.AuthOptions{ReqSignedIn: true})
-	reqGrafanaAdmin := middleware.Auth(&middleware.AuthOptions{ReqSignedIn: true, ReqGrafanaAdmin: true})
-	reqEditorRole := middleware.RoleAuth(m.ROLE_EDITOR, m.ROLE_ADMIN)
-	reqOrgAdmin := middleware.RoleAuth(m.ROLE_ADMIN)
+	reqSignedIn := middleware.ReqSignedIn
+	reqGrafanaAdmin := middleware.ReqGrafanaAdmin
+	reqEditorRole := middleware.ReqEditorRole
+	reqOrgAdmin := middleware.ReqOrgAdmin
 	redirectFromLegacyDashboardURL := middleware.RedirectFromLegacyDashboardURL()
 	redirectFromLegacyDashboardSoloURL := middleware.RedirectFromLegacyDashboardSoloURL()
 	quota := middleware.Quota
@@ -22,66 +22,66 @@ func (hs *HTTPServer) registerRoutes() {
 	r := hs.RouteRegister
 
 	// not logged in views
-	r.Get("/", reqSignedIn, Index)
+	r.Get("/", reqSignedIn, hs.Index)
 	r.Get("/logout", Logout)
 	r.Post("/login", quota("session"), bind(dtos.LoginCommand{}), Wrap(LoginPost))
 	r.Get("/login/:name", quota("session"), OAuthLogin)
-	r.Get("/login", LoginView)
-	r.Get("/invite/:code", Index)
+	r.Get("/login", hs.LoginView)
+	r.Get("/invite/:code", hs.Index)
 
 	// authed views
-	r.Get("/profile/", reqSignedIn, Index)
-	r.Get("/profile/password", reqSignedIn, Index)
-	r.Get("/profile/switch-org/:id", reqSignedIn, ChangeActiveOrgAndRedirectToHome)
-	r.Get("/org/", reqSignedIn, Index)
-	r.Get("/org/new", reqSignedIn, Index)
-	r.Get("/datasources/", reqSignedIn, Index)
-	r.Get("/datasources/new", reqSignedIn, Index)
-	r.Get("/datasources/edit/*", reqSignedIn, Index)
-	r.Get("/org/users", reqSignedIn, Index)
-	r.Get("/org/users/new", reqSignedIn, Index)
-	r.Get("/org/users/invite", reqSignedIn, Index)
-	r.Get("/org/teams", reqSignedIn, Index)
-	r.Get("/org/teams/*", reqSignedIn, Index)
-	r.Get("/org/apikeys/", reqSignedIn, Index)
-	r.Get("/dashboard/import/", reqSignedIn, Index)
-	r.Get("/configuration", reqGrafanaAdmin, Index)
-	r.Get("/admin", reqGrafanaAdmin, Index)
-	r.Get("/admin/settings", reqGrafanaAdmin, Index)
-	r.Get("/admin/users", reqGrafanaAdmin, Index)
-	r.Get("/admin/users/create", reqGrafanaAdmin, Index)
-	r.Get("/admin/users/edit/:id", reqGrafanaAdmin, Index)
-	r.Get("/admin/orgs", reqGrafanaAdmin, Index)
-	r.Get("/admin/orgs/edit/:id", reqGrafanaAdmin, Index)
-	r.Get("/admin/stats", reqGrafanaAdmin, Index)
-
-	r.Get("/styleguide", reqSignedIn, Index)
-
-	r.Get("/plugins", reqSignedIn, Index)
-	r.Get("/plugins/:id/edit", reqSignedIn, Index)
-	r.Get("/plugins/:id/page/:page", reqSignedIn, Index)
-
-	r.Get("/d/:uid/:slug", reqSignedIn, Index)
-	r.Get("/d/:uid", reqSignedIn, Index)
-	r.Get("/dashboard/db/:slug", reqSignedIn, redirectFromLegacyDashboardURL, Index)
-	r.Get("/dashboard/script/*", reqSignedIn, Index)
-	r.Get("/dashboard-solo/snapshot/*", Index)
-	r.Get("/d-solo/:uid/:slug", reqSignedIn, Index)
-	r.Get("/dashboard-solo/db/:slug", reqSignedIn, redirectFromLegacyDashboardSoloURL, Index)
-	r.Get("/dashboard-solo/script/*", reqSignedIn, Index)
-	r.Get("/import/dashboard", reqSignedIn, Index)
-	r.Get("/dashboards/", reqSignedIn, Index)
-	r.Get("/dashboards/*", reqSignedIn, Index)
-
-	r.Get("/explore", reqEditorRole, Index)
-
-	r.Get("/playlists/", reqSignedIn, Index)
-	r.Get("/playlists/*", reqSignedIn, Index)
-	r.Get("/alerting/", reqSignedIn, Index)
-	r.Get("/alerting/*", reqSignedIn, Index)
+	r.Get("/profile/", reqSignedIn, hs.Index)
+	r.Get("/profile/password", reqSignedIn, hs.Index)
+	r.Get("/profile/switch-org/:id", reqSignedIn, hs.ChangeActiveOrgAndRedirectToHome)
+	r.Get("/org/", reqSignedIn, hs.Index)
+	r.Get("/org/new", reqSignedIn, hs.Index)
+	r.Get("/datasources/", reqSignedIn, hs.Index)
+	r.Get("/datasources/new", reqSignedIn, hs.Index)
+	r.Get("/datasources/edit/*", reqSignedIn, hs.Index)
+	r.Get("/org/users", reqSignedIn, hs.Index)
+	r.Get("/org/users/new", reqSignedIn, hs.Index)
+	r.Get("/org/users/invite", reqSignedIn, hs.Index)
+	r.Get("/org/teams", reqSignedIn, hs.Index)
+	r.Get("/org/teams/*", reqSignedIn, hs.Index)
+	r.Get("/org/apikeys/", reqSignedIn, hs.Index)
+	r.Get("/dashboard/import/", reqSignedIn, hs.Index)
+	r.Get("/configuration", reqGrafanaAdmin, hs.Index)
+	r.Get("/admin", reqGrafanaAdmin, hs.Index)
+	r.Get("/admin/settings", reqGrafanaAdmin, hs.Index)
+	r.Get("/admin/users", reqGrafanaAdmin, hs.Index)
+	r.Get("/admin/users/create", reqGrafanaAdmin, hs.Index)
+	r.Get("/admin/users/edit/:id", reqGrafanaAdmin, hs.Index)
+	r.Get("/admin/orgs", reqGrafanaAdmin, hs.Index)
+	r.Get("/admin/orgs/edit/:id", reqGrafanaAdmin, hs.Index)
+	r.Get("/admin/stats", reqGrafanaAdmin, hs.Index)
+
+	r.Get("/styleguide", reqSignedIn, hs.Index)
+
+	r.Get("/plugins", reqSignedIn, hs.Index)
+	r.Get("/plugins/:id/edit", reqSignedIn, hs.Index)
+	r.Get("/plugins/:id/page/:page", reqSignedIn, hs.Index)
+
+	r.Get("/d/:uid/:slug", reqSignedIn, hs.Index)
+	r.Get("/d/:uid", reqSignedIn, hs.Index)
+	r.Get("/dashboard/db/:slug", reqSignedIn, redirectFromLegacyDashboardURL, hs.Index)
+	r.Get("/dashboard/script/*", reqSignedIn, hs.Index)
+	r.Get("/dashboard-solo/snapshot/*", hs.Index)
+	r.Get("/d-solo/:uid/:slug", reqSignedIn, hs.Index)
+	r.Get("/dashboard-solo/db/:slug", reqSignedIn, redirectFromLegacyDashboardSoloURL, hs.Index)
+	r.Get("/dashboard-solo/script/*", reqSignedIn, hs.Index)
+	r.Get("/import/dashboard", reqSignedIn, hs.Index)
+	r.Get("/dashboards/", reqSignedIn, hs.Index)
+	r.Get("/dashboards/*", reqSignedIn, hs.Index)
+
+	r.Get("/explore", reqEditorRole, hs.Index)
+
+	r.Get("/playlists/", reqSignedIn, hs.Index)
+	r.Get("/playlists/*", reqSignedIn, hs.Index)
+	r.Get("/alerting/", reqSignedIn, hs.Index)
+	r.Get("/alerting/*", reqSignedIn, hs.Index)
 
 	// sign up
-	r.Get("/signup", Index)
+	r.Get("/signup", hs.Index)
 	r.Get("/api/user/signup/options", Wrap(GetSignUpOptions))
 	r.Post("/api/user/signup", quota("user"), bind(dtos.SignUpForm{}), Wrap(SignUp))
 	r.Post("/api/user/signup/step2", bind(dtos.SignUpStep2Form{}), Wrap(SignUpStep2))
@@ -91,15 +91,15 @@ func (hs *HTTPServer) registerRoutes() {
 	r.Post("/api/user/invite/complete", bind(dtos.CompleteInviteForm{}), Wrap(CompleteInvite))
 
 	// reset password
-	r.Get("/user/password/send-reset-email", Index)
-	r.Get("/user/password/reset", Index)
+	r.Get("/user/password/send-reset-email", hs.Index)
+	r.Get("/user/password/reset", hs.Index)
 
 	r.Post("/api/user/password/send-reset-email", bind(dtos.SendResetPasswordEmailForm{}), Wrap(SendResetPasswordEmail))
 	r.Post("/api/user/password/reset", bind(dtos.ResetUserPasswordForm{}), Wrap(ResetPassword))
 
 	// dashboard snapshots
-	r.Get("/dashboard/snapshot/*", Index)
-	r.Get("/dashboard/snapshots/", reqSignedIn, Index)
+	r.Get("/dashboard/snapshot/*", hs.Index)
+	r.Get("/dashboard/snapshots/", reqSignedIn, hs.Index)
 
 	// api for dashboard snapshots
 	r.Post("/api/snapshots/", bind(m.CreateDashboardSnapshotCommand{}), CreateDashboardSnapshot)
@@ -234,13 +234,13 @@ func (hs *HTTPServer) registerRoutes() {
 			datasourceRoute.Get("/", Wrap(GetDataSources))
 			datasourceRoute.Post("/", quota("data_source"), bind(m.AddDataSourceCommand{}), Wrap(AddDataSource))
 			datasourceRoute.Put("/:id", bind(m.UpdateDataSourceCommand{}), Wrap(UpdateDataSource))
-			datasourceRoute.Delete("/:id", Wrap(DeleteDataSourceByID))
+			datasourceRoute.Delete("/:id", Wrap(DeleteDataSourceById))
 			datasourceRoute.Delete("/name/:name", Wrap(DeleteDataSourceByName))
-			datasourceRoute.Get("/:id", Wrap(GetDataSourceByID))
+			datasourceRoute.Get("/:id", Wrap(GetDataSourceById))
 			datasourceRoute.Get("/name/:name", Wrap(GetDataSourceByName))
 		}, reqOrgAdmin)
 
-		apiRoute.Get("/datasources/id/:name", Wrap(GetDataSourceIDByName), reqSignedIn)
+		apiRoute.Get("/datasources/id/:name", Wrap(GetDataSourceIdByName), reqSignedIn)
 
 		apiRoute.Get("/plugins", Wrap(GetPluginList))
 		apiRoute.Get("/plugins/:pluginId/settings", Wrap(GetPluginSettingByID))
@@ -251,7 +251,7 @@ func (hs *HTTPServer) registerRoutes() {
 			pluginRoute.Post("/:pluginId/settings", bind(m.UpdatePluginSettingCmd{}), Wrap(UpdatePluginSetting))
 		}, reqOrgAdmin)
 
-		apiRoute.Get("/frontend/settings/", GetFrontendSettings)
+		apiRoute.Get("/frontend/settings/", hs.GetFrontendSettings)
 		apiRoute.Any("/datasources/proxy/:id/*", reqSignedIn, hs.ProxyDataSourceRequest)
 		apiRoute.Any("/datasources/proxy/:id", reqSignedIn, hs.ProxyDataSourceRequest)
 

+ 0 - 9
pkg/api/avatar/avatar.go

@@ -97,15 +97,6 @@ type CacheServer struct {
 	cache    *gocache.Cache
 }
 
-func (this *CacheServer) mustInt(r *http.Request, defaultValue int, keys ...string) (v int) {
-	for _, k := range keys {
-		if _, err := fmt.Sscanf(r.FormValue(k), "%d", &v); err == nil {
-			defaultValue = v
-		}
-	}
-	return defaultValue
-}
-
 func (this *CacheServer) Handler(ctx *macaron.Context) {
 	urlPath := ctx.Req.URL.Path
 	hash := urlPath[strings.LastIndex(urlPath, "/")+1:]

+ 3 - 2
pkg/api/dashboard.go

@@ -6,6 +6,7 @@ import (
 	"os"
 	"path"
 
+	"github.com/grafana/grafana/pkg/services/alerting"
 	"github.com/grafana/grafana/pkg/services/dashboards"
 
 	"github.com/grafana/grafana/pkg/api/dtos"
@@ -251,8 +252,8 @@ func PostDashboard(c *m.ReqContext, cmd m.SaveDashboardCommand) Response {
 		return Error(403, err.Error(), err)
 	}
 
-	if err == m.ErrDashboardContainsInvalidAlertData {
-		return Error(500, "Invalid alert data. Cannot save dashboard", err)
+	if validationErr, ok := err.(alerting.ValidationError); ok {
+		return Error(422, validationErr.Error(), nil)
 	}
 
 	if err != nil {

+ 2 - 1
pkg/api/dashboard_test.go

@@ -9,6 +9,7 @@ import (
 	"github.com/grafana/grafana/pkg/bus"
 	"github.com/grafana/grafana/pkg/components/simplejson"
 	m "github.com/grafana/grafana/pkg/models"
+	"github.com/grafana/grafana/pkg/services/alerting"
 	"github.com/grafana/grafana/pkg/services/dashboards"
 	"github.com/grafana/grafana/pkg/setting"
 
@@ -725,7 +726,7 @@ func TestDashboardApiEndpoint(t *testing.T) {
 				{SaveError: m.ErrDashboardVersionMismatch, ExpectedStatusCode: 412},
 				{SaveError: m.ErrDashboardTitleEmpty, ExpectedStatusCode: 400},
 				{SaveError: m.ErrDashboardFolderCannotHaveParent, ExpectedStatusCode: 400},
-				{SaveError: m.ErrDashboardContainsInvalidAlertData, ExpectedStatusCode: 500},
+				{SaveError: alerting.ValidationError{Reason: "Mu"}, ExpectedStatusCode: 422},
 				{SaveError: m.ErrDashboardFailedToUpdateAlertData, ExpectedStatusCode: 500},
 				{SaveError: m.ErrDashboardFailedGenerateUniqueUid, ExpectedStatusCode: 500},
 				{SaveError: m.ErrDashboardTypeMismatch, ExpectedStatusCode: 400},

+ 33 - 2
pkg/api/dataproxy.go

@@ -2,6 +2,7 @@ package api
 
 import (
 	"fmt"
+	"github.com/pkg/errors"
 	"time"
 
 	"github.com/grafana/grafana/pkg/api/pluginproxy"
@@ -14,6 +15,20 @@ import (
 const HeaderNameNoBackendCache = "X-Grafana-NoCache"
 
 func (hs *HTTPServer) getDatasourceFromCache(id int64, c *m.ReqContext) (*m.DataSource, error) {
+	userPermissionsQuery := m.GetDataSourcePermissionsForUserQuery{
+		User: c.SignedInUser,
+	}
+	if err := bus.Dispatch(&userPermissionsQuery); err != nil {
+		if err != bus.ErrHandlerNotFound {
+			return nil, err
+		}
+	} else {
+		permissionType, exists := userPermissionsQuery.Result[id]
+		if exists && permissionType != m.DsPermissionQuery {
+			return nil, errors.New("User not allowed to access datasource")
+		}
+	}
+
 	nocache := c.Req.Header.Get(HeaderNameNoBackendCache) == "true"
 	cacheKey := fmt.Sprintf("ds-%d", id)
 
@@ -38,7 +53,9 @@ func (hs *HTTPServer) getDatasourceFromCache(id int64, c *m.ReqContext) (*m.Data
 func (hs *HTTPServer) ProxyDataSourceRequest(c *m.ReqContext) {
 	c.TimeRequest(metrics.M_DataSource_ProxyReq_Timer)
 
-	ds, err := hs.getDatasourceFromCache(c.ParamsInt64(":id"), c)
+	dsId := c.ParamsInt64(":id")
+	ds, err := hs.getDatasourceFromCache(dsId, c)
+
 	if err != nil {
 		c.JsonApiErr(500, "Unable to load datasource meta data", err)
 		return
@@ -51,7 +68,21 @@ func (hs *HTTPServer) ProxyDataSourceRequest(c *m.ReqContext) {
 		return
 	}
 
-	proxyPath := c.Params("*")
+	// macaron does not include trailing slashes when resolving a wildcard path
+	proxyPath := ensureProxyPathTrailingSlash(c.Req.URL.Path, c.Params("*"))
+
 	proxy := pluginproxy.NewDataSourceProxy(ds, plugin, c, proxyPath)
 	proxy.HandleRequest()
 }
+
+// ensureProxyPathTrailingSlash Check for a trailing slash in original path and makes
+// sure that a trailing slash is added to proxy path, if not already exists.
+func ensureProxyPathTrailingSlash(originalPath, proxyPath string) string {
+	if len(proxyPath) > 1 {
+		if originalPath[len(originalPath)-1] == '/' && proxyPath[len(proxyPath)-1] != '/' {
+			return proxyPath + "/"
+		}
+	}
+
+	return proxyPath
+}

+ 19 - 0
pkg/api/dataproxy_test.go

@@ -0,0 +1,19 @@
+package api
+
+import (
+	"testing"
+
+	. "github.com/smartystreets/goconvey/convey"
+)
+
+func TestDataProxy(t *testing.T) {
+	Convey("Data proxy test", t, func() {
+		Convey("Should append trailing slash to proxy path if original path has a trailing slash", func() {
+			So(ensureProxyPathTrailingSlash("/api/datasources/proxy/6/api/v1/query_range/", "api/v1/query_range/"), ShouldEqual, "api/v1/query_range/")
+		})
+
+		Convey("Should not append trailing slash to proxy path if original path doesn't have a trailing slash", func() {
+			So(ensureProxyPathTrailingSlash("/api/datasources/proxy/6/api/v1/query_range", "api/v1/query_range"), ShouldEqual, "api/v1/query_range")
+		})
+	})
+}

+ 7 - 7
pkg/api/datasources.go

@@ -20,8 +20,8 @@ func GetDataSources(c *m.ReqContext) Response {
 	result := make(dtos.DataSourceList, 0)
 	for _, ds := range query.Result {
 		dsItem := dtos.DataSourceListItemDTO{
-			Id:        ds.Id,
 			OrgId:     ds.OrgId,
+			Id:        ds.Id,
 			Name:      ds.Name,
 			Url:       ds.Url,
 			Type:      ds.Type,
@@ -49,7 +49,7 @@ func GetDataSources(c *m.ReqContext) Response {
 	return JSON(200, &result)
 }
 
-func GetDataSourceByID(c *m.ReqContext) Response {
+func GetDataSourceById(c *m.ReqContext) Response {
 	query := m.GetDataSourceByIdQuery{
 		Id:    c.ParamsInt64(":id"),
 		OrgId: c.OrgId,
@@ -68,14 +68,14 @@ func GetDataSourceByID(c *m.ReqContext) Response {
 	return JSON(200, &dtos)
 }
 
-func DeleteDataSourceByID(c *m.ReqContext) Response {
+func DeleteDataSourceById(c *m.ReqContext) Response {
 	id := c.ParamsInt64(":id")
 
 	if id <= 0 {
 		return Error(400, "Missing valid datasource id", nil)
 	}
 
-	ds, err := getRawDataSourceByID(id, c.OrgId)
+	ds, err := getRawDataSourceById(id, c.OrgId)
 	if err != nil {
 		return Error(400, "Failed to delete datasource", nil)
 	}
@@ -186,7 +186,7 @@ func fillWithSecureJSONData(cmd *m.UpdateDataSourceCommand) error {
 		return nil
 	}
 
-	ds, err := getRawDataSourceByID(cmd.Id, cmd.OrgId)
+	ds, err := getRawDataSourceById(cmd.Id, cmd.OrgId)
 	if err != nil {
 		return err
 	}
@@ -206,7 +206,7 @@ func fillWithSecureJSONData(cmd *m.UpdateDataSourceCommand) error {
 	return nil
 }
 
-func getRawDataSourceByID(id int64, orgID int64) (*m.DataSource, error) {
+func getRawDataSourceById(id int64, orgID int64) (*m.DataSource, error) {
 	query := m.GetDataSourceByIdQuery{
 		Id:    id,
 		OrgId: orgID,
@@ -236,7 +236,7 @@ func GetDataSourceByName(c *m.ReqContext) Response {
 }
 
 // Get /api/datasources/id/:name
-func GetDataSourceIDByName(c *m.ReqContext) Response {
+func GetDataSourceIdByName(c *m.ReqContext) Response {
 	query := m.GetDataSourceByNameQuery{Name: c.Params(":name"), OrgId: c.OrgId}
 
 	if err := bus.Dispatch(&query); err != nil {

+ 26 - 23
pkg/api/dtos/alerting.go

@@ -49,28 +49,30 @@ func formatShort(interval time.Duration) string {
 
 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,
+		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,
+		DisableResolveMessage: notification.DisableResolveMessage,
+		Settings:              notification.Settings,
 	}
 }
 
 type AlertNotification struct {
-	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"`
+	Id                    int64            `json:"id"`
+	Name                  string           `json:"name"`
+	Type                  string           `json:"type"`
+	IsDefault             bool             `json:"isDefault"`
+	SendReminder          bool             `json:"sendReminder"`
+	DisableResolveMessage bool             `json:"disableResolveMessage"`
+	Frequency             string           `json:"frequency"`
+	Created               time.Time        `json:"created"`
+	Updated               time.Time        `json:"updated"`
+	Settings              *simplejson.Json `json:"settings"`
 }
 
 type AlertTestCommand struct {
@@ -100,11 +102,12 @@ type EvalMatch struct {
 }
 
 type NotificationTestCommand struct {
-	Name         string           `json:"name"`
-	Type         string           `json:"type"`
-	SendReminder bool             `json:"sendReminder"`
-	Frequency    string           `json:"frequency"`
-	Settings     *simplejson.Json `json:"settings"`
+	Name                  string           `json:"name"`
+	Type                  string           `json:"type"`
+	SendReminder          bool             `json:"sendReminder"`
+	DisableResolveMessage bool             `json:"disableResolveMessage"`
+	Frequency             string           `json:"frequency"`
+	Settings              *simplejson.Json `json:"settings"`
 }
 
 type PauseAlertCommand struct {

+ 1 - 0
pkg/api/dtos/index.go

@@ -14,6 +14,7 @@ type IndexViewData struct {
 	NewGrafanaVersionExists bool
 	NewGrafanaVersion       string
 	AppName                 string
+	AppNameBodyClass        string
 }
 
 type PluginCss struct {

+ 0 - 10
pkg/api/folder_test.go

@@ -133,16 +133,6 @@ func TestFoldersApiEndpoint(t *testing.T) {
 	})
 }
 
-func callGetFolderByUID(sc *scenarioContext) {
-	sc.handlerFunc = GetFolderByUID
-	sc.fakeReqWithParams("GET", sc.url, map[string]string{}).exec()
-}
-
-func callDeleteFolder(sc *scenarioContext) {
-	sc.handlerFunc = DeleteFolder
-	sc.fakeReqWithParams("DELETE", sc.url, map[string]string{}).exec()
-}
-
 func callCreateFolder(sc *scenarioContext) {
 	sc.fakeReqWithParams("POST", sc.url, map[string]string{}).exec()
 }

+ 21 - 4
pkg/api/frontendsettings.go

@@ -11,7 +11,7 @@ import (
 	"github.com/grafana/grafana/pkg/util"
 )
 
-func getFrontendSettingsMap(c *m.ReqContext) (map[string]interface{}, error) {
+func (hs *HTTPServer) getFrontendSettingsMap(c *m.ReqContext) (map[string]interface{}, error) {
 	orgDataSources := make([]*m.DataSource, 0)
 
 	if c.OrgId != 0 {
@@ -22,7 +22,20 @@ func getFrontendSettingsMap(c *m.ReqContext) (map[string]interface{}, error) {
 			return nil, err
 		}
 
-		orgDataSources = query.Result
+		dsFilterQuery := m.DatasourcesPermissionFilterQuery{
+			User:        c.SignedInUser,
+			Datasources: query.Result,
+		}
+
+		if err := bus.Dispatch(&dsFilterQuery); err != nil {
+			if err != bus.ErrHandlerNotFound {
+				return nil, err
+			}
+
+			orgDataSources = query.Result
+		} else {
+			orgDataSources = dsFilterQuery.Result
+		}
 	}
 
 	datasources := make(map[string]interface{})
@@ -120,6 +133,10 @@ func getFrontendSettingsMap(c *m.ReqContext) (map[string]interface{}, error) {
 
 	panels := map[string]interface{}{}
 	for _, panel := range enabledPlugins.Panels {
+		if panel.State == "alpha" && !hs.Cfg.EnableAlphaPanels {
+			continue
+		}
+
 		panels[panel.Id] = map[string]interface{}{
 			"module":       panel.Module,
 			"baseUrl":      panel.BaseUrl,
@@ -183,8 +200,8 @@ func getPanelSort(id string) int {
 	return sort
 }
 
-func GetFrontendSettings(c *m.ReqContext) {
-	settings, err := getFrontendSettingsMap(c)
+func (hs *HTTPServer) GetFrontendSettings(c *m.ReqContext) {
+	settings, err := hs.getFrontendSettingsMap(c)
 	if err != nil {
 		c.JsonApiErr(400, "Failed to get frontend settings", err)
 		return

+ 3 - 1
pkg/api/http_server.go

@@ -28,6 +28,7 @@ import (
 	"github.com/grafana/grafana/pkg/models"
 	"github.com/grafana/grafana/pkg/plugins"
 	"github.com/grafana/grafana/pkg/registry"
+	"github.com/grafana/grafana/pkg/services/hooks"
 	"github.com/grafana/grafana/pkg/services/rendering"
 	"github.com/grafana/grafana/pkg/setting"
 )
@@ -52,6 +53,7 @@ type HTTPServer struct {
 	Bus           bus.Bus               `inject:""`
 	RenderService rendering.Service     `inject:""`
 	Cfg           *setting.Cfg          `inject:""`
+	HooksService  *hooks.HooksService   `inject:""`
 }
 
 func (hs *HTTPServer) Init() error {
@@ -184,7 +186,7 @@ func (hs *HTTPServer) applyRoutes() {
 	// then custom app proxy routes
 	hs.initAppPluginRoutes(hs.macaron)
 	// lastly not found route
-	hs.macaron.NotFound(NotFoundHandler)
+	hs.macaron.NotFound(hs.NotFoundHandler)
 }
 
 func (hs *HTTPServer) addMiddlewaresAndStaticRoutes() {

+ 19 - 6
pkg/api/index.go

@@ -17,8 +17,8 @@ const (
 	darkName  = "dark"
 )
 
-func setIndexViewData(c *m.ReqContext) (*dtos.IndexViewData, error) {
-	settings, err := getFrontendSettingsMap(c)
+func (hs *HTTPServer) setIndexViewData(c *m.ReqContext) (*dtos.IndexViewData, error) {
+	settings, err := hs.getFrontendSettingsMap(c)
 	if err != nil {
 		return nil, err
 	}
@@ -83,6 +83,7 @@ func setIndexViewData(c *m.ReqContext) (*dtos.IndexViewData, error) {
 		NewGrafanaVersion:       plugins.GrafanaLatestVersion,
 		NewGrafanaVersionExists: plugins.GrafanaHasUpdate,
 		AppName:                 setting.ApplicationName,
+		AppNameBodyClass:        getAppNameBodyClass(setting.ApplicationName),
 	}
 
 	if setting.DisableGravatar {
@@ -350,11 +351,12 @@ func setIndexViewData(c *m.ReqContext) (*dtos.IndexViewData, error) {
 		},
 	})
 
+	hs.HooksService.RunIndexDataHooks(&data)
 	return &data, nil
 }
 
-func Index(c *m.ReqContext) {
-	data, err := setIndexViewData(c)
+func (hs *HTTPServer) Index(c *m.ReqContext) {
+	data, err := hs.setIndexViewData(c)
 	if err != nil {
 		c.Handle(500, "Failed to get settings", err)
 		return
@@ -362,13 +364,13 @@ func Index(c *m.ReqContext) {
 	c.HTML(200, "index", data)
 }
 
-func NotFoundHandler(c *m.ReqContext) {
+func (hs *HTTPServer) NotFoundHandler(c *m.ReqContext) {
 	if c.IsApiRequest() {
 		c.JsonApiErr(404, "Not found", nil)
 		return
 	}
 
-	data, err := setIndexViewData(c)
+	data, err := hs.setIndexViewData(c)
 	if err != nil {
 		c.Handle(500, "Failed to get settings", err)
 		return
@@ -376,3 +378,14 @@ func NotFoundHandler(c *m.ReqContext) {
 
 	c.HTML(404, "index", data)
 }
+
+func getAppNameBodyClass(name string) string {
+	switch name {
+	case setting.APP_NAME:
+		return "app-grafana"
+	case setting.APP_NAME_ENTERPRISE:
+		return "app-enterprise"
+	default:
+		return ""
+	}
+}

+ 0 - 3
pkg/api/live/hub.go

@@ -37,9 +37,6 @@ func newHub() *hub {
 	}
 }
 
-func (h *hub) removeConnection() {
-}
-
 func (h *hub) run(ctx context.Context) {
 	for {
 		select {

+ 2 - 2
pkg/api/login.go

@@ -17,8 +17,8 @@ const (
 	ViewIndex = "index"
 )
 
-func LoginView(c *m.ReqContext) {
-	viewData, err := setIndexViewData(c)
+func (hs *HTTPServer) LoginView(c *m.ReqContext) {
+	viewData, err := hs.setIndexViewData(c)
 	if err != nil {
 		c.Handle(500, "Failed to get settings", err)
 		return

+ 16 - 10
pkg/api/org_users.go

@@ -45,7 +45,7 @@ func addOrgUserHelper(cmd m.AddOrgUserCommand) Response {
 
 // GET /api/org/users
 func GetOrgUsersForCurrentOrg(c *m.ReqContext) Response {
-	return getOrgUsersHelper(c.OrgId, c.Params("query"), c.ParamsInt("limit"))
+	return getOrgUsersHelper(c.OrgId, c.Query("query"), c.QueryInt("limit"))
 }
 
 // GET /api/orgs/:orgId/users
@@ -102,26 +102,32 @@ func updateOrgUserHelper(cmd m.UpdateOrgUserCommand) Response {
 
 // DELETE /api/org/users/:userId
 func RemoveOrgUserForCurrentOrg(c *m.ReqContext) Response {
-	userID := c.ParamsInt64(":userId")
-	return removeOrgUserHelper(c.OrgId, userID)
+	return removeOrgUserHelper(&m.RemoveOrgUserCommand{
+		UserId:                   c.ParamsInt64(":userId"),
+		OrgId:                    c.OrgId,
+		ShouldDeleteOrphanedUser: true,
+	})
 }
 
 // DELETE /api/orgs/:orgId/users/:userId
 func RemoveOrgUser(c *m.ReqContext) Response {
-	userID := c.ParamsInt64(":userId")
-	orgID := c.ParamsInt64(":orgId")
-	return removeOrgUserHelper(orgID, userID)
+	return removeOrgUserHelper(&m.RemoveOrgUserCommand{
+		UserId: c.ParamsInt64(":userId"),
+		OrgId:  c.ParamsInt64(":orgId"),
+	})
 }
 
-func removeOrgUserHelper(orgID int64, userID int64) Response {
-	cmd := m.RemoveOrgUserCommand{OrgId: orgID, UserId: userID}
-
-	if err := bus.Dispatch(&cmd); err != nil {
+func removeOrgUserHelper(cmd *m.RemoveOrgUserCommand) Response {
+	if err := bus.Dispatch(cmd); err != nil {
 		if err == m.ErrLastOrgAdmin {
 			return Error(400, "Cannot remove last organization admin", nil)
 		}
 		return Error(500, "Failed to remove user from organization", err)
 	}
 
+	if cmd.UserWasDeleted {
+		return Success("User deleted")
+	}
+
 	return Success("User removed from organization")
 }

+ 171 - 0
pkg/api/pluginproxy/access_token_provider.go

@@ -0,0 +1,171 @@
+package pluginproxy
+
+import (
+	"bytes"
+	"context"
+	"encoding/json"
+	"fmt"
+	"net/http"
+	"net/url"
+	"strconv"
+	"sync"
+	"time"
+
+	"golang.org/x/oauth2"
+
+	"github.com/grafana/grafana/pkg/models"
+	"github.com/grafana/grafana/pkg/plugins"
+	"golang.org/x/oauth2/jwt"
+)
+
+var (
+	tokenCache = tokenCacheType{
+		cache: map[string]*jwtToken{},
+	}
+	oauthJwtTokenCache = oauthJwtTokenCacheType{
+		cache: map[string]*oauth2.Token{},
+	}
+)
+
+type tokenCacheType struct {
+	cache map[string]*jwtToken
+	sync.Mutex
+}
+
+type oauthJwtTokenCacheType struct {
+	cache map[string]*oauth2.Token
+	sync.Mutex
+}
+
+type accessTokenProvider struct {
+	route             *plugins.AppPluginRoute
+	datasourceId      int64
+	datasourceVersion int
+}
+
+type jwtToken struct {
+	ExpiresOn       time.Time `json:"-"`
+	ExpiresOnString string    `json:"expires_on"`
+	AccessToken     string    `json:"access_token"`
+}
+
+func newAccessTokenProvider(ds *models.DataSource, pluginRoute *plugins.AppPluginRoute) *accessTokenProvider {
+	return &accessTokenProvider{
+		datasourceId:      ds.Id,
+		datasourceVersion: ds.Version,
+		route:             pluginRoute,
+	}
+}
+
+func (provider *accessTokenProvider) getAccessToken(data templateData) (string, error) {
+	tokenCache.Lock()
+	defer tokenCache.Unlock()
+	if cachedToken, found := tokenCache.cache[provider.getAccessTokenCacheKey()]; found {
+		if cachedToken.ExpiresOn.After(time.Now().Add(time.Second * 10)) {
+			logger.Info("Using token from cache")
+			return cachedToken.AccessToken, nil
+		}
+	}
+
+	urlInterpolated, err := interpolateString(provider.route.TokenAuth.Url, data)
+	if err != nil {
+		return "", err
+	}
+
+	params := make(url.Values)
+	for key, value := range provider.route.TokenAuth.Params {
+		interpolatedParam, err := interpolateString(value, data)
+		if err != nil {
+			return "", err
+		}
+		params.Add(key, interpolatedParam)
+	}
+
+	getTokenReq, _ := http.NewRequest("POST", urlInterpolated, bytes.NewBufferString(params.Encode()))
+	getTokenReq.Header.Add("Content-Type", "application/x-www-form-urlencoded")
+	getTokenReq.Header.Add("Content-Length", strconv.Itoa(len(params.Encode())))
+
+	resp, err := client.Do(getTokenReq)
+	if err != nil {
+		return "", err
+	}
+
+	defer resp.Body.Close()
+
+	var token jwtToken
+	if err := json.NewDecoder(resp.Body).Decode(&token); err != nil {
+		return "", err
+	}
+
+	expiresOnEpoch, _ := strconv.ParseInt(token.ExpiresOnString, 10, 64)
+	token.ExpiresOn = time.Unix(expiresOnEpoch, 0)
+	tokenCache.cache[provider.getAccessTokenCacheKey()] = &token
+
+	logger.Info("Got new access token", "ExpiresOn", token.ExpiresOn)
+
+	return token.AccessToken, nil
+}
+
+func (provider *accessTokenProvider) getJwtAccessToken(ctx context.Context, data templateData) (string, error) {
+	oauthJwtTokenCache.Lock()
+	defer oauthJwtTokenCache.Unlock()
+	if cachedToken, found := oauthJwtTokenCache.cache[provider.getAccessTokenCacheKey()]; found {
+		if cachedToken.Expiry.After(time.Now().Add(time.Second * 10)) {
+			logger.Debug("Using token from cache")
+			return cachedToken.AccessToken, nil
+		}
+	}
+
+	conf := &jwt.Config{}
+
+	if val, ok := provider.route.JwtTokenAuth.Params["client_email"]; ok {
+		interpolatedVal, err := interpolateString(val, data)
+		if err != nil {
+			return "", err
+		}
+		conf.Email = interpolatedVal
+	}
+
+	if val, ok := provider.route.JwtTokenAuth.Params["private_key"]; ok {
+		interpolatedVal, err := interpolateString(val, data)
+		if err != nil {
+			return "", err
+		}
+		conf.PrivateKey = []byte(interpolatedVal)
+	}
+
+	if val, ok := provider.route.JwtTokenAuth.Params["token_uri"]; ok {
+		interpolatedVal, err := interpolateString(val, data)
+		if err != nil {
+			return "", err
+		}
+		conf.TokenURL = interpolatedVal
+	}
+
+	conf.Scopes = provider.route.JwtTokenAuth.Scopes
+
+	token, err := getTokenSource(conf, ctx)
+	if err != nil {
+		return "", err
+	}
+
+	oauthJwtTokenCache.cache[provider.getAccessTokenCacheKey()] = token
+
+	logger.Info("Got new access token", "ExpiresOn", token.Expiry)
+
+	return token.AccessToken, nil
+}
+
+var getTokenSource = func(conf *jwt.Config, ctx context.Context) (*oauth2.Token, error) {
+	tokenSrc := conf.TokenSource(ctx)
+	token, err := tokenSrc.Token()
+	if err != nil {
+		return nil, err
+	}
+
+	return token, nil
+}
+
+func (provider *accessTokenProvider) getAccessTokenCacheKey() string {
+	return fmt.Sprintf("%v_%v_%v_%v", provider.datasourceId, provider.datasourceVersion, provider.route.Path, provider.route.Method)
+}

+ 94 - 0
pkg/api/pluginproxy/access_token_provider_test.go

@@ -0,0 +1,94 @@
+package pluginproxy
+
+import (
+	"context"
+	"testing"
+	"time"
+
+	"github.com/grafana/grafana/pkg/models"
+	"github.com/grafana/grafana/pkg/plugins"
+	. "github.com/smartystreets/goconvey/convey"
+	"golang.org/x/oauth2"
+	"golang.org/x/oauth2/jwt"
+)
+
+func TestAccessToken(t *testing.T) {
+	Convey("Plugin with JWT token auth route", t, func() {
+		pluginRoute := &plugins.AppPluginRoute{
+			Path:   "pathwithjwttoken1",
+			Url:    "https://api.jwt.io/some/path",
+			Method: "GET",
+			JwtTokenAuth: &plugins.JwtTokenAuth{
+				Url: "https://login.server.com/{{.JsonData.tenantId}}/oauth2/token",
+				Scopes: []string{
+					"https://www.testapi.com/auth/monitoring.read",
+					"https://www.testapi.com/auth/cloudplatformprojects.readonly",
+				},
+				Params: map[string]string{
+					"token_uri":    "{{.JsonData.tokenUri}}",
+					"client_email": "{{.JsonData.clientEmail}}",
+					"private_key":  "{{.SecureJsonData.privateKey}}",
+				},
+			},
+		}
+
+		templateData := templateData{
+			JsonData: map[string]interface{}{
+				"clientEmail": "test@test.com",
+				"tokenUri":    "login.url.com/token",
+			},
+			SecureJsonData: map[string]string{
+				"privateKey": "testkey",
+			},
+		}
+
+		ds := &models.DataSource{Id: 1, Version: 2}
+
+		Convey("should fetch token using jwt private key", func() {
+			getTokenSource = func(conf *jwt.Config, ctx context.Context) (*oauth2.Token, error) {
+				return &oauth2.Token{AccessToken: "abc"}, nil
+			}
+			provider := newAccessTokenProvider(ds, pluginRoute)
+			token, err := provider.getJwtAccessToken(context.Background(), templateData)
+			So(err, ShouldBeNil)
+
+			So(token, ShouldEqual, "abc")
+		})
+
+		Convey("should set jwt config values", func() {
+			getTokenSource = func(conf *jwt.Config, ctx context.Context) (*oauth2.Token, error) {
+				So(conf.Email, ShouldEqual, "test@test.com")
+				So(conf.PrivateKey, ShouldResemble, []byte("testkey"))
+				So(len(conf.Scopes), ShouldEqual, 2)
+				So(conf.Scopes[0], ShouldEqual, "https://www.testapi.com/auth/monitoring.read")
+				So(conf.Scopes[1], ShouldEqual, "https://www.testapi.com/auth/cloudplatformprojects.readonly")
+				So(conf.TokenURL, ShouldEqual, "login.url.com/token")
+
+				return &oauth2.Token{AccessToken: "abc"}, nil
+			}
+
+			provider := newAccessTokenProvider(ds, pluginRoute)
+			_, err := provider.getJwtAccessToken(context.Background(), templateData)
+			So(err, ShouldBeNil)
+		})
+
+		Convey("should use cached token on second call", func() {
+			getTokenSource = func(conf *jwt.Config, ctx context.Context) (*oauth2.Token, error) {
+				return &oauth2.Token{
+					AccessToken: "abc",
+					Expiry:      time.Now().Add(1 * time.Minute)}, nil
+			}
+			provider := newAccessTokenProvider(ds, pluginRoute)
+			token1, err := provider.getJwtAccessToken(context.Background(), templateData)
+			So(err, ShouldBeNil)
+			So(token1, ShouldEqual, "abc")
+
+			getTokenSource = func(conf *jwt.Config, ctx context.Context) (*oauth2.Token, error) {
+				return &oauth2.Token{AccessToken: "error: cache not used"}, nil
+			}
+			token2, err := provider.getJwtAccessToken(context.Background(), templateData)
+			So(err, ShouldBeNil)
+			So(token2, ShouldEqual, "abc")
+		})
+	})
+}

+ 109 - 0
pkg/api/pluginproxy/ds_auth_provider.go

@@ -0,0 +1,109 @@
+package pluginproxy
+
+import (
+	"bytes"
+	"context"
+	"fmt"
+	"net/http"
+	"net/url"
+	"strings"
+	"text/template"
+
+	m "github.com/grafana/grafana/pkg/models"
+	"github.com/grafana/grafana/pkg/plugins"
+	"github.com/grafana/grafana/pkg/util"
+	"golang.org/x/oauth2/google"
+)
+
+//ApplyRoute should use the plugin route data to set auth headers and custom headers
+func ApplyRoute(ctx context.Context, req *http.Request, proxyPath string, route *plugins.AppPluginRoute, ds *m.DataSource) {
+	proxyPath = strings.TrimPrefix(proxyPath, route.Path)
+
+	data := templateData{
+		JsonData:       ds.JsonData.Interface().(map[string]interface{}),
+		SecureJsonData: ds.SecureJsonData.Decrypt(),
+	}
+
+	interpolatedURL, err := interpolateString(route.Url, data)
+	if err != nil {
+		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
+	}
+
+	req.URL.Scheme = routeURL.Scheme
+	req.URL.Host = routeURL.Host
+	req.Host = routeURL.Host
+	req.URL.Path = util.JoinUrlFragments(routeURL.Path, proxyPath)
+
+	if err := addHeaders(&req.Header, route, data); err != nil {
+		logger.Error("Failed to render plugin headers", "error", err)
+	}
+
+	tokenProvider := newAccessTokenProvider(ds, route)
+
+	if route.TokenAuth != nil {
+		if token, err := tokenProvider.getAccessToken(data); err != nil {
+			logger.Error("Failed to get access token", "error", err)
+		} else {
+			req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", token))
+		}
+	}
+
+	authenticationType := ds.JsonData.Get("authenticationType").MustString("jwt")
+	if route.JwtTokenAuth != nil && authenticationType == "jwt" {
+		if token, err := tokenProvider.getJwtAccessToken(ctx, data); err != nil {
+			logger.Error("Failed to get access token", "error", err)
+		} else {
+			req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", token))
+		}
+	}
+
+	if authenticationType == "gce" {
+		tokenSrc, err := google.DefaultTokenSource(ctx, route.JwtTokenAuth.Scopes...)
+		if err != nil {
+			logger.Error("Failed to get default token from meta data server", "error", err)
+		} else {
+			token, err := tokenSrc.Token()
+			if err != nil {
+				logger.Error("Failed to get default access token from meta data server", "error", err)
+			} else {
+				req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", token.AccessToken))
+			}
+		}
+	}
+
+	logger.Info("Requesting", "url", req.URL.String())
+}
+
+func interpolateString(text string, data templateData) (string, error) {
+	t, err := template.New("content").Parse(text)
+	if err != nil {
+		return "", fmt.Errorf("could not parse template %s", text)
+	}
+
+	var contentBuf bytes.Buffer
+	err = t.Execute(&contentBuf, data)
+	if err != nil {
+		return "", fmt.Errorf("failed to execute template %s", text)
+	}
+
+	return contentBuf.String(), nil
+}
+
+func addHeaders(reqHeaders *http.Header, route *plugins.AppPluginRoute, data templateData) error {
+	for _, header := range route.Headers {
+		interpolated, err := interpolateString(header.Content, data)
+		if err != nil {
+			return err
+		}
+		reqHeaders.Add(header.Name, interpolated)
+	}
+
+	return nil
+}

+ 21 - 0
pkg/api/pluginproxy/ds_auth_provider_test.go

@@ -0,0 +1,21 @@
+package pluginproxy
+
+import (
+	"testing"
+
+	. "github.com/smartystreets/goconvey/convey"
+)
+
+func TestDsAuthProvider(t *testing.T) {
+	Convey("When interpolating string", t, func() {
+		data := templateData{
+			SecureJsonData: map[string]string{
+				"Test": "0asd+asd",
+			},
+		}
+
+		interpolated, err := interpolateString("{{.SecureJsonData.Test}}", data)
+		So(err, ShouldBeNil)
+		So(interpolated, ShouldEqual, "0asd+asd")
+	})
+}

+ 3 - 130
pkg/api/pluginproxy/ds_proxy.go

@@ -2,7 +2,6 @@ package pluginproxy
 
 import (
 	"bytes"
-	"encoding/json"
 	"errors"
 	"fmt"
 	"io/ioutil"
@@ -12,7 +11,6 @@ import (
 	"net/url"
 	"strconv"
 	"strings"
-	"text/template"
 	"time"
 
 	"github.com/opentracing/opentracing-go"
@@ -25,17 +23,10 @@ import (
 )
 
 var (
-	logger     = log.New("data-proxy-log")
-	tokenCache = map[string]*jwtToken{}
-	client     = newHTTPClient()
+	logger = log.New("data-proxy-log")
+	client = newHTTPClient()
 )
 
-type jwtToken struct {
-	ExpiresOn       time.Time `json:"-"`
-	ExpiresOnString string    `json:"expires_on"`
-	AccessToken     string    `json:"access_token"`
-}
-
 type DataSourceProxy struct {
 	ds        *m.DataSource
 	ctx       *m.ReqContext
@@ -162,7 +153,6 @@ func (proxy *DataSourceProxy) getDirector() func(req *http.Request) {
 		} else {
 			req.URL.Path = util.JoinUrlFragments(proxy.targetUrl.Path, proxy.proxyPath)
 		}
-
 		if proxy.ds.BasicAuth {
 			req.Header.Del("Authorization")
 			req.Header.Add("Authorization", util.GetBasicAuthHeader(proxy.ds.BasicAuthUser, proxy.ds.BasicAuthPassword))
@@ -219,7 +209,7 @@ func (proxy *DataSourceProxy) getDirector() func(req *http.Request) {
 		}
 
 		if proxy.route != nil {
-			proxy.applyRoute(req)
+			ApplyRoute(proxy.ctx.Req.Context(), req, proxy.proxyPath, proxy.route, proxy.ds)
 		}
 	}
 }
@@ -311,120 +301,3 @@ func checkWhiteList(c *m.ReqContext, host string) bool {
 
 	return true
 }
-
-func (proxy *DataSourceProxy) applyRoute(req *http.Request) {
-	proxy.proxyPath = strings.TrimPrefix(proxy.proxyPath, proxy.route.Path)
-
-	data := templateData{
-		JsonData:       proxy.ds.JsonData.Interface().(map[string]interface{}),
-		SecureJsonData: proxy.ds.SecureJsonData.Decrypt(),
-	}
-
-	interpolatedURL, err := interpolateString(proxy.route.Url, data)
-	if err != nil {
-		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
-	}
-
-	req.URL.Scheme = routeURL.Scheme
-	req.URL.Host = routeURL.Host
-	req.Host = routeURL.Host
-	req.URL.Path = util.JoinUrlFragments(routeURL.Path, proxy.proxyPath)
-
-	if err := addHeaders(&req.Header, proxy.route, data); err != nil {
-		logger.Error("Failed to render plugin headers", "error", err)
-	}
-
-	if proxy.route.TokenAuth != nil {
-		if token, err := proxy.getAccessToken(data); err != nil {
-			logger.Error("Failed to get access token", "error", err)
-		} else {
-			req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", token))
-		}
-	}
-
-	logger.Info("Requesting", "url", req.URL.String())
-}
-
-func (proxy *DataSourceProxy) getAccessToken(data templateData) (string, error) {
-	if cachedToken, found := tokenCache[proxy.getAccessTokenCacheKey()]; found {
-		if cachedToken.ExpiresOn.After(time.Now().Add(time.Second * 10)) {
-			logger.Info("Using token from cache")
-			return cachedToken.AccessToken, nil
-		}
-	}
-
-	urlInterpolated, err := interpolateString(proxy.route.TokenAuth.Url, data)
-	if err != nil {
-		return "", err
-	}
-
-	params := make(url.Values)
-	for key, value := range proxy.route.TokenAuth.Params {
-		interpolatedParam, err := interpolateString(value, data)
-		if err != nil {
-			return "", err
-		}
-		params.Add(key, interpolatedParam)
-	}
-
-	getTokenReq, _ := http.NewRequest("POST", urlInterpolated, bytes.NewBufferString(params.Encode()))
-	getTokenReq.Header.Add("Content-Type", "application/x-www-form-urlencoded")
-	getTokenReq.Header.Add("Content-Length", strconv.Itoa(len(params.Encode())))
-
-	resp, err := client.Do(getTokenReq)
-	if err != nil {
-		return "", err
-	}
-
-	defer resp.Body.Close()
-
-	var token jwtToken
-	if err := json.NewDecoder(resp.Body).Decode(&token); err != nil {
-		return "", err
-	}
-
-	expiresOnEpoch, _ := strconv.ParseInt(token.ExpiresOnString, 10, 64)
-	token.ExpiresOn = time.Unix(expiresOnEpoch, 0)
-	tokenCache[proxy.getAccessTokenCacheKey()] = &token
-
-	logger.Info("Got new access token", "ExpiresOn", token.ExpiresOn)
-	return token.AccessToken, nil
-}
-
-func (proxy *DataSourceProxy) getAccessTokenCacheKey() string {
-	return fmt.Sprintf("%v_%v_%v", proxy.ds.Id, proxy.route.Path, proxy.route.Method)
-}
-
-func interpolateString(text string, data templateData) (string, error) {
-	t, err := template.New("content").Parse(text)
-	if err != nil {
-		return "", fmt.Errorf("could not parse template %s", text)
-	}
-
-	var contentBuf bytes.Buffer
-	err = t.Execute(&contentBuf, data)
-	if err != nil {
-		return "", fmt.Errorf("failed to execute template %s", text)
-	}
-
-	return contentBuf.String(), nil
-}
-
-func addHeaders(reqHeaders *http.Header, route *plugins.AppPluginRoute, data templateData) error {
-	for _, header := range route.Headers {
-		interpolated, err := interpolateString(header.Content, data)
-		if err != nil {
-			return err
-		}
-		reqHeaders.Add(header.Name, interpolated)
-	}
-
-	return nil
-}

+ 22 - 17
pkg/api/pluginproxy/ds_proxy_test.go

@@ -83,7 +83,7 @@ func TestDSRouteRule(t *testing.T) {
 			Convey("When matching route path", func() {
 				proxy := NewDataSourceProxy(ds, plugin, ctx, "api/v4/some/method")
 				proxy.route = plugin.Routes[0]
-				proxy.applyRoute(req)
+				ApplyRoute(proxy.ctx.Req.Context(), req, proxy.proxyPath, proxy.route, proxy.ds)
 
 				Convey("should add headers and update url", func() {
 					So(req.URL.String(), ShouldEqual, "https://www.google.com/some/method")
@@ -94,7 +94,7 @@ 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)
+				ApplyRoute(proxy.ctx.Req.Context(), req, proxy.proxyPath, proxy.route, proxy.ds)
 
 				Convey("should add headers and interpolate the url", func() {
 					So(req.URL.String(), ShouldEqual, "https://dynamic.grafana.com/some/method")
@@ -188,7 +188,7 @@ func TestDSRouteRule(t *testing.T) {
 					client = newFakeHTTPClient(json)
 					proxy1 := NewDataSourceProxy(ds, plugin, ctx, "pathwithtoken1")
 					proxy1.route = plugin.Routes[0]
-					proxy1.applyRoute(req)
+					ApplyRoute(proxy1.ctx.Req.Context(), req, proxy1.proxyPath, proxy1.route, proxy1.ds)
 
 					authorizationHeaderCall1 = req.Header.Get("Authorization")
 					So(req.URL.String(), ShouldEqual, "https://api.nr1.io/some/path")
@@ -202,7 +202,7 @@ func TestDSRouteRule(t *testing.T) {
 						client = newFakeHTTPClient(json2)
 						proxy2 := NewDataSourceProxy(ds, plugin, ctx, "pathwithtoken2")
 						proxy2.route = plugin.Routes[1]
-						proxy2.applyRoute(req)
+						ApplyRoute(proxy2.ctx.Req.Context(), req, proxy2.proxyPath, proxy2.route, proxy2.ds)
 
 						authorizationHeaderCall2 = req.Header.Get("Authorization")
 
@@ -217,7 +217,7 @@ func TestDSRouteRule(t *testing.T) {
 							client = newFakeHTTPClient([]byte{})
 							proxy3 := NewDataSourceProxy(ds, plugin, ctx, "pathwithtoken1")
 							proxy3.route = plugin.Routes[0]
-							proxy3.applyRoute(req)
+							ApplyRoute(proxy3.ctx.Req.Context(), req, proxy3.proxyPath, proxy3.route, proxy3.ds)
 
 							authorizationHeaderCall3 := req.Header.Get("Authorization")
 							So(req.URL.String(), ShouldEqual, "https://api.nr1.io/some/path")
@@ -331,18 +331,6 @@ func TestDSRouteRule(t *testing.T) {
 			})
 		})
 
-		Convey("When interpolating string", func() {
-			data := templateData{
-				SecureJsonData: map[string]string{
-					"Test": "0asd+asd",
-				},
-			}
-
-			interpolated, err := interpolateString("{{.SecureJsonData.Test}}", data)
-			So(err, ShouldBeNil)
-			So(interpolated, ShouldEqual, "0asd+asd")
-		})
-
 		Convey("When proxying a data source with custom headers specified", func() {
 			plugin := &plugins.DataSourcePlugin{}
 
@@ -374,6 +362,23 @@ func TestDSRouteRule(t *testing.T) {
 			})
 		})
 
+		Convey("When proxying a custom datasource", func() {
+			plugin := &plugins.DataSourcePlugin{}
+			ds := &m.DataSource{
+				Type: "custom-datasource",
+				Url:  "http://host/root/",
+			}
+			ctx := &m.ReqContext{}
+			proxy := NewDataSourceProxy(ds, plugin, ctx, "/path/to/folder/")
+			req, err := http.NewRequest(http.MethodGet, "http://grafana.com/sub", nil)
+			So(err, ShouldBeNil)
+
+			proxy.getDirector()(req)
+
+			Convey("Shoudl keep user request (including trailing slash)", func() {
+				So(req.URL.String(), ShouldEqual, "http://host/root/path/to/folder/")
+			})
+		})
 	})
 }
 

+ 3 - 3
pkg/api/user.go

@@ -177,17 +177,17 @@ func UserSetUsingOrg(c *m.ReqContext) Response {
 }
 
 // GET /profile/switch-org/:id
-func ChangeActiveOrgAndRedirectToHome(c *m.ReqContext) {
+func (hs *HTTPServer) ChangeActiveOrgAndRedirectToHome(c *m.ReqContext) {
 	orgID := c.ParamsInt64(":id")
 
 	if !validateUsingOrg(c.UserId, orgID) {
-		NotFoundHandler(c)
+		hs.NotFoundHandler(c)
 	}
 
 	cmd := m.SetUsingOrgCommand{UserId: c.UserId, OrgId: orgID}
 
 	if err := bus.Dispatch(&cmd); err != nil {
-		NotFoundHandler(c)
+		hs.NotFoundHandler(c)
 	}
 
 	c.Redirect(setting.AppSubUrl + "/")

+ 2 - 0
pkg/cmd/grafana-cli/commands/commands.go

@@ -6,6 +6,7 @@ import (
 
 	"github.com/codegangsta/cli"
 	"github.com/fatih/color"
+	"github.com/grafana/grafana/pkg/bus"
 	"github.com/grafana/grafana/pkg/cmd/grafana-cli/logger"
 	"github.com/grafana/grafana/pkg/services/sqlstore"
 	"github.com/grafana/grafana/pkg/setting"
@@ -24,6 +25,7 @@ func runDbCommand(command func(commandLine CommandLine) error) func(context *cli
 
 		engine := &sqlstore.SqlStore{}
 		engine.Cfg = cfg
+		engine.Bus = bus.GetBus()
 		engine.Init()
 
 		if err := command(cmd); err != nil {

Einige Dateien werden nicht angezeigt, da zu viele Dateien in diesem Diff geändert wurden.